phewsh 0.3.1 → 0.5.1

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,11 +37,15 @@ function showBrand() {
37
37
  }
38
38
 
39
39
  const COMMANDS = {
40
- intent: () => require('../commands/intent'),
41
- login: () => require('../commands/login'),
42
- ai: () => require('../commands/ai'),
43
- music: () => require('../commands/music'),
44
- sap: () => require('../commands/sap'),
40
+ intent: () => require('../commands/intent'),
41
+ clarify: () => require('../commands/clarify'),
42
+ push: () => require('../commands/push'),
43
+ pull: () => require('../commands/pull'),
44
+ link: () => require('../commands/link'),
45
+ login: () => require('../commands/login'),
46
+ ai: () => require('../commands/ai'),
47
+ music: () => require('../commands/music'),
48
+ sap: () => require('../commands/sap'),
45
49
  help: showHelp,
46
50
  version: showVersion,
47
51
  };
@@ -55,17 +59,21 @@ function showHelp() {
55
59
  showBrand();
56
60
  console.log(` ${g('v' + pkg.version)} · ${g('phewsh.com')}\n`);
57
61
  console.log(` ${b('Commands')}`);
58
- console.log(` ${w('login')} Set up identity, API key, and cloud sync`);
59
- console.log(` ${w('intent')} Structure thinking vision, plan, next actions`);
62
+ console.log(` ${w('clarify')} Turn messy intent into a structured project spec`);
63
+ console.log(` ${w('push')} Push local .intent/ to cloud`);
64
+ console.log(` ${w('pull')} Pull project from cloud to .intent/`);
65
+ console.log(` ${w('link')} Link local .intent/ to a cloud project`);
66
+ console.log(` ${w('intent')} Manage .intent/ artifacts — status, open, evolve`);
60
67
  console.log(` ${w('ai')} Run context-aware AI prompts (reads .intent/)`);
68
+ console.log(` ${w('login')} Set up identity, API key, and cloud sync`);
61
69
  console.log(` ${w('sap')} Sustainable AI Protocol — usage and accountability`);
62
70
  console.log(` ${w('music')} MBHD music engine`);
63
71
  console.log('');
64
72
  console.log(` ${b('Quick start')}`);
65
- console.log(` ${g('phewsh login')} Set up identity + API key`);
66
- console.log(` ${g('phewsh intent --init')} Create .intent/ in any project`);
67
- console.log(` ${g('phewsh intent --sync')} Push .intent/ to cloud`);
68
- console.log(` ${g('phewsh ai run "what\'s next?"')} AI with your project context`);
73
+ console.log(` ${g('phewsh login')} Set up identity + API key`);
74
+ console.log(` ${g('phewsh clarify')} Compile messy intent structured spec`);
75
+ console.log(` ${g('phewsh push')} Sync to cloud`);
76
+ console.log(` ${g('phewsh ai run "what\'s next?"')} AI with your project context`);
69
77
  console.log('');
70
78
  }
71
79
 
@@ -0,0 +1,233 @@
1
+ // phewsh clarify
2
+ // Takes raw, messy intent and compiles it into a structured PPS.
3
+ // First run: creates .intent/ + pps.json + .md views.
4
+ // Subsequent runs: updates pps.json fields and regenerates views.
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+ const readline = require('readline');
10
+ const { readPPS, writePPS, createPPS, generateViews } = require('../lib/pps');
11
+
12
+ const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
13
+ const INTENT_DIR = path.join(process.cwd(), '.intent');
14
+
15
+ const args = process.argv.slice(3);
16
+ const textFlag = args.indexOf('--text');
17
+ const rawFromFlag = textFlag !== -1 ? args.slice(textFlag + 1).join(' ') : null;
18
+ const isUpdate = args.includes('--update') || args.includes('-u');
19
+
20
+ function loadConfig() {
21
+ if (!fs.existsSync(CONFIG_PATH)) return null;
22
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
23
+ }
24
+
25
+ function getProjectName() {
26
+ const existing = readPPS(INTENT_DIR);
27
+ if (existing?.entity) return existing.entity;
28
+ const visionPath = path.join(INTENT_DIR, 'vision.md');
29
+ if (fs.existsSync(visionPath)) {
30
+ const m = fs.readFileSync(visionPath, 'utf-8').match(/^entity:\s*(.+)$/m);
31
+ if (m) return m[1].trim();
32
+ }
33
+ return path.basename(process.cwd());
34
+ }
35
+
36
+ async function askForInput() {
37
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
38
+ return new Promise((resolve) => {
39
+ console.log('\n Describe what you\'re building. Be as messy as you want.\n');
40
+ console.log(' (You\'re the David Rose. PHEWSH is the business registration clerk.)\n');
41
+ process.stdout.write(' > ');
42
+ let input = '';
43
+ rl.on('line', (line) => { input += (input ? ' ' : '') + line.trim(); });
44
+ rl.on('close', () => resolve(input.trim()));
45
+ });
46
+ }
47
+
48
+ async function callClarifyAPI(apiKey, raw, existing) {
49
+ const isRefine = !!(existing?.intent?.goal);
50
+
51
+ const systemPrompt = `You are a project compiler. Your job is to extract clean, structured intent from messy human input.
52
+
53
+ Return ONLY valid JSON — no markdown, no explanation, no commentary. The JSON must match this exact schema:
54
+
55
+ {
56
+ "goal": "one sentence north star (what this is and why it exists)",
57
+ "success_criteria": ["measurable outcome", "measurable outcome"],
58
+ "constraints": ["constraint or non-negotiable", "..."],
59
+ "inputs": ["what this takes in or requires"],
60
+ "outputs": ["what this produces or delivers"],
61
+ "tasks": [
62
+ {"text": "first concrete action to take", "type": "do"},
63
+ {"text": "second action", "type": "do"}
64
+ ]
65
+ }
66
+
67
+ Rules:
68
+ - goal: one sentence, no buzzwords, no hedging
69
+ - success_criteria: 2-5 items, must be measurable or observable
70
+ - constraints: real limits only (budget, time, technical, ethical). 0-3 items.
71
+ - inputs: what the project needs to function (data, people, tools). 1-4 items.
72
+ - outputs: what the project delivers. 1-4 items.
73
+ - tasks: 3-7 concrete next actions, specific enough to act on immediately
74
+ - type options: "do" (manual action), "copy" (command to run), "open" (URL to visit), "install" (package to install)
75
+ ${isRefine ? '\nThis is a refinement of existing intent. The previous goal was: ' + existing.intent.goal : ''}`;
76
+
77
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
78
+ method: 'POST',
79
+ headers: {
80
+ 'x-api-key': apiKey,
81
+ 'anthropic-version': '2023-06-01',
82
+ 'content-type': 'application/json',
83
+ },
84
+ body: JSON.stringify({
85
+ model: 'claude-sonnet-4-6',
86
+ max_tokens: 1024,
87
+ system: systemPrompt,
88
+ messages: [{ role: 'user', content: raw }],
89
+ }),
90
+ });
91
+
92
+ if (!response.ok) {
93
+ const err = await response.json().catch(() => ({}));
94
+ throw new Error(err.error?.message || `API error ${response.status}`);
95
+ }
96
+
97
+ const data = await response.json();
98
+ const text = data.content?.[0]?.text || '';
99
+
100
+ // Strip markdown code fences if present
101
+ const clean = text.replace(/^```(?:json)?\n?/m, '').replace(/\n?```$/m, '').trim();
102
+ return JSON.parse(clean);
103
+ }
104
+
105
+ function writeViews(intentDir, pps) {
106
+ const { vision, plan, next } = generateViews(pps);
107
+ fs.writeFileSync(path.join(intentDir, 'vision.md'), vision);
108
+ fs.writeFileSync(path.join(intentDir, 'plan.md'), plan);
109
+ fs.writeFileSync(path.join(intentDir, 'next.md'), next);
110
+ }
111
+
112
+ async function main() {
113
+ if (args.includes('--help') || args.includes('-h')) {
114
+ console.log(`
115
+ 😮‍💨🤫 phewsh clarify
116
+
117
+ Usage:
118
+ phewsh clarify Interactive: describe your project, get structured PPS
119
+ phewsh clarify --text "..." Inline: pass raw text directly
120
+ phewsh clarify --update Refine existing PPS with new input
121
+
122
+ What it does:
123
+ Takes messy, buzzword-heavy input and compiles it into a structured project spec (PPS).
124
+ Writes .intent/pps.json as the source of truth.
125
+ Generates vision.md, plan.md, next.md as human-readable views.
126
+
127
+ Requires:
128
+ phewsh login --set-key Set your Anthropic API key first
129
+
130
+ Examples:
131
+ phewsh clarify
132
+ phewsh clarify --text "I want to build a thing that helps people track habits with AI"
133
+ phewsh clarify --update After new context or direction change
134
+ `);
135
+ return;
136
+ }
137
+
138
+ const config = loadConfig();
139
+ if (!config?.apiKey) {
140
+ console.log('\n No API key found. Run `phewsh login --set-key` first.\n');
141
+ process.exit(1);
142
+ }
143
+
144
+ const existing = readPPS(INTENT_DIR);
145
+ if (existing && !isUpdate) {
146
+ console.log('\n .intent/pps.json already exists.');
147
+ console.log(' Run `phewsh clarify --update` to refine, or `phewsh intent --status` to view.\n');
148
+ return;
149
+ }
150
+
151
+ console.log('\n 😮‍💨🤫 phewsh clarify\n');
152
+
153
+ let raw = rawFromFlag;
154
+ if (!raw) {
155
+ if (!process.stdin.isTTY) {
156
+ console.error('\n Pipe input or use --text "your description"\n');
157
+ process.exit(1);
158
+ }
159
+ raw = await askForInput();
160
+ }
161
+
162
+ if (!raw) {
163
+ console.log('\n Nothing to clarify.\n');
164
+ return;
165
+ }
166
+
167
+ console.log('\n Compiling...\n');
168
+
169
+ let extracted;
170
+ try {
171
+ extracted = await callClarifyAPI(config.apiKey, raw, existing);
172
+ } catch (err) {
173
+ console.error('\n Clarify failed:', err.message, '\n');
174
+ process.exit(1);
175
+ }
176
+
177
+ const entity = getProjectName();
178
+ let pps;
179
+
180
+ if (existing && isUpdate) {
181
+ // Patch existing PPS with new intent fields, preserve id/created/tasks state
182
+ pps = {
183
+ ...existing,
184
+ intent: {
185
+ raw,
186
+ goal: extracted.goal,
187
+ success_criteria: extracted.success_criteria || [],
188
+ constraints: extracted.constraints || [],
189
+ inputs: extracted.inputs || [],
190
+ outputs: extracted.outputs || [],
191
+ },
192
+ };
193
+ // Merge new tasks (preserve done tasks, add new ones)
194
+ const doneTasks = existing.tasks.filter(t => t.status === 'done');
195
+ const newTasks = (extracted.tasks || []).map((t, i) => ({
196
+ id: `t_${String(Date.now() + i).slice(-6)}`,
197
+ text: t.text,
198
+ status: 'open',
199
+ type: t.type || 'do',
200
+ blocked_by: null,
201
+ }));
202
+ pps.tasks = [...doneTasks, ...newTasks];
203
+ pps.state.phase = 'plan';
204
+ } else {
205
+ pps = createPPS({ entity, raw, intent: extracted });
206
+ pps.state.phase = 'plan';
207
+ }
208
+
209
+ fs.mkdirSync(INTENT_DIR, { recursive: true });
210
+ writePPS(INTENT_DIR, pps);
211
+ writeViews(INTENT_DIR, pps);
212
+
213
+ console.log(` ✓ .intent/pps.json — structured project spec`);
214
+ console.log(` ✓ .intent/vision.md — ${pps.intent.goal}`);
215
+ console.log(` ✓ .intent/plan.md — ${pps.intent.success_criteria.length} outcomes, ${pps.intent.constraints.length} constraints`);
216
+ console.log(` ✓ .intent/next.md — ${pps.tasks.length} actions\n`);
217
+ console.log(` Goal: ${pps.intent.goal}\n`);
218
+ if (pps.tasks.length > 0) {
219
+ console.log(' First actions:');
220
+ pps.tasks.slice(0, 3).forEach(t => console.log(` · ${t.text}`));
221
+ }
222
+ console.log(`
223
+ Next:
224
+ phewsh intent --status Review your artifacts
225
+ phewsh clarify --update Refine with more context
226
+ phewsh ai run "..." Run AI with this context
227
+ `);
228
+ }
229
+
230
+ main().catch(err => {
231
+ console.error('\n Error:', err.message);
232
+ process.exit(1);
233
+ });
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const readline = require('readline');
4
4
  const { execSync } = require('child_process');
5
+ const { createPPS, writePPS, generateViews } = require('../lib/pps');
5
6
 
6
7
  const args = process.argv.slice(3);
7
8
  const INTENT_DIR = path.join(process.cwd(), '.intent');
@@ -113,97 +114,40 @@ async function initIntent() {
113
114
 
114
115
  console.log('\n Creating .intent/ ...\n');
115
116
 
116
- fs.mkdirSync(INTENT_DIR, { recursive: true });
117
-
118
- const vision = `---
119
- entity: ${projectName}
120
- archetype: product
121
- created: ${date}
122
- updated: ${date}
123
- ---
124
-
125
- # Vision
126
-
127
- ## North Star
128
- ${what || `What is ${projectName} and why does it exist?`}
129
-
130
- ## Outcomes
131
- ${goal ? `- ${goal}` : '<!-- What does success look like? 3-5 concrete outcomes. -->'}
132
- - <!-- Add more outcomes here -->
133
-
134
- ## Principles
135
- <!-- Non-negotiable values and constraints. What will you never compromise on? -->
136
- -
137
- -
138
-
139
- ## Beneficiaries
140
- <!-- Who benefits from this and how? Be specific. -->
141
- `;
142
-
143
- const plan = `---
144
- entity: ${projectName}
145
- archetype: product
146
- created: ${date}
147
- updated: ${date}
148
- ---
149
-
150
- # Plan
151
-
152
- ## Current Strategy
153
- <!-- One paragraph: the approach and why it's the right one right now. -->
154
-
155
- ## Systems
156
- <!-- What needs to exist? Key components, tools, structures. -->
157
-
158
- ## Sequence
159
- <!-- Phased plan. What must come first? What is blocked on what? -->
160
- - Phase 1:
161
- - Phase 2:
162
- - Phase 3:
163
-
164
- ## Constraints
165
- <!-- What limits this? Budget, time, team, technical. Be honest. -->
166
-
167
- ## Resources
168
- <!-- What do you have available? Team, tools, existing assets. -->
169
- `;
170
-
171
- const next = `---
172
- entity: ${projectName}
173
- archetype: product
174
- created: ${date}
175
- updated: ${date}
176
- ---
177
-
178
- # Next
179
-
180
- ## Current State
181
- ${what ? `Building: ${what}` : '<!-- Where things stand right now, honestly. -->'}
182
-
183
- ## Next Actions
184
- - [ ] **Refine the vision** — Open the web compass and complete vision.md
185
- - [ ] **Define Phase 1** — What is the smallest thing you can ship?
186
- - [ ] **Identify the first blocker** — What is standing between you and execution?
187
-
188
- ## Blocked
189
- <!-- What is stuck and why? What decision is needed to unblock it? -->
190
-
191
- ## Metrics
192
- <!-- 2-3 numbers that tell you if it's working. -->
193
- `;
117
+ // Build a starter PPS and generate views from it
118
+ const pps = createPPS({
119
+ entity: projectName,
120
+ archetype: 'product',
121
+ raw: [what, goal].filter(Boolean).join(' '),
122
+ intent: {
123
+ goal: what || '',
124
+ success_criteria: goal ? [goal] : [],
125
+ constraints: [],
126
+ inputs: [],
127
+ outputs: [],
128
+ tasks: [
129
+ { text: 'Refine the vision complete vision.md', type: 'do' },
130
+ { text: 'Define Phase 1 — what is the smallest thing to ship?', type: 'do' },
131
+ { text: 'Identify the first blocker', type: 'do' },
132
+ ],
133
+ },
134
+ });
194
135
 
136
+ writePPS(INTENT_DIR, pps);
137
+ const { vision, plan, next } = generateViews(pps);
195
138
  fs.writeFileSync(path.join(INTENT_DIR, 'vision.md'), vision);
196
139
  fs.writeFileSync(path.join(INTENT_DIR, 'plan.md'), plan);
197
140
  fs.writeFileSync(path.join(INTENT_DIR, 'next.md'), next);
198
141
 
142
+ console.log(` ✓ .intent/pps.json — Structured project spec (source of truth)`);
199
143
  console.log(` ✓ .intent/vision.md — The north star`);
200
144
  console.log(` ✓ .intent/plan.md — The strategy`);
201
145
  console.log(` ✓ .intent/next.md — What to do right now`);
202
146
  console.log(`
203
- These files are your persistent context.
204
- Drop them in any AI coding session and your tools gain full understanding.
147
+ Tip: Run \`phewsh clarify\` to have AI compile your messy intent into a precise spec.
205
148
 
206
149
  Next:
150
+ phewsh clarify Compile intent → structured spec with AI
207
151
  phewsh intent --open Open the web compass to go deeper
208
152
  phewsh intent --status Check your progress any time
209
153
  `);
@@ -0,0 +1,4 @@
1
+ // phewsh link <cloud-project-id>
2
+ // Associate local .intent/ with a specific cloud project
3
+ const { main } = require('./sync');
4
+ main('link');
package/commands/login.js CHANGED
@@ -109,16 +109,26 @@ async function main() {
109
109
  process.exit(1);
110
110
  }
111
111
 
112
- console.log(` Check ${email} for a 6-digit code.\n`);
113
- const token = await ask(' Verification code\n > ');
112
+ console.log(` Check ${email} — look for a 6-digit code.`);
113
+ console.log(` (It may arrive as a link — if so, visit phewsh.com/intent to log in there first,`);
114
+ console.log(` then run: phewsh login --from-web to save that session.)\n`);
115
+ const token = await ask(' 6-digit code\n > ');
114
116
  console.log('');
115
117
 
118
+ if (!token || token.length < 4) {
119
+ close();
120
+ console.log('\n No code entered. If you received a link, log in at phewsh.com/intent\n then run `phewsh login --status` after logging in via web.\n');
121
+ process.exit(1);
122
+ }
123
+
116
124
  let session;
117
125
  try {
118
126
  session = await verifyOtp(email, token);
119
127
  } catch (err) {
120
128
  close();
121
- console.error('\n Verification failed:', err.message, '\n');
129
+ console.error('\n Verification failed:', err.message);
130
+ console.error(' If you received a link instead of a code, ask your admin to enable');
131
+ console.error(' Email OTP in the Supabase dashboard (Authentication → Email).\n');
122
132
  process.exit(1);
123
133
  }
124
134
 
@@ -0,0 +1,4 @@
1
+ // phewsh pull
2
+ // Pull pps.json + artifacts from cloud to local .intent/
3
+ const { main } = require('./sync');
4
+ main('pull');
@@ -0,0 +1,4 @@
1
+ // phewsh push
2
+ // Push local .intent/pps.json + artifacts to cloud (Supabase)
3
+ const { main } = require('./sync');
4
+ main('push');
package/commands/sync.js CHANGED
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const os = require('os');
7
7
  const crypto = require('crypto');
8
8
  const { select, upsert, refreshSession } = require('../lib/supabase');
9
+ const { readPPS, writePPS } = require('../lib/pps');
9
10
 
10
11
  const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
11
12
  const INTENT_DIR = path.join(process.cwd(), '.intent');
@@ -69,18 +70,50 @@ async function push(config, token) {
69
70
  token
70
71
  );
71
72
 
73
+ // Read pps.json if it exists
74
+ const localPPS = readPPS(INTENT_DIR);
75
+ const archetype = localPPS?.archetype || 'product';
76
+ const projectId = localPPS?.adapters?.phewsh?.cloud_id || null;
77
+
72
78
  if (existing.length > 0) {
73
79
  project = existing[0];
74
- } else {
75
- const created = await upsert('projects', {
76
- id: genProjectId(),
80
+ } else if (projectId) {
81
+ // Linked to a specific cloud project — fetch it
82
+ const linked = await select('projects', `id=eq.${projectId}&user_id=eq.${userId}&select=id,name`, token).catch(() => []);
83
+ project = linked[0] || null;
84
+ }
85
+
86
+ if (!project) {
87
+ const payload = {
88
+ id: (localPPS?.adapters?.phewsh?.cloud_id) || genProjectId(),
77
89
  user_id: userId,
78
90
  name: projectName,
79
- archetype: 'product',
80
- freeform_text: '',
81
- }, token);
91
+ archetype,
92
+ freeform_text: localPPS?.intent?.raw || '',
93
+ };
94
+ if (localPPS) payload.pps_json = localPPS;
95
+ const created = await upsert('projects', payload, token);
82
96
  project = Array.isArray(created) ? created[0] : created;
83
97
  console.log(` Created project: ${projectName}`);
98
+ } else if (localPPS) {
99
+ // Update pps_json on existing project
100
+ await upsert('projects', {
101
+ id: project.id,
102
+ user_id: userId,
103
+ name: projectName,
104
+ archetype,
105
+ freeform_text: localPPS.intent?.raw || '',
106
+ pps_json: localPPS,
107
+ }, token).catch(() => {});
108
+ }
109
+
110
+ // Store cloud project_id back into local pps.json for linking
111
+ if (localPPS && project?.id) {
112
+ if (!localPPS.adapters) localPPS.adapters = {};
113
+ if (!localPPS.adapters.phewsh) localPPS.adapters.phewsh = {};
114
+ localPPS.adapters.phewsh.cloud_id = project.id;
115
+ localPPS.adapters.phewsh.last_synced = new Date().toISOString();
116
+ writePPS(INTENT_DIR, localPPS);
84
117
  }
85
118
 
86
119
  // Push each artifact file
@@ -97,42 +130,51 @@ async function push(config, token) {
97
130
  }, token);
98
131
  pushed.push(file);
99
132
  }
133
+ if (localPPS) pushed.unshift('pps.json');
100
134
 
101
- console.log(`\n ✓ Synced to cloud — ${projectName}`);
135
+ console.log(`\n ✓ Pushed to cloud — ${projectName} (${project.id})`);
102
136
  pushed.forEach(f => console.log(` ${f}`));
103
137
  console.log('');
104
138
  }
105
139
 
106
- async function pull(config, token) {
140
+ async function pull(config, token, cloudId = null) {
107
141
  const projectName = getProjectName();
108
142
  const userId = config.supabaseUserId;
109
143
 
110
- const projects = await select(
111
- 'projects',
112
- `name=eq.${encodeURIComponent(projectName)}&user_id=eq.${userId}&select=id,name`,
113
- token
114
- );
144
+ let query = cloudId
145
+ ? `id=eq.${cloudId}&user_id=eq.${userId}&select=id,name,pps_json`
146
+ : `name=eq.${encodeURIComponent(projectName)}&user_id=eq.${userId}&select=id,name,pps_json`;
147
+
148
+ const projects = await select('projects', query, token);
115
149
 
116
150
  if (projects.length === 0) {
117
- console.log(`\n No cloud project found for "${projectName}".\n Push first with: phewsh intent --sync\n`);
151
+ console.log(`\n No cloud project found for "${projectName}".\n Push first with: phewsh push\n`);
118
152
  return;
119
153
  }
120
154
 
121
155
  const project = projects[0];
156
+ fs.mkdirSync(INTENT_DIR, { recursive: true });
157
+
158
+ const pulled = [];
159
+
160
+ // Restore pps.json from cloud if present
161
+ if (project.pps_json) {
162
+ const localPPS = readPPS(INTENT_DIR);
163
+ const merged = { ...project.pps_json };
164
+ // Keep any local adapter links
165
+ if (localPPS?.adapters) merged.adapters = { ...project.pps_json.adapters, ...localPPS.adapters };
166
+ merged.adapters = merged.adapters || {};
167
+ merged.adapters.phewsh = { cloud_id: project.id, last_synced: new Date().toISOString() };
168
+ writePPS(INTENT_DIR, merged);
169
+ pulled.push('pps.json');
170
+ }
171
+
122
172
  const artifacts = await select(
123
173
  'artifacts',
124
174
  `project_id=eq.${project.id}&user_id=eq.${userId}&select=kind,content,updated_at`,
125
175
  token
126
176
  );
127
177
 
128
- if (artifacts.length === 0) {
129
- console.log('\n No artifacts found in cloud for this project.\n');
130
- return;
131
- }
132
-
133
- fs.mkdirSync(INTENT_DIR, { recursive: true });
134
-
135
- const pulled = [];
136
178
  for (const artifact of artifacts) {
137
179
  const file = KIND_TO_FILE[artifact.kind];
138
180
  if (!file) continue;
@@ -140,11 +182,43 @@ async function pull(config, token) {
140
182
  pulled.push(file);
141
183
  }
142
184
 
143
- console.log(`\n ✓ Pulled from cloud — ${projectName}`);
185
+ if (pulled.length === 0) {
186
+ console.log('\n No data found in cloud for this project.\n');
187
+ return;
188
+ }
189
+
190
+ console.log(`\n ✓ Pulled from cloud — ${project.name} (${project.id})`);
144
191
  pulled.forEach(f => console.log(` ${f}`));
145
192
  console.log('');
146
193
  }
147
194
 
195
+ async function link(config, token, cloudId) {
196
+ const projectName = getProjectName();
197
+ const userId = config.supabaseUserId;
198
+
199
+ const projects = await select('projects', `id=eq.${cloudId}&user_id=eq.${userId}&select=id,name`, token);
200
+ if (projects.length === 0) {
201
+ console.log(`\n No cloud project found with id: ${cloudId}\n`);
202
+ process.exit(1);
203
+ }
204
+
205
+ const project = projects[0];
206
+ let localPPS = readPPS(INTENT_DIR);
207
+ if (!localPPS) {
208
+ console.log('\n No local .intent/pps.json found. Run `phewsh clarify` or `phewsh intent --init` first.\n');
209
+ process.exit(1);
210
+ }
211
+
212
+ if (!localPPS.adapters) localPPS.adapters = {};
213
+ if (!localPPS.adapters.phewsh) localPPS.adapters.phewsh = {};
214
+ localPPS.adapters.phewsh.cloud_id = project.id;
215
+ localPPS.adapters.phewsh.last_synced = null;
216
+ writePPS(INTENT_DIR, localPPS);
217
+
218
+ console.log(`\n ✓ Linked .intent/ → cloud project "${project.name}" (${project.id})\n`);
219
+ console.log(' Run `phewsh push` to sync.\n');
220
+ }
221
+
148
222
  async function main(direction = 'push') {
149
223
  const config = loadConfig();
150
224
  if (!config?.supabaseUserId) {
@@ -160,9 +234,17 @@ async function main(direction = 'push') {
160
234
 
161
235
  if (direction === 'pull') {
162
236
  await pull(config, token);
237
+ } else if (direction === 'link') {
238
+ const args = process.argv.slice(3);
239
+ const cloudId = args[1];
240
+ if (!cloudId) {
241
+ console.log('\n Usage: phewsh link <cloud-project-id>\n');
242
+ process.exit(1);
243
+ }
244
+ await link(config, token, cloudId);
163
245
  } else {
164
246
  await push(config, token);
165
247
  }
166
248
  }
167
249
 
168
- module.exports = { main };
250
+ module.exports = { main, push, pull, link, ensureValidToken, loadConfig };
package/lib/pps.js ADDED
@@ -0,0 +1,158 @@
1
+ // PPS — Portable Project Spec
2
+ // pps.json is the source of truth. .md files are generated views.
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+
8
+ function genId() {
9
+ return `p_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
10
+ }
11
+
12
+ function genTaskId(index) {
13
+ return `t_${String(index + 1).padStart(3, '0')}`;
14
+ }
15
+
16
+ function readPPS(intentDir) {
17
+ const p = path.join(intentDir, 'pps.json');
18
+ if (!fs.existsSync(p)) return null;
19
+ try { return JSON.parse(fs.readFileSync(p, 'utf-8')); } catch { return null; }
20
+ }
21
+
22
+ function writePPS(intentDir, data) {
23
+ fs.mkdirSync(intentDir, { recursive: true });
24
+ const now = new Date().toISOString().split('T')[0];
25
+ data.updated = now;
26
+ fs.writeFileSync(path.join(intentDir, 'pps.json'), JSON.stringify(data, null, 2));
27
+ }
28
+
29
+ function createPPS({ entity, archetype = 'product', raw = '', intent = {} }) {
30
+ const now = new Date().toISOString().split('T')[0];
31
+ return {
32
+ id: genId(),
33
+ version: '0.1',
34
+ entity,
35
+ archetype,
36
+ created: now,
37
+ updated: now,
38
+ intent: {
39
+ raw,
40
+ goal: intent.goal || '',
41
+ success_criteria: intent.success_criteria || [],
42
+ constraints: intent.constraints || [],
43
+ inputs: intent.inputs || [],
44
+ outputs: intent.outputs || [],
45
+ },
46
+ tasks: (intent.tasks || []).map((t, i) => ({
47
+ id: genTaskId(i),
48
+ text: t.text,
49
+ status: 'open',
50
+ type: t.type || 'do',
51
+ blocked_by: null,
52
+ })),
53
+ state: {
54
+ phase: 'clarify',
55
+ last_action: now,
56
+ progress: 0,
57
+ },
58
+ assets: [],
59
+ adapters: {
60
+ anthropic: { project_id: null, last_synced: null },
61
+ openai: { project_id: null, last_synced: null },
62
+ },
63
+ };
64
+ }
65
+
66
+ function generateViews(pps) {
67
+ const { entity, intent, tasks, created, updated, archetype } = pps;
68
+
69
+ const vision = `---
70
+ entity: ${entity}
71
+ archetype: ${archetype}
72
+ created: ${created}
73
+ updated: ${updated}
74
+ ---
75
+
76
+ # Vision
77
+
78
+ ## North Star
79
+ ${intent.goal || `What is ${entity} and why does it exist?`}
80
+
81
+ ## Outcomes
82
+ ${intent.success_criteria.length > 0
83
+ ? intent.success_criteria.map(c => `- ${c}`).join('\n')
84
+ : '<!-- What does success look like? 3-5 concrete outcomes. -->'}
85
+
86
+ ## Principles
87
+ ${intent.constraints.length > 0
88
+ ? intent.constraints.map(c => `- ${c}`).join('\n')
89
+ : '<!-- Non-negotiable values and constraints. -->'}
90
+
91
+ ## Beneficiaries
92
+ <!-- Who benefits from this and how? -->
93
+ `;
94
+
95
+ const plan = `---
96
+ entity: ${entity}
97
+ archetype: ${archetype}
98
+ created: ${created}
99
+ updated: ${updated}
100
+ ---
101
+
102
+ # Plan
103
+
104
+ ## Current Strategy
105
+ <!-- One paragraph: the approach and why it's the right one right now. -->
106
+
107
+ ## Systems
108
+ ${intent.inputs.length > 0 || intent.outputs.length > 0
109
+ ? [
110
+ intent.inputs.length > 0 ? `**Inputs:** ${intent.inputs.join(', ')}` : '',
111
+ intent.outputs.length > 0 ? `**Outputs:** ${intent.outputs.join(', ')}` : '',
112
+ ].filter(Boolean).join('\n')
113
+ : '<!-- Key components, tools, structures. -->'}
114
+
115
+ ## Sequence
116
+ ${tasks.length > 0
117
+ ? tasks.slice(0, 5).map((t, i) => `- Phase ${i + 1}: ${t.text}`).join('\n')
118
+ : '- Phase 1:\n- Phase 2:\n- Phase 3:'}
119
+
120
+ ## Constraints
121
+ ${intent.constraints.length > 0
122
+ ? intent.constraints.map(c => `- ${c}`).join('\n')
123
+ : '<!-- What limits this? Budget, time, team, technical. -->'}
124
+
125
+ ## Resources
126
+ <!-- What do you have available? Team, tools, existing assets. -->
127
+ `;
128
+
129
+ const next = `---
130
+ entity: ${entity}
131
+ archetype: ${archetype}
132
+ created: ${created}
133
+ updated: ${updated}
134
+ ---
135
+
136
+ # Next
137
+
138
+ ## Current State
139
+ ${intent.goal ? `Building: ${intent.goal}` : '<!-- Where things stand right now. -->'}
140
+
141
+ ## Next Actions
142
+ ${tasks.length > 0
143
+ ? tasks.map(t => `- [ ] **${t.text}**`).join('\n')
144
+ : `- [ ] **Refine the vision** — Open the web compass and complete vision.md
145
+ - [ ] **Define Phase 1** — What is the smallest thing you can ship?
146
+ - [ ] **Identify the first blocker** — What is standing between you and execution?`}
147
+
148
+ ## Blocked
149
+ <!-- What is stuck and why? -->
150
+
151
+ ## Metrics
152
+ <!-- 2-3 numbers that tell you if it's working. -->
153
+ `;
154
+
155
+ return { vision, plan, next };
156
+ }
157
+
158
+ module.exports = { readPPS, writePPS, createPPS, generateViews, genId };
package/lib/supabase.js CHANGED
@@ -20,9 +20,14 @@ async function req(path, options = {}, accessToken = null) {
20
20
 
21
21
  // Send magic link / OTP to email
22
22
  async function sendOtp(email) {
23
+ // redirect_to ensures magic links (if project uses them) land on /intent, not root
23
24
  const res = await req('/auth/v1/otp', {
24
25
  method: 'POST',
25
- body: JSON.stringify({ email, create_user: true }),
26
+ body: JSON.stringify({
27
+ email,
28
+ create_user: true,
29
+ options: { redirect_to: 'https://phewsh.com/intent' },
30
+ }),
26
31
  });
27
32
  return res.ok;
28
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.3.1",
3
+ "version": "0.5.1",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"