phewsh 0.15.13 → 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,9 +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');
23
24
  const { closest } = require('../lib/closest');
25
+ const cmdHistory = require('../lib/history');
24
26
  const { recordSessionEvent } = require('../lib/receipts-data');
25
27
  const configFile = require('../lib/config-file');
26
28
  const { createFailureTracker, createLineDispatcher } = require('../lib/session-input');
@@ -499,6 +501,19 @@ async function main() {
499
501
  };
500
502
  }
501
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
+
502
517
  // One subtle line under the menu — the single highest-leverage nudge, if any.
503
518
  function showInlineTip() {
504
519
  let tip = null;
@@ -522,6 +537,7 @@ async function main() {
522
537
  console.log('');
523
538
  console.log(` ${teal('●')} ${cream(projectName)} ${slate('·')} ${sage(`.intent/ ${intentFiles.length} file${intentFiles.length !== 1 ? 's' : ''} loaded`)} ${slate('· via ' + routeLabel(route, config))}`);
524
539
  console.log('');
540
+ showContinuity();
525
541
  showModeMenu();
526
542
  showInlineTip();
527
543
  console.log('');
@@ -555,6 +571,7 @@ async function main() {
555
571
  } else if (intentFiles.length === 0 && (atHome || recents.length > 0)) {
556
572
  showBootstrapMenu(recents);
557
573
  } else {
574
+ showContinuity();
558
575
  showModeMenu();
559
576
  showInlineTip();
560
577
  }
@@ -702,6 +719,7 @@ async function main() {
702
719
  output: process.stdout,
703
720
  prompt: ` ${teal('phewsh')} ${sage('>')} `,
704
721
  historySize: 100,
722
+ history: cmdHistory.loadForReadline(100), // up-arrow remembers across sessions
705
723
  });
706
724
  const promptText = ` phewsh > `;
707
725
  let lastPaste = null;
@@ -752,7 +770,7 @@ async function main() {
752
770
  'clear', 'status', 'key', 'login', 'export', 'push', 'pull', 'serve',
753
771
  'sync', 'harnesses', 'fallback', 'outcomes', 'tour', 'update', 'upgrade',
754
772
  'agents', 'context', 'gate', 'reload', 'sequence', 'seq', 'setup', 'system', 'watch',
755
- 'next', 'recommend', 'guide',
773
+ 'next', 'recommend', 'guide', 'thread', 'continuity',
756
774
  ]);
757
775
  const installedIds = harnesses.filter(h => h.installed).map(h => h.id);
758
776
  let turnAbort = null; // AbortController while an API turn streams
@@ -1106,6 +1124,40 @@ async function main() {
1106
1124
  return;
1107
1125
  }
1108
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
+
1109
1161
  if (cmd === 'help' || cmd === 'h') {
1110
1162
  const wantsAll = /^(all|more|full|everything)$/i.test(cmdArg.trim());
1111
1163
 
@@ -1134,6 +1186,7 @@ async function main() {
1134
1186
  console.log('');
1135
1187
  console.log(` ${cream('not sure what to do?')}`);
1136
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')}`);
1137
1190
  console.log('');
1138
1191
  console.log(` ${cream('author .intent/')}`);
1139
1192
  console.log(` ${teal('/init')} ${sage('Create .intent/ for this project')}`);
@@ -1883,14 +1936,27 @@ async function main() {
1883
1936
 
1884
1937
  if (cmd === 'harnesses' || cmd === 'agents') {
1885
1938
  console.log('');
1886
- 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.')}`);
1887
1940
  ui.divider('line');
1888
- 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
+ }
1889
1950
  const active = route?.type === 'harness' && route.id === h.id ? ` ${teal('● active')}` : '';
1890
- const mode = h.headless ? '' : slate(' · interactive (/work)');
1891
- const status = h.installed ? green('installed') : slate('not installed');
1892
- 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}`);
1893
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')}`);
1894
1960
  console.log('');
1895
1961
  rl.prompt();
1896
1962
  return;
@@ -2131,7 +2197,7 @@ async function main() {
2131
2197
  }
2132
2198
 
2133
2199
  const lineDispatcher = createLineDispatcher(handleInput, {
2134
- onBatch: ({ input, lines }) => collapsePastedEcho(lines, input),
2200
+ onBatch: ({ input, lines }) => { collapsePastedEcho(lines, input); cmdHistory.append(input); },
2135
2201
  onNoop: () => {
2136
2202
  // Bare Enter accepts a pending "did you mean" suggestion.
2137
2203
  if (pendingDidYouMean) {
@@ -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.13",
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"