@kevin0181/memoc 1.0.1 → 1.0.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 +1 -1
  2. package/bin/cli.js +199 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -12,7 +12,7 @@ npx @kevin0181/memoc init
12
12
 
13
13
  Run inside your project directory. Detects your stack automatically and generates everything agents need.
14
14
 
15
- `init` also creates project-local PATH helpers so agents can keep using memoc even when the global/npm bin is not on PATH:
15
+ `init` also creates PATH helpers so agents can keep using memoc even when the global/npm bin is not on PATH. It installs a `memoc` launcher into a writable directory already on the current PATH when possible, then also installs a user-local launcher and registers that launcher directory on Windows, macOS, and Linux.
16
16
 
17
17
  ```bash
18
18
  # PowerShell
package/bin/cli.js CHANGED
@@ -220,6 +220,14 @@ function tplMemocShWrapper() {
220
220
  return `#!/bin/sh\nexec npx @kevin0181/memoc "$@"\n`;
221
221
  }
222
222
 
223
+ function defaultUserBinDir() {
224
+ if (process.env.MEMOC_USER_BIN_DIR) return process.env.MEMOC_USER_BIN_DIR;
225
+ if (currentPlatform() === 'win32') {
226
+ return path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || process.cwd(), 'AppData', 'Local'), 'memoc', 'bin');
227
+ }
228
+ return path.join(process.env.HOME || process.cwd(), '.local', 'bin');
229
+ }
230
+
223
231
  function tplEnvPs1() {
224
232
  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`;
225
233
  }
@@ -245,6 +253,194 @@ function ensurePathHelpers(dir, mark) {
245
253
  }
246
254
  }
247
255
 
256
+ function ensureUserLauncher(mark) {
257
+ const userBin = defaultUserBinDir();
258
+ writeLaunchers(userBin, mark, 'user bin');
259
+ return userBin;
260
+ }
261
+
262
+ function writeLaunchers(binDir, mark, label) {
263
+ 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],
267
+ ];
268
+
269
+ for (const [fp, tpl, executable] of files) {
270
+ const added = ensure(fp, tpl());
271
+ if (executable) chmodExecutable(fp);
272
+ mark(added ? 'add' : 'skip', `${label} ${path.basename(fp)}`);
273
+ }
274
+ }
275
+
276
+ function ensurePathRegistration(dir, mark) {
277
+ ensureCurrentPathLauncher(mark);
278
+ const binDir = ensureUserLauncher(mark);
279
+ const pathSep = path.delimiter;
280
+
281
+ if ((process.env.PATH || '').split(pathSep).some(p => samePath(p, binDir))) {
282
+ mark('skip', 'PATH (user memoc bin already active)');
283
+ return;
284
+ }
285
+
286
+ process.env.PATH = `${binDir}${pathSep}${process.env.PATH || ''}`;
287
+
288
+ if (process.env.MEMOC_SKIP_PATH_REGISTER === '1') {
289
+ mark('skip', 'PATH registration (test mode)');
290
+ return;
291
+ }
292
+
293
+ if (currentPlatform() !== 'win32') {
294
+ const updated = ensureUnixPathRegistration(binDir);
295
+ mark(updated ? 'update' : 'skip', `${currentPlatform()} PATH (${userPathShellHint(binDir)})`);
296
+ return;
297
+ }
298
+
299
+ try {
300
+ const current = require('child_process')
301
+ .execFileSync('powershell.exe', [
302
+ '-NoProfile',
303
+ '-ExecutionPolicy', 'Bypass',
304
+ '-Command',
305
+ "[Environment]::GetEnvironmentVariable('Path','User')",
306
+ ], { encoding: 'utf8' })
307
+ .trim();
308
+ const parts = current.split(pathSep).filter(Boolean);
309
+ if (parts.some(p => samePath(p, binDir))) {
310
+ mark('skip', 'User PATH (memoc bin already registered)');
311
+ return;
312
+ }
313
+ const nextPath = [binDir, ...parts].join(pathSep);
314
+ require('child_process').execFileSync('powershell.exe', [
315
+ '-NoProfile',
316
+ '-ExecutionPolicy', 'Bypass',
317
+ '-Command',
318
+ `[Environment]::SetEnvironmentVariable('Path', ${JSON.stringify(nextPath)}, 'User')`,
319
+ ], { stdio: 'ignore' });
320
+ mark('update', 'User PATH (memoc bin added; open a new terminal if needed)');
321
+ } catch {
322
+ mark('skip', 'User PATH registration failed (use . .\\.memoc\\env.ps1)');
323
+ }
324
+ }
325
+
326
+ function ensureCurrentPathLauncher(mark) {
327
+ const target = findWritablePathDir();
328
+ if (!target) {
329
+ mark('skip', 'active PATH launcher (no writable PATH directory found)');
330
+ return false;
331
+ }
332
+ writeLaunchers(target, mark, 'active PATH');
333
+ return true;
334
+ }
335
+
336
+ function findWritablePathDir() {
337
+ const dirs = [...new Set((process.env.PATH || '').split(path.delimiter).filter(Boolean))];
338
+ const npmBin = npmGlobalBinDir();
339
+ const ranked = dirs
340
+ .filter(d => !isVolatilePathDir(d))
341
+ .filter(d => {
342
+ try { return fs.existsSync(d) && fs.statSync(d).isDirectory() && canWriteDir(d); }
343
+ catch { return false; }
344
+ })
345
+ .sort((a, b) => pathRank(a, npmBin) - pathRank(b, npmBin));
346
+ return ranked[0] || null;
347
+ }
348
+
349
+ function pathRank(dir, npmBin) {
350
+ if (npmBin && samePath(dir, npmBin)) return 0;
351
+ const lower = dir.toLowerCase();
352
+ for (const root of userWritableRoots()) {
353
+ if (root && lower.startsWith(root.toLowerCase())) return 1;
354
+ }
355
+ return 5;
356
+ }
357
+
358
+ function userWritableRoots() {
359
+ return [
360
+ process.env.APPDATA,
361
+ process.env.LOCALAPPDATA,
362
+ process.env.HOME,
363
+ process.env.USERPROFILE,
364
+ ].filter(Boolean).map(p => path.resolve(p));
365
+ }
366
+
367
+ function npmGlobalBinDir() {
368
+ try {
369
+ const prefix = require('child_process').execFileSync('npm', ['config', 'get', 'prefix'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
370
+ if (!prefix) return null;
371
+ return currentPlatform() === 'win32' ? prefix : path.join(prefix, 'bin');
372
+ } catch {
373
+ return null;
374
+ }
375
+ }
376
+
377
+ function isVolatilePathDir(dir) {
378
+ const lower = dir.toLowerCase();
379
+ return lower.includes(`${path.sep}_npx${path.sep}`) ||
380
+ lower.includes(`${path.sep}node_modules${path.sep}.bin`) ||
381
+ lower.includes(`${path.sep}npm-cache${path.sep}_npx${path.sep}`);
382
+ }
383
+
384
+ function canWriteDir(dir) {
385
+ const probe = path.join(dir, `.memoc-write-test-${process.pid}-${Date.now()}`);
386
+ try {
387
+ fs.writeFileSync(probe, '');
388
+ fs.unlinkSync(probe);
389
+ return true;
390
+ } catch {
391
+ return false;
392
+ }
393
+ }
394
+
395
+ function ensureUnixPathRegistration(binDir) {
396
+ if (process.env.MEMOC_SKIP_PATH_REGISTER === '1') return false;
397
+
398
+ const home = process.env.HOME;
399
+ if (!home) return false;
400
+
401
+ const block = [
402
+ '# memoc PATH',
403
+ `MEMOC_BIN=${shellSingleQuote(binDir)}`,
404
+ 'case ":$PATH:" in *":$MEMOC_BIN:"*) ;; *) PATH="$MEMOC_BIN:$PATH"; export PATH ;; esac',
405
+ '# end memoc PATH',
406
+ ].join('\n');
407
+
408
+ const candidates = [
409
+ path.join(home, '.profile'),
410
+ path.join(home, '.zshrc'),
411
+ path.join(home, '.bashrc'),
412
+ ];
413
+
414
+ let changed = false;
415
+ for (const fp of candidates) {
416
+ try {
417
+ const src = fs.existsSync(fp) ? fs.readFileSync(fp, 'utf8') : '';
418
+ if (src.includes(binDir) || src.includes('# memoc PATH')) continue;
419
+ fs.appendFileSync(fp, `${src.endsWith('\n') || !src ? '' : '\n'}\n${block}\n`, 'utf8');
420
+ changed = true;
421
+ } catch {}
422
+ }
423
+ return changed;
424
+ }
425
+
426
+ function userPathShellHint(binDir) {
427
+ return `user bin ${binDir} ${process.env.MEMOC_SKIP_PATH_REGISTER === '1' ? 'test mode' : 'registered; open a new terminal if needed'}`;
428
+ }
429
+
430
+ function currentPlatform() {
431
+ return process.env.MEMOC_PLATFORM || process.platform;
432
+ }
433
+
434
+ function shellSingleQuote(value) {
435
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
436
+ }
437
+
438
+ function samePath(a, b) {
439
+ if (!a || !b) return false;
440
+ const norm = p => path.resolve(p).toLowerCase().replace(/[\\/]+$/, '');
441
+ try { return norm(a) === norm(b); } catch { return false; }
442
+ }
443
+
248
444
  function updateSection(filePath, startMark, endMark, inner) {
249
445
  if (!fs.existsSync(filePath)) return false;
250
446
  const src = fs.readFileSync(filePath, 'utf8');
@@ -290,7 +486,7 @@ function managedBlock() {
290
486
  ## Session Start
291
487
  - [ ] Read \`.memoc/session-summary.md\`
292
488
  - [ ] \`.pending\` exists? → review changed files → update memory if needed → delete it
293
- - [ ] Put the project-local memoc wrapper on PATH when needed: PowerShell \`. .\\.memoc\\env.ps1\`; sh \`. ./.memoc/env.sh\`
489
+ - [ ] If \`memoc\` is not found in an existing shell, open a new terminal or load the local helper: PowerShell \`. .\\.memoc\\env.ps1\`; sh \`. ./.memoc/env.sh\`
294
490
 
295
491
  ## Before Opening More Files
296
492
  - [ ] 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>"\`
@@ -1074,6 +1270,7 @@ function run(dir, forceUpdate) {
1074
1270
 
1075
1271
  // PATH helpers — let agents run memoc even when the npm bin is not on PATH
1076
1272
  ensurePathHelpers(dir, mark);
1273
+ ensurePathRegistration(dir, mark);
1077
1274
 
1078
1275
  } else {
1079
1276
  // ── UPDATE MODE
@@ -1166,6 +1363,7 @@ function run(dir, forceUpdate) {
1166
1363
 
1167
1364
  // PATH helpers — let agents run memoc even when the npm bin is not on PATH
1168
1365
  ensurePathHelpers(dir, mark);
1366
+ ensurePathRegistration(dir, mark);
1169
1367
 
1170
1368
  // Append update record to log.md
1171
1369
  const logPath = path.join(memDir, 'log.md');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kevin0181/memoc",
3
- "version": "1.0.1",
3
+ "version": "1.0.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",