phewsh 0.8.0 → 0.10.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 +49 -9
- package/bin/phewsh.js +5 -19
- package/commands/{music.js → mbhd.js} +1 -1
- package/commands/session.js +434 -45
- package/commands/sync.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# phewsh
|
|
2
2
|
|
|
3
|
-
Turn intent into action.
|
|
3
|
+
Turn intent into action. Structure your thinking, execute your next step.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,24 +8,64 @@ Turn intent into action.
|
|
|
8
8
|
npm install -g phewsh
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Quick Start
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
phewsh
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
phewsh # enter the interactive shell
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Inside the shell:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
/init Create .intent/ artifacts
|
|
21
|
+
/clarify AI-assisted artifact generation
|
|
22
|
+
/push Sync local to cloud
|
|
23
|
+
/pull Sync cloud to local
|
|
24
|
+
/context View loaded artifacts
|
|
25
|
+
/sync Check sync status
|
|
26
|
+
/help All commands
|
|
18
27
|
```
|
|
19
28
|
|
|
20
29
|
## What it does
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
Creates three structured artifacts in `.intent/`:
|
|
23
32
|
|
|
24
33
|
- **vision.md** — The north star. Why this exists and where it's going.
|
|
25
34
|
- **plan.md** — The strategy. Phases, systems, sequence, constraints.
|
|
26
|
-
- **next.md** — Right now. Executable checklist with
|
|
35
|
+
- **next.md** — Right now. Executable checklist with next actions.
|
|
36
|
+
|
|
37
|
+
These artifacts become persistent context for AI conversations, both in the shell and across tools.
|
|
38
|
+
|
|
39
|
+
## Sync
|
|
40
|
+
|
|
41
|
+
CLI and web (phewsh.com/intent) share the same cloud via Supabase.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
phewsh login # authenticate
|
|
45
|
+
phewsh push # upload .intent/ to cloud
|
|
46
|
+
phewsh pull # download from cloud
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Sync is manual. The CLI shows status on startup:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
↓ Cloud is newer (2h ago) — run /pull
|
|
53
|
+
↑ Local changes not pushed (15m ago) — run /push
|
|
54
|
+
↕ synced
|
|
55
|
+
```
|
|
27
56
|
|
|
28
|
-
|
|
57
|
+
The `/intent` Claude Code skill writes local files only — run `phewsh push` after using it.
|
|
58
|
+
|
|
59
|
+
## Non-interactive mode
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
phewsh intent --init # Create .intent/ without entering the shell
|
|
63
|
+
phewsh intent --status # Check artifact state
|
|
64
|
+
phewsh clarify # AI-assisted artifact generation
|
|
65
|
+
phewsh ai run "prompt" # One-shot AI with .intent/ context
|
|
66
|
+
phewsh push # Sync to cloud
|
|
67
|
+
phewsh pull # Sync from cloud
|
|
68
|
+
```
|
|
29
69
|
|
|
30
70
|
## Web app
|
|
31
71
|
|
package/bin/phewsh.js
CHANGED
|
@@ -46,7 +46,7 @@ const COMMANDS = {
|
|
|
46
46
|
login: () => require('../commands/login'),
|
|
47
47
|
ai: () => require('../commands/ai'),
|
|
48
48
|
style: () => require('../commands/style'),
|
|
49
|
-
|
|
49
|
+
mbhd: () => require('../commands/mbhd'),
|
|
50
50
|
sap: () => require('../commands/sap'),
|
|
51
51
|
help: showHelp,
|
|
52
52
|
version: showVersion,
|
|
@@ -74,7 +74,7 @@ function showHelp() {
|
|
|
74
74
|
console.log(` ${w('login')} Set up identity, API key, and cloud sync`);
|
|
75
75
|
console.log(` ${w('sap')} Sustainable AI Protocol — usage and accountability`);
|
|
76
76
|
console.log(` ${w('style')} Build your style identity — ingest, profile, sync`);
|
|
77
|
-
console.log(` ${w('
|
|
77
|
+
console.log(` ${w('mbhd')} MBHD music engine`);
|
|
78
78
|
console.log('');
|
|
79
79
|
console.log(` ${b('Quick start')}`);
|
|
80
80
|
console.log(` ${g('phewsh login')} Set up identity + API key`);
|
|
@@ -117,23 +117,9 @@ function exitAfterUpdate(code = 0) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
if (!command) {
|
|
120
|
-
// Bare `phewsh` — drop into persistent session
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
const path = require('path');
|
|
124
|
-
const os = require('os');
|
|
125
|
-
const configPath = path.join(os.homedir(), '.phewsh', 'config.json');
|
|
126
|
-
let hasKey = false;
|
|
127
|
-
try {
|
|
128
|
-
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
129
|
-
hasKey = !!cfg.apiKey;
|
|
130
|
-
} catch {}
|
|
131
|
-
if (hasKey) {
|
|
132
|
-
COMMANDS.session();
|
|
133
|
-
} else {
|
|
134
|
-
showHelp();
|
|
135
|
-
exitAfterUpdate(0);
|
|
136
|
-
}
|
|
120
|
+
// Bare `phewsh` — always drop into persistent session
|
|
121
|
+
// Session handles missing API key gracefully with /login and /key commands
|
|
122
|
+
COMMANDS.session();
|
|
137
123
|
} else if (command === 'help' || command === '--help' || command === '-h') {
|
|
138
124
|
showHelp();
|
|
139
125
|
exitAfterUpdate(0);
|
package/commands/session.js
CHANGED
|
@@ -8,21 +8,113 @@ const os = require('os');
|
|
|
8
8
|
const readline = require('readline');
|
|
9
9
|
const { trackSap } = require('../lib/supabase');
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const CONFIG_DIR = path.join(os.homedir(), '.phewsh');
|
|
12
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
12
13
|
const INTENT_DIR = path.join(process.cwd(), '.intent');
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
const { select, refreshSession: refreshSess } = require('../lib/supabase');
|
|
16
|
+
const { readPPS } = require('../lib/pps');
|
|
17
|
+
const { push, pull, ensureValidToken } = require('./sync');
|
|
14
18
|
|
|
15
19
|
const b = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
16
20
|
const d = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
21
|
+
const w = (s) => `\x1b[97m${s}\x1b[0m`;
|
|
17
22
|
const g = (s) => `\x1b[90m${s}\x1b[0m`;
|
|
18
23
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
19
24
|
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
25
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
26
|
+
|
|
27
|
+
// Sync awareness: compare local .intent/ timestamps with cloud updated_at
|
|
28
|
+
async function checkSyncStatus(config) {
|
|
29
|
+
if (!config?.supabaseUserId || !config?.supabaseAccessToken) return null;
|
|
30
|
+
if (!fs.existsSync(INTENT_DIR)) return null;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const token = await ensureValidToken(config);
|
|
34
|
+
if (!token) return null;
|
|
35
|
+
|
|
36
|
+
const pps = readPPS(INTENT_DIR);
|
|
37
|
+
const cloudId = pps?.adapters?.phewsh?.cloud_id;
|
|
38
|
+
const projectName = path.basename(process.cwd());
|
|
39
|
+
|
|
40
|
+
// Find cloud project
|
|
41
|
+
const query = cloudId
|
|
42
|
+
? `id=eq.${cloudId}&user_id=eq.${config.supabaseUserId}&select=id,updated_at`
|
|
43
|
+
: `name=eq.${encodeURIComponent(projectName)}&user_id=eq.${config.supabaseUserId}&select=id,updated_at`;
|
|
44
|
+
|
|
45
|
+
const projects = await select('projects', query, token);
|
|
46
|
+
if (projects.length === 0) return { status: 'local-only' };
|
|
47
|
+
|
|
48
|
+
const project = projects[0];
|
|
49
|
+
|
|
50
|
+
// Get latest cloud artifact updated_at
|
|
51
|
+
const artifacts = await select(
|
|
52
|
+
'artifacts',
|
|
53
|
+
`project_id=eq.${project.id}&user_id=eq.${config.supabaseUserId}&select=kind,updated_at&order=updated_at.desc&limit=1`,
|
|
54
|
+
token
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const cloudTime = artifacts.length > 0
|
|
58
|
+
? new Date(artifacts[0].updated_at).getTime()
|
|
59
|
+
: new Date(project.updated_at).getTime();
|
|
60
|
+
|
|
61
|
+
// Get latest local file mtime
|
|
62
|
+
const localFiles = ['vision.md', 'plan.md', 'next.md'];
|
|
63
|
+
let latestLocal = 0;
|
|
64
|
+
for (const file of localFiles) {
|
|
65
|
+
const filePath = path.join(INTENT_DIR, file);
|
|
66
|
+
if (fs.existsSync(filePath)) {
|
|
67
|
+
const mtime = fs.statSync(filePath).mtimeMs;
|
|
68
|
+
if (mtime > latestLocal) latestLocal = mtime;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (latestLocal === 0) return { status: 'local-only' };
|
|
73
|
+
|
|
74
|
+
const drift = Math.abs(cloudTime - latestLocal);
|
|
75
|
+
// Within 60 seconds = synced
|
|
76
|
+
if (drift < 60000) return { status: 'synced' };
|
|
77
|
+
|
|
78
|
+
if (cloudTime > latestLocal) {
|
|
79
|
+
const ago = formatAgo(Date.now() - cloudTime);
|
|
80
|
+
return { status: 'cloud-newer', ago };
|
|
81
|
+
} else {
|
|
82
|
+
const ago = formatAgo(Date.now() - latestLocal);
|
|
83
|
+
return { status: 'local-newer', ago };
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
return null; // Network error — silently skip
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatAgo(ms) {
|
|
91
|
+
const mins = Math.floor(ms / 60000);
|
|
92
|
+
if (mins < 1) return 'just now';
|
|
93
|
+
if (mins < 60) return `${mins}m ago`;
|
|
94
|
+
const hrs = Math.floor(mins / 60);
|
|
95
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
96
|
+
const days = Math.floor(hrs / 24);
|
|
97
|
+
return `${days}d ago`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const MODELS = {
|
|
101
|
+
'claude-sonnet': { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', provider: 'anthropic' },
|
|
102
|
+
'claude-opus': { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', provider: 'anthropic' },
|
|
103
|
+
'claude-haiku': { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', provider: 'anthropic' },
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const DEFAULT_MODEL = 'claude-sonnet';
|
|
20
107
|
|
|
21
108
|
function loadConfig() {
|
|
22
109
|
if (!fs.existsSync(CONFIG_PATH)) return null;
|
|
23
110
|
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } catch { return null; }
|
|
24
111
|
}
|
|
25
112
|
|
|
113
|
+
function saveConfig(config) {
|
|
114
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
115
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
116
|
+
}
|
|
117
|
+
|
|
26
118
|
function loadIntentContext() {
|
|
27
119
|
const files = ['vision.md', 'plan.md', 'next.md'];
|
|
28
120
|
const loaded = [];
|
|
@@ -49,9 +141,8 @@ function buildSystemPrompt(intentFiles) {
|
|
|
49
141
|
return `${base}\n\nThe user has structured intent artifacts for this project. Use them as primary context — stay aligned with their vision, plan, and next actions.\n\n${sections}`;
|
|
50
142
|
}
|
|
51
143
|
|
|
52
|
-
async function streamChat(apiKey, messages, systemPrompt,
|
|
53
|
-
const
|
|
54
|
-
const body = { model, max_tokens: 2048, messages, stream: true };
|
|
144
|
+
async function streamChat(apiKey, messages, systemPrompt, modelId) {
|
|
145
|
+
const body = { model: modelId, max_tokens: 2048, messages, stream: true };
|
|
55
146
|
if (systemPrompt) body.system = systemPrompt;
|
|
56
147
|
|
|
57
148
|
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
@@ -97,42 +188,65 @@ async function streamChat(apiKey, messages, systemPrompt, config) {
|
|
|
97
188
|
|
|
98
189
|
process.stdout.write('\n');
|
|
99
190
|
|
|
100
|
-
|
|
101
|
-
trackSap({
|
|
102
|
-
userId: config.supabaseUserId,
|
|
103
|
-
source: 'cli',
|
|
104
|
-
model,
|
|
105
|
-
promptTokens,
|
|
106
|
-
completionTokens,
|
|
107
|
-
accessToken: config.supabaseAccessToken,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
return { content: fullResponse, promptTokens, completionTokens };
|
|
191
|
+
return { content: fullResponse, promptTokens, completionTokens, model: modelId };
|
|
111
192
|
}
|
|
112
193
|
|
|
113
194
|
async function main() {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
console.log('\n No API key found. Run `phewsh login --set-key` first.');
|
|
118
|
-
console.log(' Or start at: `phewsh login`\n');
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const intentFiles = loadIntentContext();
|
|
123
|
-
const systemPrompt = buildSystemPrompt(intentFiles);
|
|
195
|
+
let config = loadConfig();
|
|
196
|
+
let intentFiles = loadIntentContext();
|
|
197
|
+
let systemPrompt = buildSystemPrompt(intentFiles);
|
|
124
198
|
const messages = []; // conversation history
|
|
125
199
|
const projectName = path.basename(process.cwd());
|
|
200
|
+
let currentModel = DEFAULT_MODEL;
|
|
201
|
+
let totalPromptTokens = 0;
|
|
202
|
+
let totalCompletionTokens = 0;
|
|
126
203
|
|
|
127
|
-
// Session banner
|
|
204
|
+
// Session banner with PHEWSH branding
|
|
128
205
|
console.log('');
|
|
129
206
|
console.log(` ${d('😮💨')} ${d('🤫')}`);
|
|
130
207
|
console.log('');
|
|
208
|
+
console.log(` ${b(w('█▀█ █░█ █▀▀ █░█ █▀ █░█'))}`);
|
|
209
|
+
console.log(` ${b(w('█▀▀ █▀█ ██▄ ▀▄▀ ▄█ █▀█'))}`);
|
|
210
|
+
console.log('');
|
|
211
|
+
|
|
212
|
+
if (!config?.apiKey) {
|
|
213
|
+
console.log(` ${yellow('⚠')} No API key configured.`);
|
|
214
|
+
console.log(` ${g('Run')} /login ${g('to set up identity + API key')}`);
|
|
215
|
+
console.log(` ${g('Or')} /key ${g('to add an API key directly')}`);
|
|
216
|
+
console.log('');
|
|
217
|
+
}
|
|
218
|
+
|
|
131
219
|
if (intentFiles.length > 0) {
|
|
132
|
-
console.log(` ${green('●')}
|
|
220
|
+
console.log(` ${green('●')} ${cyan(projectName)} ${g('·')} ${intentFiles.map(f => f.file).join(', ')}`);
|
|
133
221
|
} else {
|
|
134
|
-
console.log(` ${green('●')}
|
|
222
|
+
console.log(` ${green('●')} ${cyan(projectName)} ${g('·')} no .intent/ context`);
|
|
223
|
+
console.log(` ${g(' run /init to create .intent/ artifacts')}`);
|
|
135
224
|
}
|
|
225
|
+
console.log(` ${g(' model:')} ${MODELS[currentModel].name}`);
|
|
226
|
+
if (config?.email) {
|
|
227
|
+
console.log(` ${g(' user:')} ${config.email}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Sync status check (non-blocking, 3s timeout)
|
|
231
|
+
if (config?.supabaseUserId && intentFiles.length > 0) {
|
|
232
|
+
const syncResult = await Promise.race([
|
|
233
|
+
checkSyncStatus(config),
|
|
234
|
+
new Promise(resolve => setTimeout(() => resolve(null), 3000)),
|
|
235
|
+
]);
|
|
236
|
+
if (syncResult) {
|
|
237
|
+
if (syncResult.status === 'cloud-newer') {
|
|
238
|
+
console.log(` ${yellow('↓')} Cloud is newer (${syncResult.ago}) — run /pull`);
|
|
239
|
+
} else if (syncResult.status === 'local-newer') {
|
|
240
|
+
console.log(` ${yellow('↑')} Local changes not pushed (${syncResult.ago}) — run /push`);
|
|
241
|
+
} else if (syncResult.status === 'synced') {
|
|
242
|
+
console.log(` ${green('↕')} ${g('synced')}`);
|
|
243
|
+
} else if (syncResult.status === 'local-only') {
|
|
244
|
+
console.log(` ${g('↕ not linked to cloud — run /push to sync')}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log('');
|
|
136
250
|
console.log(` ${g('type naturally · /help for commands · /quit to exit')}`);
|
|
137
251
|
console.log('');
|
|
138
252
|
|
|
@@ -155,23 +269,46 @@ async function main() {
|
|
|
155
269
|
|
|
156
270
|
// Slash commands
|
|
157
271
|
if (input.startsWith('/')) {
|
|
158
|
-
const
|
|
272
|
+
const parts = input.slice(1).split(/\s+/);
|
|
273
|
+
const cmd = parts[0].toLowerCase();
|
|
274
|
+
const cmdArg = parts.slice(1).join(' ');
|
|
159
275
|
|
|
160
276
|
if (cmd === 'quit' || cmd === 'exit' || cmd === 'q') {
|
|
161
|
-
|
|
277
|
+
const turns = messages.length / 2;
|
|
278
|
+
console.log(`\n ${g('Session ended · ' + turns + ' exchanges · ' + (totalPromptTokens + totalCompletionTokens) + ' tokens')}\n`);
|
|
162
279
|
process.exit(0);
|
|
163
280
|
}
|
|
164
281
|
|
|
165
|
-
if (cmd === 'help') {
|
|
282
|
+
if (cmd === 'help' || cmd === 'h') {
|
|
166
283
|
console.log(`
|
|
167
284
|
${b('Session commands')}
|
|
168
285
|
|
|
169
|
-
|
|
286
|
+
${w('conversation')}
|
|
170
287
|
${g('/clear')} Clear conversation history
|
|
288
|
+
${g('/run')} ${d('<prompt>')} One-shot prompt (doesn't add to conversation)
|
|
289
|
+
${g('/quit')} End session
|
|
290
|
+
|
|
291
|
+
${w('project')}
|
|
292
|
+
${g('/init')} Create .intent/ artifacts in this directory
|
|
293
|
+
${g('/clarify')} AI-assisted artifact generation
|
|
171
294
|
${g('/context')} Show loaded .intent/ files
|
|
295
|
+
${g('/reload')} Reload .intent/ context from disk
|
|
172
296
|
${g('/status')} Show session stats
|
|
173
|
-
|
|
174
|
-
|
|
297
|
+
|
|
298
|
+
${w('sync')}
|
|
299
|
+
${g('/push')} Push .intent/ to cloud
|
|
300
|
+
${g('/pull')} Pull .intent/ from cloud (reloads context)
|
|
301
|
+
${g('/sync')} Check sync status
|
|
302
|
+
|
|
303
|
+
${w('configuration')}
|
|
304
|
+
${g('/login')} Set up identity + cloud sync
|
|
305
|
+
${g('/key')} Set or update your API key
|
|
306
|
+
${g('/model')} ${d('<name>')} Switch model (sonnet, opus, haiku)
|
|
307
|
+
${g('/models')} List available models
|
|
308
|
+
${g('/provider')} Show current provider info
|
|
309
|
+
|
|
310
|
+
${w('debug')}
|
|
311
|
+
${g('/system')} Show current system prompt
|
|
175
312
|
`);
|
|
176
313
|
rl.prompt();
|
|
177
314
|
return;
|
|
@@ -190,7 +327,7 @@ async function main() {
|
|
|
190
327
|
intentFiles.forEach(f => console.log(` ${green('●')} ${f.file} ${g('(' + f.content.length + ' chars)')}`));
|
|
191
328
|
} else {
|
|
192
329
|
console.log(`\n ${g('No .intent/ context found in')} ${process.cwd()}`);
|
|
193
|
-
console.log(` ${g('Run')}
|
|
330
|
+
console.log(` ${g('Run')} /init ${g('to create one')}`);
|
|
194
331
|
}
|
|
195
332
|
console.log('');
|
|
196
333
|
rl.prompt();
|
|
@@ -199,21 +336,25 @@ async function main() {
|
|
|
199
336
|
|
|
200
337
|
if (cmd === 'status') {
|
|
201
338
|
const turns = messages.length / 2;
|
|
339
|
+
config = loadConfig(); // refresh
|
|
202
340
|
console.log(`\n ${b('Session')}`);
|
|
203
341
|
console.log(` Turns ${turns}`);
|
|
342
|
+
console.log(` Tokens ${totalPromptTokens}→${totalCompletionTokens} (in→out)`);
|
|
204
343
|
console.log(` Project ${projectName}`);
|
|
205
344
|
console.log(` Context ${intentFiles.length > 0 ? intentFiles.map(f => f.file).join(', ') : 'none'}`);
|
|
206
|
-
console.log(`
|
|
345
|
+
console.log(` Model ${MODELS[currentModel].name}`);
|
|
346
|
+
console.log(` Provider ${MODELS[currentModel].provider}`);
|
|
347
|
+
if (config?.email) console.log(` User ${config.email}`);
|
|
348
|
+
console.log(` API key ${config?.apiKey ? config.apiKey.slice(0, 8) + '...' : yellow('not set')}`);
|
|
207
349
|
console.log('');
|
|
208
350
|
rl.prompt();
|
|
209
351
|
return;
|
|
210
352
|
}
|
|
211
353
|
|
|
212
354
|
if (cmd === 'reload') {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
console.log(` ${green('●')} Reloaded ${reloaded.length} artifact${reloaded.length !== 1 ? 's' : ''}`);
|
|
355
|
+
intentFiles = loadIntentContext();
|
|
356
|
+
systemPrompt = buildSystemPrompt(intentFiles);
|
|
357
|
+
console.log(` ${green('●')} Reloaded ${intentFiles.length} artifact${intentFiles.length !== 1 ? 's' : ''}`);
|
|
217
358
|
rl.prompt();
|
|
218
359
|
return;
|
|
219
360
|
}
|
|
@@ -224,22 +365,270 @@ async function main() {
|
|
|
224
365
|
return;
|
|
225
366
|
}
|
|
226
367
|
|
|
227
|
-
|
|
368
|
+
if (cmd === 'init') {
|
|
369
|
+
if (fs.existsSync(path.join(INTENT_DIR, 'vision.md'))) {
|
|
370
|
+
console.log(`\n ${g('.intent/ already exists in')} ${process.cwd()}`);
|
|
371
|
+
console.log(` ${g('Use /reload to refresh context')}\n`);
|
|
372
|
+
} else {
|
|
373
|
+
try {
|
|
374
|
+
// Delegate to the intent --init command
|
|
375
|
+
const { execSync } = require('child_process');
|
|
376
|
+
execSync('node ' + path.join(__dirname, 'intent.js') + ' --init', { stdio: 'inherit' });
|
|
377
|
+
// Reload context after init
|
|
378
|
+
intentFiles = loadIntentContext();
|
|
379
|
+
systemPrompt = buildSystemPrompt(intentFiles);
|
|
380
|
+
if (intentFiles.length > 0) {
|
|
381
|
+
console.log(` ${green('●')} Context loaded: ${intentFiles.map(f => f.file).join(', ')}`);
|
|
382
|
+
}
|
|
383
|
+
} catch (err) {
|
|
384
|
+
console.error(` ${g('Init failed:')} ${err.message}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
console.log('');
|
|
388
|
+
rl.prompt();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (cmd === 'clarify') {
|
|
393
|
+
if (!config?.apiKey) {
|
|
394
|
+
console.log(`\n ${yellow('⚠')} No API key. Run /key to set one.\n`);
|
|
395
|
+
rl.prompt();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const { execSync } = require('child_process');
|
|
400
|
+
const args = cmdArg ? `--text "${cmdArg.replace(/"/g, '\\"')}"` : '';
|
|
401
|
+
execSync(`node ${path.join(__dirname, 'clarify.js')} ${args}`, { stdio: 'inherit' });
|
|
402
|
+
// Reload context after clarify
|
|
403
|
+
intentFiles = loadIntentContext();
|
|
404
|
+
systemPrompt = buildSystemPrompt(intentFiles);
|
|
405
|
+
if (intentFiles.length > 0) {
|
|
406
|
+
console.log(` ${green('●')} Context loaded: ${intentFiles.map(f => f.file).join(', ')}`);
|
|
407
|
+
}
|
|
408
|
+
} catch (err) {
|
|
409
|
+
console.error(` ${g('Clarify failed:')} ${err.message}`);
|
|
410
|
+
}
|
|
411
|
+
console.log('');
|
|
412
|
+
rl.prompt();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (cmd === 'push') {
|
|
417
|
+
if (!config?.supabaseUserId) {
|
|
418
|
+
console.log(`\n ${yellow('⚠')} Not logged in. Run /login first.\n`);
|
|
419
|
+
rl.prompt();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
const token = await ensureValidToken(config);
|
|
424
|
+
if (!token) { console.log(`\n ${yellow('⚠')} Session expired. Run /login.\n`); rl.prompt(); return; }
|
|
425
|
+
await push(config, token);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
console.error(` ${yellow('⚠')} Push failed: ${err.message}\n`);
|
|
428
|
+
}
|
|
429
|
+
rl.prompt();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (cmd === 'pull') {
|
|
434
|
+
if (!config?.supabaseUserId) {
|
|
435
|
+
console.log(`\n ${yellow('⚠')} Not logged in. Run /login first.\n`);
|
|
436
|
+
rl.prompt();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
const token = await ensureValidToken(config);
|
|
441
|
+
if (!token) { console.log(`\n ${yellow('⚠')} Session expired. Run /login.\n`); rl.prompt(); return; }
|
|
442
|
+
await pull(config, token);
|
|
443
|
+
// Reload context after pull
|
|
444
|
+
intentFiles = loadIntentContext();
|
|
445
|
+
systemPrompt = buildSystemPrompt(intentFiles);
|
|
446
|
+
if (intentFiles.length > 0) {
|
|
447
|
+
console.log(` ${green('●')} Context reloaded: ${intentFiles.map(f => f.file).join(', ')}`);
|
|
448
|
+
}
|
|
449
|
+
} catch (err) {
|
|
450
|
+
console.error(` ${yellow('⚠')} Pull failed: ${err.message}\n`);
|
|
451
|
+
}
|
|
452
|
+
console.log('');
|
|
453
|
+
rl.prompt();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (cmd === 'sync') {
|
|
458
|
+
// Show sync status
|
|
459
|
+
if (!config?.supabaseUserId) {
|
|
460
|
+
console.log(`\n ${yellow('⚠')} Not logged in. Run /login first.\n`);
|
|
461
|
+
rl.prompt();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const syncResult = await checkSyncStatus(config);
|
|
465
|
+
if (!syncResult) {
|
|
466
|
+
console.log(`\n ${g('Could not check sync status')}\n`);
|
|
467
|
+
} else if (syncResult.status === 'cloud-newer') {
|
|
468
|
+
console.log(`\n ${yellow('↓')} Cloud is newer (${syncResult.ago}) — run /pull\n`);
|
|
469
|
+
} else if (syncResult.status === 'local-newer') {
|
|
470
|
+
console.log(`\n ${yellow('↑')} Local changes not pushed (${syncResult.ago}) — run /push\n`);
|
|
471
|
+
} else if (syncResult.status === 'synced') {
|
|
472
|
+
console.log(`\n ${green('↕')} In sync\n`);
|
|
473
|
+
} else if (syncResult.status === 'local-only') {
|
|
474
|
+
console.log(`\n ${g('↕ Not linked to cloud — run /push to sync')}\n`);
|
|
475
|
+
}
|
|
476
|
+
rl.prompt();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (cmd === 'login') {
|
|
481
|
+
try {
|
|
482
|
+
const { execSync } = require('child_process');
|
|
483
|
+
execSync('node ' + path.join(__dirname, 'login.js'), { stdio: 'inherit' });
|
|
484
|
+
config = loadConfig(); // refresh after login
|
|
485
|
+
} catch (err) {
|
|
486
|
+
console.error(` ${g('Login failed:')} ${err.message}`);
|
|
487
|
+
}
|
|
488
|
+
rl.prompt();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (cmd === 'key') {
|
|
493
|
+
const keyRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
494
|
+
keyRl.question(`\n Anthropic API key\n > `, (apiKey) => {
|
|
495
|
+
keyRl.close();
|
|
496
|
+
apiKey = apiKey.trim();
|
|
497
|
+
if (apiKey) {
|
|
498
|
+
config = loadConfig() || {};
|
|
499
|
+
config.apiKey = apiKey;
|
|
500
|
+
saveConfig(config);
|
|
501
|
+
console.log(` ${green('●')} API key saved\n`);
|
|
502
|
+
} else {
|
|
503
|
+
console.log(` ${g('Cancelled')}\n`);
|
|
504
|
+
}
|
|
505
|
+
rl.prompt();
|
|
506
|
+
});
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (cmd === 'models') {
|
|
511
|
+
console.log(`\n ${b('Available models')}\n`);
|
|
512
|
+
for (const [key, model] of Object.entries(MODELS)) {
|
|
513
|
+
const active = key === currentModel ? ` ${green('●')}` : '';
|
|
514
|
+
console.log(` ${w(key.padEnd(16))} ${g(model.name)}${active}`);
|
|
515
|
+
}
|
|
516
|
+
console.log(`\n ${g('Switch with:')} /model <name>\n`);
|
|
517
|
+
rl.prompt();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (cmd === 'model') {
|
|
522
|
+
if (!cmdArg) {
|
|
523
|
+
console.log(` ${g('Current:')} ${MODELS[currentModel].name}`);
|
|
524
|
+
console.log(` ${g('Usage:')} /model <sonnet|opus|haiku>`);
|
|
525
|
+
rl.prompt();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
// Fuzzy match model name
|
|
529
|
+
const query = cmdArg.toLowerCase().replace('claude-', '').replace('claude', '');
|
|
530
|
+
const match = Object.keys(MODELS).find(k =>
|
|
531
|
+
k.includes(query) || MODELS[k].name.toLowerCase().includes(query)
|
|
532
|
+
);
|
|
533
|
+
if (match) {
|
|
534
|
+
currentModel = match;
|
|
535
|
+
console.log(` ${green('●')} Switched to ${MODELS[match].name}`);
|
|
536
|
+
} else {
|
|
537
|
+
console.log(` ${g('Unknown model. Available:')} ${Object.keys(MODELS).join(', ')}`);
|
|
538
|
+
}
|
|
539
|
+
rl.prompt();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (cmd === 'provider') {
|
|
544
|
+
const model = MODELS[currentModel];
|
|
545
|
+
console.log(`\n ${b('Provider')}`);
|
|
546
|
+
console.log(` API Anthropic (direct)`);
|
|
547
|
+
console.log(` Model ${model.name}`);
|
|
548
|
+
console.log(` Endpoint api.anthropic.com/v1/messages`);
|
|
549
|
+
console.log(` Key ${config?.apiKey ? config.apiKey.slice(0, 8) + '...' : yellow('not set')}`);
|
|
550
|
+
console.log('');
|
|
551
|
+
rl.prompt();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (cmd === 'run') {
|
|
556
|
+
if (!cmdArg) {
|
|
557
|
+
console.log(` ${g('Usage:')} /run <prompt>`);
|
|
558
|
+
rl.prompt();
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (!config?.apiKey) {
|
|
562
|
+
console.log(` ${yellow('⚠')} No API key. Run /key to set one.`);
|
|
563
|
+
rl.prompt();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
// One-shot: don't add to conversation history
|
|
567
|
+
console.log('');
|
|
568
|
+
try {
|
|
569
|
+
const result = await streamChat(
|
|
570
|
+
config.apiKey,
|
|
571
|
+
[{ role: 'user', content: cmdArg }],
|
|
572
|
+
systemPrompt,
|
|
573
|
+
MODELS[currentModel].id
|
|
574
|
+
);
|
|
575
|
+
if (result.promptTokens || result.completionTokens) {
|
|
576
|
+
console.log(g(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens`));
|
|
577
|
+
}
|
|
578
|
+
trackSap({
|
|
579
|
+
userId: config.supabaseUserId,
|
|
580
|
+
source: 'cli',
|
|
581
|
+
model: MODELS[currentModel].id,
|
|
582
|
+
promptTokens: result.promptTokens,
|
|
583
|
+
completionTokens: result.completionTokens,
|
|
584
|
+
accessToken: config.supabaseAccessToken,
|
|
585
|
+
});
|
|
586
|
+
} catch (err) {
|
|
587
|
+
console.error(`\n ${err.message}\n`);
|
|
588
|
+
}
|
|
589
|
+
console.log('');
|
|
590
|
+
rl.prompt();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Unknown slash command
|
|
595
|
+
console.log(` ${g('Unknown command:')} /${cmd} ${g('— type /help for available commands')}`);
|
|
596
|
+
rl.prompt();
|
|
597
|
+
return;
|
|
228
598
|
}
|
|
229
599
|
|
|
230
600
|
// Regular input → send to AI
|
|
231
|
-
|
|
601
|
+
if (!config?.apiKey) {
|
|
602
|
+
console.log(`\n ${yellow('⚠')} No API key configured. Run /key to set one.\n`);
|
|
603
|
+
rl.prompt();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
232
606
|
|
|
607
|
+
messages.push({ role: 'user', content: input });
|
|
233
608
|
console.log('');
|
|
234
609
|
|
|
235
610
|
try {
|
|
236
|
-
const result = await streamChat(config.apiKey, messages, systemPrompt,
|
|
611
|
+
const result = await streamChat(config.apiKey, messages, systemPrompt, MODELS[currentModel].id);
|
|
237
612
|
messages.push({ role: 'assistant', content: result.content });
|
|
238
613
|
|
|
614
|
+
// Track totals
|
|
615
|
+
if (result.promptTokens) totalPromptTokens += result.promptTokens;
|
|
616
|
+
if (result.completionTokens) totalCompletionTokens += result.completionTokens;
|
|
617
|
+
|
|
239
618
|
// Token count footer
|
|
240
619
|
if (result.promptTokens || result.completionTokens) {
|
|
241
|
-
console.log(g(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens`));
|
|
620
|
+
console.log(g(` ${result.promptTokens || '?'}→${result.completionTokens || '?'} tokens · ${MODELS[currentModel].name}`));
|
|
242
621
|
}
|
|
622
|
+
|
|
623
|
+
// SAP tracking (fire-and-forget)
|
|
624
|
+
trackSap({
|
|
625
|
+
userId: config.supabaseUserId,
|
|
626
|
+
source: 'cli',
|
|
627
|
+
model: MODELS[currentModel].id,
|
|
628
|
+
promptTokens: result.promptTokens,
|
|
629
|
+
completionTokens: result.completionTokens,
|
|
630
|
+
accessToken: config.supabaseAccessToken,
|
|
631
|
+
});
|
|
243
632
|
} catch (err) {
|
|
244
633
|
console.error(`\n ${err.message}\n`);
|
|
245
634
|
// Remove the failed user message
|
package/commands/sync.js
CHANGED
|
@@ -113,6 +113,7 @@ async function push(config, token) {
|
|
|
113
113
|
if (!localPPS.adapters.phewsh) localPPS.adapters.phewsh = {};
|
|
114
114
|
localPPS.adapters.phewsh.cloud_id = project.id;
|
|
115
115
|
localPPS.adapters.phewsh.last_synced = new Date().toISOString();
|
|
116
|
+
localPPS.adapters.phewsh.last_updated_by = 'cli';
|
|
116
117
|
writePPS(INTENT_DIR, localPPS);
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -164,7 +165,7 @@ async function pull(config, token, cloudId = null) {
|
|
|
164
165
|
// Keep any local adapter links
|
|
165
166
|
if (localPPS?.adapters) merged.adapters = { ...project.pps_json.adapters, ...localPPS.adapters };
|
|
166
167
|
merged.adapters = merged.adapters || {};
|
|
167
|
-
merged.adapters.phewsh = { cloud_id: project.id, last_synced: new Date().toISOString() };
|
|
168
|
+
merged.adapters.phewsh = { cloud_id: project.id, last_synced: new Date().toISOString(), last_updated_by: 'pull' };
|
|
168
169
|
writePPS(INTENT_DIR, merged);
|
|
169
170
|
pulled.push('pps.json');
|
|
170
171
|
}
|