phewsh 0.15.9 → 0.15.11
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/clarify.js +1 -1
- package/commands/sequence.js +25 -7
- package/commands/session.js +10 -5
- package/lib/harnesses.js +21 -9
- package/lib/md.js +86 -0
- package/lib/sequencer/discover.js +56 -15
- package/lib/sequencer/emitters/stdout.js +2 -1
- package/lib/sequencer/index.js +9 -1
- package/lib/sequencer/parsers/claude-md.js +4 -2
- package/lib/sequencer/parsers/generic.js +1 -0
- package/package.json +1 -1
package/commands/clarify.js
CHANGED
|
@@ -37,7 +37,7 @@ async function askForInput() {
|
|
|
37
37
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
38
38
|
return new Promise((resolve) => {
|
|
39
39
|
console.log('\n Describe what you\'re building. Be as messy as you want.\n');
|
|
40
|
-
console.log(' (You
|
|
40
|
+
console.log(' (You bring the messy idea. PHEWSH compiles it into a clear, structured spec.)\n');
|
|
41
41
|
process.stdout.write(' > ');
|
|
42
42
|
let input = '';
|
|
43
43
|
rl.on('line', (line) => { input += (input ? ' ' : '') + line.trim(); });
|
package/commands/sequence.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// phewsh sequence (phewsh seq)
|
|
2
|
-
// Universal Memory Transform — reads
|
|
2
|
+
// Universal Memory Transform — reads this directory's memory files plus the
|
|
3
|
+
// user's global per-tool memory (read-only), emits optimal context per target.
|
|
3
4
|
|
|
4
5
|
const fs = require('fs');
|
|
5
6
|
const path = require('path');
|
|
@@ -17,6 +18,7 @@ const flags = {
|
|
|
17
18
|
write: args.includes('--write') || args.includes('-w'),
|
|
18
19
|
dryRun: args.includes('--dry-run'),
|
|
19
20
|
all: args.includes('--all'),
|
|
21
|
+
includeGlobal: args.includes('--include-global'),
|
|
20
22
|
sources: getFlag('--sources', '-s'),
|
|
21
23
|
help: args.includes('--help') || args.includes('-h'),
|
|
22
24
|
};
|
|
@@ -47,15 +49,22 @@ function getPositionalTarget() {
|
|
|
47
49
|
function showHelp() {
|
|
48
50
|
console.log('');
|
|
49
51
|
console.log(` ${b(cream('phewsh sequence'))} ${slate('(phewsh seq)')}`);
|
|
50
|
-
console.log(` ${sage('Universal Memory Transform — reads
|
|
51
|
-
console.log(` ${sage('
|
|
52
|
+
console.log(` ${sage('Universal Memory Transform — reads the memory files in this')}`);
|
|
53
|
+
console.log(` ${sage('directory plus your global per-user memory across tools,')}`);
|
|
54
|
+
console.log(` ${sage('then emits optimal context for any target agent.')}`);
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log(` ${cream('reads')} ${slate('(read-only — phewsh never edits these)')}`);
|
|
57
|
+
console.log(` ${sage('project .intent/, CLAUDE.md, AGENTS.md, GEMINI.md, .cursorrules,')}`);
|
|
58
|
+
console.log(` ${sage(' copilot-instructions, README, + this project’s Claude memory')}`);
|
|
59
|
+
console.log(` ${sage('global ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md, ~/.gemini/GEMINI.md')}`);
|
|
60
|
+
console.log(` ${slate('global memory is per-user and travels across every project.')}`);
|
|
52
61
|
console.log('');
|
|
53
62
|
console.log(` ${cream('usage')}`);
|
|
54
|
-
console.log(` ${teal('phewsh seq')} ${sage('Sequence → stdout summary')}`);
|
|
55
|
-
console.log(` ${teal('phewsh seq')} ${slate('claude')} ${sage('Sequence → CLAUDE.md section')}`);
|
|
63
|
+
console.log(` ${teal('phewsh seq')} ${sage('Sequence → stdout summary (project + global)')}`);
|
|
64
|
+
console.log(` ${teal('phewsh seq')} ${slate('claude')} ${sage('Sequence → CLAUDE.md section (project only)')}`);
|
|
56
65
|
console.log(` ${teal('phewsh seq')} ${slate('-w')} ${sage('Write to target file')}`);
|
|
57
66
|
console.log(` ${teal('phewsh seq')} ${slate('--explain')} ${sage('Show full ranking breakdown')}`);
|
|
58
|
-
console.log(` ${teal('phewsh seq')} ${slate('--dry-run')} ${sage('Show sources found, no output')}`);
|
|
67
|
+
console.log(` ${teal('phewsh seq')} ${slate('--dry-run')} ${sage('Show sources found (with scope), no output')}`);
|
|
59
68
|
console.log('');
|
|
60
69
|
console.log(` ${cream('targets')}`);
|
|
61
70
|
console.log(` ${teal('claude')} ${sage('CLAUDE.md section (between markers)')}`);
|
|
@@ -63,6 +72,8 @@ function showHelp() {
|
|
|
63
72
|
console.log(` ${cream('options')}`);
|
|
64
73
|
console.log(` ${teal('--budget')} ${slate('<level>')} ${sage('Token budget: minimal|standard|full|unlimited')}`);
|
|
65
74
|
console.log(` ${teal('--sources')} ${slate('<list>')} ${sage('Limit sources: intent,claude-md,claude-memory')}`);
|
|
75
|
+
console.log(` ${teal('--include-global')} ${sage('Allow global memory into a written project file')}`);
|
|
76
|
+
console.log(` ${slate(' (off by default — keeps personal notes out of committed files)')}`);
|
|
66
77
|
console.log(` ${teal('--write, -w')} ${sage('Write output to target file')}`);
|
|
67
78
|
console.log(` ${teal('--explain, -e')} ${sage('Full ranking breakdown')}`);
|
|
68
79
|
console.log(` ${teal('--dry-run')} ${sage('Discover sources only')}`);
|
|
@@ -82,6 +93,9 @@ async function main() {
|
|
|
82
93
|
sources = sources.filter(s => sourceFilter.some(f => s.type === f || s.type.startsWith(f)));
|
|
83
94
|
}
|
|
84
95
|
|
|
96
|
+
const projectCount = sources.filter(s => s.scope !== 'global').length;
|
|
97
|
+
const globalCount = sources.filter(s => s.scope === 'global').length;
|
|
98
|
+
|
|
85
99
|
console.log('');
|
|
86
100
|
console.log(` ${b(cream('Sources discovered'))} ${slate(`(${sources.length})`)}`);
|
|
87
101
|
ui.divider('line');
|
|
@@ -89,10 +103,13 @@ async function main() {
|
|
|
89
103
|
console.log(` ${sage('No recognized memory files found in')} ${slate(process.cwd())}`);
|
|
90
104
|
} else {
|
|
91
105
|
for (const source of sources) {
|
|
92
|
-
|
|
106
|
+
const tag = source.scope === 'global' ? slate('global ') : sage('project');
|
|
107
|
+
console.log(` ${tag} ${teal(source.type.padEnd(18))} ${sage(source.name)}`);
|
|
93
108
|
}
|
|
94
109
|
}
|
|
95
110
|
ui.divider('line');
|
|
111
|
+
console.log(` ${slate(`${projectCount} project · ${globalCount} global`)}`);
|
|
112
|
+
console.log(` ${slate('global = per-user memory across all tools; summary-only unless --include-global on write')}`);
|
|
96
113
|
console.log('');
|
|
97
114
|
return;
|
|
98
115
|
}
|
|
@@ -107,6 +124,7 @@ async function main() {
|
|
|
107
124
|
sources: sourceFilter,
|
|
108
125
|
explain: flags.explain,
|
|
109
126
|
write: flags.write,
|
|
127
|
+
includeGlobal: flags.includeGlobal,
|
|
110
128
|
});
|
|
111
129
|
|
|
112
130
|
// If target is stdout, emit already printed
|
package/commands/session.js
CHANGED
|
@@ -244,6 +244,13 @@ async function streamChat(apiKey, messages, systemPrompt, modelId, opts = {}) {
|
|
|
244
244
|
let completionTokens = null;
|
|
245
245
|
let firstToken = true;
|
|
246
246
|
|
|
247
|
+
// Line-buffered markdown render at a TTY; raw passthrough otherwise. The
|
|
248
|
+
// spinner stops on first rendered output, not first raw token.
|
|
249
|
+
const stopSpin = () => { if (firstToken) { spin.stop(); firstToken = false; } };
|
|
250
|
+
const render = process.stdout.isTTY
|
|
251
|
+
? require('../lib/md').streamRenderer((out) => { stopSpin(); process.stdout.write(out); })
|
|
252
|
+
: null;
|
|
253
|
+
|
|
247
254
|
for await (const chunk of response.body) {
|
|
248
255
|
const text = Buffer.from(chunk).toString('utf-8');
|
|
249
256
|
const lines = text.split('\n').filter(l => l.startsWith('data: '));
|
|
@@ -253,11 +260,8 @@ async function streamChat(apiKey, messages, systemPrompt, modelId, opts = {}) {
|
|
|
253
260
|
try {
|
|
254
261
|
const parsed = JSON.parse(data);
|
|
255
262
|
if (parsed.type === 'content_block_delta' && parsed.delta?.text) {
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
firstToken = false;
|
|
259
|
-
}
|
|
260
|
-
process.stdout.write(parsed.delta.text);
|
|
263
|
+
if (render) render.push(parsed.delta.text);
|
|
264
|
+
else { stopSpin(); process.stdout.write(parsed.delta.text); }
|
|
261
265
|
fullResponse += parsed.delta.text;
|
|
262
266
|
}
|
|
263
267
|
if (parsed.type === 'message_start' && parsed.message?.usage) {
|
|
@@ -270,6 +274,7 @@ async function streamChat(apiKey, messages, systemPrompt, modelId, opts = {}) {
|
|
|
270
274
|
}
|
|
271
275
|
}
|
|
272
276
|
|
|
277
|
+
if (render) render.flush();
|
|
273
278
|
if (firstToken) spin.stop();
|
|
274
279
|
process.stdout.write('\n');
|
|
275
280
|
|
package/lib/harnesses.js
CHANGED
|
@@ -106,20 +106,30 @@ function runViaHarness(id, systemPrompt, userPrompt, opts = {}) {
|
|
|
106
106
|
child.stdin.end();
|
|
107
107
|
|
|
108
108
|
let stderr = '';
|
|
109
|
-
let assembled = ''; // text
|
|
109
|
+
let assembled = ''; // raw text — returned + kept in history (never ANSI)
|
|
110
110
|
let resultFallback = ''; // claude's final `result` field — never-blank guard
|
|
111
111
|
let jsonBuf = '';
|
|
112
112
|
let firstByte = false;
|
|
113
113
|
|
|
114
|
+
// Spinner stop + leading newline fire on first *displayed* output, so the
|
|
115
|
+
// line-buffered renderer never leaves a spinner-stopped-but-blank gap.
|
|
116
|
+
function onFirstShow() {
|
|
117
|
+
if (firstByte) return;
|
|
118
|
+
firstByte = true;
|
|
119
|
+
if (spin) { spin.stop(); spin = null; }
|
|
120
|
+
if (show) process.stdout.write('\n');
|
|
121
|
+
}
|
|
122
|
+
// Render markdown only at a real TTY; pipes/quiet get raw passthrough so
|
|
123
|
+
// council parsing and scripted use stay clean (no stray ANSI).
|
|
124
|
+
const render = (show && process.stdout.isTTY)
|
|
125
|
+
? require('./md').streamRenderer((out) => { onFirstShow(); process.stdout.write(out); })
|
|
126
|
+
: null;
|
|
127
|
+
|
|
114
128
|
function emit(text) {
|
|
115
129
|
if (!text) return;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (show) process.stdout.write('\n');
|
|
120
|
-
}
|
|
121
|
-
assembled += text;
|
|
122
|
-
if (show) process.stdout.write(text);
|
|
130
|
+
assembled += text; // always raw
|
|
131
|
+
if (render) render.push(text);
|
|
132
|
+
else if (show) { onFirstShow(); process.stdout.write(text); }
|
|
123
133
|
}
|
|
124
134
|
|
|
125
135
|
child.stdout.on('data', (d) => {
|
|
@@ -157,11 +167,13 @@ function runViaHarness(id, systemPrompt, userPrompt, opts = {}) {
|
|
|
157
167
|
if (obj.type === 'result' && typeof obj.result === 'string') resultFallback = obj.result;
|
|
158
168
|
} catch { /* partial trailing line */ }
|
|
159
169
|
}
|
|
170
|
+
if (render) render.flush(); // render any trailing partial line
|
|
160
171
|
// Streaming produced nothing but the result has text → show it. Never blank.
|
|
161
172
|
let finalText = assembled;
|
|
162
173
|
if (!assembled.trim() && resultFallback) {
|
|
163
174
|
finalText = resultFallback;
|
|
164
|
-
if (
|
|
175
|
+
if (render) { render.push(resultFallback); render.flush(); }
|
|
176
|
+
else if (show) { onFirstShow(); process.stdout.write(resultFallback); }
|
|
165
177
|
}
|
|
166
178
|
if (show) process.stdout.write('\n');
|
|
167
179
|
if (child._phewshCancelled) return reject(new Error(`${h.label} cancelled`));
|
package/lib/md.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Terminal markdown rendering, safe for line-buffered streaming.
|
|
4
|
+
//
|
|
5
|
+
// Each completed line is rendered on its own — no cursor movement, no
|
|
6
|
+
// in-place rewrites. That is deliberate: mid-stream cursor tricks under line
|
|
7
|
+
// wrapping are the exact Apple Terminal hazard that has bitten this project
|
|
8
|
+
// before. We hold tokens until a newline, render that finished line, and print
|
|
9
|
+
// it. The only block-level state carried across lines is the code-fence flag,
|
|
10
|
+
// so fenced code renders literally instead of as inline markdown.
|
|
11
|
+
|
|
12
|
+
const A = {
|
|
13
|
+
reset: '\x1b[0m',
|
|
14
|
+
bold: '\x1b[1m',
|
|
15
|
+
dim: '\x1b[2m',
|
|
16
|
+
ital: '\x1b[3m', italOff: '\x1b[23m',
|
|
17
|
+
uline: '\x1b[4m',
|
|
18
|
+
cream: '\x1b[38;5;230m', // bold emphasis
|
|
19
|
+
teal: '\x1b[38;5;79m', // code, links, headers
|
|
20
|
+
sage: '\x1b[38;5;151m', // list items
|
|
21
|
+
slate: '\x1b[38;5;247m', // dividers, quotes, link urls
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Inline spans: bold, italic, code, links. `base` is the SGR sequence to
|
|
25
|
+
// restore the line's colour after each span's reset, so the rest of the line
|
|
26
|
+
// keeps its block colour. Pass '' for default-foreground body text.
|
|
27
|
+
function inlineMd(t, base = '') {
|
|
28
|
+
return t
|
|
29
|
+
.replace(/\*\*([^*]+)\*\*/g, (_, x) => `${A.bold}${A.cream}${x}${A.reset}${base}`)
|
|
30
|
+
.replace(/(^|\W)\*([^*\n]+)\*(?=\W|$)/g, (_, p, x) => `${p}${A.ital}${x}${A.italOff}${base}`)
|
|
31
|
+
.replace(/`([^`]+)`/g, (_, x) => `${A.teal}${x}${A.reset}${base}`)
|
|
32
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
33
|
+
(_, x, u) => `${A.uline}${A.teal}${x}${A.reset}${base} ${A.dim}${A.slate}(${u})${A.reset}${base}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Render one complete line, mutating `state` for block-level context.
|
|
37
|
+
// Body text keeps the default terminal foreground (bright) — only structural
|
|
38
|
+
// elements get colour, so a reply reads as a reply, not as dimmed UI chrome.
|
|
39
|
+
function renderLine(line, state = {}) {
|
|
40
|
+
const fence = line.match(/^\s*```+\s*\w*\s*$/);
|
|
41
|
+
if (fence) {
|
|
42
|
+
state.inFence = !state.inFence;
|
|
43
|
+
return ` ${A.slate}${A.dim}┄┄┄${A.reset}`;
|
|
44
|
+
}
|
|
45
|
+
if (state.inFence) {
|
|
46
|
+
return ` ${A.teal}${line}${A.reset}`; // literal code, no inline md
|
|
47
|
+
}
|
|
48
|
+
if (/^#{1,2}\s/.test(line)) return `\n${A.bold}${A.teal}${inlineMd(line.replace(/^#+\s*/, ''), A.teal)}${A.reset}`;
|
|
49
|
+
if (/^#{3,}\s/.test(line)) return `${A.cream}${inlineMd(line.replace(/^#+\s*/, ''), A.cream)}${A.reset}`;
|
|
50
|
+
if (/^\s*[-*]\s/.test(line)) return ` ${A.teal}·${A.reset} ${A.sage}${inlineMd(line.replace(/^\s*[-*]\s*/, ''), A.sage)}${A.reset}`;
|
|
51
|
+
if (/^\s*\d+\.\s/.test(line)) return ` ${A.sage}${inlineMd(line.trim(), A.sage)}${A.reset}`;
|
|
52
|
+
if (/^\s*>\s?/.test(line)) return ` ${A.slate}${A.ital}${inlineMd(line.replace(/^\s*>\s?/, ''), A.slate)}${A.italOff}${A.reset}`;
|
|
53
|
+
if (/^---+\s*$/.test(line)) return ` ${A.slate}${'─'.repeat(40)}${A.reset}`;
|
|
54
|
+
if (line.trim() === '') return '';
|
|
55
|
+
return inlineMd(line, ''); // body: default foreground, inline styling only
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Line-buffered streaming renderer. Feed raw token text via push(); each time a
|
|
59
|
+
// newline completes a line, the rendered line (with its trailing '\n') is handed
|
|
60
|
+
// to write(). flush() renders any trailing partial line at stream end. The
|
|
61
|
+
// caller's write() is where first-output side effects (stop the spinner, print a
|
|
62
|
+
// leading newline) belong, so they fire on first *rendered* output, not on the
|
|
63
|
+
// first raw token — no spinner-stopped-but-blank gap.
|
|
64
|
+
function streamRenderer(write) {
|
|
65
|
+
let buf = '';
|
|
66
|
+
const state = { inFence: false };
|
|
67
|
+
return {
|
|
68
|
+
push(text) {
|
|
69
|
+
buf += text;
|
|
70
|
+
let nl;
|
|
71
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
72
|
+
const line = buf.slice(0, nl);
|
|
73
|
+
buf = buf.slice(nl + 1);
|
|
74
|
+
write(renderLine(line, state) + '\n');
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
flush() {
|
|
78
|
+
if (buf.length) {
|
|
79
|
+
write(renderLine(buf, state) + '\n');
|
|
80
|
+
buf = '';
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { inlineMd, renderLine, streamRenderer };
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
// Discover all memory/context source files
|
|
2
|
-
// Returns a list of { path, type } for each recognized source.
|
|
1
|
+
// Discover all memory/context source files for the working directory.
|
|
2
|
+
// Returns a list of { path, type, name, scope } for each recognized source.
|
|
3
|
+
//
|
|
4
|
+
// Two scopes:
|
|
5
|
+
// 'project' — files in the working directory (this repo/project)
|
|
6
|
+
// 'global' — per-user memory that travels across every project
|
|
7
|
+
// (your global CLAUDE.md, Codex AGENTS.md, Gemini GEMINI.md)
|
|
8
|
+
//
|
|
9
|
+
// Global sources enrich the summary so `phewsh seq` reflects cross-tool
|
|
10
|
+
// continuity even in a bare directory. They are deliberately kept OUT of
|
|
11
|
+
// project-file writes by default (see sequence() includeGlobal) so personal
|
|
12
|
+
// global notes never leak into a committed project CLAUDE.md.
|
|
3
13
|
|
|
4
14
|
const fs = require('fs');
|
|
5
15
|
const path = require('path');
|
|
@@ -8,8 +18,18 @@ const os = require('os');
|
|
|
8
18
|
const INTENT_FILES = ['vision.md', 'plan.md', 'status.md', 'narrative.md', 'next.md'];
|
|
9
19
|
const INTENT_JSON = ['project.json', 'pps.json', 'gate.json'];
|
|
10
20
|
|
|
11
|
-
function discover(cwd = process.cwd()) {
|
|
21
|
+
function discover(cwd = process.cwd(), home = os.homedir()) {
|
|
12
22
|
const sources = [];
|
|
23
|
+
const seenPaths = new Set();
|
|
24
|
+
|
|
25
|
+
const add = (s) => {
|
|
26
|
+
const real = path.resolve(s.path);
|
|
27
|
+
if (seenPaths.has(real)) return; // never list the same file twice
|
|
28
|
+
seenPaths.add(real);
|
|
29
|
+
sources.push(s);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ── Project scope ──────────────────────────────────────────────
|
|
13
33
|
|
|
14
34
|
// .intent/ artifacts
|
|
15
35
|
const intentDir = path.join(cwd, '.intent');
|
|
@@ -17,7 +37,7 @@ function discover(cwd = process.cwd()) {
|
|
|
17
37
|
for (const file of [...INTENT_FILES, ...INTENT_JSON]) {
|
|
18
38
|
const p = path.join(intentDir, file);
|
|
19
39
|
if (fs.existsSync(p)) {
|
|
20
|
-
|
|
40
|
+
add({ path: p, type: 'intent', name: file, scope: 'project' });
|
|
21
41
|
}
|
|
22
42
|
}
|
|
23
43
|
}
|
|
@@ -25,12 +45,12 @@ function discover(cwd = process.cwd()) {
|
|
|
25
45
|
// CLAUDE.md — split into manual and generated sections later by parser
|
|
26
46
|
const claudeMd = path.join(cwd, 'CLAUDE.md');
|
|
27
47
|
if (fs.existsSync(claudeMd)) {
|
|
28
|
-
|
|
48
|
+
add({ path: claudeMd, type: 'claude-md', name: 'CLAUDE.md', scope: 'project' });
|
|
29
49
|
}
|
|
30
50
|
|
|
31
51
|
// Claude auto-memory — project-scoped
|
|
32
|
-
//
|
|
33
|
-
const claudeDir = path.join(
|
|
52
|
+
// ~/.claude/projects/<encoded-cwd>/memory/MEMORY.md
|
|
53
|
+
const claudeDir = path.join(home, '.claude');
|
|
34
54
|
if (fs.existsSync(claudeDir)) {
|
|
35
55
|
const projectsDir = path.join(claudeDir, 'projects');
|
|
36
56
|
if (fs.existsSync(projectsDir)) {
|
|
@@ -40,7 +60,7 @@ function discover(cwd = process.cwd()) {
|
|
|
40
60
|
if (fs.existsSync(memoryDir)) {
|
|
41
61
|
const memoryIndex = path.join(memoryDir, 'MEMORY.md');
|
|
42
62
|
if (fs.existsSync(memoryIndex)) {
|
|
43
|
-
|
|
63
|
+
add({ path: memoryIndex, type: 'claude-memory', name: 'MEMORY.md', scope: 'project' });
|
|
44
64
|
|
|
45
65
|
// Also discover linked memory files from MEMORY.md
|
|
46
66
|
try {
|
|
@@ -50,7 +70,7 @@ function discover(cwd = process.cwd()) {
|
|
|
50
70
|
while ((match = linkRegex.exec(content)) !== null) {
|
|
51
71
|
const linked = path.join(memoryDir, match[2]);
|
|
52
72
|
if (fs.existsSync(linked)) {
|
|
53
|
-
|
|
73
|
+
add({ path: linked, type: 'claude-memory-file', name: match[2], scope: 'project' });
|
|
54
74
|
}
|
|
55
75
|
}
|
|
56
76
|
} catch { /* skip */ }
|
|
@@ -62,33 +82,54 @@ function discover(cwd = process.cwd()) {
|
|
|
62
82
|
// .cursorrules
|
|
63
83
|
const cursorrules = path.join(cwd, '.cursorrules');
|
|
64
84
|
if (fs.existsSync(cursorrules)) {
|
|
65
|
-
|
|
85
|
+
add({ path: cursorrules, type: 'cursor', name: '.cursorrules', scope: 'project' });
|
|
66
86
|
}
|
|
67
87
|
|
|
68
|
-
// agent.md / AGENTS.md
|
|
88
|
+
// agent.md / AGENTS.md (Codex / generic agent instructions, project scope)
|
|
69
89
|
for (const name of ['agent.md', 'AGENTS.md']) {
|
|
70
90
|
const p = path.join(cwd, name);
|
|
71
91
|
if (fs.existsSync(p)) {
|
|
72
|
-
|
|
92
|
+
add({ path: p, type: 'agent', name, scope: 'project' });
|
|
73
93
|
}
|
|
74
94
|
}
|
|
75
95
|
|
|
96
|
+
// GEMINI.md (project scope)
|
|
97
|
+
const geminiMd = path.join(cwd, 'GEMINI.md');
|
|
98
|
+
if (fs.existsSync(geminiMd)) {
|
|
99
|
+
add({ path: geminiMd, type: 'agent', name: 'GEMINI.md', scope: 'project' });
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
// soul.md
|
|
77
103
|
const soulMd = path.join(cwd, 'soul.md');
|
|
78
104
|
if (fs.existsSync(soulMd)) {
|
|
79
|
-
|
|
105
|
+
add({ path: soulMd, type: 'soul', name: 'soul.md', scope: 'project' });
|
|
80
106
|
}
|
|
81
107
|
|
|
82
108
|
// .github/copilot-instructions.md
|
|
83
109
|
const copilot = path.join(cwd, '.github', 'copilot-instructions.md');
|
|
84
110
|
if (fs.existsSync(copilot)) {
|
|
85
|
-
|
|
111
|
+
add({ path: copilot, type: 'copilot', name: 'copilot-instructions.md', scope: 'project' });
|
|
86
112
|
}
|
|
87
113
|
|
|
88
114
|
// README.md (low priority but useful for identity)
|
|
89
115
|
const readme = path.join(cwd, 'README.md');
|
|
90
116
|
if (fs.existsSync(readme)) {
|
|
91
|
-
|
|
117
|
+
add({ path: readme, type: 'readme', name: 'README.md', scope: 'project' });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Global scope (per-user, travels across every project) ──────
|
|
121
|
+
// Canonical global memory files for each agent CLI. Read-only; never
|
|
122
|
+
// written to. These give value even with no .intent/ in the directory.
|
|
123
|
+
// De-dup via seenPaths handles the edge where cwd === home.
|
|
124
|
+
const globals = [
|
|
125
|
+
{ path: path.join(home, '.claude', 'CLAUDE.md'), type: 'claude-md', name: '~/.claude/CLAUDE.md' },
|
|
126
|
+
{ path: path.join(home, '.codex', 'AGENTS.md'), type: 'agent', name: '~/.codex/AGENTS.md' },
|
|
127
|
+
{ path: path.join(home, '.gemini', 'GEMINI.md'), type: 'agent', name: '~/.gemini/GEMINI.md' },
|
|
128
|
+
];
|
|
129
|
+
for (const g of globals) {
|
|
130
|
+
if (fs.existsSync(g.path)) {
|
|
131
|
+
add({ path: g.path, type: g.type, name: g.name, scope: 'global' });
|
|
132
|
+
}
|
|
92
133
|
}
|
|
93
134
|
|
|
94
135
|
return sources;
|
|
@@ -58,7 +58,8 @@ function emitExplain(chunks, sources, { b, w, g, sage, slate, teal, cream, green
|
|
|
58
58
|
// Sources discovered
|
|
59
59
|
console.log(` ${b(cream('Sources'))}`);
|
|
60
60
|
for (const source of sources) {
|
|
61
|
-
|
|
61
|
+
const tag = source.scope === 'global' ? slate('global ') : sage('project');
|
|
62
|
+
console.log(` ${tag} ${teal(source.type.padEnd(14))} ${sage(source.name)}`);
|
|
62
63
|
}
|
|
63
64
|
console.log('');
|
|
64
65
|
|
package/lib/sequencer/index.js
CHANGED
|
@@ -45,12 +45,20 @@ function sequence(options = {}) {
|
|
|
45
45
|
sources: sourceFilter = null,
|
|
46
46
|
explain = false,
|
|
47
47
|
write = false,
|
|
48
|
+
includeGlobal = false,
|
|
48
49
|
cwd = process.cwd(),
|
|
49
50
|
} = options;
|
|
50
51
|
|
|
51
|
-
// 1. Discover all source files
|
|
52
|
+
// 1. Discover all source files (project + global)
|
|
52
53
|
let sources = discover(cwd);
|
|
53
54
|
|
|
55
|
+
// Privacy guard: global per-user memory enriches the summary (stdout), but
|
|
56
|
+
// never bleeds into a project-file target (CLAUDE.md) unless asked for —
|
|
57
|
+
// so personal global notes don't land in a committed project file.
|
|
58
|
+
if (target !== 'stdout' && !includeGlobal) {
|
|
59
|
+
sources = sources.filter(s => s.scope !== 'global');
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
// Filter sources if requested
|
|
55
63
|
if (sourceFilter && sourceFilter.length > 0) {
|
|
56
64
|
sources = sources.filter(s =>
|
|
@@ -39,8 +39,9 @@ function parse(source) {
|
|
|
39
39
|
|
|
40
40
|
const kind = classifySection(section.title);
|
|
41
41
|
chunks.push({
|
|
42
|
-
source:
|
|
42
|
+
source: `${source.name}:${section.title || 'root'}`,
|
|
43
43
|
sourceType: 'claude-md-manual',
|
|
44
|
+
scope: source.scope || 'project',
|
|
44
45
|
kind,
|
|
45
46
|
content: section.body.trim(),
|
|
46
47
|
timestamp: mtime,
|
|
@@ -52,8 +53,9 @@ function parse(source) {
|
|
|
52
53
|
// Generated section — lower authority, will get deduped against .intent/
|
|
53
54
|
if (generatedContent) {
|
|
54
55
|
chunks.push({
|
|
55
|
-
source:
|
|
56
|
+
source: `${source.name}:phewsh-generated`,
|
|
56
57
|
sourceType: 'claude-md-generated',
|
|
58
|
+
scope: source.scope || 'project',
|
|
57
59
|
kind: 'context',
|
|
58
60
|
content: generatedContent,
|
|
59
61
|
timestamp: mtime,
|