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.
@@ -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
- console.log(` ${sage('Unknown command:')} ${cream('/' + cmd)} ${slate('— type /help for available commands')}`);
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: () => rl.prompt(),
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.12",
3
+ "version": "0.15.13",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"