phewsh 0.11.15 → 0.11.16
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 +3 -0
- package/commands/sequence.js +136 -0
- package/commands/session.js +25 -0
- package/lib/sequencer/compressor.js +51 -0
- package/lib/sequencer/discover.js +97 -0
- package/lib/sequencer/emitters/claude-md.js +173 -0
- package/lib/sequencer/emitters/stdout.js +89 -0
- package/lib/sequencer/index.js +116 -0
- package/lib/sequencer/parsers/claude-md.js +106 -0
- package/lib/sequencer/parsers/claude-memory.js +53 -0
- package/lib/sequencer/parsers/generic.js +31 -0
- package/lib/sequencer/parsers/intent.js +237 -0
- package/lib/sequencer/ranker.js +84 -0
- package/package.json +1 -1
package/bin/phewsh.js
CHANGED
|
@@ -63,6 +63,8 @@ const COMMANDS = {
|
|
|
63
63
|
watch: () => require('../commands/watch')(),
|
|
64
64
|
mcp: () => require('../commands/mcp')(),
|
|
65
65
|
serve: () => require('../commands/serve')(),
|
|
66
|
+
sequence: () => require('../commands/sequence')(),
|
|
67
|
+
seq: () => require('../commands/sequence')(),
|
|
66
68
|
help: showHelp,
|
|
67
69
|
version: showVersion,
|
|
68
70
|
};
|
|
@@ -88,6 +90,7 @@ function showHelp() {
|
|
|
88
90
|
console.log(` ${cyan('ai')} ${g('One-shot prompt with .intent/ context')}`);
|
|
89
91
|
console.log('');
|
|
90
92
|
console.log(` ${b(w('sync everywhere'))}`);
|
|
93
|
+
console.log(` ${cyan('seq')} ${g('Sequence all memory → optimal context for any agent')}`);
|
|
91
94
|
console.log(` ${cyan('watch')} ${g('Auto-sync .intent/ → CLAUDE.md + cloud')}`);
|
|
92
95
|
console.log(` ${cyan('push/pull')} ${g('Manual sync to/from phewsh.com/intent')}`);
|
|
93
96
|
console.log(` ${cyan('serve')} ${g('Execution bridge — run from phewsh.com/intent')}`);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// phewsh sequence (phewsh seq)
|
|
2
|
+
// Universal Memory Transform — reads all AI memory files, emits optimal context.
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { sequence } = require('../lib/sequencer');
|
|
7
|
+
const ui = require('../lib/ui');
|
|
8
|
+
|
|
9
|
+
const { b, w, sage, slate, teal, cream, green, ember } = ui;
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(3);
|
|
12
|
+
|
|
13
|
+
const flags = {
|
|
14
|
+
target: getFlag('--target', '-t') || getPositionalTarget(),
|
|
15
|
+
budget: getFlag('--budget', '-b') || 'standard',
|
|
16
|
+
explain: args.includes('--explain') || args.includes('-e'),
|
|
17
|
+
write: args.includes('--write') || args.includes('-w'),
|
|
18
|
+
dryRun: args.includes('--dry-run'),
|
|
19
|
+
all: args.includes('--all'),
|
|
20
|
+
sources: getFlag('--sources', '-s'),
|
|
21
|
+
help: args.includes('--help') || args.includes('-h'),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function getFlag(long, short) {
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
if ((args[i] === long || args[i] === short) && args[i + 1]) {
|
|
27
|
+
return args[i + 1];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getPositionalTarget() {
|
|
34
|
+
// phewsh seq claude → target claude-md
|
|
35
|
+
// phewsh seq cursor → target cursorrules
|
|
36
|
+
const aliases = {
|
|
37
|
+
claude: 'claude-md',
|
|
38
|
+
cursor: 'cursorrules',
|
|
39
|
+
agent: 'agent-md',
|
|
40
|
+
soul: 'soul-md',
|
|
41
|
+
json: 'json',
|
|
42
|
+
};
|
|
43
|
+
const first = args.find(a => !a.startsWith('-'));
|
|
44
|
+
return aliases[first] || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function showHelp() {
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(` ${b(cream('phewsh sequence'))} ${slate('(phewsh seq)')}`);
|
|
50
|
+
console.log(` ${sage('Universal Memory Transform — reads all AI memory files,')}`);
|
|
51
|
+
console.log(` ${sage('emits optimal context for any target agent.')}`);
|
|
52
|
+
console.log('');
|
|
53
|
+
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')}`);
|
|
56
|
+
console.log(` ${teal('phewsh seq')} ${slate('-w')} ${sage('Write to target file')}`);
|
|
57
|
+
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')}`);
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(` ${cream('targets')}`);
|
|
61
|
+
console.log(` ${teal('claude')} ${sage('CLAUDE.md section (between markers)')}`);
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log(` ${cream('options')}`);
|
|
64
|
+
console.log(` ${teal('--budget')} ${slate('<level>')} ${sage('Token budget: minimal|standard|full|unlimited')}`);
|
|
65
|
+
console.log(` ${teal('--sources')} ${slate('<list>')} ${sage('Limit sources: intent,claude-md,claude-memory')}`);
|
|
66
|
+
console.log(` ${teal('--write, -w')} ${sage('Write output to target file')}`);
|
|
67
|
+
console.log(` ${teal('--explain, -e')} ${sage('Full ranking breakdown')}`);
|
|
68
|
+
console.log(` ${teal('--dry-run')} ${sage('Discover sources only')}`);
|
|
69
|
+
console.log('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function main() {
|
|
73
|
+
if (flags.help) { showHelp(); return; }
|
|
74
|
+
|
|
75
|
+
const sourceFilter = flags.sources ? flags.sources.split(',') : null;
|
|
76
|
+
|
|
77
|
+
// Dry run: just show what was found
|
|
78
|
+
if (flags.dryRun) {
|
|
79
|
+
const { discover } = require('../lib/sequencer/discover');
|
|
80
|
+
let sources = discover();
|
|
81
|
+
if (sourceFilter) {
|
|
82
|
+
sources = sources.filter(s => sourceFilter.some(f => s.type === f || s.type.startsWith(f)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(` ${b(cream('Sources discovered'))} ${slate(`(${sources.length})`)}`);
|
|
87
|
+
ui.divider('line');
|
|
88
|
+
if (sources.length === 0) {
|
|
89
|
+
console.log(` ${sage('No recognized memory files found in')} ${slate(process.cwd())}`);
|
|
90
|
+
} else {
|
|
91
|
+
for (const source of sources) {
|
|
92
|
+
console.log(` ${teal(source.type.padEnd(20))} ${sage(source.name)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
ui.divider('line');
|
|
96
|
+
console.log('');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Default behavior: no target = stdout summary
|
|
101
|
+
const target = flags.target || 'stdout';
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const result = sequence({
|
|
105
|
+
target,
|
|
106
|
+
budget: flags.budget,
|
|
107
|
+
sources: sourceFilter,
|
|
108
|
+
explain: flags.explain,
|
|
109
|
+
write: flags.write,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// If target is stdout, emit already printed
|
|
113
|
+
if (target === 'stdout') return;
|
|
114
|
+
|
|
115
|
+
// If writing to file
|
|
116
|
+
if (flags.write) {
|
|
117
|
+
if (result.writeResult === 'updated') {
|
|
118
|
+
console.log(`\n ${green('\u2713')} ${sage('Updated CLAUDE.md')}`);
|
|
119
|
+
} else if (result.writeResult === 'created') {
|
|
120
|
+
console.log(`\n ${green('\u2713')} ${sage('Created CLAUDE.md')}`);
|
|
121
|
+
}
|
|
122
|
+
console.log(` ${slate(`${result.chunks.length} chunks from ${result.sources.length} sources`)}`);
|
|
123
|
+
console.log('');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Otherwise print the output
|
|
128
|
+
console.log(result.output);
|
|
129
|
+
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(`\n ${ember('!')} ${sage(err.message)}\n`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = main;
|
package/commands/session.js
CHANGED
|
@@ -307,6 +307,8 @@ async function main() {
|
|
|
307
307
|
console.log(` ${teal('/reload')} ${sage('Reload .intent/ from disk')}`);
|
|
308
308
|
console.log('');
|
|
309
309
|
console.log(` ${cream('sync everywhere')}`);
|
|
310
|
+
console.log(` ${teal('/seq')} ${sage('Sequence all memory → optimal context')}`);
|
|
311
|
+
console.log(` ${teal('/seq claude')} ${sage('Sequence → write to CLAUDE.md')}`);
|
|
310
312
|
console.log(` ${teal('/watch')} ${sage('Sync .intent/ → CLAUDE.md + cloud (background)')}`);
|
|
311
313
|
console.log(` ${teal('/export')} ${sage('Export .intent/ for any AI tool')}`);
|
|
312
314
|
console.log(` ${teal('/push')} ${sage('Push to phewsh.com/intent')}`);
|
|
@@ -468,6 +470,29 @@ async function main() {
|
|
|
468
470
|
return;
|
|
469
471
|
}
|
|
470
472
|
|
|
473
|
+
if (cmd === 'seq' || cmd === 'sequence') {
|
|
474
|
+
try {
|
|
475
|
+
const { sequence } = require('../lib/sequencer');
|
|
476
|
+
const target = cmdArg?.split(/\s+/)[0];
|
|
477
|
+
const explain = cmdArg?.includes('explain') || cmdArg?.includes('-e');
|
|
478
|
+
const write = cmdArg?.includes('-w') || cmdArg?.includes('write');
|
|
479
|
+
|
|
480
|
+
if (target === 'claude' || target === 'claude-md') {
|
|
481
|
+
const result = sequence({ target: 'claude-md', write: true });
|
|
482
|
+
if (result.writeResult) {
|
|
483
|
+
console.log(`\n ${teal('●')} ${sage('CLAUDE.md ' + result.writeResult)} ${slate('(' + result.chunks.length + ' chunks)')}\n`);
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
// Default: stdout summary
|
|
487
|
+
sequence({ target: 'stdout', explain });
|
|
488
|
+
}
|
|
489
|
+
} catch (err) {
|
|
490
|
+
console.error(` ${ember('!')} ${sage('Sequence failed:')} ${err.message}`);
|
|
491
|
+
}
|
|
492
|
+
rl.prompt();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
471
496
|
if (cmd === 'export') {
|
|
472
497
|
try {
|
|
473
498
|
const { generateContext } = require('./context');
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Compress ranked chunks to fit a target token budget.
|
|
2
|
+
// Rough estimate: 1 token ~= 4 chars (conservative for markdown).
|
|
3
|
+
|
|
4
|
+
const TOKEN_BUDGETS = {
|
|
5
|
+
minimal: 500,
|
|
6
|
+
standard: 2000,
|
|
7
|
+
full: 5000,
|
|
8
|
+
unlimited: Infinity,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function estimateTokens(text) {
|
|
12
|
+
return Math.ceil(text.length / 4);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function compress(rankedChunks, budget = 'standard') {
|
|
16
|
+
const maxTokens = TOKEN_BUDGETS[budget] || TOKEN_BUDGETS.standard;
|
|
17
|
+
if (maxTokens === Infinity) return rankedChunks;
|
|
18
|
+
|
|
19
|
+
const result = [];
|
|
20
|
+
let totalTokens = 0;
|
|
21
|
+
|
|
22
|
+
for (const chunk of rankedChunks) {
|
|
23
|
+
const tokens = estimateTokens(chunk.content);
|
|
24
|
+
|
|
25
|
+
if (totalTokens + tokens <= maxTokens) {
|
|
26
|
+
result.push(chunk);
|
|
27
|
+
totalTokens += tokens;
|
|
28
|
+
} else {
|
|
29
|
+
// Partial inclusion: if chunk is large but high-weight, take first lines
|
|
30
|
+
const remainingTokens = maxTokens - totalTokens;
|
|
31
|
+
if (remainingTokens > 50 && chunk.weight > 0.3) {
|
|
32
|
+
const charBudget = remainingTokens * 4;
|
|
33
|
+
const lines = chunk.content.split('\n');
|
|
34
|
+
let partial = '';
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
if (partial.length + line.length + 1 > charBudget) break;
|
|
37
|
+
partial += line + '\n';
|
|
38
|
+
}
|
|
39
|
+
if (partial.trim()) {
|
|
40
|
+
result.push({ ...chunk, content: partial.trim(), _truncated: true });
|
|
41
|
+
totalTokens += estimateTokens(partial);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
break; // Budget hit
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { compress, estimateTokens, TOKEN_BUDGETS };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Discover all memory/context source files in the working directory.
|
|
2
|
+
// Returns a list of { path, type } for each recognized source.
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const INTENT_FILES = ['vision.md', 'plan.md', 'status.md', 'narrative.md', 'next.md'];
|
|
9
|
+
const INTENT_JSON = ['project.json', 'pps.json', 'gate.json'];
|
|
10
|
+
|
|
11
|
+
function discover(cwd = process.cwd()) {
|
|
12
|
+
const sources = [];
|
|
13
|
+
|
|
14
|
+
// .intent/ artifacts
|
|
15
|
+
const intentDir = path.join(cwd, '.intent');
|
|
16
|
+
if (fs.existsSync(intentDir)) {
|
|
17
|
+
for (const file of [...INTENT_FILES, ...INTENT_JSON]) {
|
|
18
|
+
const p = path.join(intentDir, file);
|
|
19
|
+
if (fs.existsSync(p)) {
|
|
20
|
+
sources.push({ path: p, type: 'intent', name: file });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// CLAUDE.md — split into manual and generated sections later by parser
|
|
26
|
+
const claudeMd = path.join(cwd, 'CLAUDE.md');
|
|
27
|
+
if (fs.existsSync(claudeMd)) {
|
|
28
|
+
sources.push({ path: claudeMd, type: 'claude-md', name: 'CLAUDE.md' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Claude auto-memory — project-scoped
|
|
32
|
+
// .claude/projects/<encoded-cwd>/memory/MEMORY.md
|
|
33
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
34
|
+
if (fs.existsSync(claudeDir)) {
|
|
35
|
+
const projectsDir = path.join(claudeDir, 'projects');
|
|
36
|
+
if (fs.existsSync(projectsDir)) {
|
|
37
|
+
// Claude encodes cwd as path with - replacing /
|
|
38
|
+
const encoded = cwd.replace(/\//g, '-');
|
|
39
|
+
const memoryDir = path.join(projectsDir, encoded, 'memory');
|
|
40
|
+
if (fs.existsSync(memoryDir)) {
|
|
41
|
+
const memoryIndex = path.join(memoryDir, 'MEMORY.md');
|
|
42
|
+
if (fs.existsSync(memoryIndex)) {
|
|
43
|
+
sources.push({ path: memoryIndex, type: 'claude-memory', name: 'MEMORY.md' });
|
|
44
|
+
|
|
45
|
+
// Also discover linked memory files from MEMORY.md
|
|
46
|
+
try {
|
|
47
|
+
const content = fs.readFileSync(memoryIndex, 'utf-8');
|
|
48
|
+
const linkRegex = /\[([^\]]+\.md)\]\(([^)]+\.md)\)/g;
|
|
49
|
+
let match;
|
|
50
|
+
while ((match = linkRegex.exec(content)) !== null) {
|
|
51
|
+
const linked = path.join(memoryDir, match[2]);
|
|
52
|
+
if (fs.existsSync(linked)) {
|
|
53
|
+
sources.push({ path: linked, type: 'claude-memory-file', name: match[2] });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch { /* skip */ }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// .cursorrules
|
|
63
|
+
const cursorrules = path.join(cwd, '.cursorrules');
|
|
64
|
+
if (fs.existsSync(cursorrules)) {
|
|
65
|
+
sources.push({ path: cursorrules, type: 'cursor', name: '.cursorrules' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// agent.md / AGENTS.md
|
|
69
|
+
for (const name of ['agent.md', 'AGENTS.md']) {
|
|
70
|
+
const p = path.join(cwd, name);
|
|
71
|
+
if (fs.existsSync(p)) {
|
|
72
|
+
sources.push({ path: p, type: 'agent', name });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// soul.md
|
|
77
|
+
const soulMd = path.join(cwd, 'soul.md');
|
|
78
|
+
if (fs.existsSync(soulMd)) {
|
|
79
|
+
sources.push({ path: soulMd, type: 'soul', name: 'soul.md' });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// .github/copilot-instructions.md
|
|
83
|
+
const copilot = path.join(cwd, '.github', 'copilot-instructions.md');
|
|
84
|
+
if (fs.existsSync(copilot)) {
|
|
85
|
+
sources.push({ path: copilot, type: 'copilot', name: 'copilot-instructions.md' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// README.md (low priority but useful for identity)
|
|
89
|
+
const readme = path.join(cwd, 'README.md');
|
|
90
|
+
if (fs.existsSync(readme)) {
|
|
91
|
+
sources.push({ path: readme, type: 'readme', name: 'README.md' });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return sources;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { discover };
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Emit a CLAUDE.md section from ranked, compressed chunks.
|
|
2
|
+
// This is the primary "continuity" output — what makes Claude Code
|
|
3
|
+
// instantly aware of everything across all sources.
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const START_MARKER = '<!-- PHEWSH:START -->';
|
|
9
|
+
const END_MARKER = '<!-- PHEWSH:END -->';
|
|
10
|
+
|
|
11
|
+
function emit(chunks, options = {}) {
|
|
12
|
+
const projectName = options.projectName || path.basename(process.cwd());
|
|
13
|
+
const sections = [];
|
|
14
|
+
|
|
15
|
+
sections.push(`# PHEWSH Adaptive Context — ${projectName}`);
|
|
16
|
+
sections.push(`> Auto-synced by \`phewsh seq\` | ${new Date().toISOString().split('T')[0]}`);
|
|
17
|
+
sections.push(`> This section is regenerated from ${countSources(chunks)} sources. Do not edit manually.`);
|
|
18
|
+
sections.push('');
|
|
19
|
+
|
|
20
|
+
// Group chunks by kind for structured output
|
|
21
|
+
const byKind = groupByKind(chunks);
|
|
22
|
+
|
|
23
|
+
// 1. Identity — what this project is
|
|
24
|
+
if (byKind.identity?.length > 0) {
|
|
25
|
+
sections.push('## Project');
|
|
26
|
+
for (const chunk of byKind.identity) {
|
|
27
|
+
sections.push(chunk.content);
|
|
28
|
+
}
|
|
29
|
+
sections.push('');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 2. Constraints — operational reality
|
|
33
|
+
if (byKind.constraint?.length > 0) {
|
|
34
|
+
sections.push('## Operational Reality');
|
|
35
|
+
sections.push('These constraints MUST shape every suggestion and implementation decision:');
|
|
36
|
+
sections.push('');
|
|
37
|
+
for (const chunk of byKind.constraint) {
|
|
38
|
+
// If this has structured constraint metadata, emit rich format
|
|
39
|
+
if (chunk.metadata?.constraints) {
|
|
40
|
+
sections.push(formatConstraints(chunk.metadata.constraints));
|
|
41
|
+
} else {
|
|
42
|
+
sections.push(chunk.content);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
sections.push('');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. State — what's happening now
|
|
49
|
+
if (byKind.state?.length > 0) {
|
|
50
|
+
sections.push('## Current State');
|
|
51
|
+
for (const chunk of byKind.state) {
|
|
52
|
+
sections.push(chunk.content);
|
|
53
|
+
}
|
|
54
|
+
sections.push('');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 4. Feedback — learned behaviors
|
|
58
|
+
if (byKind.feedback?.length > 0) {
|
|
59
|
+
sections.push('## Behavioral Rules');
|
|
60
|
+
sections.push('Learned from prior work:');
|
|
61
|
+
sections.push('');
|
|
62
|
+
for (const chunk of byKind.feedback) {
|
|
63
|
+
// Memory files are already concise — include as-is
|
|
64
|
+
const label = chunk.metadata?.name || chunk.source;
|
|
65
|
+
sections.push(`**${label}**: ${chunk.content.split('\n')[0]}`);
|
|
66
|
+
// If multi-line, include the rest indented
|
|
67
|
+
const rest = chunk.content.split('\n').slice(1).filter(l => l.trim());
|
|
68
|
+
if (rest.length > 0) {
|
|
69
|
+
rest.forEach(l => sections.push(` ${l}`));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
sections.push('');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 5. Context — architecture, systems, plan
|
|
76
|
+
if (byKind.context?.length > 0) {
|
|
77
|
+
sections.push('## Context');
|
|
78
|
+
for (const chunk of byKind.context) {
|
|
79
|
+
// Skip generated PHEWSH sections (they're derivative)
|
|
80
|
+
if (chunk.metadata?.generated) continue;
|
|
81
|
+
sections.push(chunk.content);
|
|
82
|
+
}
|
|
83
|
+
sections.push('');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 6. Actions — current tasks
|
|
87
|
+
if (byKind.action?.length > 0) {
|
|
88
|
+
sections.push('## Active Actions');
|
|
89
|
+
for (const chunk of byKind.action) {
|
|
90
|
+
sections.push(chunk.content);
|
|
91
|
+
}
|
|
92
|
+
sections.push('');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 7. References — pointers to external systems
|
|
96
|
+
if (byKind.reference?.length > 0) {
|
|
97
|
+
sections.push('## References');
|
|
98
|
+
for (const chunk of byKind.reference) {
|
|
99
|
+
const label = chunk.metadata?.name || chunk.source;
|
|
100
|
+
sections.push(`- **${label}**: ${chunk.content.split('\n')[0]}`);
|
|
101
|
+
}
|
|
102
|
+
sections.push('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
sections.push('---');
|
|
106
|
+
sections.push('*Auto-synced from [PHEWSH](https://phewsh.com/intent)*');
|
|
107
|
+
|
|
108
|
+
return sections.join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatConstraints(c) {
|
|
112
|
+
const lines = [];
|
|
113
|
+
|
|
114
|
+
if (c.budget > 0) {
|
|
115
|
+
lines.push(`- Budget: $${c.budget}. ${
|
|
116
|
+
c.budget < 100 ? 'Extremely tight — free/open-source only.'
|
|
117
|
+
: c.budget < 500 ? 'Limited — justify any paid tool.'
|
|
118
|
+
: c.budget < 2000 ? 'Moderate — strategic spending OK.'
|
|
119
|
+
: 'Substantial — professional tools welcome.'
|
|
120
|
+
}`);
|
|
121
|
+
}
|
|
122
|
+
if (c.timeHoursPerWeek > 0) {
|
|
123
|
+
lines.push(`- Time: ${c.timeHoursPerWeek} hrs/week. ${
|
|
124
|
+
c.timeHoursPerWeek <= 5 ? 'Micro-steps only.'
|
|
125
|
+
: c.timeHoursPerWeek <= 15 ? 'Part-time. Clear stopping points.'
|
|
126
|
+
: c.timeHoursPerWeek <= 30 ? 'Near full-time.'
|
|
127
|
+
: 'Full-time+.'
|
|
128
|
+
}`);
|
|
129
|
+
}
|
|
130
|
+
if (c.skillLevel) lines.push(`- Skill: ${c.skillLevel}`);
|
|
131
|
+
if (c.urgency) lines.push(`- Urgency: ${c.urgency}`);
|
|
132
|
+
if (c.autonomy) lines.push(`- Autonomy: ${c.autonomy}`);
|
|
133
|
+
|
|
134
|
+
return lines.join('\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function groupByKind(chunks) {
|
|
138
|
+
const groups = {};
|
|
139
|
+
for (const chunk of chunks) {
|
|
140
|
+
if (!groups[chunk.kind]) groups[chunk.kind] = [];
|
|
141
|
+
groups[chunk.kind].push(chunk);
|
|
142
|
+
}
|
|
143
|
+
return groups;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function countSources(chunks) {
|
|
147
|
+
return new Set(chunks.map(c => c.source.split(':')[0])).size;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Write the emitted content into CLAUDE.md (between markers)
|
|
151
|
+
function writeToFile(content, cwd = process.cwd()) {
|
|
152
|
+
const claudePath = path.join(cwd, 'CLAUDE.md');
|
|
153
|
+
const wrapped = `${START_MARKER}\n${content}\n${END_MARKER}`;
|
|
154
|
+
|
|
155
|
+
if (fs.existsSync(claudePath)) {
|
|
156
|
+
let existing = fs.readFileSync(claudePath, 'utf-8');
|
|
157
|
+
const startIdx = existing.indexOf(START_MARKER);
|
|
158
|
+
const endIdx = existing.indexOf(END_MARKER);
|
|
159
|
+
|
|
160
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
161
|
+
existing = existing.slice(0, startIdx) + wrapped + existing.slice(endIdx + END_MARKER.length);
|
|
162
|
+
} else {
|
|
163
|
+
existing = existing.trimEnd() + '\n\n' + wrapped + '\n';
|
|
164
|
+
}
|
|
165
|
+
fs.writeFileSync(claudePath, existing);
|
|
166
|
+
return 'updated';
|
|
167
|
+
} else {
|
|
168
|
+
fs.writeFileSync(claudePath, wrapped + '\n');
|
|
169
|
+
return 'created';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { emit, writeToFile };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Human-readable terminal output of the sequencing result.
|
|
2
|
+
// Shows what was found, how it was ranked, and the final output.
|
|
3
|
+
|
|
4
|
+
const ui = require('../../ui');
|
|
5
|
+
|
|
6
|
+
function emit(chunks, options = {}) {
|
|
7
|
+
const { explain = false, sources = [] } = options;
|
|
8
|
+
const { b, w, g, sage, slate, teal, cream, green, yellow, ember, peach } = ui;
|
|
9
|
+
|
|
10
|
+
console.log('');
|
|
11
|
+
|
|
12
|
+
if (explain) {
|
|
13
|
+
return emitExplain(chunks, sources, ui);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Compact summary
|
|
17
|
+
const sourceCount = new Set(chunks.map(c => c.source.split(':')[0])).size;
|
|
18
|
+
const kindCounts = {};
|
|
19
|
+
for (const chunk of chunks) {
|
|
20
|
+
kindCounts[chunk.kind] = (kindCounts[chunk.kind] || 0) + 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log(` ${b(cream('Sequenced'))} ${teal(String(chunks.length))} ${sage('chunks from')} ${teal(String(sourceCount))} ${sage('sources')}`);
|
|
24
|
+
ui.divider('line');
|
|
25
|
+
|
|
26
|
+
// Show kind breakdown
|
|
27
|
+
const kindOrder = ['constraint', 'identity', 'feedback', 'state', 'action', 'context', 'reference'];
|
|
28
|
+
for (const kind of kindOrder) {
|
|
29
|
+
if (!kindCounts[kind]) continue;
|
|
30
|
+
const icon = KIND_ICONS[kind] || '?';
|
|
31
|
+
console.log(` ${icon} ${cream(kind.padEnd(12))} ${slate(String(kindCounts[kind]))}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
ui.divider('line');
|
|
35
|
+
|
|
36
|
+
// Show top 5 chunks by weight
|
|
37
|
+
console.log(` ${sage('top signal:')}`);
|
|
38
|
+
const top = chunks.slice(0, 5);
|
|
39
|
+
for (const chunk of top) {
|
|
40
|
+
const weight = chunk.weight.toFixed(2);
|
|
41
|
+
const preview = chunk.content.split('\n')[0].slice(0, 60);
|
|
42
|
+
console.log(` ${slate(weight)} ${teal(chunk.kind.padEnd(10))} ${sage(preview)}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Token estimate
|
|
46
|
+
const totalChars = chunks.reduce((sum, c) => sum + c.content.length, 0);
|
|
47
|
+
const estTokens = Math.ceil(totalChars / 4);
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(` ${slate(`~${estTokens} tokens`)}`);
|
|
50
|
+
console.log('');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function emitExplain(chunks, sources, { b, w, g, sage, slate, teal, cream, green, yellow, ember }) {
|
|
54
|
+
// Full ranking breakdown
|
|
55
|
+
console.log(` ${b(cream('Sequencer Explain'))}`);
|
|
56
|
+
console.log('');
|
|
57
|
+
|
|
58
|
+
// Sources discovered
|
|
59
|
+
console.log(` ${b(cream('Sources'))}`);
|
|
60
|
+
for (const source of sources) {
|
|
61
|
+
console.log(` ${teal(source.type.padEnd(16))} ${sage(source.name)}`);
|
|
62
|
+
}
|
|
63
|
+
console.log('');
|
|
64
|
+
|
|
65
|
+
// All chunks with full weight breakdown
|
|
66
|
+
console.log(` ${b(cream('Chunks'))} ${slate(`(${chunks.length} total, ranked by weight)`)}`);
|
|
67
|
+
console.log('');
|
|
68
|
+
|
|
69
|
+
for (const chunk of chunks) {
|
|
70
|
+
const weight = chunk.weight.toFixed(3);
|
|
71
|
+
const preview = chunk.content.split('\n')[0].slice(0, 50);
|
|
72
|
+
const truncated = chunk._truncated ? ` ${ember('[truncated]')}` : '';
|
|
73
|
+
console.log(` ${cream(weight)} ${teal(chunk.kind.padEnd(12))} ${sage(chunk.source)}`);
|
|
74
|
+
console.log(` ${slate(preview)}${truncated}`);
|
|
75
|
+
}
|
|
76
|
+
console.log('');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const KIND_ICONS = {
|
|
80
|
+
constraint: '\u2588', // solid block — constraints are load-bearing
|
|
81
|
+
identity: '\u25C6', // diamond — core
|
|
82
|
+
feedback: '\u25B6', // triangle — directional
|
|
83
|
+
state: '\u25CF', // circle — current
|
|
84
|
+
action: '\u25A0', // square — tasks
|
|
85
|
+
context: '\u25CB', // open circle — background
|
|
86
|
+
reference: '\u25B7', // open triangle — pointer
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
module.exports = { emit };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Sequencer — Universal Memory Transform Layer
|
|
2
|
+
//
|
|
3
|
+
// The core pipeline: discover → parse → rank → compress → emit
|
|
4
|
+
// One function. N inputs. 1 optimal output per target format.
|
|
5
|
+
|
|
6
|
+
const { discover } = require('./discover');
|
|
7
|
+
const { rank } = require('./ranker');
|
|
8
|
+
const { compress } = require('./compressor');
|
|
9
|
+
|
|
10
|
+
// Parsers by source type
|
|
11
|
+
const parsers = {
|
|
12
|
+
intent: require('./parsers/intent'),
|
|
13
|
+
'claude-md': require('./parsers/claude-md'),
|
|
14
|
+
'claude-memory': require('./parsers/claude-memory'),
|
|
15
|
+
'claude-memory-file': require('./parsers/claude-memory'),
|
|
16
|
+
cursor: require('./parsers/generic'),
|
|
17
|
+
agent: require('./parsers/generic'),
|
|
18
|
+
soul: require('./parsers/generic'),
|
|
19
|
+
copilot: require('./parsers/generic'),
|
|
20
|
+
readme: require('./parsers/generic'),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Emitters by target format
|
|
24
|
+
const emitters = {
|
|
25
|
+
'claude-md': require('./emitters/claude-md'),
|
|
26
|
+
stdout: require('./emitters/stdout'),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Run the full sequencing pipeline.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} options
|
|
33
|
+
* @param {string} options.target - Output format: 'claude-md' | 'stdout' | 'json'
|
|
34
|
+
* @param {string} options.budget - Token budget: 'minimal' | 'standard' | 'full' | 'unlimited'
|
|
35
|
+
* @param {string[]} options.sources - Limit to specific source types (null = all)
|
|
36
|
+
* @param {boolean} options.explain - Show full ranking breakdown
|
|
37
|
+
* @param {boolean} options.write - Write to file (for claude-md target)
|
|
38
|
+
* @param {string} options.cwd - Working directory
|
|
39
|
+
* @returns {{ chunks: object[], output: string, sources: object[] }}
|
|
40
|
+
*/
|
|
41
|
+
function sequence(options = {}) {
|
|
42
|
+
const {
|
|
43
|
+
target = 'claude-md',
|
|
44
|
+
budget = 'standard',
|
|
45
|
+
sources: sourceFilter = null,
|
|
46
|
+
explain = false,
|
|
47
|
+
write = false,
|
|
48
|
+
cwd = process.cwd(),
|
|
49
|
+
} = options;
|
|
50
|
+
|
|
51
|
+
// 1. Discover all source files
|
|
52
|
+
let sources = discover(cwd);
|
|
53
|
+
|
|
54
|
+
// Filter sources if requested
|
|
55
|
+
if (sourceFilter && sourceFilter.length > 0) {
|
|
56
|
+
sources = sources.filter(s =>
|
|
57
|
+
sourceFilter.some(f => s.type === f || s.type.startsWith(f))
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Parse all sources into chunks
|
|
62
|
+
let chunks = [];
|
|
63
|
+
for (const source of sources) {
|
|
64
|
+
const parser = parsers[source.type];
|
|
65
|
+
if (!parser) continue;
|
|
66
|
+
|
|
67
|
+
const parsed = parser.parse(source);
|
|
68
|
+
chunks.push(...parsed);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Rank all chunks
|
|
72
|
+
chunks = rank(chunks);
|
|
73
|
+
|
|
74
|
+
// 4. Compress to budget
|
|
75
|
+
chunks = compress(chunks, budget);
|
|
76
|
+
|
|
77
|
+
// 5. Emit
|
|
78
|
+
const emitter = emitters[target];
|
|
79
|
+
if (!emitter) {
|
|
80
|
+
throw new Error(`Unknown target format: ${target}. Available: ${Object.keys(emitters).join(', ')}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (target === 'stdout') {
|
|
84
|
+
emitter.emit(chunks, { explain, sources });
|
|
85
|
+
return { chunks, output: null, sources };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const output = emitter.emit(chunks, {
|
|
89
|
+
projectName: getProjectName(chunks, cwd),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Write to file if requested
|
|
93
|
+
if (write && emitter.writeToFile) {
|
|
94
|
+
const result = emitter.writeToFile(output, cwd);
|
|
95
|
+
return { chunks, output, sources, writeResult: result };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { chunks, output, sources };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get project name from chunks or fall back to directory name.
|
|
103
|
+
*/
|
|
104
|
+
function getProjectName(chunks, cwd) {
|
|
105
|
+
// Check identity chunks from intent source for a project name
|
|
106
|
+
for (const chunk of chunks) {
|
|
107
|
+
if (chunk.sourceType === 'intent' && chunk.kind === 'identity') {
|
|
108
|
+
const nameMatch = chunk.content.match(/\*\*Name\*\*:\s*(.+)/);
|
|
109
|
+
if (nameMatch) return nameMatch[1].trim();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const path = require('path');
|
|
113
|
+
return path.basename(cwd);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { sequence };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Parse CLAUDE.md into MemoryChunks.
|
|
2
|
+
// Critical distinction: manual sections (user-curated, high authority)
|
|
3
|
+
// vs generated sections (PHEWSH markers, lower authority — derivative).
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const PHEWSH_START = '<!-- PHEWSH:START -->';
|
|
8
|
+
const PHEWSH_END = '<!-- PHEWSH:END -->';
|
|
9
|
+
|
|
10
|
+
function parse(source) {
|
|
11
|
+
const content = fs.readFileSync(source.path, 'utf-8');
|
|
12
|
+
const mtime = fs.statSync(source.path).mtime.toISOString();
|
|
13
|
+
const chunks = [];
|
|
14
|
+
|
|
15
|
+
const startIdx = content.indexOf(PHEWSH_START);
|
|
16
|
+
const endIdx = content.indexOf(PHEWSH_END);
|
|
17
|
+
|
|
18
|
+
let manualContent = content;
|
|
19
|
+
let generatedContent = null;
|
|
20
|
+
|
|
21
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
22
|
+
// Split manual from generated
|
|
23
|
+
manualContent = (
|
|
24
|
+
content.slice(0, startIdx) +
|
|
25
|
+
content.slice(endIdx + PHEWSH_END.length)
|
|
26
|
+
).trim();
|
|
27
|
+
|
|
28
|
+
generatedContent = content.slice(
|
|
29
|
+
startIdx + PHEWSH_START.length,
|
|
30
|
+
endIdx
|
|
31
|
+
).trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Parse manual sections
|
|
35
|
+
if (manualContent) {
|
|
36
|
+
const sections = splitSections(manualContent);
|
|
37
|
+
for (const section of sections) {
|
|
38
|
+
if (!section.body.trim()) continue;
|
|
39
|
+
|
|
40
|
+
const kind = classifySection(section.title);
|
|
41
|
+
chunks.push({
|
|
42
|
+
source: `CLAUDE.md:${section.title || 'root'}`,
|
|
43
|
+
sourceType: 'claude-md-manual',
|
|
44
|
+
kind,
|
|
45
|
+
content: section.body.trim(),
|
|
46
|
+
timestamp: mtime,
|
|
47
|
+
metadata: { section: section.title },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Generated section — lower authority, will get deduped against .intent/
|
|
53
|
+
if (generatedContent) {
|
|
54
|
+
chunks.push({
|
|
55
|
+
source: 'CLAUDE.md:phewsh-generated',
|
|
56
|
+
sourceType: 'claude-md-generated',
|
|
57
|
+
kind: 'context',
|
|
58
|
+
content: generatedContent,
|
|
59
|
+
timestamp: mtime,
|
|
60
|
+
metadata: { generated: true },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return chunks;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function splitSections(content) {
|
|
68
|
+
const sections = [];
|
|
69
|
+
let current = { title: '', body: '' };
|
|
70
|
+
|
|
71
|
+
for (const line of content.split('\n')) {
|
|
72
|
+
const heading = line.match(/^(#{1,2})\s+(.+)/);
|
|
73
|
+
if (heading) {
|
|
74
|
+
if (current.body.trim() || current.title) {
|
|
75
|
+
sections.push(current);
|
|
76
|
+
}
|
|
77
|
+
current = { title: heading[2], body: '' };
|
|
78
|
+
} else {
|
|
79
|
+
current.body += line + '\n';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (current.body.trim() || current.title) {
|
|
83
|
+
sections.push(current);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return sections;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function classifySection(title) {
|
|
90
|
+
if (!title) return 'context';
|
|
91
|
+
const t = title.toLowerCase();
|
|
92
|
+
|
|
93
|
+
if (/architect|structure|infrastructure|system|stack/.test(t)) return 'context';
|
|
94
|
+
if (/convention|rule|style|format|lint/.test(t)) return 'feedback';
|
|
95
|
+
if (/deploy|build|ship|ci|cd/.test(t)) return 'context';
|
|
96
|
+
if (/constraint|budget|time|limit/.test(t)) return 'constraint';
|
|
97
|
+
if (/product|about|what|mission|vision/.test(t)) return 'identity';
|
|
98
|
+
if (/command|usage|api|endpoint/.test(t)) return 'reference';
|
|
99
|
+
if (/status|progress|current|state/.test(t)) return 'state';
|
|
100
|
+
if (/key|secret|env|config/.test(t)) return 'reference';
|
|
101
|
+
if (/monetiz|payment|stripe|billing/.test(t)) return 'context';
|
|
102
|
+
|
|
103
|
+
return 'context';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { parse };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Parse Claude auto-memory files into MemoryChunks.
|
|
2
|
+
// Reads individual .md files with YAML-like frontmatter (name, description, type).
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
function parseFrontmatter(content) {
|
|
7
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
8
|
+
if (!match) return { meta: {}, body: content };
|
|
9
|
+
|
|
10
|
+
const meta = {};
|
|
11
|
+
for (const line of match[1].split('\n')) {
|
|
12
|
+
const kv = line.match(/^(\w+):\s*(.+)$/);
|
|
13
|
+
if (kv) meta[kv[1]] = kv[2].trim();
|
|
14
|
+
}
|
|
15
|
+
return { meta, body: match[2] };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Map Claude memory types to our chunk kinds
|
|
19
|
+
const TYPE_TO_KIND = {
|
|
20
|
+
user: 'identity',
|
|
21
|
+
feedback: 'feedback',
|
|
22
|
+
project: 'context',
|
|
23
|
+
reference: 'reference',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function parse(source) {
|
|
27
|
+
const content = fs.readFileSync(source.path, 'utf-8');
|
|
28
|
+
const mtime = fs.statSync(source.path).mtime.toISOString();
|
|
29
|
+
|
|
30
|
+
// MEMORY.md index file — skip, we read the linked files directly
|
|
31
|
+
if (source.type === 'claude-memory') return [];
|
|
32
|
+
|
|
33
|
+
// Individual memory files
|
|
34
|
+
const { meta, body } = parseFrontmatter(content);
|
|
35
|
+
if (!body.trim()) return [];
|
|
36
|
+
|
|
37
|
+
const kind = TYPE_TO_KIND[meta.type] || 'context';
|
|
38
|
+
|
|
39
|
+
return [{
|
|
40
|
+
source: `claude-memory:${source.name}`,
|
|
41
|
+
sourceType: 'claude-memory-file',
|
|
42
|
+
kind,
|
|
43
|
+
content: body.trim(),
|
|
44
|
+
timestamp: mtime,
|
|
45
|
+
metadata: {
|
|
46
|
+
name: meta.name,
|
|
47
|
+
description: meta.description,
|
|
48
|
+
memoryType: meta.type,
|
|
49
|
+
},
|
|
50
|
+
}];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { parse };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Generic parser for .cursorrules, agent.md, soul.md, README.md, copilot-instructions.md
|
|
2
|
+
// These are all "read the whole file as one chunk" sources with type-appropriate kind.
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
const TYPE_TO_KIND = {
|
|
7
|
+
cursor: 'feedback', // .cursorrules are behavioral rules
|
|
8
|
+
agent: 'identity', // agent.md defines agent capabilities
|
|
9
|
+
soul: 'identity', // soul.md defines project soul
|
|
10
|
+
copilot: 'feedback', // copilot-instructions are behavioral rules
|
|
11
|
+
readme: 'context', // README is broad project context
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function parse(source) {
|
|
15
|
+
const content = fs.readFileSync(source.path, 'utf-8');
|
|
16
|
+
if (!content.trim()) return [];
|
|
17
|
+
|
|
18
|
+
const mtime = fs.statSync(source.path).mtime.toISOString();
|
|
19
|
+
const kind = TYPE_TO_KIND[source.type] || 'context';
|
|
20
|
+
|
|
21
|
+
return [{
|
|
22
|
+
source: source.name,
|
|
23
|
+
sourceType: source.type,
|
|
24
|
+
kind,
|
|
25
|
+
content: content.trim(),
|
|
26
|
+
timestamp: mtime,
|
|
27
|
+
metadata: {},
|
|
28
|
+
}];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { parse };
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Parse .intent/ artifacts into MemoryChunks.
|
|
2
|
+
// This is the highest-authority source — user-authored canonical intent.
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function parseFrontmatter(content) {
|
|
8
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
9
|
+
if (!match) return { meta: {}, body: content };
|
|
10
|
+
|
|
11
|
+
const meta = {};
|
|
12
|
+
for (const line of match[1].split('\n')) {
|
|
13
|
+
const kv = line.match(/^(\w+):\s*(.+)$/);
|
|
14
|
+
if (kv) meta[kv[1]] = kv[2].trim();
|
|
15
|
+
}
|
|
16
|
+
return { meta, body: match[2] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function extractSections(body) {
|
|
20
|
+
const sections = [];
|
|
21
|
+
let current = null;
|
|
22
|
+
|
|
23
|
+
for (const line of body.split('\n')) {
|
|
24
|
+
const heading = line.match(/^##\s+(.+)/);
|
|
25
|
+
if (heading) {
|
|
26
|
+
if (current) sections.push(current);
|
|
27
|
+
current = { title: heading[1], lines: [] };
|
|
28
|
+
} else if (current) {
|
|
29
|
+
current.lines.push(line);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (current) sections.push(current);
|
|
33
|
+
return sections;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseVision(filePath) {
|
|
37
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
38
|
+
const { meta, body } = parseFrontmatter(content);
|
|
39
|
+
const mtime = fs.statSync(filePath).mtime.toISOString();
|
|
40
|
+
const timestamp = meta.updated || meta.created || mtime;
|
|
41
|
+
const chunks = [];
|
|
42
|
+
|
|
43
|
+
// Whole vision as identity
|
|
44
|
+
chunks.push({
|
|
45
|
+
source: '.intent/vision.md',
|
|
46
|
+
sourceType: 'intent',
|
|
47
|
+
kind: 'identity',
|
|
48
|
+
content: body.trim(),
|
|
49
|
+
timestamp,
|
|
50
|
+
metadata: { frontmatter: meta },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return chunks;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parsePlan(filePath) {
|
|
57
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
58
|
+
const { meta, body } = parseFrontmatter(content);
|
|
59
|
+
const mtime = fs.statSync(filePath).mtime.toISOString();
|
|
60
|
+
const timestamp = meta.updated || meta.created || mtime;
|
|
61
|
+
|
|
62
|
+
return [{
|
|
63
|
+
source: '.intent/plan.md',
|
|
64
|
+
sourceType: 'intent',
|
|
65
|
+
kind: 'context',
|
|
66
|
+
content: body.trim(),
|
|
67
|
+
timestamp,
|
|
68
|
+
metadata: { frontmatter: meta },
|
|
69
|
+
}];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseStatus(filePath) {
|
|
73
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
74
|
+
const { meta, body } = parseFrontmatter(content);
|
|
75
|
+
const mtime = fs.statSync(filePath).mtime.toISOString();
|
|
76
|
+
const timestamp = meta.updated || meta.created || mtime;
|
|
77
|
+
|
|
78
|
+
return [{
|
|
79
|
+
source: '.intent/status.md',
|
|
80
|
+
sourceType: 'intent',
|
|
81
|
+
kind: 'state',
|
|
82
|
+
content: body.trim(),
|
|
83
|
+
timestamp,
|
|
84
|
+
metadata: { frontmatter: meta },
|
|
85
|
+
}];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseNext(filePath) {
|
|
89
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
90
|
+
const { meta, body } = parseFrontmatter(content);
|
|
91
|
+
const mtime = fs.statSync(filePath).mtime.toISOString();
|
|
92
|
+
const timestamp = meta.updated || meta.created || mtime;
|
|
93
|
+
|
|
94
|
+
return [{
|
|
95
|
+
source: '.intent/next.md',
|
|
96
|
+
sourceType: 'intent',
|
|
97
|
+
kind: 'state',
|
|
98
|
+
content: body.trim(),
|
|
99
|
+
timestamp,
|
|
100
|
+
metadata: { frontmatter: meta },
|
|
101
|
+
}];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseNarrative(filePath) {
|
|
105
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
106
|
+
const { meta, body } = parseFrontmatter(content);
|
|
107
|
+
const mtime = fs.statSync(filePath).mtime.toISOString();
|
|
108
|
+
const timestamp = meta.updated || meta.created || mtime;
|
|
109
|
+
|
|
110
|
+
return [{
|
|
111
|
+
source: '.intent/narrative.md',
|
|
112
|
+
sourceType: 'intent',
|
|
113
|
+
kind: 'identity',
|
|
114
|
+
content: body.trim(),
|
|
115
|
+
timestamp,
|
|
116
|
+
metadata: { frontmatter: meta },
|
|
117
|
+
}];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseProjectJson(filePath) {
|
|
121
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
122
|
+
const mtime = fs.statSync(filePath).mtime.toISOString();
|
|
123
|
+
let data;
|
|
124
|
+
try { data = JSON.parse(raw); } catch { return []; }
|
|
125
|
+
|
|
126
|
+
const chunks = [];
|
|
127
|
+
|
|
128
|
+
// Project identity
|
|
129
|
+
const identityParts = [];
|
|
130
|
+
if (data.name) identityParts.push(`**Name**: ${data.name}`);
|
|
131
|
+
if (data.tldr) identityParts.push(`**TLDR**: ${data.tldr}`);
|
|
132
|
+
if (data.archetype) identityParts.push(`**Type**: ${data.archetype}`);
|
|
133
|
+
if (identityParts.length > 0) {
|
|
134
|
+
chunks.push({
|
|
135
|
+
source: '.intent/project.json',
|
|
136
|
+
sourceType: 'intent',
|
|
137
|
+
kind: 'identity',
|
|
138
|
+
content: identityParts.join('\n'),
|
|
139
|
+
timestamp: mtime,
|
|
140
|
+
metadata: {},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Decision gate constraints
|
|
145
|
+
const gate = data.decisionGate;
|
|
146
|
+
if (gate?.constraints) {
|
|
147
|
+
const c = gate.constraints;
|
|
148
|
+
const lines = [];
|
|
149
|
+
if (c.budget > 0) lines.push(`Budget: $${c.budget}`);
|
|
150
|
+
if (c.timeHoursPerWeek > 0) lines.push(`Time: ${c.timeHoursPerWeek} hrs/week`);
|
|
151
|
+
if (c.skillLevel) lines.push(`Skill: ${c.skillLevel}`);
|
|
152
|
+
if (c.urgency) lines.push(`Urgency: ${c.urgency}`);
|
|
153
|
+
if (c.autonomy) lines.push(`Autonomy: ${c.autonomy}`);
|
|
154
|
+
|
|
155
|
+
chunks.push({
|
|
156
|
+
source: '.intent/project.json:constraints',
|
|
157
|
+
sourceType: 'intent',
|
|
158
|
+
kind: 'constraint',
|
|
159
|
+
content: lines.join('\n'),
|
|
160
|
+
timestamp: gate.createdAt || mtime,
|
|
161
|
+
metadata: { constraints: c },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Success criteria
|
|
166
|
+
if (gate?.successCriteria?.length > 0) {
|
|
167
|
+
chunks.push({
|
|
168
|
+
source: '.intent/project.json:success',
|
|
169
|
+
sourceType: 'intent',
|
|
170
|
+
kind: 'identity',
|
|
171
|
+
content: 'Success criteria:\n' + gate.successCriteria.map(c => `- ${c}`).join('\n'),
|
|
172
|
+
timestamp: gate.createdAt || mtime,
|
|
173
|
+
metadata: {},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Responsibility split
|
|
178
|
+
if (gate?.responsibilitySplit) {
|
|
179
|
+
const lines = [];
|
|
180
|
+
if (gate.responsibilitySplit.ai?.length > 0) {
|
|
181
|
+
lines.push('AI can handle:');
|
|
182
|
+
gate.responsibilitySplit.ai.forEach(r => lines.push(`- ${r}`));
|
|
183
|
+
}
|
|
184
|
+
if (gate.responsibilitySplit.human?.length > 0) {
|
|
185
|
+
lines.push('Requires human:');
|
|
186
|
+
gate.responsibilitySplit.human.forEach(r => lines.push(`- ${r}`));
|
|
187
|
+
}
|
|
188
|
+
if (lines.length > 0) {
|
|
189
|
+
chunks.push({
|
|
190
|
+
source: '.intent/project.json:responsibilities',
|
|
191
|
+
sourceType: 'intent',
|
|
192
|
+
kind: 'constraint',
|
|
193
|
+
content: lines.join('\n'),
|
|
194
|
+
timestamp: gate.createdAt || mtime,
|
|
195
|
+
metadata: {},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Actions as state
|
|
201
|
+
if (data.actions?.length > 0) {
|
|
202
|
+
const active = data.actions.filter(a => a.state !== 'reconciled');
|
|
203
|
+
if (active.length > 0) {
|
|
204
|
+
chunks.push({
|
|
205
|
+
source: '.intent/project.json:actions',
|
|
206
|
+
sourceType: 'intent',
|
|
207
|
+
kind: 'action',
|
|
208
|
+
content: active.map(a => `- [${a.state}] ${a.intent} (${a.category})`).join('\n'),
|
|
209
|
+
timestamp: mtime,
|
|
210
|
+
metadata: {},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return chunks;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const FILE_PARSERS = {
|
|
219
|
+
'vision.md': parseVision,
|
|
220
|
+
'plan.md': parsePlan,
|
|
221
|
+
'status.md': parseStatus,
|
|
222
|
+
'next.md': parseNext,
|
|
223
|
+
'narrative.md': parseNarrative,
|
|
224
|
+
'project.json': parseProjectJson,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
function parse(source) {
|
|
228
|
+
const parser = FILE_PARSERS[source.name];
|
|
229
|
+
if (!parser) return [];
|
|
230
|
+
try {
|
|
231
|
+
return parser(source.path);
|
|
232
|
+
} catch {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { parse };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Rank memory chunks by recency, impact, source authority, and deduplication.
|
|
2
|
+
// Weight = recency * impact * sourceAuthority * dedupPenalty
|
|
3
|
+
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
// Impact by chunk kind — what matters most across all contexts
|
|
7
|
+
const KIND_IMPACT = {
|
|
8
|
+
constraint: 1.0,
|
|
9
|
+
identity: 0.9,
|
|
10
|
+
feedback: 0.8,
|
|
11
|
+
state: 0.7,
|
|
12
|
+
action: 0.6,
|
|
13
|
+
reference: 0.4,
|
|
14
|
+
context: 0.5,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Source authority — how much to trust this origin
|
|
18
|
+
const SOURCE_AUTHORITY = {
|
|
19
|
+
'intent': 1.0, // User-authored canonical intent
|
|
20
|
+
'claude-md-manual': 0.9, // User-curated CLAUDE.md sections
|
|
21
|
+
'claude-md-generated': 0.5, // Machine-generated CLAUDE.md sections (derivative)
|
|
22
|
+
'agent': 0.8, // Deliberate agent identity docs
|
|
23
|
+
'soul': 0.8, // Project soul/values
|
|
24
|
+
'claude-memory-file': 0.7, // AI-observed memories (may be stale)
|
|
25
|
+
'claude-memory': 0.6, // Memory index (pointers, not content)
|
|
26
|
+
'cursor': 0.7, // Tool-specific rules
|
|
27
|
+
'copilot': 0.7, // Tool-specific rules
|
|
28
|
+
'readme': 0.4, // Often stale, broad
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function recencyScore(timestamp) {
|
|
32
|
+
if (!timestamp) return 0.3;
|
|
33
|
+
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const then = new Date(timestamp).getTime();
|
|
36
|
+
if (isNaN(then)) return 0.3;
|
|
37
|
+
|
|
38
|
+
const daysAgo = (now - then) / (1000 * 60 * 60 * 24);
|
|
39
|
+
|
|
40
|
+
if (daysAgo < 1) return 1.0;
|
|
41
|
+
if (daysAgo < 7) return 0.8;
|
|
42
|
+
if (daysAgo < 30) return 0.6;
|
|
43
|
+
return 0.3;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function contentHash(content) {
|
|
47
|
+
return crypto.createHash('md5').update(content.trim().toLowerCase()).digest('hex');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function rank(chunks) {
|
|
51
|
+
// Assign raw weights
|
|
52
|
+
for (const chunk of chunks) {
|
|
53
|
+
const recency = recencyScore(chunk.timestamp);
|
|
54
|
+
const impact = KIND_IMPACT[chunk.kind] || 0.5;
|
|
55
|
+
const authority = SOURCE_AUTHORITY[chunk.sourceType] || 0.5;
|
|
56
|
+
chunk.weight = recency * impact * authority;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Deduplication: penalize near-duplicate content across sources
|
|
60
|
+
const seen = new Map(); // hash → highest-weight chunk
|
|
61
|
+
for (const chunk of chunks) {
|
|
62
|
+
const hash = contentHash(chunk.content);
|
|
63
|
+
chunk._hash = hash;
|
|
64
|
+
|
|
65
|
+
if (seen.has(hash)) {
|
|
66
|
+
const existing = seen.get(hash);
|
|
67
|
+
if (chunk.weight > existing.weight) {
|
|
68
|
+
existing.weight *= 0.05; // near-zero the weaker duplicate
|
|
69
|
+
seen.set(hash, chunk);
|
|
70
|
+
} else {
|
|
71
|
+
chunk.weight *= 0.05;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
seen.set(hash, chunk);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Sort by weight descending
|
|
79
|
+
chunks.sort((a, b) => b.weight - a.weight);
|
|
80
|
+
|
|
81
|
+
return chunks;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { rank, recencyScore, KIND_IMPACT, SOURCE_AUTHORITY };
|