phewsh 0.15.6 → 0.15.9
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/commands/session.js +20 -7
- package/lib/harnesses.js +73 -9
- package/package.json +1 -1
package/commands/session.js
CHANGED
|
@@ -331,9 +331,12 @@ async function main() {
|
|
|
331
331
|
await ui.brandReveal();
|
|
332
332
|
|
|
333
333
|
if (config?.apiKey && !config.apiKey.startsWith('sk-')) {
|
|
334
|
-
|
|
334
|
+
// Persist the cleanup so this never nags again — an unusable key
|
|
335
|
+
// (not sk-…) helps nobody, and harness routes need no key at all.
|
|
336
|
+
console.log(` ${ember('!')} ${sage('Cleared an unusable stored API key.')} ${slate('harness routes need none — /key to add one')}`);
|
|
335
337
|
console.log('');
|
|
336
338
|
config.apiKey = null;
|
|
339
|
+
try { saveConfig(config); } catch { /* read-only home — in-memory clear still holds */ }
|
|
337
340
|
route = resolveRoute(config, harnesses);
|
|
338
341
|
}
|
|
339
342
|
|
|
@@ -774,7 +777,10 @@ async function main() {
|
|
|
774
777
|
const lineCount = text.split('\n').length;
|
|
775
778
|
if (lineCount > 1 || text.length > 200) {
|
|
776
779
|
pasteCounter++;
|
|
777
|
-
const
|
|
780
|
+
const chars = text.length.toLocaleString('en-US');
|
|
781
|
+
const tag = lineCount > 1
|
|
782
|
+
? `[paste #${pasteCounter}: ${chars} chars, ${lineCount} lines]`
|
|
783
|
+
: `[paste #${pasteCounter}: ${chars} chars]`;
|
|
778
784
|
pendingPastes.set(tag, text);
|
|
779
785
|
lastPaste = text;
|
|
780
786
|
rl.write(tag);
|
|
@@ -825,11 +831,18 @@ async function main() {
|
|
|
825
831
|
return;
|
|
826
832
|
}
|
|
827
833
|
// Re-render so token coloring tracks edits — including the keystroke
|
|
828
|
-
// where the token stops matching and must un-color.
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
834
|
+
// where the token stops matching and must un-color. Deferred one tick:
|
|
835
|
+
// this is a prependListener, so readline hasn't appended the just-typed
|
|
836
|
+
// char yet; without the defer, rl.line is stale by one and /model only
|
|
837
|
+
// ever evaluates as /mode.
|
|
838
|
+
setImmediate(() => {
|
|
839
|
+
try {
|
|
840
|
+
const cur = rl.line || '';
|
|
841
|
+
const special = cur[0] === '/' || cur[0] === '@';
|
|
842
|
+
if (special || wasSpecialInput) rl._refreshLine();
|
|
843
|
+
wasSpecialInput = special;
|
|
844
|
+
} catch { /* never break input */ }
|
|
845
|
+
});
|
|
833
846
|
} catch { /* never break input */ }
|
|
834
847
|
};
|
|
835
848
|
process.stdin.prependListener('keypress', phewshKeypress);
|
package/lib/harnesses.js
CHANGED
|
@@ -20,7 +20,7 @@ const { execSync, spawn } = require('child_process');
|
|
|
20
20
|
// list of its own, so it can never go stale. Harnesses without a known
|
|
21
21
|
// model flag ignore the preference and use their own config.
|
|
22
22
|
const HARNESSES = {
|
|
23
|
-
'claude-code': { bin: 'claude', label: 'Claude Code', role: 'writes code', auth: 'Claude subscription / Console', models: true, modelHints: ['sonnet', 'opus', 'haiku', '
|
|
23
|
+
'claude-code': { bin: 'claude', label: 'Claude Code', role: 'writes code', auth: 'Claude subscription / Console', models: true, modelHints: ['sonnet', 'opus', 'haiku'], streamFormat: 'claude-json', args: (p, m) => ['-p', p, '--output-format', 'stream-json', '--include-partial-messages', '--verbose', ...(m ? ['--model', m] : [])] },
|
|
24
24
|
'codex': { bin: 'codex', label: 'Codex CLI', role: 'reasons & reviews', auth: 'ChatGPT plan', models: true, args: (p, m) => ['exec', '--skip-git-repo-check', ...(m ? ['-m', m] : []), p] },
|
|
25
25
|
'gemini': { bin: 'gemini', label: 'Gemini CLI', role: "another model's take", auth: 'Google login', models: true, args: (p, m) => ['-p', p, ...(m ? ['-m', m] : [])] },
|
|
26
26
|
'cursor': { bin: 'cursor-agent', label: 'Cursor Agent', role: 'edits files', auth: 'Cursor account', models: true, args: (p, m) => ['-p', p, '--output-format', 'text', ...(m ? ['--model', m] : [])] },
|
|
@@ -87,6 +87,17 @@ function runViaHarness(id, systemPrompt, userPrompt, opts = {}) {
|
|
|
87
87
|
const prompt = systemPrompt ? `${systemPrompt}\n\n---\n\n${userPrompt}` : userPrompt;
|
|
88
88
|
const model = h.models ? opts.model : undefined;
|
|
89
89
|
|
|
90
|
+
// Spinner during the wait, and live token streaming where the harness
|
|
91
|
+
// supports it — no path is ever a dead blank screen. quiet (council) parses
|
|
92
|
+
// silently so parallel runs don't interleave into soup.
|
|
93
|
+
// Output is written whenever this isn't a quiet (council) run. The spinner
|
|
94
|
+
// additionally needs a TTY — piped/test contexts get the text, no spinner.
|
|
95
|
+
const show = !opts.quiet;
|
|
96
|
+
let spin = null;
|
|
97
|
+
if (show && process.stdout.isTTY) {
|
|
98
|
+
try { spin = require('./ui').spinner('thinking'); } catch { spin = null; }
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
return new Promise((resolve, reject) => {
|
|
91
102
|
const child = spawn(h.bin, h.args(prompt, model), { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
92
103
|
ACTIVE_CHILDREN.add(child);
|
|
@@ -94,20 +105,73 @@ function runViaHarness(id, systemPrompt, userPrompt, opts = {}) {
|
|
|
94
105
|
// Some harnesses (codex exec, gemini) wait for stdin EOF before running.
|
|
95
106
|
child.stdin.end();
|
|
96
107
|
|
|
97
|
-
let stdout = '';
|
|
98
108
|
let stderr = '';
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
let assembled = ''; // text shown to the user and returned
|
|
110
|
+
let resultFallback = ''; // claude's final `result` field — never-blank guard
|
|
111
|
+
let jsonBuf = '';
|
|
112
|
+
let firstByte = false;
|
|
113
|
+
|
|
114
|
+
function emit(text) {
|
|
115
|
+
if (!text) return;
|
|
116
|
+
if (!firstByte) {
|
|
117
|
+
firstByte = true;
|
|
118
|
+
if (spin) { spin.stop(); spin = null; }
|
|
119
|
+
if (show) process.stdout.write('\n');
|
|
120
|
+
}
|
|
121
|
+
assembled += text;
|
|
122
|
+
if (show) process.stdout.write(text);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
child.stdout.on('data', (d) => {
|
|
126
|
+
if (h.streamFormat === 'claude-json') {
|
|
127
|
+
// Newline-delimited JSON: each line is one event. text_delta events
|
|
128
|
+
// carry the live tokens; the final `result` is the fallback.
|
|
129
|
+
jsonBuf += d.toString();
|
|
130
|
+
let nl;
|
|
131
|
+
while ((nl = jsonBuf.indexOf('\n')) !== -1) {
|
|
132
|
+
const line = jsonBuf.slice(0, nl).trim();
|
|
133
|
+
jsonBuf = jsonBuf.slice(nl + 1);
|
|
134
|
+
if (!line) continue;
|
|
135
|
+
let obj;
|
|
136
|
+
// A complete line that isn't JSON is real output (plain-text error,
|
|
137
|
+
// or a non-streaming stub) — show it rather than swallow it.
|
|
138
|
+
try { obj = JSON.parse(line); } catch { emit(line + '\n'); continue; }
|
|
139
|
+
if (obj.type === 'stream_event'
|
|
140
|
+
&& obj.event?.type === 'content_block_delta'
|
|
141
|
+
&& obj.event.delta?.type === 'text_delta') {
|
|
142
|
+
emit(obj.event.delta.text);
|
|
143
|
+
} else if (obj.type === 'result' && typeof obj.result === 'string') {
|
|
144
|
+
resultFallback = obj.result;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
emit(d.toString());
|
|
149
|
+
}
|
|
150
|
+
});
|
|
103
151
|
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
104
152
|
child.on('close', (code) => {
|
|
105
|
-
if (
|
|
153
|
+
if (spin) { spin.stop(); spin = null; }
|
|
154
|
+
if (h.streamFormat === 'claude-json' && jsonBuf.trim()) {
|
|
155
|
+
try {
|
|
156
|
+
const obj = JSON.parse(jsonBuf.trim());
|
|
157
|
+
if (obj.type === 'result' && typeof obj.result === 'string') resultFallback = obj.result;
|
|
158
|
+
} catch { /* partial trailing line */ }
|
|
159
|
+
}
|
|
160
|
+
// Streaming produced nothing but the result has text → show it. Never blank.
|
|
161
|
+
let finalText = assembled;
|
|
162
|
+
if (!assembled.trim() && resultFallback) {
|
|
163
|
+
finalText = resultFallback;
|
|
164
|
+
if (show) process.stdout.write('\n' + resultFallback);
|
|
165
|
+
}
|
|
166
|
+
if (show) process.stdout.write('\n');
|
|
106
167
|
if (child._phewshCancelled) return reject(new Error(`${h.label} cancelled`));
|
|
107
|
-
if (code === 0) resolve(
|
|
168
|
+
if (code === 0) resolve(finalText);
|
|
108
169
|
else reject(new Error(`${h.label} exited ${code}${stderr ? `\n ${stderr.trim().split('\n').slice(-3).join('\n ')}` : ''}`));
|
|
109
170
|
});
|
|
110
|
-
child.on('error', (e) =>
|
|
171
|
+
child.on('error', (e) => {
|
|
172
|
+
if (spin) { spin.stop(); spin = null; }
|
|
173
|
+
reject(new Error(`Could not run ${h.bin}: ${e.message}`));
|
|
174
|
+
});
|
|
111
175
|
});
|
|
112
176
|
}
|
|
113
177
|
|