@kevin0181/memoc 1.0.5 → 1.0.8

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 +7 -3
  2. package/bin/cli.js +138 -49
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,9 +46,13 @@ npx @kevin0181/memoc update
46
46
  # Print current status in ~10 lines
47
47
  npx @kevin0181/memoc summary
48
48
 
49
- # Find relevant files before opening them (token-efficient)
50
- npx @kevin0181/memoc search "auth"
51
- npx @kevin0181/memoc search "auth" --snippets --limit 5
49
+ # Search memory/agent docs first (token-efficient)
50
+ npx @kevin0181/memoc search "auth"
51
+ npx @kevin0181/memoc search "auth" --snippets --limit 5
52
+
53
+ # Search project source/text files only when memory is not enough
54
+ npx @kevin0181/memoc grep "GetParticles"
55
+ npx @kevin0181/memoc grep "GetParticles" --snippets --limit 5
52
56
 
53
57
  # Estimate token cost of current memory files
54
58
  npx @kevin0181/memoc tokens
package/bin/cli.js CHANGED
@@ -208,16 +208,16 @@ function write(filePath, content) {
208
208
  fs.writeFileSync(filePath, content, 'utf8');
209
209
  }
210
210
 
211
- function tplMemocCmdWrapper() {
212
- return `@echo off\r\nnpm exec --yes --package "@kevin0181/memoc" -- memoc %*\r\n`;
211
+ function tplMemocCmdWrapper(cliPath = runtimeCliPath()) {
212
+ return `@echo off\r\nnode "${escapeCmdPath(cliPath)}" %*\r\n`;
213
213
  }
214
214
 
215
- function tplMemocPs1Wrapper() {
216
- return `npm exec --yes --package '@kevin0181/memoc' -- memoc @args\nexit $LASTEXITCODE\n`;
215
+ function tplMemocPs1Wrapper(cliPath = runtimeCliPath()) {
216
+ return `& node ${psSingleQuote(cliPath)} @args\nexit $LASTEXITCODE\n`;
217
217
  }
218
218
 
219
- function tplMemocShWrapper() {
220
- return `#!/bin/sh\nexec npm exec --yes --package '@kevin0181/memoc' -- memoc "$@"\n`;
219
+ function tplMemocShWrapper(cliPath = runtimeCliPath()) {
220
+ return `#!/bin/sh\nexec node ${shellSingleQuote(cliPath)} "$@"\n`;
221
221
  }
222
222
 
223
223
  function defaultUserBinDir() {
@@ -228,6 +228,18 @@ function defaultUserBinDir() {
228
228
  return path.join(process.env.HOME || process.cwd(), '.local', 'bin');
229
229
  }
230
230
 
231
+ function defaultRuntimeDir() {
232
+ if (process.env.MEMOC_RUNTIME_DIR) return process.env.MEMOC_RUNTIME_DIR;
233
+ if (currentPlatform() === 'win32') {
234
+ return path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || process.cwd(), 'AppData', 'Local'), 'memoc', 'runtime');
235
+ }
236
+ return path.join(process.env.HOME || process.cwd(), '.local', 'share', 'memoc', 'runtime');
237
+ }
238
+
239
+ function runtimeCliPath() {
240
+ return path.join(defaultRuntimeDir(), 'bin', 'cli.js');
241
+ }
242
+
231
243
  function tplEnvPs1() {
232
244
  return `$memocBin = Join-Path $PSScriptRoot 'bin'\n$parts = $env:PATH -split [IO.Path]::PathSeparator\nif ($parts -notcontains $memocBin) {\n $env:PATH = \"$memocBin$([IO.Path]::PathSeparator)$env:PATH\"\n}\n`;
233
245
  }
@@ -237,10 +249,11 @@ function tplEnvSh() {
237
249
  }
238
250
 
239
251
  function ensurePathHelpers(dir, mark) {
252
+ const cliPath = ensureRuntimeInstall(mark);
240
253
  const files = [
241
- [path.join(dir, '.memoc', 'bin', 'memoc.cmd'), tplMemocCmdWrapper, false],
242
- [path.join(dir, '.memoc', 'bin', 'memoc.ps1'), tplMemocPs1Wrapper, false],
243
- [path.join(dir, '.memoc', 'bin', 'memoc'), tplMemocShWrapper, true],
254
+ [path.join(dir, '.memoc', 'bin', 'memoc.cmd'), () => tplMemocCmdWrapper(cliPath), false],
255
+ [path.join(dir, '.memoc', 'bin', 'memoc.ps1'), () => tplMemocPs1Wrapper(cliPath), false],
256
+ [path.join(dir, '.memoc', 'bin', 'memoc'), () => tplMemocShWrapper(cliPath), true],
244
257
  [path.join(dir, '.memoc', 'env.ps1'), tplEnvPs1, false],
245
258
  [path.join(dir, '.memoc', 'env.sh'), tplEnvSh, true],
246
259
  ];
@@ -255,15 +268,15 @@ function ensurePathHelpers(dir, mark) {
255
268
 
256
269
  function ensureUserLauncher(mark) {
257
270
  const userBin = defaultUserBinDir();
258
- writeLaunchers(userBin, mark, 'user bin');
271
+ writeLaunchers(userBin, mark, 'user bin', ensureRuntimeInstall(mark));
259
272
  return userBin;
260
273
  }
261
274
 
262
- function writeLaunchers(binDir, mark, label) {
275
+ function writeLaunchers(binDir, mark, label, cliPath = ensureRuntimeInstall(mark)) {
263
276
  const files = [
264
- [path.join(binDir, 'memoc.cmd'), tplMemocCmdWrapper, false],
265
- [path.join(binDir, 'memoc.ps1'), tplMemocPs1Wrapper, false],
266
- [path.join(binDir, 'memoc'), tplMemocShWrapper, true],
277
+ [path.join(binDir, 'memoc.cmd'), () => tplMemocCmdWrapper(cliPath), false],
278
+ [path.join(binDir, 'memoc.ps1'), () => tplMemocPs1Wrapper(cliPath), false],
279
+ [path.join(binDir, 'memoc'), () => tplMemocShWrapper(cliPath), true],
267
280
  ];
268
281
 
269
282
  for (const [fp, tpl, executable] of files) {
@@ -341,10 +354,32 @@ function ensureCurrentPathLauncher(mark) {
341
354
  mark('skip', 'active PATH launcher (no writable PATH directory found)');
342
355
  return false;
343
356
  }
344
- writeLaunchers(target, mark, 'active PATH');
357
+ writeLaunchers(target, mark, 'active PATH', ensureRuntimeInstall(mark));
345
358
  return true;
346
359
  }
347
360
 
361
+ function ensureRuntimeInstall(mark) {
362
+ const runtimeDir = defaultRuntimeDir();
363
+ const sourceRoot = path.join(__dirname, '..');
364
+ const files = [
365
+ [path.join(sourceRoot, 'bin', 'cli.js'), path.join(runtimeDir, 'bin', 'cli.js')],
366
+ [path.join(sourceRoot, 'package.json'), path.join(runtimeDir, 'package.json')],
367
+ ];
368
+
369
+ for (const [src, dest] of files) {
370
+ try {
371
+ const content = fs.readFileSync(src, 'utf8');
372
+ const changed = writeIfChanged(dest, content);
373
+ mark(changed, `runtime ${path.relative(runtimeDir, dest)}`);
374
+ } catch {
375
+ mark('skip', `runtime ${path.basename(dest)} unavailable`);
376
+ }
377
+ }
378
+
379
+ chmodExecutable(path.join(runtimeDir, 'bin', 'cli.js'));
380
+ return path.join(runtimeDir, 'bin', 'cli.js');
381
+ }
382
+
348
383
  function findWritablePathDir() {
349
384
  const dirs = [...new Set((process.env.PATH || '').split(path.delimiter).filter(Boolean))];
350
385
  const npmBin = npmGlobalBinDir();
@@ -447,6 +482,14 @@ function shellSingleQuote(value) {
447
482
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
448
483
  }
449
484
 
485
+ function psSingleQuote(value) {
486
+ return `'${String(value).replace(/'/g, "''")}'`;
487
+ }
488
+
489
+ function escapeCmdPath(value) {
490
+ return String(value).replace(/"/g, '""');
491
+ }
492
+
450
493
  function samePath(a, b) {
451
494
  if (!a || !b) return false;
452
495
  const norm = p => path.resolve(p).toLowerCase().replace(/[\\/]+$/, '');
@@ -503,7 +546,8 @@ function managedBlock() {
503
546
  ## Before Opening More Files
504
547
  - [ ] Run memoc commands in this order: \`memoc search "<query>"\` → \`.\\.memoc\\bin\\memoc.cmd search "<query>"\` (Windows) or \`.memoc/bin/memoc search "<query>"\` (sh) → \`npx @kevin0181/memoc search "<query>"\`
505
548
  - [ ] Open on demand: \`02\` status · \`04\` resume · \`06\` rules · \`llms.txt\` map
506
- - [ ] Keep output small: \`summary\`, \`search --limit\`, \`search --snippets\`
549
+ - [ ] If memory search is not enough, search project files with \`memoc grep "<query>" --limit 5\`
550
+ - [ ] Keep output small: \`summary\`, \`search --limit\`, \`grep --limit\`, \`--snippets\`
507
551
 
508
552
  ## Before Finishing _(update only applicable files; skip Q&A / throwaway exploration)_
509
553
  - [ ] Code/config/deps changed → \`02\` (version, commands list, Last synced) + \`session-summary.md\` (status, changed, open tasks)
@@ -816,7 +860,8 @@ On-demand reference only. The entry-file managed block is authoritative.
816
860
 
817
861
  ## Search First
818
862
 
819
- \`memoc search "<query>"\` — returns file:line matches across all memory files.
863
+ \`memoc search "<query>"\` — returns file:line matches across memory and agent docs only.
864
+ \`memoc grep "<query>"\` — searches project source/text files when memory docs are not enough.
820
865
  If \`memoc\` is not on PATH, try \`.\\.memoc\\bin\\memoc.cmd search "<query>"\` on Windows or \`.memoc/bin/memoc search "<query>"\` in sh, then \`npx @kevin0181/memoc search "<query>"\`.
821
866
  Use it before opening any file to avoid reading more than needed.
822
867
  `;
@@ -996,9 +1041,13 @@ memoc update
996
1041
  # Tiny status overview
997
1042
  memoc summary
998
1043
 
999
- # Find files first; add --snippets only when needed
1000
- memoc search "<query>" --limit 12
1044
+ # Search memory first; add --snippets only when needed
1045
+ memoc search "<query>" --limit 12
1001
1046
  memoc search "<query>" --snippets --limit 5
1047
+
1048
+ # Search project source/text files when memory is not enough
1049
+ memoc grep "<query>" --limit 12
1050
+ memoc grep "<query>" --snippets --limit 5
1002
1051
  \`\`\`
1003
1052
 
1004
1053
  If \`memoc\` is not on PATH, use \`.\\.memoc\\bin\\memoc.cmd <command>\` on Windows or \`.memoc/bin/memoc <command>\` in sh. If that is unavailable, use \`npx @kevin0181/memoc <command>\`.
@@ -1007,9 +1056,9 @@ If \`memoc\` is not on PATH, use \`.\\.memoc\\bin\\memoc.cmd <command>\` on Wind
1007
1056
 
1008
1057
  1. Entry-file managed block.
1009
1058
  2. \`.memoc/session-summary.md\` only.
1010
- 3. Search in this order: \`memoc search "<query>"\`, \`.\\.memoc\\bin\\memoc.cmd search "<query>"\` or \`.memoc/bin/memoc search "<query>"\`, \`npx @kevin0181/memoc search "<query>"\`.
1011
- 4. Use \`--snippets\` only when file names are not enough.
1012
- 5. Open only task-relevant memory files.
1059
+ 3. Search memory first: \`memoc search "<query>"\`.
1060
+ 4. If memory is not enough, search project files: \`memoc grep "<query>" --limit 5\`.
1061
+ 5. Use \`--snippets\` only when file names are not enough.
1013
1062
 
1014
1063
  ## When To Run Memory Updates
1015
1064
 
@@ -1103,7 +1152,7 @@ Use this local skill after meaningful project work so future agents can continue
1103
1152
  ## Required Reads
1104
1153
 
1105
1154
  1. \`.memoc/session-summary.md\`
1106
- 2. \`memoc summary\` or \`memoc search "<query>"\`; if unavailable, use \`.\\.memoc\\bin\\memoc.cmd <command>\` or \`.memoc/bin/memoc <command>\`, then \`npx @kevin0181/memoc <command>\`
1155
+ 2. \`memoc summary\` or \`memoc search "<query>"\`; use \`memoc grep "<query>"\` only when source/text search is needed
1107
1156
  3. Open only files you will use or update.
1108
1157
 
1109
1158
  ## Maintenance Checklist
@@ -1427,9 +1476,9 @@ function runAdd(dir) {
1427
1476
  // SEARCH
1428
1477
  // ═══════════════════════════════════════════════════════════════════
1429
1478
 
1430
- function runSearch(dir) {
1431
- const rawArgs = process.argv.slice(3);
1432
- const opts = { mode: 'files', limit: 12, all: false };
1479
+ function runSearch(dir, scope = 'memory') {
1480
+ const rawArgs = process.argv.slice(3);
1481
+ const opts = { mode: 'files', limit: 12, all: false };
1433
1482
  const queryParts = [];
1434
1483
 
1435
1484
  for (let i = 0; i < rawArgs.length; i++) {
@@ -1450,19 +1499,12 @@ function runSearch(dir) {
1450
1499
  queryParts.push(arg);
1451
1500
  }
1452
1501
 
1453
- const query = queryParts.join(' ').toLowerCase();
1454
-
1455
- const searchRoots = [
1456
- path.join(dir, '.memoc'),
1457
- path.join(dir, 'skills'),
1458
- path.join(dir, 'llms.txt'),
1459
- path.join(dir, 'AGENTS.md'),
1460
- path.join(dir, 'CLAUDE.md'),
1461
- ...Object.values(AGENT_REGISTRY).map(agent => path.join(dir, agent.file)),
1462
- ];
1502
+ const query = queryParts.join(' ').toLowerCase();
1503
+
1504
+ const searchRoots = scope === 'project' ? [dir] : memorySearchRoots(dir);
1463
1505
 
1464
1506
  if (!query) {
1465
- // No query — list all memory files sorted by recency
1507
+ // No query — list searchable files sorted by recency
1466
1508
  const allFiles = [];
1467
1509
  function collectFile(fp) {
1468
1510
  if (!fs.existsSync(fp)) return;
@@ -1476,8 +1518,10 @@ function runSearch(dir) {
1476
1518
  for (const entry of fs.readdirSync(d)) {
1477
1519
  const fp = path.join(d, entry);
1478
1520
  try {
1479
- if (fs.statSync(fp).isDirectory()) collectDir(fp);
1480
- else if (entry.endsWith('.md') || entry === 'llms.txt' || entry.endsWith('rules')) collectFile(fp);
1521
+ const st = fs.statSync(fp);
1522
+ if (st.isDirectory()) {
1523
+ if (!shouldSkipSearchDir(entry)) collectDir(fp);
1524
+ } else if (isSearchableFile(fp, entry, st, scope)) collectFile(fp);
1481
1525
  } catch {}
1482
1526
  }
1483
1527
  }
@@ -1498,11 +1542,15 @@ function runSearch(dir) {
1498
1542
 
1499
1543
  const matchesByFile = new Map(); // rel -> { matches: [], mtime: number }
1500
1544
 
1501
- function searchFile(fp) {
1502
- if (!fs.existsSync(fp)) return;
1503
- const rel = path.relative(dir, fp);
1504
- let mtime = 0;
1505
- try { mtime = fs.statSync(fp).mtimeMs; } catch {}
1545
+ function searchFile(fp) {
1546
+ if (!fs.existsSync(fp)) return;
1547
+ const rel = path.relative(dir, fp);
1548
+ let mtime = 0;
1549
+ try {
1550
+ const st = fs.statSync(fp);
1551
+ if (!isSearchableFile(fp, path.basename(fp), st, scope)) return;
1552
+ mtime = st.mtimeMs;
1553
+ } catch {}
1506
1554
  const lines = fs.readFileSync(fp, 'utf8').split('\n');
1507
1555
  lines.forEach((line, i) => {
1508
1556
  if (line.toLowerCase().includes(query)) {
@@ -1517,8 +1565,10 @@ function runSearch(dir) {
1517
1565
  for (const entry of fs.readdirSync(d)) {
1518
1566
  const fp = path.join(d, entry);
1519
1567
  try {
1520
- if (fs.statSync(fp).isDirectory()) walkDir(fp);
1521
- else if (entry.endsWith('.md') || entry === 'llms.txt' || entry.endsWith('rules')) searchFile(fp);
1568
+ const st = fs.statSync(fp);
1569
+ if (st.isDirectory()) {
1570
+ if (!shouldSkipSearchDir(entry)) walkDir(fp);
1571
+ } else if (isSearchableFile(fp, entry, st, scope)) searchFile(fp);
1522
1572
  } catch {}
1523
1573
  }
1524
1574
  }
@@ -1552,7 +1602,44 @@ function runSearch(dir) {
1552
1602
  console.log(`... ${snippets.length - limited.length} more matches. Use --all to show all, or --limit N.`);
1553
1603
  }
1554
1604
  }
1555
- }
1605
+ }
1606
+
1607
+ function memorySearchRoots(dir) {
1608
+ return [
1609
+ path.join(dir, '.memoc'),
1610
+ path.join(dir, 'skills'),
1611
+ path.join(dir, 'llms.txt'),
1612
+ path.join(dir, 'AGENTS.md'),
1613
+ path.join(dir, 'CLAUDE.md'),
1614
+ ...Object.values(AGENT_REGISTRY).map(agent => path.join(dir, agent.file)),
1615
+ ];
1616
+ }
1617
+
1618
+ function shouldSkipSearchDir(name) {
1619
+ return new Set([
1620
+ '.git', 'node_modules', '.next', 'dist', 'build', 'out', 'coverage',
1621
+ 'Saved', 'Intermediate', 'DerivedDataCache', 'Binaries',
1622
+ '.venv', 'venv', '__pycache__', '.pytest_cache',
1623
+ ]).has(name);
1624
+ }
1625
+
1626
+ function isSearchableFile(fp, name, st, scope = 'memory') {
1627
+ if (!st || !st.isFile()) return false;
1628
+ if (st.size > 1024 * 1024) return false;
1629
+ if (name === 'llms.txt' || name.endsWith('rules')) return true;
1630
+ const ext = path.extname(fp).toLowerCase();
1631
+ if (scope === 'memory') {
1632
+ return new Set(['.md', '.txt']).has(ext);
1633
+ }
1634
+ return new Set([
1635
+ '.md', '.txt', '.json', '.jsonc', '.yaml', '.yml', '.toml', '.ini', '.env',
1636
+ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
1637
+ '.py', '.rs', '.go', '.java', '.cs', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.hxx',
1638
+ '.html', '.css', '.scss', '.sass', '.vue', '.svelte',
1639
+ '.sql', '.graphql', '.gql', '.sh', '.bash', '.zsh', '.ps1', '.bat', '.cmd',
1640
+ '.xml', '.gradle', '.kts', '.cmake',
1641
+ ]).has(ext);
1642
+ }
1556
1643
 
1557
1644
  // ═══════════════════════════════════════════════════════════════════
1558
1645
  // TOKENS — estimate token cost of current memory state
@@ -1730,7 +1817,8 @@ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
1730
1817
  console.log(' tokens Estimate token cost of current memory files');
1731
1818
  console.log(' compress Archive old log.md entries to keep file small');
1732
1819
  console.log(' add <agent> Add entry file for a specific agent (run without args to list)');
1733
- console.log(' search "<query>" Find matching files (use --snippets for line matches)');
1820
+ console.log(' search "<query>" Search memory/agent docs (use --snippets for line matches)');
1821
+ console.log(' grep "<query>" Search project source/text files (use --snippets for line matches)');
1734
1822
  console.log('\nSearch flags:');
1735
1823
  console.log(' --files Show file names and match counts, sorted by relevance + recency (default)');
1736
1824
  console.log(' --snippets Show matching lines');
@@ -1747,7 +1835,8 @@ if (cmd === 'summary') { runSummary(cwd); process.exit(0); }
1747
1835
  if (cmd === 'tokens') { runTokens(cwd); process.exit(0); }
1748
1836
  if (cmd === 'compress') { runCompress(cwd); process.exit(0); }
1749
1837
  if (cmd === 'add') { runAdd(cwd); process.exit(0); }
1750
- if (cmd === 'search') { runSearch(cwd); process.exit(0); }
1838
+ if (cmd === 'search') { runSearch(cwd, 'memory'); process.exit(0); }
1839
+ if (cmd === 'grep') { runSearch(cwd, 'project'); process.exit(0); }
1751
1840
 
1752
1841
  console.error(`Unknown command: ${cmd}`);
1753
1842
  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.0.5",
3
+ "version": "1.0.8",
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",