phewsh 0.15.3 → 0.15.5
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 +11 -0
- package/commands/session.js +57 -1
- package/lib/harnesses.js +12 -1
- package/lib/session-display.js +54 -0
- package/lib/session-input.js +2 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -84,6 +84,17 @@ Inside the shell:
|
|
|
84
84
|
/help All commands
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
The prompt includes a compact status rail with the current folder, route,
|
|
88
|
+
loaded `.intent/` files, and approximate context size. Multi-line pastes and
|
|
89
|
+
single-line pastes of 300+ characters collapse after submission:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
[pasted 1,284 chars · 12 lines · Ctrl+O to expand]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Press `Ctrl+O` to inspect the last submitted paste and `Esc` to clear input or
|
|
96
|
+
cancel an in-flight provider turn.
|
|
97
|
+
|
|
87
98
|
## What it does
|
|
88
99
|
|
|
89
100
|
Creates three structured artifacts in `.intent/`:
|
package/commands/session.js
CHANGED
|
@@ -22,6 +22,14 @@ const { recordDecision, labelOutcome, pendingDecisions, outcomeStats, OUTCOMES }
|
|
|
22
22
|
const { recordSessionEvent } = require('../lib/receipts-data');
|
|
23
23
|
const configFile = require('../lib/config-file');
|
|
24
24
|
const { createFailureTracker, createLineDispatcher } = require('../lib/session-input');
|
|
25
|
+
const {
|
|
26
|
+
echoedRows,
|
|
27
|
+
estimateTokens,
|
|
28
|
+
formatPasteSummary,
|
|
29
|
+
formatTokenCount,
|
|
30
|
+
relativeFolder,
|
|
31
|
+
shouldCollapsePaste,
|
|
32
|
+
} = require('../lib/session-display');
|
|
25
33
|
const { recordProject, listProjects, scanForProjects, fmtAgo } = require('../lib/projects-index');
|
|
26
34
|
|
|
27
35
|
// Brand palette shortcuts
|
|
@@ -503,6 +511,7 @@ async function main() {
|
|
|
503
511
|
turnInFlight = true;
|
|
504
512
|
const output = await runViaHarness(harnessId, fullSystem, buildHarnessPrompt(messages, input), { model: harnessModel });
|
|
505
513
|
turnInFlight = false;
|
|
514
|
+
userCancelled = false; // a SIGTERM the harness rode out must not mislabel the next turn
|
|
506
515
|
messages.push({ role: 'user', content: input });
|
|
507
516
|
messages.push({ role: 'assistant', content: (output || '').trim() });
|
|
508
517
|
recordSessionEvent(harnessId, projectName, 'task_complete', {
|
|
@@ -631,6 +640,39 @@ async function main() {
|
|
|
631
640
|
prompt: ` ${teal('phewsh')} ${sage('>')} `,
|
|
632
641
|
historySize: 100,
|
|
633
642
|
});
|
|
643
|
+
const promptText = ` phewsh > `;
|
|
644
|
+
let lastPaste = null;
|
|
645
|
+
|
|
646
|
+
function currentContextTokens() {
|
|
647
|
+
const conversation = messages.map(message => message.content).join('\n');
|
|
648
|
+
return estimateTokens(`${systemPrompt}\n${conversation}`);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function renderStatusRail() {
|
|
652
|
+
if (!process.stdout.isTTY) return;
|
|
653
|
+
const folder = relativeFolder(process.cwd(), os.homedir());
|
|
654
|
+
const contextLabel = intentFiles.length > 0
|
|
655
|
+
? `${intentFiles.length} intent file${intentFiles.length === 1 ? '' : 's'}`
|
|
656
|
+
: 'no project context';
|
|
657
|
+
const routeName = routeLabel(route, config);
|
|
658
|
+
const tokens = formatTokenCount(currentContextTokens());
|
|
659
|
+
console.log(` ${slate(folder)} ${slate('·')} ${cream(routeName)} ${slate('·')} ${sage(contextLabel)} ${slate('·')} ${sage(`~${tokens} ctx tokens`)}`);
|
|
660
|
+
console.log(` ${slate('/help commands · /use route · /context memory · Ctrl+O last paste · Esc cancel')}`);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const readlinePrompt = rl.prompt.bind(rl);
|
|
664
|
+
rl.prompt = function promptWithStatusRail(preserveCursor) {
|
|
665
|
+
renderStatusRail();
|
|
666
|
+
return readlinePrompt(preserveCursor);
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
function collapsePastedEcho(lines, input) {
|
|
670
|
+
if (!process.stdout.isTTY || !shouldCollapsePaste(lines, input)) return;
|
|
671
|
+
const rows = echoedRows(lines, promptText, process.stdout.columns || 80);
|
|
672
|
+
for (let i = 0; i < rows; i++) process.stdout.write('\x1b[1A\x1b[2K\r');
|
|
673
|
+
lastPaste = input;
|
|
674
|
+
console.log(` ${peach(formatPasteSummary(input, lines.length))}`);
|
|
675
|
+
}
|
|
634
676
|
|
|
635
677
|
// Live input coloring — like Claude Code: text stays normal, and only a
|
|
636
678
|
// RECOGNIZED leading /command (or @harness) token turns teal (peach for @)
|
|
@@ -674,12 +716,25 @@ async function main() {
|
|
|
674
716
|
}
|
|
675
717
|
|
|
676
718
|
if (process.stdin.isTTY) {
|
|
677
|
-
process.stdin.
|
|
719
|
+
process.stdin.prependListener('keypress', (str, key) => {
|
|
678
720
|
try {
|
|
721
|
+
if (key?.ctrl && key.name === 'o' && lastPaste) {
|
|
722
|
+
setImmediate(() => {
|
|
723
|
+
rl.line = '';
|
|
724
|
+
rl.cursor = 0;
|
|
725
|
+
process.stdout.write('\x1b[2K\r');
|
|
726
|
+
console.log(` ${b(cream('Last paste'))} ${slate(`(${lastPaste.length.toLocaleString('en-US')} chars)`)}`);
|
|
727
|
+
console.log(lastPaste.split('\n').map(line => ` ${line}`).join('\n'));
|
|
728
|
+
console.log('');
|
|
729
|
+
rl.prompt();
|
|
730
|
+
});
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
679
733
|
// ESC: cancel an in-flight turn, or clear the input line.
|
|
680
734
|
if (key && key.name === 'escape') {
|
|
681
735
|
if (turnInFlight) {
|
|
682
736
|
userCancelled = true;
|
|
737
|
+
process.stdout.write('\n \x1b[38;5;247mcancelling…\x1b[0m\n');
|
|
683
738
|
if (turnAbort) turnAbort.abort();
|
|
684
739
|
cancelActive();
|
|
685
740
|
} else if (rl.line) {
|
|
@@ -1827,6 +1882,7 @@ async function main() {
|
|
|
1827
1882
|
}
|
|
1828
1883
|
|
|
1829
1884
|
const lineDispatcher = createLineDispatcher(handleInput, {
|
|
1885
|
+
onBatch: ({ input, lines }) => collapsePastedEcho(lines, input),
|
|
1830
1886
|
onNoop: () => rl.prompt(),
|
|
1831
1887
|
onError: (err) => {
|
|
1832
1888
|
console.error(`\n ${ember('!')} ${sage('Input failed:')} ${err.message}`);
|
package/lib/harnesses.js
CHANGED
|
@@ -42,7 +42,17 @@ const ACTIVE_CHILDREN = new Set();
|
|
|
42
42
|
function cancelActive() {
|
|
43
43
|
let n = 0;
|
|
44
44
|
for (const c of ACTIVE_CHILDREN) {
|
|
45
|
-
try {
|
|
45
|
+
try {
|
|
46
|
+
c._phewshCancelled = true; // close handler rejects even if exit is 0
|
|
47
|
+
c.kill('SIGTERM');
|
|
48
|
+
// Some harnesses (codex) ride out SIGTERM and finish anyway —
|
|
49
|
+
// escalate so esc means esc.
|
|
50
|
+
const t = setTimeout(() => {
|
|
51
|
+
try { if (c.exitCode === null) c.kill('SIGKILL'); } catch { /* gone */ }
|
|
52
|
+
}, 1200);
|
|
53
|
+
if (t.unref) t.unref();
|
|
54
|
+
n++;
|
|
55
|
+
} catch { /* already gone */ }
|
|
46
56
|
}
|
|
47
57
|
return n;
|
|
48
58
|
}
|
|
@@ -93,6 +103,7 @@ function runViaHarness(id, systemPrompt, userPrompt, opts = {}) {
|
|
|
93
103
|
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
94
104
|
child.on('close', (code) => {
|
|
95
105
|
if (!opts.quiet) process.stdout.write('\n');
|
|
106
|
+
if (child._phewshCancelled) return reject(new Error(`${h.label} cancelled`));
|
|
96
107
|
if (code === 0) resolve(stdout);
|
|
97
108
|
else reject(new Error(`${h.label} exited ${code}${stderr ? `\n ${stderr.trim().split('\n').slice(-3).join('\n ')}` : ''}`));
|
|
98
109
|
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
4
|
+
|
|
5
|
+
function visibleLength(value) {
|
|
6
|
+
return String(value || '').replace(ANSI_RE, '').length;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function estimateTokens(value) {
|
|
10
|
+
return Math.max(0, Math.ceil(String(value || '').length / 4));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatTokenCount(tokens) {
|
|
14
|
+
if (tokens < 1000) return String(tokens);
|
|
15
|
+
return `${(tokens / 1000).toFixed(tokens < 10000 ? 1 : 0)}k`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function shouldCollapsePaste(lines, input, threshold = 300) {
|
|
19
|
+
return lines.length > 1 || input.length >= threshold;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatPasteSummary(input, lineCount) {
|
|
23
|
+
const chars = input.length.toLocaleString('en-US');
|
|
24
|
+
const lines = lineCount > 1 ? ` · ${lineCount} lines` : '';
|
|
25
|
+
return `[pasted ${chars} chars${lines} · Ctrl+O to expand]`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function echoedRows(lines, prompt, columns = 80) {
|
|
29
|
+
const width = Math.max(20, columns || 80);
|
|
30
|
+
return lines.reduce((total, line, index) => {
|
|
31
|
+
const prefix = index === 0 ? visibleLength(prompt) : 0;
|
|
32
|
+
return total + Math.max(1, Math.ceil((prefix + visibleLength(line)) / width));
|
|
33
|
+
}, 0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function relativeFolder(cwd, home) {
|
|
37
|
+
if (!cwd) return '';
|
|
38
|
+
const relative = home ? path.relative(home, cwd) : cwd;
|
|
39
|
+
if (home && relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
40
|
+
return `~/${relative}`;
|
|
41
|
+
}
|
|
42
|
+
if (home && relative === '') return '~';
|
|
43
|
+
return cwd;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
echoedRows,
|
|
48
|
+
estimateTokens,
|
|
49
|
+
formatPasteSummary,
|
|
50
|
+
formatTokenCount,
|
|
51
|
+
relativeFolder,
|
|
52
|
+
shouldCollapsePaste,
|
|
53
|
+
visibleLength,
|
|
54
|
+
};
|
package/lib/session-input.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
function createLineDispatcher(handleInput, {
|
|
2
2
|
onError = (err) => { throw err; },
|
|
3
3
|
onNoop = () => {},
|
|
4
|
+
onBatch = () => {},
|
|
4
5
|
schedule = setImmediate,
|
|
5
6
|
} = {}) {
|
|
6
7
|
let pendingLines = [];
|
|
@@ -16,6 +17,7 @@ function createLineDispatcher(handleInput, {
|
|
|
16
17
|
onNoop();
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
20
|
+
onBatch({ input, lines });
|
|
19
21
|
chain = chain.then(() => handleInput(input)).catch(onError);
|
|
20
22
|
}
|
|
21
23
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phewsh",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.5",
|
|
4
4
|
"description": "Turn intent into action. Structure your thinking, execute your next step.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"phewsh": "bin/phewsh.js"
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=18.0.0"
|
|
37
37
|
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "node --test test/*.test.js"
|
|
40
|
+
},
|
|
38
41
|
"dependencies": {
|
|
39
42
|
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
40
43
|
}
|