@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.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/bin/cli.js +787 -5
  3. 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
- Keep each section 3 bullets. Agent-owned updated by you, not by \`memoc update\`.
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. Append \`.memoc/log.md\`.
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. Trim it manually.`);
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(`^## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`, 'm');
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.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kevin0181/memoc",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Give AI agents a memory. Scaffolds session-to-session context for Claude Code, Codex, Cursor, and more.",
5
5
  "keywords": [
6
6
  "ai",