phewsh 0.15.58 → 0.15.60

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/bin/phewsh.js CHANGED
@@ -18,7 +18,18 @@ if (command === 'shim-preflight') {
18
18
  // instead of the terminal's mid-word hard wrap. TTY-only, idempotent, and it
19
19
  // leaves process.stdout.write paths (spinner, streamed AI) alone. Installed
20
20
  // after the shim-preflight fast path so that stays instant and untouched.
21
- try { require('../lib/ui').installSmartWrap(); } catch { /* never block the CLI on cosmetics */ }
21
+ try {
22
+ const ui = require('../lib/ui');
23
+ // Restore a width the user pinned with `/width` in a past session, so the fix
24
+ // for a misreporting terminal sticks without re-typing it every launch.
25
+ try {
26
+ const os = require('os');
27
+ const path = require('path');
28
+ const cfg = require('../lib/config-file').loadConfig(path.join(os.homedir(), '.phewsh', 'config.json'));
29
+ if (cfg && cfg.displayWidth) ui.setWidth(cfg.displayWidth);
30
+ } catch { /* no saved width is fine */ }
31
+ ui.installSmartWrap();
32
+ } catch { /* never block the CLI on cosmetics */ }
22
33
 
23
34
  // ── ANSI helpers (no chalk dependency)
24
35
  const b = (s) => `\x1b[1m${s}\x1b[0m`; // bold
@@ -73,6 +73,35 @@ function copyToClipboard(text) {
73
73
  return false;
74
74
  }
75
75
 
76
+ // Show the cross-harness handoff — the heart of phewsh's continuity promise:
77
+ // the verified brief that travels to whatever AI tool you pick up next, now
78
+ // rendered as readable markdown instead of raw text and saved so the next tool
79
+ // (or the next phewsh session) inherits it. Best-effort and fail-soft; a
80
+ // handoff that can't generate must never block exit or a switch.
81
+ async function showHandoff({ projectName, route, reason = '', nextHint = true } = {}) {
82
+ let result = { shown: false, file: null };
83
+ try {
84
+ const { content } = await generateBrief();
85
+ const { renderMarkdown } = require('../lib/md');
86
+ const saved = persistBrief(content, { project: projectName, route: route || 'unknown' });
87
+ console.log('');
88
+ console.log(` ${teal('●')} ${b(cream('Handoff ready'))}${reason ? ' ' + slate('— ' + reason) : ''}`);
89
+ console.log(` ${slate('This is exactly what travels to the next AI tool — nothing re-explained.')}`);
90
+ ui.divider('line');
91
+ console.log(renderMarkdown(content));
92
+ ui.divider('line');
93
+ if (saved.written) console.log(` ${slate('saved · ' + saved.file.replace(require('os').homedir(), '~'))}`);
94
+ copyToClipboard(content);
95
+ console.log(` ${slate('copied to clipboard · paste into any tool, or')} ${cream('/use codex')} ${slate('to continue here')}`);
96
+ if (nextHint) console.log(` ${slate('full verified brief anytime:')} ${cream('/brief')} ${slate('·')} ${cream('/handoff')}`);
97
+ console.log('');
98
+ result = { shown: true, file: saved.file };
99
+ } catch (err) {
100
+ console.log(` ${ember('!')} ${slate('Could not build the handoff: ' + err.message)}`);
101
+ }
102
+ return result;
103
+ }
104
+
76
105
  // Sync awareness: compare local .intent/ timestamps with cloud updated_at
77
106
  async function checkSyncStatus(config) {
78
107
  if (!config?.supabaseUserId || !config?.supabaseAccessToken) return null;
@@ -938,7 +967,7 @@ async function main() {
938
967
  'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
939
968
  'agents', 'context', 'truth', 'brief', 'wrap', 'reconcile', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
940
969
  'next', 'recommend', 'guide', 'thread', 'continuity', 'learn', 'stats',
941
- 'pack', 'packs', 'remember', 'commands',
970
+ 'pack', 'packs', 'remember', 'commands', 'width', 'handoff',
942
971
  ]);
943
972
  const installedIds = harnesses.filter(h => h.installed).map(h => h.id);
944
973
  let turnAbort = null; // AbortController while an API turn streams
@@ -1346,6 +1375,12 @@ async function main() {
1346
1375
  const h = selfheal.heal();
1347
1376
  if (h.healed) console.log(` ${teal('↻')} ${sage("Harness context refreshed from .intent/ — you didn't have to")}`);
1348
1377
  } catch { /* self-heal must never block exit */ }
1378
+ // The handoff is given, not asked for: leaving a project hands you the
1379
+ // verified brief that the next AI tool inherits — the continuity promise
1380
+ // made visible on the way out. Only in a real project; never blocks exit.
1381
+ if (intentFiles.length > 0) {
1382
+ await showHandoff({ projectName, route: route?.id, reason: 'carry this into your next tool', nextHint: false });
1383
+ }
1349
1384
  try { require('../lib/intro').farewell(); } catch { /* sign-off is a nicety */ }
1350
1385
  console.log(` ${sage('session ended · ' + turns + ' exchanges · ' + (totalPromptTokens + totalCompletionTokens) + ' tokens')}`);
1351
1386
  if (decisionsThisSession > 0) {
@@ -1658,6 +1693,7 @@ async function main() {
1658
1693
  console.log('');
1659
1694
  console.log(` ${cream('session')}`);
1660
1695
  console.log(` ${teal('/work')} ${slate('[harness]')} ${sage('Preflight, brief, native handoff, automatic postflight')}`);
1696
+ console.log(` ${teal('/handoff')} ${sage('See the verified brief the next AI tool inherits — rendered + saved')}`);
1661
1697
  console.log(` ${teal('/switch')} ${slate('<harness>')} ${sage('Launch another native tool with a freshly verified briefing')}`);
1662
1698
  console.log(` ${teal('/run')} ${slate('<prompt>')} ${sage('One-shot prompt (no history)')}`);
1663
1699
  console.log(` ${teal('esc')} ${sage('Cancel a running turn · clear the input line')}`);
@@ -1669,6 +1705,7 @@ async function main() {
1669
1705
  console.log(` ${teal('/key')} ${sage('Set API key (optional — harnesses need none)')}`);
1670
1706
  console.log(` ${teal('/login')} ${sage('Identity + cloud sync')}`);
1671
1707
  console.log(` ${teal('/model')} ${slate('<name>')} ${sage('Switch model — passed through, the provider validates')}`);
1708
+ console.log(` ${teal('/width')} ${slate('[n]')} ${sage('Fix mid-word wrapping if your terminal misreports its size')}`);
1672
1709
  console.log(` ${teal('/update')} ${sage('Update phewsh')}`);
1673
1710
  console.log(` ${teal('/tour')} ${sage('Quick walkthrough')}`);
1674
1711
  console.log('');
@@ -1714,6 +1751,48 @@ async function main() {
1714
1751
  return;
1715
1752
  }
1716
1753
 
1754
+ // /width — fix mid-word breaks when the terminal misreports its size.
1755
+ // `/width` shows what phewsh thinks; `/width 80` pins it (saved, sticks
1756
+ // across sessions); `/width auto` clears the pin back to detection.
1757
+ if (cmd === 'width') {
1758
+ const arg = (cmdArg || '').trim().toLowerCase();
1759
+ if (!arg) {
1760
+ console.log('');
1761
+ console.log(` ${b(cream('Display width'))} ${slate('— used to wrap text at word boundaries')}`);
1762
+ console.log(` ${sage('phewsh is wrapping at')} ${cream(String(ui.rawWidth()))} ${sage('columns')} ${slate(config?.displayWidth ? '(pinned)' : '(auto-detected)')}`);
1763
+ console.log(` ${slate('If words still break mid-line, your terminal reports a wider size than it shows.')}`);
1764
+ console.log(` ${sage('Pin your real width:')} ${cream('/width 80')} ${slate('·')} ${sage('back to auto:')} ${cream('/width auto')}`);
1765
+ console.log('');
1766
+ rl.prompt();
1767
+ return;
1768
+ }
1769
+ if (arg === 'auto' || arg === 'off' || arg === 'reset') {
1770
+ ui.setWidth(null);
1771
+ config = config || {};
1772
+ delete config.displayWidth;
1773
+ try { configFile.saveConfig(CONFIG_PATH, config); } catch { /* best effort */ }
1774
+ console.log(` ${teal('●')} ${sage('Width back to auto-detect')} ${slate('(' + ui.rawWidth() + ' columns)')}`);
1775
+ console.log('');
1776
+ rl.prompt();
1777
+ return;
1778
+ }
1779
+ const set = ui.setWidth(arg);
1780
+ if (!set) {
1781
+ console.log(` ${ember('!')} ${sage('Give a number ≥ 20, e.g.')} ${cream('/width 80')} ${slate('· or')} ${cream('/width auto')}`);
1782
+ console.log('');
1783
+ rl.prompt();
1784
+ return;
1785
+ }
1786
+ config = config || {};
1787
+ config.displayWidth = set;
1788
+ try { configFile.saveConfig(CONFIG_PATH, config); } catch { /* best effort */ }
1789
+ console.log(` ${teal('●')} ${sage('Wrapping at')} ${cream(String(set))} ${sage('columns now')} ${slate('— saved, sticks across sessions')}`);
1790
+ console.log(` ${slate('This sentence is a quick check: if it wraps cleanly at a word and never cuts a word in half, you are set.')}`);
1791
+ console.log('');
1792
+ rl.prompt();
1793
+ return;
1794
+ }
1795
+
1717
1796
  if (cmd === 'context') {
1718
1797
  if (intentFiles.length > 0) {
1719
1798
  console.log('');
@@ -1742,12 +1821,24 @@ async function main() {
1742
1821
  if (cmd === 'brief') {
1743
1822
  console.log('');
1744
1823
  const { content } = await generateBrief();
1745
- console.log(content);
1824
+ const { renderMarkdown } = require('../lib/md');
1825
+ console.log(renderMarkdown(content));
1826
+ console.log('');
1827
+ console.log(` ${slate('this is your handoff — it travels to the next AI tool ·')} ${cream('/handoff')} ${slate('to save + copy it')}`);
1746
1828
  console.log('');
1747
1829
  rl.prompt();
1748
1830
  return;
1749
1831
  }
1750
1832
 
1833
+ // /handoff — explicitly produce, render, save, and copy the cross-harness
1834
+ // handoff so you can SEE what would carry into the next tool.
1835
+ if (cmd === 'handoff') {
1836
+ console.log(` ${sage('Building your handoff…')}`);
1837
+ await showHandoff({ projectName, route: route?.id, reason: 'what the next AI tool will pick up' });
1838
+ rl.prompt();
1839
+ return;
1840
+ }
1841
+
1751
1842
  if (cmd === 'status') {
1752
1843
  const turns = messages.length / 2;
1753
1844
  config = loadConfig();
@@ -2618,6 +2709,7 @@ async function main() {
2618
2709
  } else {
2619
2710
  console.log(` ${teal('●')} ${sage('Back in phewsh.')} ${slate('No changes detected. 1 kept · 2 undid · 3 redid · 4 flopped · /reconcile · /switch <tool>')}`);
2620
2711
  }
2712
+ console.log(` ${slate('continuing elsewhere?')} ${cream('/handoff')} ${slate('shows the updated brief the next tool inherits')}`);
2621
2713
  console.log('');
2622
2714
  rl.prompt();
2623
2715
  return;
package/lib/md.js CHANGED
@@ -120,4 +120,13 @@ function streamRenderer(write) {
120
120
  };
121
121
  }
122
122
 
123
- module.exports = { inlineMd, renderLine, streamRenderer };
123
+ // Render a complete markdown string to styled, word-wrapped terminal text —
124
+ // the non-streaming counterpart to streamRenderer, for content we already have
125
+ // in full (a handoff brief, a saved record). Carries fence state across lines
126
+ // so code blocks render literally.
127
+ function renderMarkdown(md) {
128
+ const state = { inFence: false };
129
+ return String(md).split('\n').map((line) => renderLine(line, state)).join('\n');
130
+ }
131
+
132
+ module.exports = { inlineMd, renderLine, streamRenderer, renderMarkdown };
package/lib/ui.js CHANGED
@@ -121,18 +121,34 @@ async function brandReveal(fast = false) {
121
121
  const SGR = /\x1b\[[0-9;]*m/g;
122
122
  const visW = (s) => s.replace(SGR, '').length;
123
123
 
124
- // Best available terminal width, with one column of safety margin so content
125
- // never sits flush against the edge (where terminals add their own wrap).
126
- // `PHEWSH_WIDTH` pins it explicitly — the escape hatch for host terminals that
127
- // misreport their size (some embedded/agent terminals advertise a wider pty
128
- // than the visible pane, which makes text break mid-word even with smart wrap
129
- // on). Falls back to COLUMNS, then the live tty size, then 80.
130
- function termWidth() {
124
+ // Best available terminal width, with a safety margin so content never sits
125
+ // flush against the edge (where terminals add their own mid-word wrap). The
126
+ // width is pinnable, the escape hatch for host terminals that misreport their
127
+ // size some embedded/agent terminals (e.g. Grok Build) advertise a wider pty
128
+ // than the visible pane, so text breaks mid-word even with smart wrap on.
129
+ // Resolution order: live override (set by `/width`) PHEWSH_WIDTH env
130
+ // the tty's reported columns → COLUMNS env → 80. Two columns are held back as
131
+ // margin so an off-by-one in the host's report can't cause a mid-word break.
132
+ let _widthOverride = null;
133
+ const WIDTH_MARGIN = 2;
134
+
135
+ function setWidth(n) {
136
+ const v = parseInt(n, 10);
137
+ _widthOverride = (Number.isFinite(v) && v >= 20) ? v : null;
138
+ return _widthOverride;
139
+ }
140
+
141
+ // The raw width phewsh believes the terminal is, before margin — what `/width`
142
+ // reports back so the user can compare it to their visible pane.
143
+ function rawWidth() {
144
+ if (_widthOverride) return _widthOverride;
131
145
  const pin = parseInt(process.env.PHEWSH_WIDTH, 10);
132
- const base = (Number.isFinite(pin) && pin >= 20)
133
- ? pin
134
- : (process.stdout.columns || parseInt(process.env.COLUMNS, 10) || 80);
135
- return Math.max(24, base - 1);
146
+ if (Number.isFinite(pin) && pin >= 20) return pin;
147
+ return process.stdout.columns || parseInt(process.env.COLUMNS, 10) || 80;
148
+ }
149
+
150
+ function termWidth() {
151
+ return Math.max(24, rawWidth() - WIDTH_MARGIN);
136
152
  }
137
153
 
138
154
  // Active SGR state after consuming `s`, starting from `prev`. A reset clears it;
@@ -348,7 +364,7 @@ module.exports = {
348
364
  spinner, brandReveal, statusPanel, interopLine, divider, typewrite,
349
365
  randomTip, TOUR_PAGES,
350
366
  // Smart wrap
351
- wrapAnsi, installSmartWrap, termWidth,
367
+ wrapAnsi, installSmartWrap, termWidth, rawWidth, setWidth,
352
368
  // ANSI helpers
353
369
  hide, show, up, clearLine, sleep,
354
370
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.58",
3
+ "version": "0.15.60",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"