phewsh 0.2.1 → 0.3.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
@@ -3,6 +3,39 @@
3
3
  const args = process.argv.slice(2);
4
4
  const command = args[0];
5
5
 
6
+ // ── ANSI helpers (no chalk dependency)
7
+ const b = (s) => `\x1b[1m${s}\x1b[0m`; // bold
8
+ const d = (s) => `\x1b[2m${s}\x1b[0m`; // dim
9
+ const w = (s) => `\x1b[97m${s}\x1b[0m`; // bright white
10
+ const g = (s) => `\x1b[90m${s}\x1b[0m`; // dark gray
11
+
12
+ function showBrand() {
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+ const hasIntent = fs.existsSync(path.join(process.cwd(), '.intent', 'vision.md'));
17
+ const configPath = path.join(os.homedir(), '.phewsh', 'config.json');
18
+ let hint = g(' run "phewsh intent --init" to start');
19
+ if (hasIntent) {
20
+ hint = g(' .intent/ loaded · run "phewsh ai run \\"...\\"" to execute');
21
+ } else {
22
+ try {
23
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
24
+ if (config?.email) hint = g(` logged in as ${config.email} · run "phewsh intent --init" to start`);
25
+ } catch { /* no config */ }
26
+ }
27
+
28
+ console.log('');
29
+ console.log(` ${d('😮\u200d💨')} ${d('🤫')}`);
30
+ console.log('');
31
+ console.log(` ${b(w('█▀█ █░█ █▀▀ █░█ █▀ █░█'))}`);
32
+ console.log(` ${b(w('█▀▀ █▀█ ██▄ ▀▄▀ ▄█ █▀█'))}`);
33
+ console.log(` ${g('Build with clarity. Execute without drift.')}`);
34
+ console.log('');
35
+ console.log(hint);
36
+ console.log('');
37
+ }
38
+
6
39
  const COMMANDS = {
7
40
  intent: () => require('../commands/intent'),
8
41
  login: () => require('../commands/login'),
@@ -19,28 +52,21 @@ function showVersion() {
19
52
 
20
53
  function showHelp() {
21
54
  const pkg = require('../package.json');
22
- console.log(`
23
- 😮‍💨🤫 phewsh v${pkg.version}
24
-
25
- Usage:
26
- phewsh <command> [options]
27
-
28
- Commands:
29
- login Set up your local identity and API key
30
- ai Run context-aware AI prompts (reads .intent/)
31
- intent Structure your thinking into vision, plan, and next actions
32
- sap Sustainable AI Protocol usage and accountability
33
- music MBHD music engine
34
- version Show version
35
-
36
- Quick start:
37
- phewsh login Set up identity and API key
38
- phewsh intent --init Create .intent/ in any project
39
- phewsh ai run "what's next?" AI with your project context
40
- phewsh ai run "write a release checklist"
41
-
42
- Learn more: https://phewsh.com
43
- `);
55
+ showBrand();
56
+ console.log(` ${g('v' + pkg.version)} · ${g('phewsh.com')}\n`);
57
+ 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`);
60
+ console.log(` ${w('ai')} Run context-aware AI prompts (reads .intent/)`);
61
+ console.log(` ${w('sap')} Sustainable AI Protocol — usage and accountability`);
62
+ console.log(` ${w('music')} MBHD music engine`);
63
+ console.log('');
64
+ 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`);
69
+ console.log('');
44
70
  }
45
71
 
46
72
  // Non-blocking update check
@@ -54,12 +80,12 @@ function checkForUpdates() {
54
80
  const current = pkg.version.split('.').map(Number);
55
81
  const isNewer = newer[0] > current[0] || newer[1] > current[1] || newer[2] > current[2];
56
82
  if (isNewer) {
57
- console.log(`\n Update available: ${pkg.version} → ${data.version}`);
58
- console.log(` Run: npm install -g phewsh\n`);
83
+ console.log(g(`\n Update available: ${pkg.version} → ${data.version}`));
84
+ console.log(g(` npm install -g phewsh\n`));
59
85
  }
60
86
  }
61
87
  })
62
- .catch(() => {}); // silently ignore — never block execution
88
+ .catch(() => {});
63
89
  }
64
90
 
65
91
  if (!command || command === 'help' || command === '--help' || command === '-h') {
@@ -13,6 +13,8 @@ const flags = {
13
13
  status: args.includes('--status') || args.includes('-s'),
14
14
  init: args.includes('--init') || args.includes('-i'),
15
15
  help: args.includes('--help') || args.includes('-h'),
16
+ sync: args.includes('--sync'),
17
+ pull: args.includes('--pull'),
16
18
  };
17
19
 
18
20
  function hasExistingArtifacts() {
@@ -215,6 +217,8 @@ function showHelp() {
215
217
  phewsh intent Show status (or prompt to init if new)
216
218
  phewsh intent --init Create .intent/ with structured artifacts
217
219
  phewsh intent --status Show artifact state and next actions
220
+ phewsh intent --sync Push .intent/ to cloud (requires login)
221
+ phewsh intent --pull Pull .intent/ from cloud
218
222
  phewsh intent --open Open the web compass at phewsh.com/intent
219
223
  phewsh intent --evolve Open compass to update existing artifacts
220
224
 
@@ -237,6 +241,12 @@ async function main() {
237
241
  await initIntent();
238
242
  } else if (flags.status) {
239
243
  showStatus();
244
+ } else if (flags.sync) {
245
+ const { main: sync } = require('./sync');
246
+ await sync('push');
247
+ } else if (flags.pull) {
248
+ const { main: sync } = require('./sync');
249
+ await sync('pull');
240
250
  } else if (flags.evolve) {
241
251
  if (!hasExistingArtifacts()) {
242
252
  console.log('\n No .intent/ found. Run `phewsh intent --init` first.\n');
package/commands/login.js CHANGED
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const readline = require('readline');
4
4
  const os = require('os');
5
5
  const crypto = require('crypto');
6
+ const { sendOtp, verifyOtp, refreshSession } = require('../lib/supabase');
6
7
 
7
8
  const CONFIG_DIR = path.join(os.homedir(), '.phewsh');
8
9
  const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
@@ -32,11 +33,11 @@ async function main() {
32
33
  if (!config) {
33
34
  console.log('\n Not logged in. Run `phewsh login` to get started.\n');
34
35
  } else {
35
- console.log('\n Logged in\n');
36
- console.log(` Email ${config.email || '(not set)'}`);
37
- console.log(` User ID ${config.userId}`);
38
- console.log(` Provider ${config.defaultProvider || 'anthropic'}`);
39
- console.log(` API key ${config.apiKey ? config.apiKey.slice(0, 8) + '...' : '(not set)'}`);
36
+ console.log('\n phewsh — identity\n');
37
+ console.log(` Email ${config.email || '(not set)'}`);
38
+ console.log(` Synced ${config.supabaseUserId ? '✓ cloud sync enabled' : '✗ local only'}`);
39
+ console.log(` API key ${config.apiKey ? config.apiKey.slice(0, 8) + '...' : '(not set)'}`);
40
+ console.log(` Provider ${config.defaultProvider || 'anthropic'}`);
40
41
  console.log('');
41
42
  }
42
43
  return;
@@ -45,57 +46,105 @@ async function main() {
45
46
  if (args.includes('--logout')) {
46
47
  if (fs.existsSync(CONFIG_PATH)) {
47
48
  fs.unlinkSync(CONFIG_PATH);
48
- console.log('\n Logged out. Config removed.\n');
49
+ console.log('\n Logged out.\n');
49
50
  } else {
50
51
  console.log('\n Not logged in.\n');
51
52
  }
52
53
  return;
53
54
  }
54
55
 
55
- const existing = loadConfig();
56
- if (existing) {
57
- console.log(`\n Already logged in as ${existing.email || existing.userId}`);
58
- console.log(' Run `phewsh login --logout` to reset, or `phewsh login --status` to view.\n');
56
+ if (args.includes('--set-key')) {
57
+ const config = loadConfig() || {};
58
+ const { ask, close } = createPrompter();
59
+ const apiKey = await ask('\n Anthropic API key\n > ');
60
+ close();
61
+ if (apiKey) {
62
+ saveConfig({ ...config, apiKey });
63
+ console.log('\n API key saved.\n');
64
+ }
59
65
  return;
60
66
  }
61
67
 
62
- console.log('\n 😮‍💨🤫 phewsh login\n');
63
- console.log(' Set up your local Phewsh identity.\n');
68
+ // --refresh: try to refresh the Supabase token silently
69
+ if (args.includes('--refresh')) {
70
+ const config = loadConfig();
71
+ if (!config?.supabaseRefreshToken) {
72
+ console.log('\n No session to refresh. Run `phewsh login`.\n');
73
+ return;
74
+ }
75
+ try {
76
+ const session = await refreshSession(config.supabaseRefreshToken);
77
+ if (session?.access_token) {
78
+ saveConfig({
79
+ ...config,
80
+ supabaseAccessToken: session.access_token,
81
+ supabaseRefreshToken: session.refresh_token,
82
+ });
83
+ console.log('\n Session refreshed.\n');
84
+ }
85
+ } catch (err) {
86
+ console.error('\n Refresh failed:', err.message, '\n');
87
+ }
88
+ return;
89
+ }
90
+
91
+ const existing = loadConfig();
92
+ if (existing?.supabaseUserId) {
93
+ console.log(`\n Already logged in as ${existing.email || existing.supabaseUserId}`);
94
+ console.log(' Run `phewsh login --logout` to reset or `phewsh login --status` to view.\n');
95
+ return;
96
+ }
64
97
 
98
+ console.log('\n 😮\u200d💨🤫 phewsh login\n');
65
99
  const { ask, close } = createPrompter();
66
100
 
67
101
  const email = await ask(' Email address\n > ');
68
- console.log('');
102
+ if (!email) { close(); console.log('\n Cancelled.\n'); return; }
103
+
104
+ console.log('\n Sending verification code...');
105
+ const sent = await sendOtp(email);
106
+ if (!sent) {
107
+ close();
108
+ console.error('\n Failed to send code. Check your email address.\n');
109
+ process.exit(1);
110
+ }
69
111
 
70
- console.log(' To use `phewsh ai`, you need an Anthropic API key.');
71
- console.log(' Get one at console.anthropic.com/settings/keys');
72
- console.log(' (Leave blank to skip for now)\n');
73
- const apiKey = await ask(' Anthropic API key\n > ');
112
+ console.log(` Check ${email} for a 6-digit code.\n`);
113
+ const token = await ask(' Verification code\n > ');
74
114
  console.log('');
75
115
 
76
- close();
116
+ let session;
117
+ try {
118
+ session = await verifyOtp(email, token);
119
+ } catch (err) {
120
+ close();
121
+ console.error('\n Verification failed:', err.message, '\n');
122
+ process.exit(1);
123
+ }
77
124
 
78
- const userId = crypto.randomBytes(12).toString('hex');
125
+ console.log('\n To use `phewsh ai`, you need an Anthropic API key.');
126
+ console.log(' Get one at console.anthropic.com/settings/keys');
127
+ console.log(' (Leave blank to skip)\n');
128
+ const apiKey = await ask(' Anthropic API key (optional)\n > ');
129
+ close();
130
+ console.log('');
79
131
 
80
132
  const config = {
81
- userId,
82
- email: email || undefined,
83
- apiKey: apiKey || undefined,
133
+ userId: session.user.id,
134
+ email: session.user.email,
135
+ supabaseUserId: session.user.id,
136
+ supabaseAccessToken: session.access_token,
137
+ supabaseRefreshToken: session.refresh_token,
84
138
  defaultProvider: 'anthropic',
85
139
  createdAt: new Date().toISOString(),
86
140
  };
87
-
88
- // Clean up undefined fields
89
- Object.keys(config).forEach(k => config[k] === undefined && delete config[k]);
141
+ if (apiKey) config.apiKey = apiKey;
90
142
 
91
143
  saveConfig(config);
92
144
 
93
- console.log(` Logged in as ${email || userId}`);
94
- if (apiKey) {
95
- console.log(' API key saved. Run `phewsh ai run "..."` to start.\n');
96
- } else {
97
- console.log(' No API key set. Add one later with `phewsh login --set-key`.\n');
98
- }
145
+ console.log(` Logged in as ${session.user.email}`);
146
+ console.log(' ✓ Cloud sync enabled — run `phewsh intent --sync` in any project\n');
147
+ if (!apiKey) console.log(' Add an API key any time with `phewsh login --set-key`\n');
99
148
  }
100
149
 
101
150
  main().catch(err => {
@@ -0,0 +1,168 @@
1
+ // phewsh intent --sync
2
+ // Syncs .intent/ artifacts to/from Supabase — same tables used by phewsh.com/intent
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const crypto = require('crypto');
8
+ const { select, upsert, refreshSession } = require('../lib/supabase');
9
+
10
+ const CONFIG_PATH = path.join(os.homedir(), '.phewsh', 'config.json');
11
+ const INTENT_DIR = path.join(process.cwd(), '.intent');
12
+
13
+ const FILE_TO_KIND = { 'vision.md': 'vision', 'plan.md': 'plan', 'next.md': 'next' };
14
+ const KIND_TO_FILE = { vision: 'vision.md', plan: 'plan.md', next: 'next.md' };
15
+
16
+ function loadConfig() {
17
+ if (!fs.existsSync(CONFIG_PATH)) return null;
18
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
19
+ }
20
+
21
+ function saveConfig(config) {
22
+ const dir = path.dirname(CONFIG_PATH);
23
+ fs.mkdirSync(dir, { recursive: true });
24
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
25
+ }
26
+
27
+ function genProjectId() {
28
+ return `p_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
29
+ }
30
+
31
+ function getProjectName() {
32
+ // Try to read entity from vision.md frontmatter, fall back to directory name
33
+ const visionPath = path.join(INTENT_DIR, 'vision.md');
34
+ if (fs.existsSync(visionPath)) {
35
+ const content = fs.readFileSync(visionPath, 'utf-8');
36
+ const match = content.match(/^entity:\s*(.+)$/m);
37
+ if (match) return match[1].trim();
38
+ }
39
+ return path.basename(process.cwd());
40
+ }
41
+
42
+ async function ensureValidToken(config) {
43
+ // Try to refresh if we have a refresh token
44
+ if (!config.supabaseAccessToken && config.supabaseRefreshToken) {
45
+ const session = await refreshSession(config.supabaseRefreshToken);
46
+ if (session?.access_token) {
47
+ config.supabaseAccessToken = session.access_token;
48
+ config.supabaseRefreshToken = session.refresh_token;
49
+ saveConfig(config);
50
+ }
51
+ }
52
+ return config.supabaseAccessToken;
53
+ }
54
+
55
+ async function push(config, token) {
56
+ if (!fs.existsSync(INTENT_DIR)) {
57
+ console.log('\n No .intent/ found. Run `phewsh intent --init` first.\n');
58
+ process.exit(1);
59
+ }
60
+
61
+ const projectName = getProjectName();
62
+ const userId = config.supabaseUserId;
63
+
64
+ // Find or create the project
65
+ let project;
66
+ const existing = await select(
67
+ 'projects',
68
+ `name=eq.${encodeURIComponent(projectName)}&user_id=eq.${userId}&select=id,name`,
69
+ token
70
+ );
71
+
72
+ if (existing.length > 0) {
73
+ project = existing[0];
74
+ } else {
75
+ const created = await upsert('projects', {
76
+ id: genProjectId(),
77
+ user_id: userId,
78
+ name: projectName,
79
+ archetype: 'product',
80
+ freeform_text: '',
81
+ }, token);
82
+ project = Array.isArray(created) ? created[0] : created;
83
+ console.log(` Created project: ${projectName}`);
84
+ }
85
+
86
+ // Push each artifact file
87
+ const pushed = [];
88
+ for (const [file, kind] of Object.entries(FILE_TO_KIND)) {
89
+ const filePath = path.join(INTENT_DIR, file);
90
+ if (!fs.existsSync(filePath)) continue;
91
+ const content = fs.readFileSync(filePath, 'utf-8');
92
+ await upsert('artifacts', {
93
+ project_id: project.id,
94
+ user_id: userId,
95
+ kind,
96
+ content,
97
+ }, token);
98
+ pushed.push(file);
99
+ }
100
+
101
+ console.log(`\n ✓ Synced to cloud — ${projectName}`);
102
+ pushed.forEach(f => console.log(` ${f}`));
103
+ console.log('');
104
+ }
105
+
106
+ async function pull(config, token) {
107
+ const projectName = getProjectName();
108
+ const userId = config.supabaseUserId;
109
+
110
+ const projects = await select(
111
+ 'projects',
112
+ `name=eq.${encodeURIComponent(projectName)}&user_id=eq.${userId}&select=id,name`,
113
+ token
114
+ );
115
+
116
+ if (projects.length === 0) {
117
+ console.log(`\n No cloud project found for "${projectName}".\n Push first with: phewsh intent --sync\n`);
118
+ return;
119
+ }
120
+
121
+ const project = projects[0];
122
+ const artifacts = await select(
123
+ 'artifacts',
124
+ `project_id=eq.${project.id}&user_id=eq.${userId}&select=kind,content,updated_at`,
125
+ token
126
+ );
127
+
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
+ for (const artifact of artifacts) {
137
+ const file = KIND_TO_FILE[artifact.kind];
138
+ if (!file) continue;
139
+ fs.writeFileSync(path.join(INTENT_DIR, file), artifact.content);
140
+ pulled.push(file);
141
+ }
142
+
143
+ console.log(`\n ✓ Pulled from cloud — ${projectName}`);
144
+ pulled.forEach(f => console.log(` ${f}`));
145
+ console.log('');
146
+ }
147
+
148
+ async function main(direction = 'push') {
149
+ const config = loadConfig();
150
+ if (!config?.supabaseUserId) {
151
+ console.log('\n Not logged in. Run `phewsh login` first.\n');
152
+ process.exit(1);
153
+ }
154
+
155
+ const token = await ensureValidToken(config);
156
+ if (!token) {
157
+ console.log('\n Session expired. Run `phewsh login` to re-authenticate.\n');
158
+ process.exit(1);
159
+ }
160
+
161
+ if (direction === 'pull') {
162
+ await pull(config, token);
163
+ } else {
164
+ await push(config, token);
165
+ }
166
+ }
167
+
168
+ module.exports = { main };
@@ -0,0 +1,80 @@
1
+ // Supabase REST client for the CLI — no SDK, just fetch (Node 18+ built-in)
2
+
3
+ const SUPABASE_URL = 'https://fpnpfnahwaztdlxuayyv.supabase.co';
4
+ const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZwbnBmbmFod2F6dGRseHVheXl2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA1NDY2NTcsImV4cCI6MjA3NjEyMjY1N30.Q6mn8RIvXujBXbd10aFkeY7yGHVsAQPEHM5OzoPMsFQ';
5
+
6
+ async function req(path, options = {}, accessToken = null) {
7
+ const headers = {
8
+ 'Content-Type': 'application/json',
9
+ 'apikey': SUPABASE_ANON_KEY,
10
+ ...options.headers,
11
+ };
12
+ if (accessToken) headers['Authorization'] = `Bearer ${accessToken}`;
13
+
14
+ const res = await fetch(`${SUPABASE_URL}${path}`, {
15
+ ...options,
16
+ headers,
17
+ });
18
+ return res;
19
+ }
20
+
21
+ // Send magic link / OTP to email
22
+ async function sendOtp(email) {
23
+ const res = await req('/auth/v1/otp', {
24
+ method: 'POST',
25
+ body: JSON.stringify({ email, create_user: true }),
26
+ });
27
+ return res.ok;
28
+ }
29
+
30
+ // Verify OTP token and return session
31
+ async function verifyOtp(email, token) {
32
+ const res = await req('/auth/v1/verify', {
33
+ method: 'POST',
34
+ body: JSON.stringify({ type: 'email', email, token }),
35
+ });
36
+ if (!res.ok) {
37
+ const err = await res.json().catch(() => ({}));
38
+ throw new Error(err.error_description || err.msg || 'OTP verification failed');
39
+ }
40
+ return res.json(); // { access_token, refresh_token, user }
41
+ }
42
+
43
+ // Refresh a Supabase session
44
+ async function refreshSession(refreshToken) {
45
+ const res = await req('/auth/v1/token?grant_type=refresh_token', {
46
+ method: 'POST',
47
+ body: JSON.stringify({ refresh_token: refreshToken }),
48
+ });
49
+ if (!res.ok) return null;
50
+ return res.json();
51
+ }
52
+
53
+ // REST: select rows
54
+ async function select(table, query = '', accessToken) {
55
+ const res = await req(`/rest/v1/${table}?${query}`, {
56
+ method: 'GET',
57
+ headers: { 'Prefer': 'return=representation' },
58
+ }, accessToken);
59
+ if (!res.ok) {
60
+ const err = await res.json().catch(() => ({}));
61
+ throw new Error(err.message || `SELECT ${table} failed`);
62
+ }
63
+ return res.json();
64
+ }
65
+
66
+ // REST: upsert (insert or update)
67
+ async function upsert(table, data, accessToken) {
68
+ const res = await req(`/rest/v1/${table}`, {
69
+ method: 'POST',
70
+ headers: { 'Prefer': 'resolution=merge-duplicates,return=representation' },
71
+ body: JSON.stringify(data),
72
+ }, accessToken);
73
+ if (!res.ok) {
74
+ const err = await res.json().catch(() => ({}));
75
+ throw new Error(err.message || `UPSERT ${table} failed`);
76
+ }
77
+ return res.json();
78
+ }
79
+
80
+ module.exports = { sendOtp, verifyOtp, refreshSession, select, upsert, SUPABASE_URL, SUPABASE_ANON_KEY };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"
@@ -8,6 +8,7 @@
8
8
  "files": [
9
9
  "bin/",
10
10
  "commands/",
11
+ "lib/",
11
12
  "README.md"
12
13
  ],
13
14
  "keywords": [