phewsh 0.15.13 → 0.15.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/phewsh.js +2 -2
- package/commands/clarify.js +116 -19
- package/commands/hook.js +22 -0
- package/commands/serve.js +7 -0
- package/commands/session.js +74 -8
- package/commands/sync.js +33 -11
- package/lib/continuity.js +86 -0
- package/lib/history.js +40 -0
- package/package.json +1 -1
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('
|
|
33
|
+
console.log(` ${g('Keep all your AI tools. phewsh is the one memory they share.')}`);
|
|
34
34
|
console.log('');
|
|
35
35
|
|
|
36
36
|
// Context-aware hint
|
|
@@ -49,7 +49,7 @@ function showBrand() {
|
|
|
49
49
|
const COMMANDS = {
|
|
50
50
|
session: () => require('../commands/session'),
|
|
51
51
|
intent: () => require('../commands/intent'),
|
|
52
|
-
clarify: () => require('../commands/clarify'),
|
|
52
|
+
clarify: () => require('../commands/clarify').run(),
|
|
53
53
|
push: () => require('../commands/push'),
|
|
54
54
|
pull: () => require('../commands/pull'),
|
|
55
55
|
link: () => require('../commands/link'),
|
package/commands/clarify.js
CHANGED
|
@@ -45,10 +45,52 @@ async function askForInput() {
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
+
return extractJson(data.content?.[0]?.text || '');
|
|
160
|
+
}
|
|
99
161
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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/hook.js
CHANGED
|
@@ -12,10 +12,27 @@ const fs = require('fs');
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const os = require('os');
|
|
14
14
|
|
|
15
|
+
const continuity = require('../lib/continuity');
|
|
16
|
+
|
|
15
17
|
const PHEWSH_DIR = path.join(os.homedir(), '.phewsh');
|
|
16
18
|
const AMBIENT_LOG = path.join(PHEWSH_DIR, 'ambient-sessions.jsonl');
|
|
19
|
+
const DECISIONS_FILE = path.join(PHEWSH_DIR, 'outcomes', 'decisions.json');
|
|
17
20
|
const INTENT_DIR = path.join(process.cwd(), '.intent');
|
|
18
21
|
|
|
22
|
+
// "Where you left off, across every tool" — drawn from phewsh's own decision
|
|
23
|
+
// record (not the host transcript). This is what makes opening Claude Code
|
|
24
|
+
// standalone feel like resuming: it sees the thread Codex (or phewsh) left.
|
|
25
|
+
function continuityBrief(project) {
|
|
26
|
+
try {
|
|
27
|
+
const decisions = JSON.parse(fs.readFileSync(DECISIONS_FILE, 'utf-8'));
|
|
28
|
+
const line = continuity.continuityLine(decisions, { project });
|
|
29
|
+
if (!line) return null;
|
|
30
|
+
const tools = continuity.toolsInThread(decisions, { project });
|
|
31
|
+
const span = tools >= 2 ? ` (${tools} tools, one thread)` : '';
|
|
32
|
+
return `Continuity${span}: you were ${line}. Nothing's lost — continue it.`;
|
|
33
|
+
} catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
function readIfExists(p, maxBytes = 16384) {
|
|
20
37
|
try { return fs.readFileSync(p, 'utf-8').slice(0, maxBytes); } catch { return null; }
|
|
21
38
|
}
|
|
@@ -89,6 +106,11 @@ function sessionStart() {
|
|
|
89
106
|
const status = readIfExists(path.join(INTENT_DIR, 'status.md'));
|
|
90
107
|
if (status) parts.push(`\n## Status (excerpt)\n${firstLines(status, 5)}`);
|
|
91
108
|
|
|
109
|
+
// Decisions are tagged with the cwd basename (how recordDecision keys them),
|
|
110
|
+
// not the project.json display name — match on that so the thread connects.
|
|
111
|
+
const cont = continuityBrief(path.basename(process.cwd()));
|
|
112
|
+
if (cont) parts.push(`\n## Continuity (across your tools)\n${cont}`);
|
|
113
|
+
|
|
92
114
|
parts.push(`\n(Brief injected by PHEWSH ambient from .intent/. Honor the constraints above. The human can run \`phewsh\` for mission control — council, outcomes, the decision record.)`);
|
|
93
115
|
|
|
94
116
|
process.stdout.write(parts.join('\n') + '\n');
|
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/session.js
CHANGED
|
@@ -18,9 +18,11 @@ const { select, refreshSession: refreshSess } = require('../lib/supabase');
|
|
|
18
18
|
const { readPPS } = require('../lib/pps');
|
|
19
19
|
const { push, pull, ensureValidToken } = require('./sync');
|
|
20
20
|
const { HARNESSES, listHarnesses, runViaHarness, cancelActive } = require('../lib/harnesses');
|
|
21
|
-
const { recordDecision, labelOutcome, pendingDecisions, outcomeStats, OUTCOMES } = require('../lib/outcomes');
|
|
21
|
+
const { recordDecision, labelOutcome, pendingDecisions, recentDecisions, outcomeStats, OUTCOMES } = require('../lib/outcomes');
|
|
22
22
|
const { suggest, suggestAll } = require('../lib/suggest');
|
|
23
|
+
const continuity = require('../lib/continuity');
|
|
23
24
|
const { closest } = require('../lib/closest');
|
|
25
|
+
const cmdHistory = require('../lib/history');
|
|
24
26
|
const { recordSessionEvent } = require('../lib/receipts-data');
|
|
25
27
|
const configFile = require('../lib/config-file');
|
|
26
28
|
const { createFailureTracker, createLineDispatcher } = require('../lib/session-input');
|
|
@@ -499,6 +501,19 @@ async function main() {
|
|
|
499
501
|
};
|
|
500
502
|
}
|
|
501
503
|
|
|
504
|
+
// "Nothing lost" — surface where you left off, across every tool, so opening
|
|
505
|
+
// phewsh feels like resuming a thread rather than starting cold.
|
|
506
|
+
function showContinuity() {
|
|
507
|
+
try {
|
|
508
|
+
const decisions = recentDecisions(50, { project: projectName });
|
|
509
|
+
const line = continuity.continuityLine(decisions, { project: projectName });
|
|
510
|
+
if (!line) return;
|
|
511
|
+
const tools = continuity.toolsInThread(decisions, { project: projectName });
|
|
512
|
+
const span = tools >= 2 ? slate(` · ${tools} tools, one thread`) : '';
|
|
513
|
+
console.log(` ${teal('↻')} ${sage('Picking up — ' + line)}${span} ${slate('· /thread')}`);
|
|
514
|
+
} catch { /* continuity is a nicety, never a blocker */ }
|
|
515
|
+
}
|
|
516
|
+
|
|
502
517
|
// One subtle line under the menu — the single highest-leverage nudge, if any.
|
|
503
518
|
function showInlineTip() {
|
|
504
519
|
let tip = null;
|
|
@@ -522,6 +537,7 @@ async function main() {
|
|
|
522
537
|
console.log('');
|
|
523
538
|
console.log(` ${teal('●')} ${cream(projectName)} ${slate('·')} ${sage(`.intent/ ${intentFiles.length} file${intentFiles.length !== 1 ? 's' : ''} loaded`)} ${slate('· via ' + routeLabel(route, config))}`);
|
|
524
539
|
console.log('');
|
|
540
|
+
showContinuity();
|
|
525
541
|
showModeMenu();
|
|
526
542
|
showInlineTip();
|
|
527
543
|
console.log('');
|
|
@@ -555,6 +571,7 @@ async function main() {
|
|
|
555
571
|
} else if (intentFiles.length === 0 && (atHome || recents.length > 0)) {
|
|
556
572
|
showBootstrapMenu(recents);
|
|
557
573
|
} else {
|
|
574
|
+
showContinuity();
|
|
558
575
|
showModeMenu();
|
|
559
576
|
showInlineTip();
|
|
560
577
|
}
|
|
@@ -702,6 +719,7 @@ async function main() {
|
|
|
702
719
|
output: process.stdout,
|
|
703
720
|
prompt: ` ${teal('phewsh')} ${sage('>')} `,
|
|
704
721
|
historySize: 100,
|
|
722
|
+
history: cmdHistory.loadForReadline(100), // up-arrow remembers across sessions
|
|
705
723
|
});
|
|
706
724
|
const promptText = ` phewsh > `;
|
|
707
725
|
let lastPaste = null;
|
|
@@ -752,7 +770,7 @@ async function main() {
|
|
|
752
770
|
'clear', 'status', 'key', 'login', 'export', 'push', 'pull', 'serve',
|
|
753
771
|
'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
|
|
754
772
|
'agents', 'context', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
|
|
755
|
-
'next', 'recommend', 'guide',
|
|
773
|
+
'next', 'recommend', 'guide', 'thread', 'continuity',
|
|
756
774
|
]);
|
|
757
775
|
const installedIds = harnesses.filter(h => h.installed).map(h => h.id);
|
|
758
776
|
let turnAbort = null; // AbortController while an API turn streams
|
|
@@ -1106,6 +1124,40 @@ async function main() {
|
|
|
1106
1124
|
return;
|
|
1107
1125
|
}
|
|
1108
1126
|
|
|
1127
|
+
// ── /thread ────────────────────────────────────────
|
|
1128
|
+
// The cross-tool thread: one continuous record of your work, whichever
|
|
1129
|
+
// tool ran each step. The "nothing lost" proof, made visible.
|
|
1130
|
+
if (cmd === 'thread' || cmd === 'continuity') {
|
|
1131
|
+
const decisions = recentDecisions(50, { project: projectName });
|
|
1132
|
+
const thread = continuity.threadFor(decisions, { project: projectName });
|
|
1133
|
+
console.log('');
|
|
1134
|
+
if (thread.length === 0) {
|
|
1135
|
+
console.log(` ${teal('●')} ${sage('No thread yet for')} ${cream(projectName)}${sage('.')} ${slate('Do something — every action joins the thread, whichever tool runs it.')}`);
|
|
1136
|
+
console.log('');
|
|
1137
|
+
rl.prompt();
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const tools = continuity.toolsInThread(decisions, { project: projectName });
|
|
1141
|
+
console.log(` ${b(cream('Your thread'))} ${slate('— ' + projectName + ' · phewsh remembers across every tool')}`);
|
|
1142
|
+
ui.divider('line');
|
|
1143
|
+
for (const d of thread.slice(0, 12)) {
|
|
1144
|
+
const ago = continuity.agoText(d.ts).padEnd(9);
|
|
1145
|
+
const via = continuity.labelFor(d.route).padEnd(13);
|
|
1146
|
+
const oc = d.outcome
|
|
1147
|
+
? (d.outcome === 'kept' ? green('kept') : d.outcome === 'superseded' ? peach(d.outcome) : ember(d.outcome))
|
|
1148
|
+
: slate('open');
|
|
1149
|
+
let s = (d.summary || '').replace(/\s+/g, ' ');
|
|
1150
|
+
if (s.length > 46) s = s.slice(0, 45).trimEnd() + '…';
|
|
1151
|
+
console.log(` ${slate(ago)} ${sage(via)} ${cream(s || '—')} ${oc}`);
|
|
1152
|
+
}
|
|
1153
|
+
ui.divider('line');
|
|
1154
|
+
const span = tools >= 2 ? `${tools} tools, one thread` : `${tools} tool`;
|
|
1155
|
+
console.log(` ${sage(thread.length + ' action' + (thread.length !== 1 ? 's' : '') + ' · ' + span + ' · nothing re-explained')}`);
|
|
1156
|
+
console.log('');
|
|
1157
|
+
rl.prompt();
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1109
1161
|
if (cmd === 'help' || cmd === 'h') {
|
|
1110
1162
|
const wantsAll = /^(all|more|full|everything)$/i.test(cmdArg.trim());
|
|
1111
1163
|
|
|
@@ -1134,6 +1186,7 @@ async function main() {
|
|
|
1134
1186
|
console.log('');
|
|
1135
1187
|
console.log(` ${cream('not sure what to do?')}`);
|
|
1136
1188
|
console.log(` ${teal('/next')} ${sage("phewsh reads your state and hands back the next step worth taking")}`);
|
|
1189
|
+
console.log(` ${teal('/thread')} ${sage('where you left off — your work across every tool, one record')}`);
|
|
1137
1190
|
console.log('');
|
|
1138
1191
|
console.log(` ${cream('author .intent/')}`);
|
|
1139
1192
|
console.log(` ${teal('/init')} ${sage('Create .intent/ for this project')}`);
|
|
@@ -1883,14 +1936,27 @@ async function main() {
|
|
|
1883
1936
|
|
|
1884
1937
|
if (cmd === 'harnesses' || cmd === 'agents') {
|
|
1885
1938
|
console.log('');
|
|
1886
|
-
console.log(` ${b(cream('
|
|
1939
|
+
console.log(` ${b(cream('Your AI tools'))} ${slate('— phewsh keeps them all, aligned. You never pick just one.')}`);
|
|
1887
1940
|
ui.divider('line');
|
|
1888
|
-
|
|
1941
|
+
// Installed first, then the rest so the table also teaches what exists.
|
|
1942
|
+
const sorted = [...harnesses].sort((a, b) => (b.installed - a.installed));
|
|
1943
|
+
let lastGroup = null;
|
|
1944
|
+
for (const h of sorted) {
|
|
1945
|
+
const group = h.installed ? 'in' : 'out';
|
|
1946
|
+
if (group !== lastGroup) {
|
|
1947
|
+
console.log(` ${slate(h.installed ? 'on this machine — context routes straight through their login:' : 'available — install any of these and phewsh picks it up:')}`);
|
|
1948
|
+
lastGroup = group;
|
|
1949
|
+
}
|
|
1889
1950
|
const active = route?.type === 'harness' && route.id === h.id ? ` ${teal('● active')}` : '';
|
|
1890
|
-
const
|
|
1891
|
-
const
|
|
1892
|
-
console.log(` ${cream(h.id.padEnd(
|
|
1951
|
+
const dot = h.installed ? green('●') : slate('○');
|
|
1952
|
+
const mode = h.headless ? '' : slate(' · /work only');
|
|
1953
|
+
console.log(` ${dot} ${cream(h.id.padEnd(11))} ${sage((h.role || h.label).padEnd(20))} ${slate(h.label)}${mode}${active}`);
|
|
1893
1954
|
}
|
|
1955
|
+
ui.divider('line');
|
|
1956
|
+
console.log(` ${sage('keep your tools, keep one record:')}`);
|
|
1957
|
+
console.log(` ${teal('/use')} ${slate('<id>')} ${sage('route your typing through that tool')}`);
|
|
1958
|
+
console.log(` ${teal('@<id>')} ${slate('<msg>')} ${sage('one message to one tool — context stays shared')}`);
|
|
1959
|
+
console.log(` ${teal('/council')} ${slate('<q>')} ${sage('ask every installed tool at once, keep the best answer')}`);
|
|
1894
1960
|
console.log('');
|
|
1895
1961
|
rl.prompt();
|
|
1896
1962
|
return;
|
|
@@ -2131,7 +2197,7 @@ async function main() {
|
|
|
2131
2197
|
}
|
|
2132
2198
|
|
|
2133
2199
|
const lineDispatcher = createLineDispatcher(handleInput, {
|
|
2134
|
-
onBatch: ({ input, lines }) => collapsePastedEcho(lines, input),
|
|
2200
|
+
onBatch: ({ input, lines }) => { collapsePastedEcho(lines, input); cmdHistory.append(input); },
|
|
2135
2201
|
onNoop: () => {
|
|
2136
2202
|
// Bare Enter accepts a pending "did you mean" suggestion.
|
|
2137
2203
|
if (pendingDidYouMean) {
|
package/commands/sync.js
CHANGED
|
@@ -256,7 +256,19 @@ async function link(config, token, cloudId) {
|
|
|
256
256
|
console.log(' Run `phewsh push` to sync.\n');
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
function isAuthError(err) {
|
|
260
|
+
const m = (err && err.message || '').toLowerCase();
|
|
261
|
+
return m.includes('jwt') || m.includes('expired') || m.includes('401') || m.includes('unauthorized');
|
|
262
|
+
}
|
|
263
|
+
|
|
259
264
|
async function main(direction = 'push') {
|
|
265
|
+
const argv = process.argv.slice(3);
|
|
266
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
267
|
+
console.log(`\n phewsh ${direction === 'pull' ? 'pull' : direction === 'link' ? 'link <cloud-id>' : 'push'} — sync .intent/ with phewsh.com/intent`);
|
|
268
|
+
console.log(` push .intent/ → cloud pull cloud → .intent/ link adopt a cloud project\n`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
260
272
|
const config = loadConfig();
|
|
261
273
|
if (!config?.supabaseUserId) {
|
|
262
274
|
console.log('\n Not logged in. Run `phewsh login` first.\n');
|
|
@@ -269,18 +281,28 @@ async function main(direction = 'push') {
|
|
|
269
281
|
process.exit(1);
|
|
270
282
|
}
|
|
271
283
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
284
|
+
// The token can still be rejected server-side (refresh token also expired).
|
|
285
|
+
// Convert that into the same friendly nudge instead of a raw stack trace.
|
|
286
|
+
try {
|
|
287
|
+
if (direction === 'pull') {
|
|
288
|
+
await pull(config, token);
|
|
289
|
+
} else if (direction === 'link') {
|
|
290
|
+
const cloudId = argv[0] && !argv[0].startsWith('-') ? argv[0] : process.argv[4];
|
|
291
|
+
if (!cloudId) {
|
|
292
|
+
console.log('\n Usage: phewsh link <cloud-project-id>\n');
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
await link(config, token, cloudId);
|
|
296
|
+
} else {
|
|
297
|
+
await push(config, token);
|
|
280
298
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
299
|
+
} catch (err) {
|
|
300
|
+
if (isAuthError(err)) {
|
|
301
|
+
console.log('\n Session expired. Run `phewsh login` to re-authenticate.\n');
|
|
302
|
+
} else {
|
|
303
|
+
console.log(`\n ${direction} failed: ${err.message}\n`);
|
|
304
|
+
}
|
|
305
|
+
process.exit(1);
|
|
284
306
|
}
|
|
285
307
|
}
|
|
286
308
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Continuity — make "nothing lost across tools" visible.
|
|
2
|
+
//
|
|
3
|
+
// Every routed action phewsh records is tagged with the tool that ran it
|
|
4
|
+
// (claude-code, codex, gemini…). Read newest-first, that list IS a thread of
|
|
5
|
+
// your work across every tool. This module turns it into something a human
|
|
6
|
+
// (front door, /thread) or a standalone harness (ambient brief) can feel:
|
|
7
|
+
// "last you were doing X, via Codex, 3h ago — keep going."
|
|
8
|
+
//
|
|
9
|
+
// Pure + deterministic: feed it the decision records, get back strings.
|
|
10
|
+
|
|
11
|
+
const ROUTE_LABELS = {
|
|
12
|
+
'claude-code': 'Claude Code',
|
|
13
|
+
codex: 'Codex',
|
|
14
|
+
gemini: 'Gemini',
|
|
15
|
+
cursor: 'Cursor',
|
|
16
|
+
opencode: 'OpenCode',
|
|
17
|
+
grok: 'Grok',
|
|
18
|
+
kiro: 'Kiro',
|
|
19
|
+
copilot: 'Copilot',
|
|
20
|
+
hermes: 'Hermes',
|
|
21
|
+
pi: 'Pi',
|
|
22
|
+
aider: 'Aider',
|
|
23
|
+
goose: 'Goose',
|
|
24
|
+
amp: 'Amp',
|
|
25
|
+
droid: 'Droid',
|
|
26
|
+
api: 'direct API',
|
|
27
|
+
council: 'council',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function labelFor(route, labeler) {
|
|
31
|
+
if (labeler) { const l = labeler(route); if (l) return l; }
|
|
32
|
+
return ROUTE_LABELS[route] || route || 'a tool';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function agoText(ts, now = Date.now()) {
|
|
36
|
+
const then = new Date(ts).getTime();
|
|
37
|
+
if (isNaN(then)) return '';
|
|
38
|
+
const s = Math.max(0, Math.floor((now - then) / 1000));
|
|
39
|
+
if (s < 45) return 'just now';
|
|
40
|
+
const m = Math.floor(s / 60);
|
|
41
|
+
if (m < 60) return `${m}m ago`;
|
|
42
|
+
const h = Math.floor(m / 60);
|
|
43
|
+
if (h < 24) return `${h}h ago`;
|
|
44
|
+
const d = Math.floor(h / 24);
|
|
45
|
+
return `${d}d ago`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Decisions for a project (or all), newest first. */
|
|
49
|
+
function threadFor(decisions, { project = null } = {}) {
|
|
50
|
+
return (decisions || [])
|
|
51
|
+
.filter(d => d && d.ts && (!project || d.project === project))
|
|
52
|
+
.sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The single most recent action — "where you left off", or null. */
|
|
56
|
+
function lastLeftOff(decisions, { project = null } = {}) {
|
|
57
|
+
const t = threadFor(decisions, { project });
|
|
58
|
+
if (!t.length) return null;
|
|
59
|
+
const d = t[0];
|
|
60
|
+
return {
|
|
61
|
+
summary: (d.summary || '').trim(),
|
|
62
|
+
route: d.route,
|
|
63
|
+
ts: d.ts,
|
|
64
|
+
outcome: d.outcome || null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A one-line "picking up where you left off" string, or null. */
|
|
69
|
+
function continuityLine(decisions, { project = null, now = Date.now(), labeler = null, maxLen = 52 } = {}) {
|
|
70
|
+
const last = lastLeftOff(decisions, { project });
|
|
71
|
+
if (!last) return null;
|
|
72
|
+
let s = last.summary.replace(/\s+/g, ' ');
|
|
73
|
+
if (s.length > maxLen) s = s.slice(0, maxLen - 1).trimEnd() + '…';
|
|
74
|
+
const via = labelFor(last.route, labeler);
|
|
75
|
+
const ago = agoText(last.ts, now);
|
|
76
|
+
const when = ago ? ` · ${ago}` : '';
|
|
77
|
+
return s ? `last: “${s}” · via ${via}${when}` : `last action via ${via}${when}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** How many distinct tools appear in the thread (the "across tools" proof). */
|
|
81
|
+
function toolsInThread(decisions, { project = null } = {}) {
|
|
82
|
+
const set = new Set(threadFor(decisions, { project }).map(d => d.route).filter(Boolean));
|
|
83
|
+
return set.size;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { agoText, threadFor, lastLeftOff, continuityLine, toolsInThread, labelFor, ROUTE_LABELS };
|
package/lib/history.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Persistent command history — so up-arrow remembers across sessions, like
|
|
2
|
+
// every serious shell and harness. Stored at ~/.phewsh/history, newest last.
|
|
3
|
+
//
|
|
4
|
+
// Never persists anything secret: lines that set an API key are skipped, so a
|
|
5
|
+
// key typed at the prompt can't linger on disk.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
const FILE = path.join(os.homedir(), '.phewsh', 'history');
|
|
12
|
+
|
|
13
|
+
const SECRET = /^\/key\b/i; // /key <token> — never write the token to disk
|
|
14
|
+
|
|
15
|
+
/** Load up to `max` recent entries, oldest→newest (file order). */
|
|
16
|
+
function load(max = 100, file = FILE) {
|
|
17
|
+
try {
|
|
18
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n').filter(l => l.trim().length);
|
|
19
|
+
return lines.slice(-max);
|
|
20
|
+
} catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Most-recent-first, the order Node's readline `history` option expects. */
|
|
26
|
+
function loadForReadline(max = 100, file = FILE) {
|
|
27
|
+
return load(max, file).reverse();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Append one submitted line. No-ops on blank or secret-bearing input. */
|
|
31
|
+
function append(line, file = FILE) {
|
|
32
|
+
if (!line || !line.trim()) return;
|
|
33
|
+
if (SECRET.test(line.trim())) return;
|
|
34
|
+
try {
|
|
35
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
36
|
+
fs.appendFileSync(file, line.replace(/\r?\n/g, ' ') + '\n');
|
|
37
|
+
} catch { /* read-only home — in-session history still works */ }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { load, loadForReadline, append, FILE };
|