@kevin0181/memoc 1.1.3 → 1.1.4
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/README.md +12 -0
- package/bin/cli.js +787 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,9 +93,18 @@ npx @kevin0181/memoc search "auth" --snippets --limit 5
|
|
|
93
93
|
npx @kevin0181/memoc grep "GetParticles"
|
|
94
94
|
npx @kevin0181/memoc grep "GetParticles" --snippets --limit 5
|
|
95
95
|
|
|
96
|
+
# Create raw/source records and durable wiki topic notes
|
|
97
|
+
npx @kevin0181/memoc ingest path/to/source.md
|
|
98
|
+
npx @kevin0181/memoc ingest https://example.com/spec
|
|
99
|
+
npx @kevin0181/memoc note "Auth flow comparison"
|
|
100
|
+
npx @kevin0181/memoc lint-wiki
|
|
101
|
+
|
|
96
102
|
# Estimate token cost of current memory files
|
|
97
103
|
npx @kevin0181/memoc tokens
|
|
98
104
|
|
|
105
|
+
# Archive and compact an oversized startup summary
|
|
106
|
+
npx @kevin0181/memoc trim-summary
|
|
107
|
+
|
|
99
108
|
# Archive old log entries to keep log.md small
|
|
100
109
|
npx @kevin0181/memoc compress
|
|
101
110
|
|
|
@@ -156,6 +165,7 @@ llms.txt ← LLM-facing project map
|
|
|
156
165
|
04-handoff.md ← Resume context, verified/unverified
|
|
157
166
|
06-project-rules.md ← User preferences
|
|
158
167
|
log.md ← Append-only activity log
|
|
168
|
+
raw/ ← Immutable source material, not a startup read
|
|
159
169
|
systems/ ← Subsystem docs
|
|
160
170
|
wiki/ ← Synthesized knowledge base
|
|
161
171
|
|
|
@@ -202,6 +212,8 @@ Startup cost is kept minimal by design.
|
|
|
202
212
|
|
|
203
213
|
Everything else is on-demand. Use `memoc tokens` to see the live breakdown for your project.
|
|
204
214
|
|
|
215
|
+
`session-summary.md` is a replace-only startup snapshot, not a timeline. If it grows beyond the warning threshold, run `memoc trim-summary`; completed history belongs in `.memoc/log.md`, and unfinished/risky resume detail belongs in `.memoc/04-handoff.md`.
|
|
216
|
+
|
|
205
217
|
---
|
|
206
218
|
|
|
207
219
|
## Claude Code Auto-Detection
|
package/bin/cli.js
CHANGED
|
@@ -172,6 +172,7 @@ function scanProject(dir, depth = 0) {
|
|
|
172
172
|
// ═══════════════════════════════════════════════════════════════════
|
|
173
173
|
|
|
174
174
|
function nowISO() { return new Date().toISOString().slice(0, 19); }
|
|
175
|
+
function todayISO() { return new Date().toISOString().slice(0, 10); }
|
|
175
176
|
|
|
176
177
|
function stackStr(stack) { return stack.length ? stack.join(', ') : 'Not detected'; }
|
|
177
178
|
|
|
@@ -208,6 +209,30 @@ function write(filePath, content) {
|
|
|
208
209
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
function slugify(value, fallback = 'note') {
|
|
213
|
+
const slug = String(value || '')
|
|
214
|
+
.toLowerCase()
|
|
215
|
+
.replace(/['"]/g, '')
|
|
216
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
217
|
+
.replace(/^-+|-+$/g, '')
|
|
218
|
+
.slice(0, 80);
|
|
219
|
+
return slug || fallback;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function uniquePath(filePath) {
|
|
223
|
+
if (!fs.existsSync(filePath)) return filePath;
|
|
224
|
+
const ext = path.extname(filePath);
|
|
225
|
+
const base = filePath.slice(0, filePath.length - ext.length);
|
|
226
|
+
let i = 2;
|
|
227
|
+
while (fs.existsSync(`${base}-${i}${ext}`)) i += 1;
|
|
228
|
+
return `${base}-${i}${ext}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function markdownTitle(src, fallback) {
|
|
232
|
+
const m = String(src || '').match(/^#\s+(.+)$/m);
|
|
233
|
+
return m ? m[1].trim() : fallback;
|
|
234
|
+
}
|
|
235
|
+
|
|
211
236
|
function tplMemocCmdWrapper(cliPath = runtimeCliPath()) {
|
|
212
237
|
return `@echo off\r\nnode "${escapeCmdPath(cliPath)}" %*\r\n`;
|
|
213
238
|
}
|
|
@@ -604,6 +629,8 @@ function managedBlock() {
|
|
|
604
629
|
- [ ] Search memory first: \`memoc search "<query>" --limit 5\`, or wrapper fallback above if PATH fails
|
|
605
630
|
- [ ] Open on demand: \`02\` status, \`04\` resume, \`06\` rules, \`llms.txt\` map
|
|
606
631
|
- [ ] If memory search is not enough, search project files with \`memoc grep "<query>" --limit 5\` (or wrapper fallback)
|
|
632
|
+
- [ ] If asked to refresh/update memoc project memory, run \`memoc update\` first; this refreshes managed sections, wiki links, and Obsidian tags.
|
|
633
|
+
- [ ] For durable source material use \`memoc ingest <path-or-url>\`; for durable analysis/query results use \`memoc note "<title>"\`; after wiki edits run \`memoc lint-wiki\`.
|
|
607
634
|
- [ ] Keep output small: \`summary\`, \`search --limit\`, \`grep --limit\`, \`--snippets\`
|
|
608
635
|
|
|
609
636
|
## Before Finishing _(update only applicable files; skip Q&A / throwaway exploration)_
|
|
@@ -612,6 +639,8 @@ function managedBlock() {
|
|
|
612
639
|
- [ ] Work incomplete or risky? Update \`04-handoff.md\`
|
|
613
640
|
- [ ] Rule/preference set? Update \`06-project-rules.md\`
|
|
614
641
|
- [ ] Wiki/systems work? Read \`skills/project-memory-maintainer/SKILL.md\`
|
|
642
|
+
- [ ] User asked to update memoc/project memory? Run \`memoc update\`, then update the smallest relevant agent-owned memory files.
|
|
643
|
+
- [ ] Keep \`session-summary.md\` as a replace-only snapshot under 800B; move completed history to \`log.md\` and resume details to \`04-handoff.md\`. If it grew, run \`memoc trim-summary\`.
|
|
615
644
|
${MGMT_E}`;
|
|
616
645
|
}
|
|
617
646
|
|
|
@@ -648,6 +677,7 @@ function coreLlmsInner() {
|
|
|
648
677
|
- [Decisions](.memoc/03-decisions.md): durable decisions.
|
|
649
678
|
- [Log](.memoc/log.md): append-only history.
|
|
650
679
|
- [Systems](.memoc/systems/README.md): subsystem docs.
|
|
680
|
+
- [Raw Sources](.memoc/raw/README.md): immutable source material; do not read by default.
|
|
651
681
|
- [Wiki](.memoc/wiki/index.md): synthesized knowledge.`;
|
|
652
682
|
}
|
|
653
683
|
|
|
@@ -741,6 +771,201 @@ function ensureWikiScaffoldLinks(memDir, mark) {
|
|
|
741
771
|
}
|
|
742
772
|
}
|
|
743
773
|
|
|
774
|
+
function ensureObsidianFrontmatter(dir, mark) {
|
|
775
|
+
const files = collectMemocMarkdownFiles(dir);
|
|
776
|
+
let changed = 0;
|
|
777
|
+
for (const fp of files) {
|
|
778
|
+
if (ensureMemocFrontmatter(fp, dir)) changed += 1;
|
|
779
|
+
}
|
|
780
|
+
mark(changed ? 'update' : 'skip', `Obsidian tags (${changed || 'already present'})`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function collectMemocMarkdownFiles(dir) {
|
|
784
|
+
const files = [];
|
|
785
|
+
function walk(root) {
|
|
786
|
+
if (!fs.existsSync(root)) return;
|
|
787
|
+
try {
|
|
788
|
+
const st = fs.statSync(root);
|
|
789
|
+
if (st.isFile()) {
|
|
790
|
+
if (root.endsWith('.md')) files.push(root);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (!st.isDirectory()) return;
|
|
794
|
+
for (const entry of fs.readdirSync(root)) walk(path.join(root, entry));
|
|
795
|
+
} catch {}
|
|
796
|
+
}
|
|
797
|
+
walk(path.join(dir, '.memoc'));
|
|
798
|
+
walk(path.join(dir, 'skills', 'project-memory-maintainer'));
|
|
799
|
+
return files.sort();
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function ensureMemocFrontmatter(filePath, dir) {
|
|
803
|
+
let src = '';
|
|
804
|
+
try { src = fs.readFileSync(filePath, 'utf8'); } catch { return false; }
|
|
805
|
+
const spec = obsidianFrontmatterSpec(path.relative(dir, filePath));
|
|
806
|
+
const next = mergeYamlFrontmatter(src, spec);
|
|
807
|
+
if (next === src) return false;
|
|
808
|
+
write(filePath, next);
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function obsidianFrontmatterSpec(relPath) {
|
|
813
|
+
const rel = relPath.replace(/\\/g, '/');
|
|
814
|
+
let type = 'core';
|
|
815
|
+
const tags = ['memoc'];
|
|
816
|
+
const now = nowISO();
|
|
817
|
+
const extra = {
|
|
818
|
+
created: now,
|
|
819
|
+
updated: now,
|
|
820
|
+
status: 'active',
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
if (rel.startsWith('.memoc/wiki/')) {
|
|
824
|
+
type = 'wiki';
|
|
825
|
+
extra.confidence = 'medium';
|
|
826
|
+
tags.push('memoc/wiki');
|
|
827
|
+
if (rel.startsWith('.memoc/wiki/sources/')) {
|
|
828
|
+
tags.push('memoc/source');
|
|
829
|
+
extra.status = 'needs-synthesis';
|
|
830
|
+
} else if (rel.startsWith('.memoc/wiki/topics/')) {
|
|
831
|
+
tags.push('memoc/topic');
|
|
832
|
+
} else if (rel.startsWith('.memoc/wiki/global/')) {
|
|
833
|
+
tags.push('memoc/global');
|
|
834
|
+
} else if (rel.endsWith('/sources.md')) {
|
|
835
|
+
tags.push('memoc/source');
|
|
836
|
+
} else if (rel.endsWith('/glossary.md')) {
|
|
837
|
+
tags.push('memoc/glossary');
|
|
838
|
+
} else if (rel.endsWith('/questions.md')) {
|
|
839
|
+
tags.push('memoc/question');
|
|
840
|
+
extra.status = 'needs-review';
|
|
841
|
+
} else if (rel.endsWith('/lint.md')) {
|
|
842
|
+
tags.push('memoc/lint');
|
|
843
|
+
extra.status = 'generated';
|
|
844
|
+
}
|
|
845
|
+
} else if (rel.startsWith('.memoc/systems/')) {
|
|
846
|
+
type = 'system';
|
|
847
|
+
tags.push('memoc/system');
|
|
848
|
+
} else if (rel.startsWith('.memoc/raw/')) {
|
|
849
|
+
type = 'raw';
|
|
850
|
+
tags.push('memoc/raw');
|
|
851
|
+
} else if (rel.startsWith('skills/project-memory-maintainer/')) {
|
|
852
|
+
type = 'skill';
|
|
853
|
+
tags.push('memoc/skill');
|
|
854
|
+
} else if (/(session-summary|current-project-state|handoff|project-rules|decisions|log)\.md$/.test(rel)) {
|
|
855
|
+
type = 'state';
|
|
856
|
+
tags.push('memoc/state');
|
|
857
|
+
} else {
|
|
858
|
+
tags.push('memoc/core');
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return { type, tags, extra };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function mergeYamlFrontmatter(src, spec) {
|
|
865
|
+
const fm = parseYamlFrontmatter(src);
|
|
866
|
+
if (!fm) {
|
|
867
|
+
return `${formatMemocFrontmatter(spec)}\n${src}`;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const lines = fm.body.split(/\r?\n/);
|
|
871
|
+
const existingTags = readYamlTags(lines);
|
|
872
|
+
const mergedTags = [...new Set([...existingTags, ...spec.tags])];
|
|
873
|
+
const nextLines = mergeYamlScalar(lines, 'memoc', 'true');
|
|
874
|
+
mergeYamlScalar(nextLines, 'type', spec.type);
|
|
875
|
+
mergeYamlScalar(nextLines, 'scope', 'project-memory');
|
|
876
|
+
mergeYamlScalarIfMissing(nextLines, 'updated', spec.extra.updated);
|
|
877
|
+
mergeYamlScalarIfMissing(nextLines, 'created', spec.extra.created);
|
|
878
|
+
mergeYamlScalarIfMissing(nextLines, 'status', spec.extra.status);
|
|
879
|
+
if (spec.extra.confidence) mergeYamlScalarIfMissing(nextLines, 'confidence', spec.extra.confidence);
|
|
880
|
+
mergeYamlTags(nextLines, mergedTags);
|
|
881
|
+
|
|
882
|
+
const nextFm = ['---', ...nextLines, '---'].join('\n');
|
|
883
|
+
return nextFm + src.slice(fm.end);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function parseYamlFrontmatter(src) {
|
|
887
|
+
if (!src.startsWith('---\n') && !src.startsWith('---\r\n')) return null;
|
|
888
|
+
const m = src.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
889
|
+
if (!m) return null;
|
|
890
|
+
return { body: m[1], end: m[0].length };
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function formatMemocFrontmatter(spec) {
|
|
894
|
+
return [
|
|
895
|
+
'---',
|
|
896
|
+
'memoc: true',
|
|
897
|
+
`type: ${spec.type}`,
|
|
898
|
+
'scope: project-memory',
|
|
899
|
+
`created: ${spec.extra.created}`,
|
|
900
|
+
`updated: ${spec.extra.updated}`,
|
|
901
|
+
`status: ${spec.extra.status}`,
|
|
902
|
+
...(spec.extra.confidence ? [`confidence: ${spec.extra.confidence}`] : []),
|
|
903
|
+
'tags:',
|
|
904
|
+
...spec.tags.map(tag => ` - ${tag}`),
|
|
905
|
+
'---',
|
|
906
|
+
].join('\n');
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function mergeYamlScalar(lines, key, value) {
|
|
910
|
+
const re = new RegExp(`^${escapeRegExp(key)}\\s*:`);
|
|
911
|
+
const idx = lines.findIndex(line => re.test(line.trim()));
|
|
912
|
+
if (idx === -1) lines.push(`${key}: ${value}`);
|
|
913
|
+
else lines[idx] = `${key}: ${value}`;
|
|
914
|
+
return lines;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function mergeYamlScalarIfMissing(lines, key, value) {
|
|
918
|
+
const re = new RegExp(`^${escapeRegExp(key)}\\s*:`);
|
|
919
|
+
if (lines.findIndex(line => re.test(line.trim())) === -1) lines.push(`${key}: ${value}`);
|
|
920
|
+
return lines;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function mergeYamlTags(lines, tags) {
|
|
924
|
+
const idx = lines.findIndex(line => /^tags\s*:/.test(line.trim()));
|
|
925
|
+
const tagLines = ['tags:', ...tags.map(tag => ` - ${tag}`)];
|
|
926
|
+
if (idx === -1) {
|
|
927
|
+
lines.push(...tagLines);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
let end = idx + 1;
|
|
932
|
+
while (end < lines.length && (/^\s+-\s+/.test(lines[end]) || lines[end].trim() === '')) end += 1;
|
|
933
|
+
lines.splice(idx, end - idx, ...tagLines);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function readYamlTags(lines) {
|
|
937
|
+
const tags = [];
|
|
938
|
+
const inline = lines.find(line => /^tags\s*:\s*\[/.test(line.trim()));
|
|
939
|
+
if (inline) {
|
|
940
|
+
const m = inline.match(/\[(.*)\]/);
|
|
941
|
+
if (m) {
|
|
942
|
+
for (const item of m[1].split(',')) {
|
|
943
|
+
const tag = item.trim().replace(/^['"]|['"]$/g, '').replace(/^#/, '');
|
|
944
|
+
if (tag) tags.push(tag);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const idx = lines.findIndex(line => /^tags\s*:/.test(line.trim()));
|
|
950
|
+
if (idx !== -1) {
|
|
951
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
952
|
+
const m = lines[i].match(/^\s+-\s+(.+?)\s*$/);
|
|
953
|
+
if (!m) {
|
|
954
|
+
if (lines[i].trim() === '') continue;
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
const tag = m[1].trim().replace(/^['"]|['"]$/g, '').replace(/^#/, '');
|
|
958
|
+
if (tag) tags.push(tag);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return [...new Set(tags)];
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function escapeRegExp(value) {
|
|
966
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
967
|
+
}
|
|
968
|
+
|
|
744
969
|
// ═══════════════════════════════════════════════════════════════════
|
|
745
970
|
// TEMPLATES — entry files
|
|
746
971
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -874,6 +1099,7 @@ ${SNAP_E}
|
|
|
874
1099
|
- [Session Summary](session-summary.md)
|
|
875
1100
|
- [Project Log](log.md)
|
|
876
1101
|
- [Wiki Index](wiki/index.md)
|
|
1102
|
+
- [Raw Sources](raw/README.md)
|
|
877
1103
|
- [Systems Index](systems/README.md)
|
|
878
1104
|
|
|
879
1105
|
## System Docs
|
|
@@ -930,7 +1156,9 @@ See \`.memoc/log.md\`.
|
|
|
930
1156
|
function tplSessionSummary() {
|
|
931
1157
|
return `# Session Summary
|
|
932
1158
|
Last: ${nowISO()}
|
|
933
|
-
|
|
1159
|
+
Replace this file instead of appending to it. Keep total size <800B and each section ≤3 bullets.
|
|
1160
|
+
Completed history belongs in \`log.md\`; incomplete/risky resume detail belongs in \`04-handoff.md\`.
|
|
1161
|
+
Agent-owned — updated by you, not by \`memoc update\`.
|
|
934
1162
|
|
|
935
1163
|
## Status
|
|
936
1164
|
_What is the current state of the project?_
|
|
@@ -998,12 +1226,16 @@ Shared protocol for any coding agent.
|
|
|
998
1226
|
|
|
999
1227
|
| Trigger | Update |
|
|
1000
1228
|
| --- | --- |
|
|
1229
|
+
| User asks "update memoc", "refresh project memory", or similar | Run \`memoc update\` first, then update relevant agent-owned memory files |
|
|
1001
1230
|
| User creates or changes a requirement | \`02-current-project-state.md\`, \`06-project-rules.md\`, \`log.md\` |
|
|
1002
1231
|
| Code, config, data, or assets changed | \`02-current-project-state.md\`, relevant \`systems/*.md\`, \`log.md\` |
|
|
1003
1232
|
| Architecture or system behavior changed | relevant \`systems/*.md\`, \`03-decisions.md\` |
|
|
1004
1233
|
| A decision should affect future agents | \`03-decisions.md\`, \`02-current-project-state.md\` |
|
|
1005
1234
|
| Work is substantial enough to resume later | \`04-handoff.md\`, \`02-current-project-state.md\`, \`log.md\` |
|
|
1006
1235
|
| Durable knowledge was learned | \`wiki/*.md\`, \`wiki/index.md\` |
|
|
1236
|
+
| Source material should feed the wiki | \`memoc ingest <path-or-url>\`, then synthesize affected \`wiki/topics/*.md\` |
|
|
1237
|
+
| A useful query answer should persist | \`memoc note "<title>"\`, then link related sources/topics |
|
|
1238
|
+
| \`session-summary.md\` exceeds 800B or starts accumulating history | Run \`memoc trim-summary\`; move history to \`log.md\`, resume details to \`04-handoff.md\` |
|
|
1007
1239
|
|
|
1008
1240
|
## Usually No Update Needed
|
|
1009
1241
|
|
|
@@ -1014,7 +1246,7 @@ Shared protocol for any coding agent.
|
|
|
1014
1246
|
## Documentation Shape
|
|
1015
1247
|
|
|
1016
1248
|
- Entry files: protocol only.
|
|
1017
|
-
- \`session-summary.md\`: latest snapshot, max 3 bullets per section.
|
|
1249
|
+
- \`session-summary.md\`: replace-only latest snapshot, <800B, max 3 bullets per section; never use as history.
|
|
1018
1250
|
- \`02-current-project-state.md\`: current status, tasks, commands, recent notes.
|
|
1019
1251
|
- \`04-handoff.md\`: resume context, blockers, verified/unverified checks.
|
|
1020
1252
|
- \`03-decisions.md\`: append durable decisions only.
|
|
@@ -1154,6 +1386,7 @@ memoc upgrade
|
|
|
1154
1386
|
|
|
1155
1387
|
# Explicitly update managed sections based on current project state
|
|
1156
1388
|
memoc update
|
|
1389
|
+
memoc trim-summary
|
|
1157
1390
|
|
|
1158
1391
|
# Tiny status overview
|
|
1159
1392
|
memoc summary
|
|
@@ -1165,6 +1398,11 @@ memoc search "<query>" --snippets --limit 5
|
|
|
1165
1398
|
# Search project source/text files when memory is not enough
|
|
1166
1399
|
memoc grep "<query>" --limit 12
|
|
1167
1400
|
memoc grep "<query>" --snippets --limit 5
|
|
1401
|
+
|
|
1402
|
+
# Wiki operations
|
|
1403
|
+
memoc ingest <path-or-url>
|
|
1404
|
+
memoc note "Durable topic or query result"
|
|
1405
|
+
memoc lint-wiki
|
|
1168
1406
|
\`\`\`
|
|
1169
1407
|
|
|
1170
1408
|
If \`memoc\` is not on PATH, use \`.\\.memoc\\bin\\memoc.cmd <command>\` on Windows or \`.memoc/bin/memoc <command>\` in sh for the rest of the session. If the local wrapper is missing, use \`npx @kevin0181/memoc <command>\` or re-run init.
|
|
@@ -1180,10 +1418,13 @@ If \`memoc\` is not on PATH, use \`.\\.memoc\\bin\\memoc.cmd <command>\` on Wind
|
|
|
1180
1418
|
|
|
1181
1419
|
Use \`memoc search\` for known concepts, changed areas, decisions, tasks, or handoff notes. Skip it for brand-new questions where no prior memory can exist.
|
|
1182
1420
|
|
|
1421
|
+
Raw files under \`.memoc/raw/\` are intentionally not part of normal memory search. Open them only through a linked source record when provenance is needed.
|
|
1422
|
+
|
|
1183
1423
|
## When To Run Memory Updates
|
|
1184
1424
|
|
|
1185
1425
|
Use \`memoc update\` or \`skills/project-memory-maintainer/SKILL.md\` when:
|
|
1186
1426
|
|
|
1427
|
+
- The user asks to update memoc, refresh project memory, sync project memory, or "update the project in memoc".
|
|
1187
1428
|
- Requirements, acceptance criteria, user preferences, or project rules changed.
|
|
1188
1429
|
- Source code, config, data, content, or package scripts changed.
|
|
1189
1430
|
- Architecture, data flow, routing, auth, or deployment behavior changed.
|
|
@@ -1193,17 +1434,32 @@ Use \`memoc update\` or \`skills/project-memory-maintainer/SKILL.md\` when:
|
|
|
1193
1434
|
|
|
1194
1435
|
Usually skip for pure Q&A, throwaway exploration, or tiny edits with no future impact.
|
|
1195
1436
|
|
|
1437
|
+
When the user asks for a general memoc/project-memory refresh, run \`memoc update\` first. It refreshes managed sections, reconnects default wiki scaffold links, and applies Obsidian frontmatter tags. Then update only the agent-owned files whose content actually changed, such as \`.memoc/session-summary.md\`, \`.memoc/02-current-project-state.md\`, \`.memoc/04-handoff.md\`, \`.memoc/wiki/index.md\`, or \`.memoc/log.md\`.
|
|
1438
|
+
|
|
1439
|
+
\`.memoc/session-summary.md\` is a startup snapshot, not a timeline. Rewrite it in place, do not append old work. If it exceeds 800B, run \`memoc trim-summary\`; it archives the previous summary and rewrites a compact version. Put completed history in \`.memoc/log.md\`, and put unfinished/risky resume detail in \`.memoc/04-handoff.md\`.
|
|
1440
|
+
|
|
1196
1441
|
## Updating The Wiki
|
|
1197
1442
|
|
|
1198
1443
|
Create a new Markdown file under \`.memoc/wiki/\` when synthesized knowledge should compound across sessions.
|
|
1199
1444
|
|
|
1445
|
+
- \`.memoc/raw/\`: immutable source material copied or referenced by \`memoc ingest\`.
|
|
1200
1446
|
- \`.memoc/wiki/sources/\`: provenance records.
|
|
1201
1447
|
- \`.memoc/wiki/topics/\`: synthesized topic pages.
|
|
1202
1448
|
- \`.memoc/wiki/global/\`: project-wide principles.
|
|
1203
1449
|
|
|
1204
1450
|
After creating or editing wiki pages:
|
|
1205
1451
|
1. Update \`.memoc/wiki/index.md\`.
|
|
1206
|
-
2.
|
|
1452
|
+
2. Run \`memoc lint-wiki\`.
|
|
1453
|
+
3. Append \`.memoc/log.md\`.
|
|
1454
|
+
|
|
1455
|
+
Useful scaffolds:
|
|
1456
|
+
|
|
1457
|
+
\`\`\`bash
|
|
1458
|
+
memoc ingest path/to/source.md
|
|
1459
|
+
memoc ingest https://example.com/spec
|
|
1460
|
+
memoc note "Auth flow comparison"
|
|
1461
|
+
memoc lint-wiki
|
|
1462
|
+
\`\`\`
|
|
1207
1463
|
|
|
1208
1464
|
## Updating System Docs
|
|
1209
1465
|
|
|
@@ -1232,6 +1488,74 @@ Create a new \`.md\` file here when a subsystem becomes important enough that fu
|
|
|
1232
1488
|
`;
|
|
1233
1489
|
}
|
|
1234
1490
|
|
|
1491
|
+
function tplRawReadme() {
|
|
1492
|
+
return `# Raw Sources
|
|
1493
|
+
|
|
1494
|
+
Immutable source material for the memoc wiki.
|
|
1495
|
+
|
|
1496
|
+
## Rules
|
|
1497
|
+
|
|
1498
|
+
- Do not edit raw files after ingest; create a new raw file or source record when material changes.
|
|
1499
|
+
- Do not read raw files at session start. Search or open the linked source/topic page first.
|
|
1500
|
+
- Source records under [wiki/sources](../wiki/sources/README.md) summarize raw material and link to affected topics.
|
|
1501
|
+
|
|
1502
|
+
## Subdirectories
|
|
1503
|
+
|
|
1504
|
+
- [files](files/README.md) — local files copied during ingest
|
|
1505
|
+
- [urls](urls/README.md) — URL references and fetched/exported material
|
|
1506
|
+
- [conversations](conversations/README.md) — conversation excerpts worth preserving
|
|
1507
|
+
- [docs](docs/README.md) — external docs, specs, and long references
|
|
1508
|
+
`;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function tplRawFilesReadme() {
|
|
1512
|
+
return `# Raw Files
|
|
1513
|
+
|
|
1514
|
+
Local files copied by \`memoc ingest <path>\`.
|
|
1515
|
+
|
|
1516
|
+
## Related
|
|
1517
|
+
|
|
1518
|
+
- [Raw Sources](../README.md)
|
|
1519
|
+
- [Source Records](../../wiki/sources/README.md)
|
|
1520
|
+
`;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function tplRawUrlsReadme() {
|
|
1524
|
+
return `# Raw URLs
|
|
1525
|
+
|
|
1526
|
+
URL references recorded by \`memoc ingest <url>\`.
|
|
1527
|
+
|
|
1528
|
+
## Related
|
|
1529
|
+
|
|
1530
|
+
- [Raw Sources](../README.md)
|
|
1531
|
+
- [Source Records](../../wiki/sources/README.md)
|
|
1532
|
+
`;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function tplRawConversationsReadme() {
|
|
1536
|
+
return `# Raw Conversations
|
|
1537
|
+
|
|
1538
|
+
Conversation excerpts that should feed durable wiki synthesis.
|
|
1539
|
+
|
|
1540
|
+
## Related
|
|
1541
|
+
|
|
1542
|
+
- [Raw Sources](../README.md)
|
|
1543
|
+
- [Source Records](../../wiki/sources/README.md)
|
|
1544
|
+
`;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function tplRawDocsReadme() {
|
|
1548
|
+
return `# Raw Docs
|
|
1549
|
+
|
|
1550
|
+
Long-form docs, specs, and references kept separate from synthesized topic pages.
|
|
1551
|
+
|
|
1552
|
+
## Related
|
|
1553
|
+
|
|
1554
|
+
- [Raw Sources](../README.md)
|
|
1555
|
+
- [Source Records](../../wiki/sources/README.md)
|
|
1556
|
+
`;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1235
1559
|
function tplWikiIndex() {
|
|
1236
1560
|
return `# Wiki Index
|
|
1237
1561
|
|
|
@@ -1239,6 +1563,7 @@ Persistent LLM-maintained project wiki.
|
|
|
1239
1563
|
|
|
1240
1564
|
## Graph Hubs
|
|
1241
1565
|
|
|
1566
|
+
- [Raw Sources](../raw/README.md) — immutable source material before synthesis.
|
|
1242
1567
|
- [Sources](sources.md) — provenance, ingests, and source-to-topic links.
|
|
1243
1568
|
- [Topics](topics/README.md) — synthesized topic pages.
|
|
1244
1569
|
- [Global](global/README.md) — project-wide principles and long-lived direction.
|
|
@@ -1250,6 +1575,10 @@ Persistent LLM-maintained project wiki.
|
|
|
1250
1575
|
|
|
1251
1576
|
_None yet. Add every wiki page here with a relative Markdown link and one-line summary._
|
|
1252
1577
|
|
|
1578
|
+
## Saved Queries
|
|
1579
|
+
|
|
1580
|
+
_None yet. Use \`memoc note "<title>"\` for durable analysis or query results that should become a topic._
|
|
1581
|
+
|
|
1253
1582
|
## Subdirectories
|
|
1254
1583
|
|
|
1255
1584
|
- [sources/](sources/README.md) — provenance records
|
|
@@ -1274,9 +1603,12 @@ Provenance index for conversations, URLs, docs, issues, and files that feed the
|
|
|
1274
1603
|
|
|
1275
1604
|
_No sources recorded yet. Link each source record to the topic/global pages it affects._
|
|
1276
1605
|
|
|
1606
|
+
Use \`memoc ingest <path-or-url>\` to create source records without loading raw material into startup context.
|
|
1607
|
+
|
|
1277
1608
|
## Related
|
|
1278
1609
|
|
|
1279
1610
|
- [Wiki Index](index.md)
|
|
1611
|
+
- [Raw Sources](../raw/README.md)
|
|
1280
1612
|
- [Source Records Directory](sources/README.md)
|
|
1281
1613
|
- [Topics](topics/README.md)
|
|
1282
1614
|
- [Open Questions](questions.md)
|
|
@@ -1326,6 +1658,7 @@ Provenance records for conversations, URLs, docs, and issues.
|
|
|
1326
1658
|
|
|
1327
1659
|
## How To Link
|
|
1328
1660
|
|
|
1661
|
+
- Keep source pages short: summary, raw location, affected pages, open synthesis work.
|
|
1329
1662
|
- Link each source record back to [Sources](../sources.md).
|
|
1330
1663
|
- Link outward to every topic, global page, system doc, or question that the source changes.
|
|
1331
1664
|
- Prefer one source per file when the source is substantial enough to cite later.
|
|
@@ -1432,8 +1765,10 @@ Use this local skill after meaningful project work so future agents can continue
|
|
|
1432
1765
|
|
|
1433
1766
|
## Maintenance Checklist
|
|
1434
1767
|
|
|
1768
|
+
- If the user asked to update/refresh memoc project memory, run \`memoc update\` first so managed sections, wiki scaffold links, and Obsidian tags are current.
|
|
1435
1769
|
- Keep \`llms.txt\` and \`.memoc/00-agent-index.md\` as concise maps.
|
|
1436
1770
|
- Keep \`.memoc/00-project-brief.md\` as the shortest project summary.
|
|
1771
|
+
- Rewrite \`.memoc/session-summary.md\` as the latest snapshot only; never append a timeline. If it is over 800B, run \`memoc trim-summary\`.
|
|
1437
1772
|
- Update \`.memoc/02-current-project-state.md\` with new status, tasks, commands, and change log entries.
|
|
1438
1773
|
- Update \`.memoc/03-decisions.md\` when a durable decision is made.
|
|
1439
1774
|
- Update \`.memoc/04-handoff.md\` before ending substantial work.
|
|
@@ -1442,8 +1777,11 @@ Use this local skill after meaningful project work so future agents can continue
|
|
|
1442
1777
|
- Append \`.memoc/log.md\` for meaningful changes, decisions, and handoffs.
|
|
1443
1778
|
- Create or update \`.memoc/systems/*.md\` when a subsystem needs durable explanation.
|
|
1444
1779
|
- Create or update \`.memoc/wiki/*.md\` when synthesized knowledge should compound over time.
|
|
1780
|
+
- Use \`memoc ingest <path-or-url>\` for source material and \`memoc note "<title>"\` for durable query results or analysis.
|
|
1445
1781
|
- Keep the wiki graph connected: update \`.memoc/wiki/index.md\`, add relative Markdown links between related pages, and include a \`## Related\` section on every new wiki page.
|
|
1782
|
+
- Run \`memoc lint-wiki\` after wiki/source/topic edits and address broken links before finishing.
|
|
1446
1783
|
- Keep completed history in \`.memoc/log.md\`; keep current-state files short.
|
|
1784
|
+
- Move completed session details out of \`session-summary.md\` into \`log.md\`; move incomplete/risky resume details into \`04-handoff.md\`.
|
|
1447
1785
|
- Keep tool output small; prefer \`summary\`, file-only search, \`--limit\`, and targeted reads.
|
|
1448
1786
|
|
|
1449
1787
|
## Wiki Link Rules
|
|
@@ -1640,6 +1978,11 @@ function run(dir, forceUpdate, action = 'update') {
|
|
|
1640
1978
|
[path.join(memDir, 'log.md'), tplLog],
|
|
1641
1979
|
[path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
|
|
1642
1980
|
[path.join(memDir, 'systems/README.md'), tplSystemsReadme],
|
|
1981
|
+
[path.join(memDir, 'raw/README.md'), tplRawReadme],
|
|
1982
|
+
[path.join(memDir, 'raw/files/README.md'), tplRawFilesReadme],
|
|
1983
|
+
[path.join(memDir, 'raw/urls/README.md'), tplRawUrlsReadme],
|
|
1984
|
+
[path.join(memDir, 'raw/conversations/README.md'), tplRawConversationsReadme],
|
|
1985
|
+
[path.join(memDir, 'raw/docs/README.md'), tplRawDocsReadme],
|
|
1643
1986
|
[path.join(memDir, 'wiki/index.md'), tplWikiIndex],
|
|
1644
1987
|
[path.join(memDir, 'wiki/sources.md'), tplWikiSources],
|
|
1645
1988
|
[path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
|
|
@@ -1661,6 +2004,9 @@ function run(dir, forceUpdate, action = 'update') {
|
|
|
1661
2004
|
// .gitignore — add .memoc/.pending if not already present
|
|
1662
2005
|
ensurePendingGitignore(dir, mark);
|
|
1663
2006
|
|
|
2007
|
+
// Obsidian graph filters — tag memoc-owned Markdown without touching unrelated docs
|
|
2008
|
+
ensureObsidianFrontmatter(dir, mark);
|
|
2009
|
+
|
|
1664
2010
|
// PATH helpers — let agents run memoc even when the npm bin is not on PATH
|
|
1665
2011
|
ensurePathHelpers(dir, mark);
|
|
1666
2012
|
ensurePathRegistration(dir, mark);
|
|
@@ -1738,6 +2084,11 @@ function run(dir, forceUpdate, action = 'update') {
|
|
|
1738
2084
|
[path.join(memDir, 'log.md'), tplLog],
|
|
1739
2085
|
[path.join(memDir, 'memoc-usage.md'), tplMemocUsage],
|
|
1740
2086
|
[path.join(memDir, 'systems/README.md'), tplSystemsReadme],
|
|
2087
|
+
[path.join(memDir, 'raw/README.md'), tplRawReadme],
|
|
2088
|
+
[path.join(memDir, 'raw/files/README.md'), tplRawFilesReadme],
|
|
2089
|
+
[path.join(memDir, 'raw/urls/README.md'), tplRawUrlsReadme],
|
|
2090
|
+
[path.join(memDir, 'raw/conversations/README.md'), tplRawConversationsReadme],
|
|
2091
|
+
[path.join(memDir, 'raw/docs/README.md'), tplRawDocsReadme],
|
|
1741
2092
|
[path.join(memDir, 'wiki/index.md'), tplWikiIndex],
|
|
1742
2093
|
[path.join(memDir, 'wiki/sources.md'), tplWikiSources],
|
|
1743
2094
|
[path.join(memDir, 'wiki/glossary.md'), tplWikiGlossary],
|
|
@@ -1755,6 +2106,9 @@ function run(dir, forceUpdate, action = 'update') {
|
|
|
1755
2106
|
}
|
|
1756
2107
|
ensureWikiScaffoldLinks(memDir, mark);
|
|
1757
2108
|
|
|
2109
|
+
// Obsidian graph filters — add/merge memoc tags for existing installs too
|
|
2110
|
+
ensureObsidianFrontmatter(dir, mark);
|
|
2111
|
+
|
|
1758
2112
|
// PATH helpers — let agents run memoc even when the npm bin is not on PATH
|
|
1759
2113
|
ensureClaudeStopHookFile(dir, mark);
|
|
1760
2114
|
ensurePendingGitignore(dir, mark);
|
|
@@ -1808,6 +2162,338 @@ function runAdd(dir) {
|
|
|
1808
2162
|
console.log('\n Done.');
|
|
1809
2163
|
}
|
|
1810
2164
|
|
|
2165
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
2166
|
+
// WIKI OPERATIONS — lint, ingest, and durable topic notes
|
|
2167
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
2168
|
+
|
|
2169
|
+
function runWikiLint(dir) {
|
|
2170
|
+
ensureObsidianFrontmatter(dir, () => {});
|
|
2171
|
+
const wikiDir = path.join(dir, '.memoc', 'wiki');
|
|
2172
|
+
const files = listMarkdownFiles(wikiDir);
|
|
2173
|
+
const issues = [];
|
|
2174
|
+
const warnings = [];
|
|
2175
|
+
const inbound = new Map(files.map(fp => [normRel(dir, fp), 0]));
|
|
2176
|
+
|
|
2177
|
+
for (const fp of files) {
|
|
2178
|
+
const rel = normRel(dir, fp);
|
|
2179
|
+
const src = safeRead(fp);
|
|
2180
|
+
if (!parseYamlFrontmatter(src)) warnings.push(`${rel}: missing YAML frontmatter`);
|
|
2181
|
+
if (!src.includes('memoc/wiki')) warnings.push(`${rel}: missing memoc/wiki tag`);
|
|
2182
|
+
if (!/^## Related\b/m.test(src) && !rel.endsWith('wiki/index.md')) warnings.push(`${rel}: missing ## Related section`);
|
|
2183
|
+
|
|
2184
|
+
for (const link of markdownLinks(src)) {
|
|
2185
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(link) || link.startsWith('#')) continue;
|
|
2186
|
+
const target = resolveMarkdownLink(fp, link);
|
|
2187
|
+
if (!target) continue;
|
|
2188
|
+
if (!fs.existsSync(target)) {
|
|
2189
|
+
issues.push(`${rel}: broken link ${link}`);
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
const targetRel = normRel(dir, target);
|
|
2193
|
+
if (inbound.has(targetRel)) inbound.set(targetRel, inbound.get(targetRel) + 1);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
for (const [rel, count] of inbound.entries()) {
|
|
2198
|
+
if (rel.endsWith('wiki/index.md')) continue;
|
|
2199
|
+
if (count === 0) warnings.push(`${rel}: no inbound wiki links`);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
const lintPath = path.join(wikiDir, 'lint.md');
|
|
2203
|
+
write(lintPath, wikiLintReport(issues, warnings));
|
|
2204
|
+
ensureMemocFrontmatter(lintPath, dir);
|
|
2205
|
+
|
|
2206
|
+
console.log('\n memoc lint-wiki\n');
|
|
2207
|
+
console.log(` Files ${files.length}`);
|
|
2208
|
+
console.log(` Issues ${issues.length}`);
|
|
2209
|
+
console.log(` Warnings ${warnings.length}`);
|
|
2210
|
+
console.log(' Report .memoc/wiki/lint.md');
|
|
2211
|
+
if (issues.length) {
|
|
2212
|
+
console.log('\n Issues:');
|
|
2213
|
+
for (const issue of issues.slice(0, 10)) console.log(` - ${issue}`);
|
|
2214
|
+
}
|
|
2215
|
+
if (!issues.length && !warnings.length) console.log('\n No issues found.');
|
|
2216
|
+
console.log();
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
function wikiLintReport(issues, warnings) {
|
|
2220
|
+
return `# Wiki Lint
|
|
2221
|
+
|
|
2222
|
+
Last checked: ${nowISO()}
|
|
2223
|
+
|
|
2224
|
+
## Graph Checks
|
|
2225
|
+
|
|
2226
|
+
- Every wiki page is listed from [Wiki Index](index.md) or a directory README.
|
|
2227
|
+
- Every wiki page links back to an index, hub, source, topic, or related page.
|
|
2228
|
+
- Important concepts mentioned in two or more places have their own linked page.
|
|
2229
|
+
- Source records link to the pages they update, and those pages link back to sources when provenance matters.
|
|
2230
|
+
|
|
2231
|
+
## Issues
|
|
2232
|
+
|
|
2233
|
+
${issues.length ? issues.map(x => `- ${x}`).join('\n') : '_No issues found._'}
|
|
2234
|
+
|
|
2235
|
+
## Warnings
|
|
2236
|
+
|
|
2237
|
+
${warnings.length ? warnings.map(x => `- ${x}`).join('\n') : '_None._'}
|
|
2238
|
+
|
|
2239
|
+
## Related
|
|
2240
|
+
|
|
2241
|
+
- [Wiki Index](index.md)
|
|
2242
|
+
- [Sources](sources.md)
|
|
2243
|
+
- [Topics](topics/README.md)
|
|
2244
|
+
- [Open Questions](questions.md)
|
|
2245
|
+
`;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
function runIngest(dir) {
|
|
2249
|
+
const target = process.argv[3];
|
|
2250
|
+
if (!target) {
|
|
2251
|
+
console.error('\n Usage: memoc ingest <path-or-url>');
|
|
2252
|
+
process.exit(1);
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
ensureMemocBase(dir);
|
|
2256
|
+
const isUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(target);
|
|
2257
|
+
const title = ingestTitle(dir, target, isUrl);
|
|
2258
|
+
const slug = `${todayISO()}-${slugify(title, 'source')}`;
|
|
2259
|
+
let rawRef;
|
|
2260
|
+
let rawDisplay;
|
|
2261
|
+
|
|
2262
|
+
if (isUrl) {
|
|
2263
|
+
const rawPath = uniquePath(path.join(dir, '.memoc', 'raw', 'urls', `${slug}.md`));
|
|
2264
|
+
write(rawPath, rawUrlRecord(title, target));
|
|
2265
|
+
ensureMemocFrontmatter(rawPath, dir);
|
|
2266
|
+
rawRef = pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki', 'sources'), rawPath);
|
|
2267
|
+
rawDisplay = normRel(dir, rawPath);
|
|
2268
|
+
} else {
|
|
2269
|
+
const abs = path.resolve(dir, target);
|
|
2270
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
2271
|
+
console.error(`\n Source file not found: ${target}`);
|
|
2272
|
+
process.exit(1);
|
|
2273
|
+
}
|
|
2274
|
+
const ext = path.extname(abs) || '.txt';
|
|
2275
|
+
const rawPath = uniquePath(path.join(dir, '.memoc', 'raw', 'files', `${slug}${ext}`));
|
|
2276
|
+
fs.mkdirSync(path.dirname(rawPath), { recursive: true });
|
|
2277
|
+
fs.copyFileSync(abs, rawPath);
|
|
2278
|
+
rawRef = pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki', 'sources'), rawPath);
|
|
2279
|
+
rawDisplay = normRel(dir, rawPath);
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
const sourcePath = uniquePath(path.join(dir, '.memoc', 'wiki', 'sources', `${slug}.md`));
|
|
2283
|
+
write(sourcePath, sourceRecord(title, rawRef, target, isUrl));
|
|
2284
|
+
ensureMemocFrontmatter(sourcePath, dir);
|
|
2285
|
+
addWikiListItem(path.join(dir, '.memoc', 'wiki', 'sources.md'), 'Source Records', pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki'), sourcePath), title, 'needs synthesis');
|
|
2286
|
+
addWikiListItem(path.join(dir, '.memoc', 'wiki', 'sources', 'README.md'), 'Source Records', path.basename(sourcePath), title, 'source record');
|
|
2287
|
+
addWikiListItem(path.join(dir, '.memoc', 'wiki', 'index.md'), 'Pages', pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki'), sourcePath), title, 'source record');
|
|
2288
|
+
appendMemocLog(dir, `ingest | Added source record ${normRel(dir, sourcePath)} from ${isUrl ? target : normRel(dir, path.resolve(dir, target))}.`);
|
|
2289
|
+
|
|
2290
|
+
console.log('\n memoc ingest\n');
|
|
2291
|
+
console.log(` Source record ${normRel(dir, sourcePath)}`);
|
|
2292
|
+
console.log(` Raw reference ${rawDisplay}`);
|
|
2293
|
+
console.log(' Next Synthesize affected topics, then run memoc lint-wiki.');
|
|
2294
|
+
console.log();
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
function runNote(dir) {
|
|
2298
|
+
const rawArgs = process.argv.slice(3);
|
|
2299
|
+
const bodyIndex = rawArgs.indexOf('--body');
|
|
2300
|
+
let body = '';
|
|
2301
|
+
let titleArgs = rawArgs;
|
|
2302
|
+
if (bodyIndex !== -1) {
|
|
2303
|
+
titleArgs = rawArgs.slice(0, bodyIndex);
|
|
2304
|
+
body = rawArgs.slice(bodyIndex + 1).join(' ');
|
|
2305
|
+
}
|
|
2306
|
+
const title = titleArgs.join(' ').trim();
|
|
2307
|
+
if (!title) {
|
|
2308
|
+
console.error('\n Usage: memoc note "<topic title>" [--body "short note"]');
|
|
2309
|
+
process.exit(1);
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
ensureMemocBase(dir);
|
|
2313
|
+
const topicPath = uniquePath(path.join(dir, '.memoc', 'wiki', 'topics', `${slugify(title, 'topic')}.md`));
|
|
2314
|
+
write(topicPath, topicNote(title, body));
|
|
2315
|
+
ensureMemocFrontmatter(topicPath, dir);
|
|
2316
|
+
addWikiListItem(path.join(dir, '.memoc', 'wiki', 'topics', 'README.md'), 'Topic Pages', path.basename(topicPath), title, 'topic note');
|
|
2317
|
+
addWikiListItem(path.join(dir, '.memoc', 'wiki', 'index.md'), 'Saved Queries', pathRelativeMarkdown(path.join(dir, '.memoc', 'wiki'), topicPath), title, 'saved query/topic note');
|
|
2318
|
+
appendMemocLog(dir, `note | Saved wiki topic ${normRel(dir, topicPath)}.`);
|
|
2319
|
+
|
|
2320
|
+
console.log('\n memoc note\n');
|
|
2321
|
+
console.log(` Topic ${normRel(dir, topicPath)}`);
|
|
2322
|
+
console.log(' Next Link related sources/topics, then run memoc lint-wiki.');
|
|
2323
|
+
console.log();
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
function ensureMemocBase(dir) {
|
|
2327
|
+
const p = scanProject(dir);
|
|
2328
|
+
const memDir = path.join(dir, '.memoc');
|
|
2329
|
+
const files = [
|
|
2330
|
+
[path.join(memDir, 'wiki/index.md'), tplWikiIndex],
|
|
2331
|
+
[path.join(memDir, 'wiki/sources.md'), tplWikiSources],
|
|
2332
|
+
[path.join(memDir, 'wiki/sources/README.md'), tplWikiSourcesReadme],
|
|
2333
|
+
[path.join(memDir, 'wiki/topics/README.md'), tplWikiTopicsReadme],
|
|
2334
|
+
[path.join(memDir, 'raw/README.md'), tplRawReadme],
|
|
2335
|
+
[path.join(memDir, 'raw/files/README.md'), tplRawFilesReadme],
|
|
2336
|
+
[path.join(memDir, 'raw/urls/README.md'), tplRawUrlsReadme],
|
|
2337
|
+
[path.join(memDir, 'log.md'), tplLog],
|
|
2338
|
+
[path.join(memDir, '00-agent-index.md'), () => tplAgentIndex(p)],
|
|
2339
|
+
];
|
|
2340
|
+
for (const [fp, tpl] of files) ensure(fp, tpl());
|
|
2341
|
+
ensureObsidianFrontmatter(dir, () => {});
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
function ingestTitle(dir, target, isUrl) {
|
|
2345
|
+
if (isUrl) {
|
|
2346
|
+
try {
|
|
2347
|
+
const u = new URL(target);
|
|
2348
|
+
return path.basename(u.pathname) || u.hostname;
|
|
2349
|
+
} catch {
|
|
2350
|
+
return target;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
try {
|
|
2354
|
+
const src = fs.readFileSync(path.resolve(dir, target), 'utf8');
|
|
2355
|
+
return markdownTitle(src, path.basename(target, path.extname(target)));
|
|
2356
|
+
} catch {
|
|
2357
|
+
return path.basename(target, path.extname(target));
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
function rawUrlRecord(title, url) {
|
|
2362
|
+
return `# ${title}
|
|
2363
|
+
|
|
2364
|
+
Original URL: ${url}
|
|
2365
|
+
|
|
2366
|
+
This raw URL record stores provenance only. Summarize it in a source record before using it as durable project knowledge.
|
|
2367
|
+
`;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
function sourceRecord(title, rawRef, original, isUrl) {
|
|
2371
|
+
return `# ${title}
|
|
2372
|
+
|
|
2373
|
+
## Source
|
|
2374
|
+
|
|
2375
|
+
- Raw: [${isUrl ? 'URL record' : 'raw file'}](${rawRef})
|
|
2376
|
+
- Original: ${original}
|
|
2377
|
+
- Ingested: ${nowISO()}
|
|
2378
|
+
|
|
2379
|
+
## Summary
|
|
2380
|
+
|
|
2381
|
+
_Summarize only the durable facts future agents should reuse._
|
|
2382
|
+
|
|
2383
|
+
## Affects
|
|
2384
|
+
|
|
2385
|
+
- [Sources](../sources.md)
|
|
2386
|
+
- [Topics](../topics/README.md)
|
|
2387
|
+
- [Open Questions](../questions.md)
|
|
2388
|
+
|
|
2389
|
+
## Synthesis Tasks
|
|
2390
|
+
|
|
2391
|
+
- [ ] Create or update affected topic/global/system pages.
|
|
2392
|
+
- [ ] Link those pages back to this source when provenance matters.
|
|
2393
|
+
- [ ] Run \`memoc lint-wiki\`.
|
|
2394
|
+
|
|
2395
|
+
## Related
|
|
2396
|
+
|
|
2397
|
+
- [Sources Index](../sources.md)
|
|
2398
|
+
- [Source Records](README.md)
|
|
2399
|
+
- [Wiki Index](../index.md)
|
|
2400
|
+
`;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
function topicNote(title, body) {
|
|
2404
|
+
return `# ${title}
|
|
2405
|
+
|
|
2406
|
+
## Summary
|
|
2407
|
+
|
|
2408
|
+
${body ? `- ${body}` : '_Capture the durable answer, analysis, or query result here._'}
|
|
2409
|
+
|
|
2410
|
+
## Evidence
|
|
2411
|
+
|
|
2412
|
+
- [Sources](../sources.md)
|
|
2413
|
+
|
|
2414
|
+
## Open Questions
|
|
2415
|
+
|
|
2416
|
+
_None yet._
|
|
2417
|
+
|
|
2418
|
+
## Related
|
|
2419
|
+
|
|
2420
|
+
- [Wiki Index](../index.md)
|
|
2421
|
+
- [Topics](README.md)
|
|
2422
|
+
- [Glossary](../glossary.md)
|
|
2423
|
+
`;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
function listMarkdownFiles(root) {
|
|
2427
|
+
const files = [];
|
|
2428
|
+
function walk(d) {
|
|
2429
|
+
if (!fs.existsSync(d)) return;
|
|
2430
|
+
for (const entry of fs.readdirSync(d)) {
|
|
2431
|
+
const fp = path.join(d, entry);
|
|
2432
|
+
try {
|
|
2433
|
+
const st = fs.statSync(fp);
|
|
2434
|
+
if (st.isDirectory()) walk(fp);
|
|
2435
|
+
else if (entry.endsWith('.md')) files.push(fp);
|
|
2436
|
+
} catch {}
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
walk(root);
|
|
2440
|
+
return files.sort();
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
function safeRead(fp) {
|
|
2444
|
+
try { return fs.readFileSync(fp, 'utf8'); } catch { return ''; }
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
function normRel(dir, fp) {
|
|
2448
|
+
return path.relative(dir, fp).replace(/\\/g, '/');
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function pathRelativeMarkdown(fromDir, toFile) {
|
|
2452
|
+
let rel = path.relative(fromDir, toFile).replace(/\\/g, '/');
|
|
2453
|
+
if (!rel.startsWith('.')) rel = `./${rel}`;
|
|
2454
|
+
return rel;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function markdownLinks(src) {
|
|
2458
|
+
const links = [];
|
|
2459
|
+
const re = /!?\[[^\]]*]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
2460
|
+
let m;
|
|
2461
|
+
while ((m = re.exec(src))) links.push(m[1]);
|
|
2462
|
+
return links;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
function resolveMarkdownLink(fromFile, link) {
|
|
2466
|
+
const clean = decodeURIComponent(String(link).split('#')[0]);
|
|
2467
|
+
if (!clean) return null;
|
|
2468
|
+
const base = path.resolve(path.dirname(fromFile), clean);
|
|
2469
|
+
if (path.extname(base)) return base;
|
|
2470
|
+
if (fs.existsSync(`${base}.md`)) return `${base}.md`;
|
|
2471
|
+
return base;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
function addWikiListItem(filePath, heading, link, title, note) {
|
|
2475
|
+
const src = safeRead(filePath);
|
|
2476
|
+
if (!src) return;
|
|
2477
|
+
if (src.includes(`](${link})`) || src.includes(`](${link.replace(/^\.\//, '')})`)) return;
|
|
2478
|
+
const item = `- [${title}](${link.replace(/^\.\//, '')}) — ${note}.`;
|
|
2479
|
+
const re = new RegExp(`(## ${escapeRegExp(heading)}\\n)([\\s\\S]*?)(?=\\n## |$)`, 'm');
|
|
2480
|
+
const m = src.match(re);
|
|
2481
|
+
if (!m) {
|
|
2482
|
+
write(filePath, `${src.trimEnd()}\n\n## ${heading}\n\n${item}\n`);
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
const replacementBody = m[2].includes('_None yet') || m[2].includes('_No sources recorded yet')
|
|
2486
|
+
? `\n${item}\n`
|
|
2487
|
+
: `${m[2].trimEnd()}\n${item}\n`;
|
|
2488
|
+
write(filePath, src.replace(re, `$1${replacementBody}`));
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
function appendMemocLog(dir, text) {
|
|
2492
|
+
const fp = path.join(dir, '.memoc', 'log.md');
|
|
2493
|
+
ensure(fp, tplLog());
|
|
2494
|
+
fs.appendFileSync(fp, `\n## [${nowISO()}] ${text}\n`, 'utf8');
|
|
2495
|
+
}
|
|
2496
|
+
|
|
1811
2497
|
// ═══════════════════════════════════════════════════════════════════
|
|
1812
2498
|
// SEARCH
|
|
1813
2499
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -1971,6 +2657,8 @@ function shouldSkipSearchDir(name, scope = 'memory') {
|
|
|
1971
2657
|
skipped.add('.memoc');
|
|
1972
2658
|
skipped.add('skills');
|
|
1973
2659
|
skipped.add('.claude');
|
|
2660
|
+
} else {
|
|
2661
|
+
skipped.add('raw');
|
|
1974
2662
|
}
|
|
1975
2663
|
return skipped.has(name);
|
|
1976
2664
|
}
|
|
@@ -2082,7 +2770,7 @@ function runTokens(dir) {
|
|
|
2082
2770
|
const summaryContent = read(path.join(memDir, 'session-summary.md'));
|
|
2083
2771
|
const summaryBytes = Buffer.byteLength(summaryContent, 'utf8');
|
|
2084
2772
|
if (summaryBytes > 800) {
|
|
2085
|
-
console.log(`\n ⚠ session-summary.md is ${summaryBytes}B — recommended <800B.
|
|
2773
|
+
console.log(`\n ⚠ session-summary.md is ${summaryBytes}B — recommended <800B. Run \`memoc trim-summary\`, then move completed history to log.md and resume details to 04-handoff.md.`);
|
|
2086
2774
|
}
|
|
2087
2775
|
console.log();
|
|
2088
2776
|
}
|
|
@@ -2131,6 +2819,92 @@ function runCompress(dir) {
|
|
|
2131
2819
|
console.log('\n Done.\n');
|
|
2132
2820
|
}
|
|
2133
2821
|
|
|
2822
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
2823
|
+
// TRIM SUMMARY — keep startup memory small and move bulky text aside
|
|
2824
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
2825
|
+
|
|
2826
|
+
function runTrimSummary(dir) {
|
|
2827
|
+
const summaryPath = path.join(dir, '.memoc', 'session-summary.md');
|
|
2828
|
+
const archivePath = path.join(dir, '.memoc', 'session-summary-archive.md');
|
|
2829
|
+
if (!fs.existsSync(summaryPath)) {
|
|
2830
|
+
write(summaryPath, tplSessionSummary());
|
|
2831
|
+
console.log('\n memoc trim-summary\n');
|
|
2832
|
+
console.log(' Added .memoc/session-summary.md');
|
|
2833
|
+
console.log('\n Done.\n');
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
const src = fs.readFileSync(summaryPath, 'utf8');
|
|
2838
|
+
const beforeBytes = Buffer.byteLength(src, 'utf8');
|
|
2839
|
+
const compact = compactSessionSummary(src);
|
|
2840
|
+
const afterBytes = Buffer.byteLength(compact, 'utf8');
|
|
2841
|
+
|
|
2842
|
+
if (src === compact && beforeBytes <= 800) {
|
|
2843
|
+
console.log('\n memoc trim-summary\n');
|
|
2844
|
+
console.log(` session-summary.md is already compact (${beforeBytes}B).`);
|
|
2845
|
+
console.log('\n Done.\n');
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
const archiveHeader = fs.existsSync(archivePath)
|
|
2850
|
+
? ''
|
|
2851
|
+
: '# Session Summary Archive\n\nOlder oversized startup summaries moved by `memoc trim-summary`.\n';
|
|
2852
|
+
fs.appendFileSync(archivePath, `${archiveHeader}\n## [${nowISO()}] archived summary (${beforeBytes}B)\n\n${src.trimEnd()}\n`, 'utf8');
|
|
2853
|
+
write(summaryPath, compact);
|
|
2854
|
+
appendMemocLog(dir, `trim-summary | Archived oversized session summary (${beforeBytes}B → ${afterBytes}B).`);
|
|
2855
|
+
|
|
2856
|
+
console.log('\n memoc trim-summary\n');
|
|
2857
|
+
console.log(` Archived .memoc/session-summary-archive.md`);
|
|
2858
|
+
console.log(` Rewrote .memoc/session-summary.md (${beforeBytes}B → ${afterBytes}B)`);
|
|
2859
|
+
console.log(' Reminder Completed history belongs in log.md; resume details belong in 04-handoff.md.');
|
|
2860
|
+
console.log('\n Done.\n');
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
function compactSessionSummary(src) {
|
|
2864
|
+
const sections = ['Status', 'Changed', 'Open Tasks', 'Resume'];
|
|
2865
|
+
const lines = [
|
|
2866
|
+
'# Session Summary',
|
|
2867
|
+
`Last: ${nowISO()}`,
|
|
2868
|
+
'Replace this file instead of appending to it. Keep total size <800B and each section ≤3 bullets.',
|
|
2869
|
+
'Completed history belongs in `log.md`; incomplete/risky resume detail belongs in `04-handoff.md`.',
|
|
2870
|
+
'',
|
|
2871
|
+
];
|
|
2872
|
+
|
|
2873
|
+
for (const heading of sections) {
|
|
2874
|
+
lines.push(`## ${heading}`);
|
|
2875
|
+
const bullets = compactSummaryBullets(sectionText(src, heading));
|
|
2876
|
+
if (bullets.length) lines.push(...bullets);
|
|
2877
|
+
else lines.push(summaryPlaceholder(heading));
|
|
2878
|
+
lines.push('');
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
return lines.join('\n').trimEnd() + '\n';
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
function sectionText(src, heading) {
|
|
2885
|
+
const re = new RegExp(`(?:^|\\n)## ${escapeRegExp(heading)}\\n([\\s\\S]*?)(?=\\n## |$)`);
|
|
2886
|
+
const m = String(src || '').match(re);
|
|
2887
|
+
return m ? m[1].trim() : '';
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
function compactSummaryBullets(text) {
|
|
2891
|
+
return String(text || '')
|
|
2892
|
+
.split(/\r?\n/)
|
|
2893
|
+
.map(line => line.trim())
|
|
2894
|
+
.filter(line => line && !line.startsWith('#') && !/^_.*_$/.test(line))
|
|
2895
|
+
.map(line => line.replace(/^[-*]\s+/, '').replace(/^\d+[.)]\s+/, '').trim())
|
|
2896
|
+
.filter(Boolean)
|
|
2897
|
+
.slice(0, 3)
|
|
2898
|
+
.map(line => `- ${line.length > 140 ? `${line.slice(0, 137)}...` : line}`);
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
function summaryPlaceholder(heading) {
|
|
2902
|
+
if (heading === 'Status') return '_Current state in 1-3 bullets._';
|
|
2903
|
+
if (heading === 'Changed') return '_Recent durable changes only._';
|
|
2904
|
+
if (heading === 'Open Tasks') return '_Current open tasks only._';
|
|
2905
|
+
return '_Where the next agent should resume._';
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2134
2908
|
// ═══════════════════════════════════════════════════════════════════
|
|
2135
2909
|
// SUMMARY
|
|
2136
2910
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -2147,7 +2921,7 @@ function runSummary(dir) {
|
|
|
2147
2921
|
}
|
|
2148
2922
|
|
|
2149
2923
|
function section(src, heading) {
|
|
2150
|
-
const re = new RegExp(
|
|
2924
|
+
const re = new RegExp(`(?:^|\\n)## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`);
|
|
2151
2925
|
const m = src.match(re);
|
|
2152
2926
|
return m ? m[1].trim() : '';
|
|
2153
2927
|
}
|
|
@@ -2205,10 +2979,14 @@ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
|
2205
2979
|
console.log(' upgrade Refresh memoc runtime/wrappers and managed sections; preserve memory');
|
|
2206
2980
|
console.log(' summary Print a tiny status/resume overview');
|
|
2207
2981
|
console.log(' tokens Estimate token cost of current memory files');
|
|
2982
|
+
console.log(' trim-summary Archive and compact oversized session-summary.md');
|
|
2208
2983
|
console.log(' compress Archive old log.md entries to keep file small');
|
|
2209
2984
|
console.log(' add <agent> Add entry file for a specific agent (run without args to list)');
|
|
2210
2985
|
console.log(' search "<query>" Search memory/agent docs (use --snippets for line matches)');
|
|
2211
2986
|
console.log(' grep "<query>" Search project source/text files (use --snippets for line matches)');
|
|
2987
|
+
console.log(' ingest <path|url> Create a raw/source record scaffold for wiki synthesis');
|
|
2988
|
+
console.log(' note "<title>" Save a durable topic/query-result scaffold');
|
|
2989
|
+
console.log(' lint-wiki Check wiki links, tags, backlinks, and Related sections');
|
|
2212
2990
|
console.log('\nSearch flags:');
|
|
2213
2991
|
console.log(' --files Show file names and match counts, sorted by relevance + recency (default)');
|
|
2214
2992
|
console.log(' --snippets Show matching lines');
|
|
@@ -2224,10 +3002,14 @@ if (cmd === 'update') { run(cwd, true, 'update'); process.exit(0); }
|
|
|
2224
3002
|
if (cmd === 'upgrade') { run(cwd, true, 'upgrade'); process.exit(0); }
|
|
2225
3003
|
if (cmd === 'summary') { runSummary(cwd); process.exit(0); }
|
|
2226
3004
|
if (cmd === 'tokens') { runTokens(cwd); process.exit(0); }
|
|
3005
|
+
if (cmd === 'trim-summary') { runTrimSummary(cwd); process.exit(0); }
|
|
2227
3006
|
if (cmd === 'compress') { runCompress(cwd); process.exit(0); }
|
|
2228
3007
|
if (cmd === 'add') { runAdd(cwd); process.exit(0); }
|
|
2229
3008
|
if (cmd === 'search') { runSearch(cwd, 'memory'); process.exit(0); }
|
|
2230
3009
|
if (cmd === 'grep') { runSearch(cwd, 'project'); process.exit(0); }
|
|
3010
|
+
if (cmd === 'ingest') { runIngest(cwd); process.exit(0); }
|
|
3011
|
+
if (cmd === 'note') { runNote(cwd); process.exit(0); }
|
|
3012
|
+
if (cmd === 'lint-wiki') { runWikiLint(cwd); process.exit(0); }
|
|
2231
3013
|
|
|
2232
3014
|
console.error(`Unknown command: ${cmd}`);
|
|
2233
3015
|
console.error('Run "memoc --help" for usage.');
|