phewsh 0.15.12 → 0.15.15

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/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');
@@ -18,8 +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');
24
+ const { closest } = require('../lib/closest');
25
+ const cmdHistory = require('../lib/history');
23
26
  const { recordSessionEvent } = require('../lib/receipts-data');
24
27
  const configFile = require('../lib/config-file');
25
28
  const { createFailureTracker, createLineDispatcher } = require('../lib/session-input');
@@ -332,6 +335,7 @@ async function main() {
332
335
  let awaitingFallback = null; // { input, fullSystem, options } after a route failure
333
336
  let bootstrapChoices = null; // root-bootstrap menu entries when no project here
334
337
  let nextChoices = null; // ranked /next suggestions awaiting a numeric pick
338
+ let pendingDidYouMean = null; // a suggested command; bare Enter accepts + runs it
335
339
  let decisionsThisSession = 0;
336
340
 
337
341
  // ── The Exhale: animated brand reveal ──────────────────
@@ -497,6 +501,19 @@ async function main() {
497
501
  };
498
502
  }
499
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
+
500
517
  // One subtle line under the menu — the single highest-leverage nudge, if any.
501
518
  function showInlineTip() {
502
519
  let tip = null;
@@ -520,6 +537,7 @@ async function main() {
520
537
  console.log('');
521
538
  console.log(` ${teal('●')} ${cream(projectName)} ${slate('·')} ${sage(`.intent/ ${intentFiles.length} file${intentFiles.length !== 1 ? 's' : ''} loaded`)} ${slate('· via ' + routeLabel(route, config))}`);
522
539
  console.log('');
540
+ showContinuity();
523
541
  showModeMenu();
524
542
  showInlineTip();
525
543
  console.log('');
@@ -553,6 +571,7 @@ async function main() {
553
571
  } else if (intentFiles.length === 0 && (atHome || recents.length > 0)) {
554
572
  showBootstrapMenu(recents);
555
573
  } else {
574
+ showContinuity();
556
575
  showModeMenu();
557
576
  showInlineTip();
558
577
  }
@@ -700,6 +719,7 @@ async function main() {
700
719
  output: process.stdout,
701
720
  prompt: ` ${teal('phewsh')} ${sage('>')} `,
702
721
  historySize: 100,
722
+ history: cmdHistory.loadForReadline(100), // up-arrow remembers across sessions
703
723
  });
704
724
  const promptText = ` phewsh > `;
705
725
  let lastPaste = null;
@@ -750,7 +770,7 @@ async function main() {
750
770
  'clear', 'status', 'key', 'login', 'export', 'push', 'pull', 'serve',
751
771
  'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
752
772
  'agents', 'context', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
753
- 'next', 'recommend', 'guide',
773
+ 'next', 'recommend', 'guide', 'thread', 'continuity',
754
774
  ]);
755
775
  const installedIds = harnesses.filter(h => h.installed).map(h => h.id);
756
776
  let turnAbort = null; // AbortController while an API turn streams
@@ -914,6 +934,9 @@ async function main() {
914
934
  async function handleInput(input) {
915
935
  input = expandPastes(input);
916
936
 
937
+ // Any typed input supersedes a pending "did you mean" offer.
938
+ if (pendingDidYouMean) pendingDidYouMean = null;
939
+
917
940
  // A bare number right after a route failure picks the fallback
918
941
  if (awaitingFallback) {
919
942
  const af = awaitingFallback;
@@ -1101,12 +1124,69 @@ async function main() {
1101
1124
  return;
1102
1125
  }
1103
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
+
1104
1161
  if (cmd === 'help' || cmd === 'h') {
1162
+ const wantsAll = /^(all|more|full|everything)$/i.test(cmdArg.trim());
1163
+
1164
+ // Bare /help → the short essentials a newcomer actually needs.
1165
+ if (!wantsAll) {
1166
+ console.log('');
1167
+ console.log(` ${sage('the loop: define .intent/ → sync → work → evolve → repeat')}`);
1168
+ console.log('');
1169
+ console.log(` ${cream('the essentials')}`);
1170
+ console.log(` ${teal('just type')} ${sage('chat — your .intent/ context travels with every route')}`);
1171
+ console.log(` ${teal('/next')} ${sage('not sure? phewsh names the next step worth taking')}`);
1172
+ console.log(` ${teal('/use')} ${slate('<tool>')} ${sage('route through Claude Code, Codex, Gemini… (their login, no key)')}`);
1173
+ console.log(` ${teal('@name')} ${slate('<msg>')} ${sage('one message to one tool — @codex review this')}`);
1174
+ console.log(` ${teal('/work')} ${slate('[tool]')} ${sage('hand off to the full interactive tool, outcome on return')}`);
1175
+ console.log(` ${teal('/clarify')} ${sage('turn messy thoughts into .intent/ artifacts')}`);
1176
+ console.log(` ${teal('/outcomes')} ${sage('label what you kept — the record that gets smarter')}`);
1177
+ console.log('');
1178
+ console.log(` ${slate('/help all')} ${sage('everything')} ${slate('·')} ${slate('/tour')} ${sage('walkthrough')} ${slate('·')} ${slate('/quit')} ${sage('exit')}`);
1179
+ console.log('');
1180
+ rl.prompt();
1181
+ return;
1182
+ }
1183
+
1105
1184
  console.log('');
1106
1185
  console.log(` ${sage('the loop: define .intent/ → sync → work → evolve → repeat')}`);
1107
1186
  console.log('');
1108
1187
  console.log(` ${cream('not sure what to do?')}`);
1109
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')}`);
1110
1190
  console.log('');
1111
1191
  console.log(` ${cream('author .intent/')}`);
1112
1192
  console.log(` ${teal('/init')} ${sage('Create .intent/ for this project')}`);
@@ -1856,14 +1936,27 @@ async function main() {
1856
1936
 
1857
1937
  if (cmd === 'harnesses' || cmd === 'agents') {
1858
1938
  console.log('');
1859
- console.log(` ${b(cream('Agent CLIs on this machine'))} ${slate('— each carries its own login, no API key')}`);
1939
+ console.log(` ${b(cream('Your AI tools'))} ${slate('— phewsh keeps them all, aligned. You never pick just one.')}`);
1860
1940
  ui.divider('line');
1861
- for (const h of harnesses) {
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
+ }
1862
1950
  const active = route?.type === 'harness' && route.id === h.id ? ` ${teal('● active')}` : '';
1863
- const mode = h.headless ? '' : slate(' · interactive (/work)');
1864
- const status = h.installed ? green('installed') : slate('not installed');
1865
- console.log(` ${cream(h.id.padEnd(12))} ${sage(h.label.padEnd(14))} ${status}${mode}${active}`);
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}`);
1866
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')}`);
1867
1960
  console.log('');
1868
1961
  rl.prompt();
1869
1962
  return;
@@ -2044,8 +2137,14 @@ async function main() {
2044
2137
  return;
2045
2138
  }
2046
2139
 
2047
- // Unknown slash command
2048
- console.log(` ${sage('Unknown command:')} ${cream('/' + cmd)} ${slate('— type /help for available commands')}`);
2140
+ // Unknown slash command — suggest the nearest real one instead of a dead end.
2141
+ const guess = closest(cmd, [...KNOWN_COMMANDS]);
2142
+ if (guess) {
2143
+ console.log(` ${sage('No command')} ${cream('/' + cmd)}${sage('. Did you mean')} ${teal('/' + guess)}${sage('?')} ${slate('· enter = run it · /help all for everything')}`);
2144
+ pendingDidYouMean = '/' + guess + (cmdArg ? ' ' + cmdArg : '');
2145
+ } else {
2146
+ console.log(` ${sage('No command')} ${cream('/' + cmd)}${sage('.')} ${slate('Type')} ${teal('/next')} ${slate('for what to do, or')} ${teal('/help')} ${slate('for everything.')}`);
2147
+ }
2049
2148
  rl.prompt();
2050
2149
  return;
2051
2150
  }
@@ -2098,8 +2197,17 @@ async function main() {
2098
2197
  }
2099
2198
 
2100
2199
  const lineDispatcher = createLineDispatcher(handleInput, {
2101
- onBatch: ({ input, lines }) => collapsePastedEcho(lines, input),
2102
- onNoop: () => rl.prompt(),
2200
+ onBatch: ({ input, lines }) => { collapsePastedEcho(lines, input); cmdHistory.append(input); },
2201
+ onNoop: () => {
2202
+ // Bare Enter accepts a pending "did you mean" suggestion.
2203
+ if (pendingDidYouMean) {
2204
+ const cmd = pendingDidYouMean;
2205
+ pendingDidYouMean = null;
2206
+ handleInput(cmd).catch(() => {});
2207
+ return;
2208
+ }
2209
+ rl.prompt();
2210
+ },
2103
2211
  onError: (err) => {
2104
2212
  console.error(`\n ${ember('!')} ${sage('Input failed:')} ${err.message}`);
2105
2213
  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 };
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.12",
3
+ "version": "0.15.15",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"