phewsh 0.15.12 → 0.15.13
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 +45 -3
- package/lib/closest.js +59 -0
- package/package.json +1 -1
package/commands/session.js
CHANGED
|
@@ -20,6 +20,7 @@ const { push, pull, ensureValidToken } = require('./sync');
|
|
|
20
20
|
const { HARNESSES, listHarnesses, runViaHarness, cancelActive } = require('../lib/harnesses');
|
|
21
21
|
const { recordDecision, labelOutcome, pendingDecisions, outcomeStats, OUTCOMES } = require('../lib/outcomes');
|
|
22
22
|
const { suggest, suggestAll } = require('../lib/suggest');
|
|
23
|
+
const { closest } = require('../lib/closest');
|
|
23
24
|
const { recordSessionEvent } = require('../lib/receipts-data');
|
|
24
25
|
const configFile = require('../lib/config-file');
|
|
25
26
|
const { createFailureTracker, createLineDispatcher } = require('../lib/session-input');
|
|
@@ -332,6 +333,7 @@ async function main() {
|
|
|
332
333
|
let awaitingFallback = null; // { input, fullSystem, options } after a route failure
|
|
333
334
|
let bootstrapChoices = null; // root-bootstrap menu entries when no project here
|
|
334
335
|
let nextChoices = null; // ranked /next suggestions awaiting a numeric pick
|
|
336
|
+
let pendingDidYouMean = null; // a suggested command; bare Enter accepts + runs it
|
|
335
337
|
let decisionsThisSession = 0;
|
|
336
338
|
|
|
337
339
|
// ── The Exhale: animated brand reveal ──────────────────
|
|
@@ -914,6 +916,9 @@ async function main() {
|
|
|
914
916
|
async function handleInput(input) {
|
|
915
917
|
input = expandPastes(input);
|
|
916
918
|
|
|
919
|
+
// Any typed input supersedes a pending "did you mean" offer.
|
|
920
|
+
if (pendingDidYouMean) pendingDidYouMean = null;
|
|
921
|
+
|
|
917
922
|
// A bare number right after a route failure picks the fallback
|
|
918
923
|
if (awaitingFallback) {
|
|
919
924
|
const af = awaitingFallback;
|
|
@@ -1102,6 +1107,28 @@ async function main() {
|
|
|
1102
1107
|
}
|
|
1103
1108
|
|
|
1104
1109
|
if (cmd === 'help' || cmd === 'h') {
|
|
1110
|
+
const wantsAll = /^(all|more|full|everything)$/i.test(cmdArg.trim());
|
|
1111
|
+
|
|
1112
|
+
// Bare /help → the short essentials a newcomer actually needs.
|
|
1113
|
+
if (!wantsAll) {
|
|
1114
|
+
console.log('');
|
|
1115
|
+
console.log(` ${sage('the loop: define .intent/ → sync → work → evolve → repeat')}`);
|
|
1116
|
+
console.log('');
|
|
1117
|
+
console.log(` ${cream('the essentials')}`);
|
|
1118
|
+
console.log(` ${teal('just type')} ${sage('chat — your .intent/ context travels with every route')}`);
|
|
1119
|
+
console.log(` ${teal('/next')} ${sage('not sure? phewsh names the next step worth taking')}`);
|
|
1120
|
+
console.log(` ${teal('/use')} ${slate('<tool>')} ${sage('route through Claude Code, Codex, Gemini… (their login, no key)')}`);
|
|
1121
|
+
console.log(` ${teal('@name')} ${slate('<msg>')} ${sage('one message to one tool — @codex review this')}`);
|
|
1122
|
+
console.log(` ${teal('/work')} ${slate('[tool]')} ${sage('hand off to the full interactive tool, outcome on return')}`);
|
|
1123
|
+
console.log(` ${teal('/clarify')} ${sage('turn messy thoughts into .intent/ artifacts')}`);
|
|
1124
|
+
console.log(` ${teal('/outcomes')} ${sage('label what you kept — the record that gets smarter')}`);
|
|
1125
|
+
console.log('');
|
|
1126
|
+
console.log(` ${slate('/help all')} ${sage('everything')} ${slate('·')} ${slate('/tour')} ${sage('walkthrough')} ${slate('·')} ${slate('/quit')} ${sage('exit')}`);
|
|
1127
|
+
console.log('');
|
|
1128
|
+
rl.prompt();
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1105
1132
|
console.log('');
|
|
1106
1133
|
console.log(` ${sage('the loop: define .intent/ → sync → work → evolve → repeat')}`);
|
|
1107
1134
|
console.log('');
|
|
@@ -2044,8 +2071,14 @@ async function main() {
|
|
|
2044
2071
|
return;
|
|
2045
2072
|
}
|
|
2046
2073
|
|
|
2047
|
-
// Unknown slash command
|
|
2048
|
-
|
|
2074
|
+
// Unknown slash command — suggest the nearest real one instead of a dead end.
|
|
2075
|
+
const guess = closest(cmd, [...KNOWN_COMMANDS]);
|
|
2076
|
+
if (guess) {
|
|
2077
|
+
console.log(` ${sage('No command')} ${cream('/' + cmd)}${sage('. Did you mean')} ${teal('/' + guess)}${sage('?')} ${slate('· enter = run it · /help all for everything')}`);
|
|
2078
|
+
pendingDidYouMean = '/' + guess + (cmdArg ? ' ' + cmdArg : '');
|
|
2079
|
+
} else {
|
|
2080
|
+
console.log(` ${sage('No command')} ${cream('/' + cmd)}${sage('.')} ${slate('Type')} ${teal('/next')} ${slate('for what to do, or')} ${teal('/help')} ${slate('for everything.')}`);
|
|
2081
|
+
}
|
|
2049
2082
|
rl.prompt();
|
|
2050
2083
|
return;
|
|
2051
2084
|
}
|
|
@@ -2099,7 +2132,16 @@ async function main() {
|
|
|
2099
2132
|
|
|
2100
2133
|
const lineDispatcher = createLineDispatcher(handleInput, {
|
|
2101
2134
|
onBatch: ({ input, lines }) => collapsePastedEcho(lines, input),
|
|
2102
|
-
onNoop: () =>
|
|
2135
|
+
onNoop: () => {
|
|
2136
|
+
// Bare Enter accepts a pending "did you mean" suggestion.
|
|
2137
|
+
if (pendingDidYouMean) {
|
|
2138
|
+
const cmd = pendingDidYouMean;
|
|
2139
|
+
pendingDidYouMean = null;
|
|
2140
|
+
handleInput(cmd).catch(() => {});
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
rl.prompt();
|
|
2144
|
+
},
|
|
2103
2145
|
onError: (err) => {
|
|
2104
2146
|
console.error(`\n ${ember('!')} ${sage('Input failed:')} ${err.message}`);
|
|
2105
2147
|
rl.prompt();
|
package/lib/closest.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Find the nearest known command to a typo — so an unknown slash command
|
|
2
|
+
// becomes "did you mean /clarify?" instead of a dead end. Pure + deterministic.
|
|
3
|
+
//
|
|
4
|
+
// Strategy, in order of confidence:
|
|
5
|
+
// 1. Exact prefix — user typed a real abbreviation (/cla → /clarify).
|
|
6
|
+
// 2. Small edit distance — a genuine typo (/claify → /clarify).
|
|
7
|
+
// Returns the single best candidate, or null when nothing is close enough.
|
|
8
|
+
|
|
9
|
+
function levenshtein(a, b) {
|
|
10
|
+
if (a === b) return 0;
|
|
11
|
+
if (!a.length) return b.length;
|
|
12
|
+
if (!b.length) return a.length;
|
|
13
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
14
|
+
let curr = new Array(b.length + 1);
|
|
15
|
+
for (let i = 1; i <= a.length; i++) {
|
|
16
|
+
curr[0] = i;
|
|
17
|
+
for (let j = 1; j <= b.length; j++) {
|
|
18
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
19
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
20
|
+
}
|
|
21
|
+
[prev, curr] = [curr, prev];
|
|
22
|
+
}
|
|
23
|
+
return prev[b.length];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} input — the unknown token (no leading slash)
|
|
28
|
+
* @param {string[]} candidates — known command names
|
|
29
|
+
* @param {object} [opts]
|
|
30
|
+
* @param {number} [opts.maxDistance=2] — max edit distance to still suggest
|
|
31
|
+
* @returns {string|null} best candidate or null
|
|
32
|
+
*/
|
|
33
|
+
function closest(input, candidates, opts = {}) {
|
|
34
|
+
const maxDistance = opts.maxDistance ?? 2;
|
|
35
|
+
if (!input || !candidates || candidates.length === 0) return null;
|
|
36
|
+
const q = input.toLowerCase();
|
|
37
|
+
|
|
38
|
+
// 1. Prefix match — shortest matching command wins (most specific abbrev).
|
|
39
|
+
// Require at least 2 chars so a stray "/a" doesn't latch onto everything.
|
|
40
|
+
if (q.length >= 2) {
|
|
41
|
+
const prefixes = candidates
|
|
42
|
+
.filter(c => c.toLowerCase().startsWith(q))
|
|
43
|
+
.sort((a, b) => a.length - b.length);
|
|
44
|
+
if (prefixes.length) return prefixes[0];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. Edit distance — nearest within threshold; scale threshold down for
|
|
48
|
+
// very short inputs so "x" doesn't "match" every 1-2 char command.
|
|
49
|
+
const cap = Math.min(maxDistance, Math.max(1, Math.floor(q.length / 2) + 1));
|
|
50
|
+
let best = null;
|
|
51
|
+
let bestD = Infinity;
|
|
52
|
+
for (const c of candidates) {
|
|
53
|
+
const d = levenshtein(q, c.toLowerCase());
|
|
54
|
+
if (d < bestD) { bestD = d; best = c; }
|
|
55
|
+
}
|
|
56
|
+
return bestD <= cap ? best : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { closest, levenshtein };
|