neurain 0.1.0-alpha.2 → 0.1.0-alpha.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/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@
4
4
 
5
5
  - No unreleased changes recorded.
6
6
 
7
+ ## 0.1.0-alpha.4
8
+
9
+ - Performance: lazy dynamic-import CLI dispatch. Each command now imports only its own `core/*.mjs` module on demand instead of loading all ~54 command modules on every invocation. Engine subprocess latency drops ~60-75ms across the board (`tidy` 150->90ms, `structure-audit` 120->60ms, `--help`/`--version` 50ms), with no change to the command surface or behavior (npm test 153/153; reviewed).
10
+
11
+
12
+ ## 0.1.0-alpha.3
13
+
14
+ - Destination resolver (`resolve_target.mjs`): `capture` and `compile` now derive one canonical target path from area, write intent, and sensitivity instead of leaving placement to the caller. Private, multi-area, and decision-required items still gate on the `<N>건 저장 진행` confirmation and are never auto-filed.
15
+
16
+ - Janitor (`neurain tidy`): read-only detection of provably-junk residue only (empty Obsidian `Untitled*.canvas/.base`, zero-byte daily notes, stray empty top-level directories); quarantine is recoverable (30-day trash). Registered areas, archives, and structural directories are never touched.
17
+ - Auto-filer (`neurain file`): finds loose durable markdown outside any canonical home and, gated by confirmation, relocates it recoverably and secret-gated; also surfaces read-only `raw/_inbox` routing proposals. Registered area roots and root-contract files are excluded.
18
+ - Structure audit (`neurain structure-audit`): advisory flags for area-adapter gaps, the `output/` vs `outputs/` split, and `sources_map` drift; composed into `lint`. Never writes and never fails a build.
19
+ - New-user organize (`neurain organize`): turns an unknown folder into a searchable area. Reuses the adopt scan, decides a per-file disposition, and on a gated apply scaffolds the area in copy mode (originals are never moved or deleted) and reindexes; hash-keyed receipt with `--rollback`. Secrets and already-structured KBs are protected.
20
+ - Organize and adopt preserve non-ASCII (for example Korean) folder names as area slugs.
21
+
7
22
  ## 0.1.0-alpha.2
8
23
 
9
24
  - Documentation currency gate: README and development-status version strings are asserted against package.json in readiness; volatile metric snapshots de-hardcoded (verified green in CI, not pinned in docs).
package/README.md CHANGED
@@ -176,6 +176,22 @@ Neurain scans first and recommends one of three modes:
176
176
 
177
177
  Writes require an explicit confirmation phrase shown by the scan.
178
178
 
179
+ ## Keeping the Vault Tidy
180
+
181
+ Neurain keeps a working vault organized through read-only detection plus gated, recoverable writes. Nothing here moves or deletes a file without an explicit confirmation phrase, and registered areas, archives, and root-contract files are never touched.
182
+
183
+ ```bash
184
+ npx neurain tidy ~/NeurainDemo # report provably-junk residue (read-only)
185
+ npx neurain structure-audit ~/NeurainDemo # advisory structure flags (read-only)
186
+ npx neurain file ~/NeurainDemo # propose homes for loose docs (read-only)
187
+ npx neurain organize ~/SomeUnknownFolder --dry-run # turn an unknown folder into a searchable area
188
+ ```
189
+
190
+ - `tidy` detects only provably-junk residue (empty Obsidian `Untitled*` canvas/base files, zero-byte daily notes, stray empty top-level directories) and quarantines recoverably.
191
+ - `structure-audit` is advisory only: it flags area-adapter gaps, the `output/` vs `outputs/` split, and `sources_map` drift, and never fails a build.
192
+ - `file` relocates loose durable markdown into its canonical home behind a confirmation gate, and surfaces `raw/_inbox` routing proposals.
193
+ - `organize` turns an unknown folder into a searchable area in copy mode (originals are never moved or deleted), with a hash-keyed receipt and `--rollback`. Secrets and already-structured knowledge bases are protected.
194
+
179
195
  ## MCP
180
196
 
181
197
  The alpha MCP server is stdio-only:
@@ -188,7 +204,7 @@ It exposes read/capture/scan/preview tools only. It does not silently compile, p
188
204
 
189
205
  ## Status
190
206
 
191
- This is `0.1.0-alpha.2`. It is not a public SaaS GA release. The alpha exists to prove installability, local-first onboarding, Codex, Claude, Gemini, and Runtime connectivity, plus safety receipts.
207
+ This is `0.1.0-alpha.4`. It is not a public SaaS GA release. The alpha exists to prove installability, local-first onboarding, Codex, Claude, Gemini, and Runtime connectivity, plus safety receipts.
192
208
 
193
209
  Alpha publish command:
194
210
 
@@ -1,9 +1,9 @@
1
1
  # Development Status
2
2
 
3
3
  Version: v0.1
4
- Last updated: 2026-06-14 KST
5
- Package: `neurain@0.1.0-alpha.2`
6
- Latest documented commit: `66016ba docs(dev-status): note unified reindex + adopt auto-reindex in the command surface`
4
+ Last updated: 2026-06-19 KST
5
+ Package: `neurain@0.1.0-alpha.4`
6
+ Latest documented commit: `53aba29 perf(cli): lazy dynamic-import dispatch (load only the dispatched command)`
7
7
 
8
8
  This document is the canonical product development snapshot for the public package. It tracks what is shipped, what has evidence, and what must not be claimed yet.
9
9
 
@@ -91,7 +91,7 @@ Status: shipped as publish-ready alpha.
91
91
  - README, quickstart, safety, privacy, support, pricing, troubleshooting, and release checklist exist.
92
92
  - CI workflow, issue templates, PR template, license, and security doc exist.
93
93
  - Full readiness verifies npm audit, pack dry-run, and temporary tarball install smoke.
94
- - Honest scope: npm publish or package name reservation still requires an explicit release action.
94
+ - Status: npm publish is done. `neurain@0.1.0-alpha.3` is published under the `alpha` tag and the package name is secured.
95
95
 
96
96
  ### E26: Thin MCP Connector
97
97
 
@@ -104,6 +104,18 @@ Status: shipped.
104
104
  - MCP server exposes bounded alpha tools for status, search, scan, recall, eval, live-case scaffold, lesson preview, scheduler preview, lifecycle report/eval, and wrap preview.
105
105
  - MCP does not expose silent durable wiki writes, lifecycle emit, daemon run/stop, curator write, recall rebuild write, or lesson promotion.
106
106
 
107
+ ### E27: Auto-Organization System
108
+
109
+ Status: shipped.
110
+
111
+ - CLI: `neurain tidy`, `neurain file`, `neurain structure-audit`, `neurain organize`.
112
+ - Destination resolver: `capture` and `compile` derive one canonical target path from area, write intent, and sensitivity. Private, multi-area, and decision-required items still gate on the `<N>건 저장 진행` confirmation and are never auto-filed.
113
+ - Janitor (`tidy`) detects provably-junk residue only (empty Obsidian canvas/base files, zero-byte daily notes, stray empty top-level directories). Read-only; quarantine is recoverable. Registered areas, archives, and structural directories are never touched.
114
+ - Auto-filer (`file`) relocates loose durable markdown into its canonical home behind a confirmation gate, recoverably and secret-gated, and surfaces read-only `raw/_inbox` routing proposals.
115
+ - Structure audit (`structure-audit`) is advisory: it flags area-adapter gaps, the `output/` vs `outputs/` split, and `sources_map` drift, and composes into `lint` without ever writing or failing a build.
116
+ - New-user organize (`organize`) turns an unknown folder into a searchable area in copy mode (originals are never moved or deleted), with a hash-keyed receipt and `--rollback`. Secrets and already-structured KBs are protected. Non-ASCII (for example Korean) folder names are preserved as area slugs.
117
+ - Honest scope: these are vault-hygiene helpers. Detection is read-only and every relocation is gated and recoverable; Neurain does not autonomously reorganize a user's folder.
118
+
107
119
  ## Current Connector Truth Table
108
120
 
109
121
  | Host | MCP connection | Lifecycle hook preview | Notes |
@@ -119,7 +131,6 @@ Status: shipped.
119
131
  - Non-developer GUI.
120
132
  - Hosted control plane.
121
133
  - Public Trust Center and status page.
122
- - npm publish or package name reservation.
123
134
  - Public GitHub publication if the repo is intentionally kept private during beta.
124
135
 
125
136
  ## Documentation Maintenance Rule
@@ -1,9 +1,9 @@
1
1
  # 개발 진행 상태
2
2
 
3
3
  Version: v0.1
4
- Last updated: 2026-06-14 KST
5
- Package: `neurain@0.1.0-alpha.2`
6
- Latest documented commit: `66016ba docs(dev-status): note unified reindex + adopt auto-reindex in the command surface`
4
+ Last updated: 2026-06-19 KST
5
+ Package: `neurain@0.1.0-alpha.4`
6
+ Latest documented commit: `53aba29 perf(cli): lazy dynamic-import dispatch (load only the dispatched command)`
7
7
 
8
8
  이 문서는 public package 기준의 canonical 개발 상태 스냅샷입니다. 무엇이 shipped인지, 어떤 증거가 있는지, 아직 주장하면 안 되는 것이 무엇인지 함께 기록합니다.
9
9
 
@@ -91,7 +91,7 @@ Status: publish-ready alpha로 shipped.
91
91
  - README, quickstart, safety, privacy, support, pricing, troubleshooting, release checklist가 존재합니다.
92
92
  - CI workflow, issue template, PR template, license, security doc이 존재합니다.
93
93
  - full readiness가 npm audit, pack dry-run, temporary tarball install smoke를 검증합니다.
94
- - 정직한 범위: npm publish 또는 package name reservation은 별도 release action이 필요합니다.
94
+ - 상태: npm publish 완료. `neurain@0.1.0-alpha.3`가 `alpha` 태그로 발행됐고 패키지 이름도 선점됨.
95
95
 
96
96
  ### E26: Thin MCP Connector
97
97
 
@@ -104,6 +104,18 @@ Status: shipped.
104
104
  - MCP server는 status, search, scan, recall, eval, live-case scaffold, lesson preview, scheduler preview, lifecycle report/eval, wrap preview용 bounded alpha tool을 노출합니다.
105
105
  - MCP는 silent durable wiki write, lifecycle emit, daemon run/stop, curator write, recall rebuild write, lesson promotion을 노출하지 않습니다.
106
106
 
107
+ ### E27: 자동 정리 시스템
108
+
109
+ Status: shipped.
110
+
111
+ - CLI: `neurain tidy`, `neurain file`, `neurain structure-audit`, `neurain organize`.
112
+ - 목적지 리졸버: `capture`·`compile`이 영역·쓰기의도·민감도로부터 단일 canonical 목적지 경로를 도출합니다. private·다중영역·결정필요 항목은 여전히 `<N>건 저장 진행` 확인을 거치며 절대 자동 파일링되지 않습니다.
113
+ - Janitor(`tidy`)는 증명 가능한 잔여물만 탐지합니다(빈 Obsidian canvas/base 파일, 0바이트 데일리노트, 빈 최상위 디렉터리). 읽기전용이고 격리는 복구 가능하며, 등록된 영역·아카이브·구조 디렉터리는 건드리지 않습니다.
114
+ - Auto-filer(`file`)는 떠도는 durable 마크다운을 확인 게이트 뒤에서 복구 가능·시크릿 게이트로 canonical 위치로 옮기고, `raw/_inbox` 라우팅 제안을 읽기전용으로 보여줍니다.
115
+ - 구조 점검(`structure-audit`)은 권고용입니다: 영역 어댑터 결손, `output/` 대 `outputs/` 분리, `sources_map` 드리프트를 표시하고 `lint`에 합성되며, 쓰기나 빌드 실패를 일으키지 않습니다.
116
+ - 신규 사용자 organize(`organize`)는 미지의 폴더를 검색 가능한 영역으로 COPY 모드로 전환합니다(원본은 절대 이동·삭제 안 함). 해시 키 receipt + `--rollback`. 시크릿과 이미 구조화된 KB는 보호됩니다. 비ASCII(예: 한글) 폴더명은 영역 슬러그로 보존됩니다.
117
+ - 정직한 범위: 이들은 vault 위생 도구입니다. 탐지는 읽기전용이고 모든 재배치는 게이트·복구 가능하며, Neurain이 사용자 폴더를 자율적으로 재편하지 않습니다.
118
+
107
119
  ## 현재 connector truth table
108
120
 
109
121
  | Host | MCP connection | Lifecycle hook preview | Notes |
@@ -119,7 +131,6 @@ Status: shipped.
119
131
  - non-developer GUI.
120
132
  - hosted control plane.
121
133
  - public Trust Center 및 status page.
122
- - npm publish 또는 package name reservation.
123
134
  - beta 동안 repo를 private으로 유지한다면 public GitHub publication.
124
135
 
125
136
  ## 문서 유지관리 규칙
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neurain",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.4",
4
4
  "description": "Local-first Neurain Knowledge OS CLI and MCP connector.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/src/cli.mjs CHANGED
@@ -1,56 +1,62 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import path from 'node:path';
3
- import { adoptCommand, rollbackAdoption, scanAdoption } from './core/adopt.mjs';
4
- import { answerCommand } from './core/answer_eval.mjs';
5
- import { connectCommand } from './core/connect.mjs';
6
- import { capabilitiesCommand } from './core/capabilities.mjs';
7
- import { curatorCommand } from './core/curator.mjs';
8
- import { daemonCommand } from './core/daemon.mjs';
9
- import { doctorCommand } from './core/doctor.mjs';
10
- import { initCommand } from './core/init.mjs';
11
- import { journalCommand } from './core/journal.mjs';
12
- import { lessonsCommand } from './core/lessons.mjs';
13
- import { lifecycleCommand } from './core/lifecycle.mjs';
14
- import { liveCasesCommand } from './core/live_cases.mjs';
15
- import { onboardCommand } from './core/onboard.mjs';
16
- import { recapCommand } from './core/recap.mjs';
17
- import { recallCommand } from './core/recall.mjs';
18
- import { reviewCommand } from './core/review_worker.mjs';
19
- import { statusCommand } from './core/status.mjs';
20
- import { digestCommand } from './core/digest.mjs';
21
- import { queueCommand } from './core/queue.mjs';
22
- import { reviewQueueCommand } from './core/review_queue.mjs';
23
- import { routeCommand } from './core/route.mjs';
24
- import { planWritebackCommand } from './core/plan_writeback.mjs';
25
- import { flushCommand, sessionFlushCommand } from './core/flush.mjs';
26
- import { compileCommand } from './core/compile_desk.mjs';
27
- import { planReceiptCommand } from './core/plan_receipt.mjs';
28
- import { sourceDigestCommand } from './core/source_digest_gen.mjs';
29
- import { stageCommand } from './core/stage.mjs';
30
- import { queueArchiveCommand } from './core/queue_archive.mjs';
31
- import { captureCommand } from './core/capture_durable.mjs';
32
- import { completeCommand } from './core/complete.mjs';
33
- import { linkCheckCommand } from './core/link_check.mjs';
34
- import { sessionLintCommand } from './core/session_lint.mjs';
35
- import { labelCommand } from './core/label.mjs';
36
- import { orphansCommand } from './core/orphans.mjs';
37
- import { hubsCommand } from './core/hubs.mjs';
38
- import { areaIndexCommand } from './core/area_index.mjs';
39
- import { reindexCommand } from './core/reindex.mjs';
40
- import { lintCommand } from './core/lint.mjs';
41
- import { sessionPulseCommand } from './core/session_pulse.mjs';
42
- import { syncCommand } from './core/sync.mjs';
43
- import { healthCommand } from './core/health.mjs';
44
- import { memoryCommand } from './core/memory.mjs';
45
- import { retentionCommand } from './core/retention.mjs';
46
- import { freezeCommand } from './core/freeze.mjs';
47
- import { memoryWriteCommand } from './core/memory_write_cli.mjs';
48
- import { backupCommand } from './core/backup.mjs';
49
- import { schedulerCommand } from './core/scheduler.mjs';
50
- import { searchCommand } from './core/search.mjs';
51
- import { watchCommand } from './core/watch.mjs';
52
- import { wrapCommand } from './core/wrap.mjs';
53
- import { startMcpServer } from './mcp/server.mjs';
3
+
4
+ const COMMAND_HANDLERS = {
5
+ init: async (args) => (await import('./core/init.mjs')).initCommand(args),
6
+ onboard: async (args) => (await import('./core/onboard.mjs')).onboardCommand(args),
7
+ answer: async (args) => (await import('./core/answer_eval.mjs')).answerCommand(args),
8
+ doctor: async (args) => (await import('./core/doctor.mjs')).doctorCommand(args),
9
+ search: async (args) => (await import('./core/search.mjs')).searchCommand(args),
10
+ connect: async (args) => (await import('./core/connect.mjs')).connectCommand(args),
11
+ journal: async (args) => (await import('./core/journal.mjs')).journalCommand(args),
12
+ lifecycle: async (args) => (await import('./core/lifecycle.mjs')).lifecycleCommand(args),
13
+ 'live-cases': async (args) => (await import('./core/live_cases.mjs')).liveCasesCommand(args),
14
+ lessons: async (args) => (await import('./core/lessons.mjs')).lessonsCommand(args),
15
+ curator: async (args) => (await import('./core/curator.mjs')).curatorCommand(args),
16
+ daemon: async (args) => (await import('./core/daemon.mjs')).daemonCommand(args),
17
+ recall: async (args) => (await import('./core/recall.mjs')).recallCommand(args),
18
+ reindex: async (args) => (await import('./core/reindex.mjs')).reindexCommand(args),
19
+ status: async (args) => (await import('./core/status.mjs')).statusCommand(args),
20
+ digest: async (args) => (await import('./core/digest.mjs')).digestCommand(args),
21
+ queue: async (args) => (await import('./core/queue.mjs')).queueCommand(args),
22
+ 'review-queue': async (args) => (await import('./core/review_queue.mjs')).reviewQueueCommand(args),
23
+ route: async (args) => (await import('./core/route.mjs')).routeCommand(args),
24
+ 'plan-writeback': async (args) => (await import('./core/plan_writeback.mjs')).planWritebackCommand(args),
25
+ flush: async (args) => (await import('./core/flush.mjs')).flushCommand(args),
26
+ 'session-flush': async (args) => (await import('./core/flush.mjs')).sessionFlushCommand(args),
27
+ compile: async (args) => (await import('./core/compile_desk.mjs')).compileCommand(args),
28
+ 'plan-receipt': async (args) => (await import('./core/plan_receipt.mjs')).planReceiptCommand(args),
29
+ 'source-digest': async (args) => (await import('./core/source_digest_gen.mjs')).sourceDigestCommand(args),
30
+ stage: async (args) => (await import('./core/stage.mjs')).stageCommand(args),
31
+ 'queue-archive': async (args) => (await import('./core/queue_archive.mjs')).queueArchiveCommand(args),
32
+ capture: async (args) => (await import('./core/capture_durable.mjs')).captureCommand(args),
33
+ complete: async (args) => (await import('./core/complete.mjs')).completeCommand(args),
34
+ 'link-check': async (args) => (await import('./core/link_check.mjs')).linkCheckCommand(args),
35
+ 'session-lint': async (args) => (await import('./core/session_lint.mjs')).sessionLintCommand(args),
36
+ label: async (args) => (await import('./core/label.mjs')).labelCommand(args),
37
+ orphans: async (args) => (await import('./core/orphans.mjs')).orphansCommand(args),
38
+ hubs: async (args) => (await import('./core/hubs.mjs')).hubsCommand(args),
39
+ 'area-index': async (args) => (await import('./core/area_index.mjs')).areaIndexCommand(args),
40
+ lint: async (args) => (await import('./core/lint.mjs')).lintCommand(args),
41
+ tidy: async (args) => (await import('./core/tidy.mjs')).tidyCommand(args),
42
+ 'structure-audit': async (args) => (await import('./core/structure_audit.mjs')).structureAuditCommand(args),
43
+ file: async (args) => (await import('./core/file_loose.mjs')).fileCommand(args),
44
+ organize: async (args) => (await import('./core/organize.mjs')).organizeCommand(args),
45
+ 'session-pulse': async (args) => (await import('./core/session_pulse.mjs')).sessionPulseCommand(args),
46
+ sync: async (args) => (await import('./core/sync.mjs')).syncCommand(args),
47
+ health: async (args) => (await import('./core/health.mjs')).healthCommand(args),
48
+ memory: async (args) => (await import('./core/memory.mjs')).memoryCommand(args),
49
+ retention: async (args) => (await import('./core/retention.mjs')).retentionCommand(args),
50
+ freeze: async (args) => (await import('./core/freeze.mjs')).freezeCommand(args),
51
+ 'memory-write': async (args) => (await import('./core/memory_write_cli.mjs')).memoryWriteCommand(args),
52
+ backup: async (args) => (await import('./core/backup.mjs')).backupCommand(args),
53
+ capabilities: async (args) => (await import('./core/capabilities.mjs')).capabilitiesCommand(args),
54
+ recap: async (args) => (await import('./core/recap.mjs')).recapCommand(args),
55
+ watch: async (args) => (await import('./core/watch.mjs')).watchCommand(args),
56
+ review: async (args) => (await import('./core/review_worker.mjs')).reviewCommand(args),
57
+ scheduler: async (args) => (await import('./core/scheduler.mjs')).schedulerCommand(args),
58
+ wrap: async (args) => (await import('./core/wrap.mjs')).wrapCommand(args),
59
+ };
54
60
 
55
61
  export async function runCli(argv) {
56
62
  const [command, ...rest] = argv;
@@ -65,61 +71,17 @@ export async function runCli(argv) {
65
71
  }
66
72
 
67
73
  const args = parseArgs(rest);
68
- if (command === 'init') return render(await initCommand(args));
69
- if (command === 'onboard') return render(await onboardCommand(args));
70
74
  if (command === 'adopt') {
75
+ const { adoptCommand, rollbackAdoption } = await import('./core/adopt.mjs');
71
76
  if (args.rollback) return render(await rollbackAdoption(args));
72
77
  return render(await adoptCommand(args));
73
78
  }
74
- if (command === 'answer') return render(await answerCommand(args));
75
- if (command === 'doctor') return render(await doctorCommand(args));
76
- if (command === 'search') return render(await searchCommand(args));
77
- if (command === 'connect') return render(await connectCommand(args));
78
- if (command === 'journal') return render(await journalCommand(args));
79
- if (command === 'lifecycle') return render(await lifecycleCommand(args));
80
- if (command === 'live-cases') return render(await liveCasesCommand(args));
81
- if (command === 'lessons') return render(await lessonsCommand(args));
82
- if (command === 'curator') return render(await curatorCommand(args));
83
- if (command === 'daemon') return render(await daemonCommand(args));
84
- if (command === 'recall') return render(await recallCommand(args));
85
- if (command === 'reindex') return render(await reindexCommand(args));
86
- if (command === 'status') return render(await statusCommand(args));
87
- if (command === 'digest') return render(await digestCommand(args));
88
- if (command === 'queue') return render(await queueCommand(args));
89
- if (command === 'review-queue') return render(await reviewQueueCommand(args));
90
- if (command === 'route') return render(await routeCommand(args));
91
- if (command === 'plan-writeback') return render(await planWritebackCommand(args));
92
- if (command === 'flush') return render(await flushCommand(args));
93
- if (command === 'session-flush') return render(await sessionFlushCommand(args));
94
- if (command === 'compile') return render(await compileCommand(args));
95
- if (command === 'plan-receipt') return render(await planReceiptCommand(args));
96
- if (command === 'source-digest') return render(await sourceDigestCommand(args));
97
- if (command === 'stage') return render(await stageCommand(args));
98
- if (command === 'queue-archive') return render(await queueArchiveCommand(args));
99
- if (command === 'capture') return render(await captureCommand(args));
100
- if (command === 'complete') return render(await completeCommand(args));
101
- if (command === 'link-check') return render(await linkCheckCommand(args));
102
- if (command === 'session-lint') return render(await sessionLintCommand(args));
103
- if (command === 'label') return render(await labelCommand(args));
104
- if (command === 'orphans') return render(await orphansCommand(args));
105
- if (command === 'hubs') return render(await hubsCommand(args));
106
- if (command === 'area-index') return render(await areaIndexCommand(args));
107
- if (command === 'lint') return render(await lintCommand(args));
108
- if (command === 'session-pulse') return render(await sessionPulseCommand(args));
109
- if (command === 'sync') return render(await syncCommand(args));
110
- if (command === 'health') return render(await healthCommand(args));
111
- if (command === 'memory') return render(await memoryCommand(args));
112
- if (command === 'retention') return render(await retentionCommand(args));
113
- if (command === 'freeze') return render(await freezeCommand(args));
114
- if (command === 'memory-write') return render(await memoryWriteCommand(args));
115
- if (command === 'backup') return render(await backupCommand(args));
116
- if (command === 'capabilities') return render(await capabilitiesCommand(args));
117
- if (command === 'recap') return render(await recapCommand(args));
118
- if (command === 'watch') return render(await watchCommand(args));
119
- if (command === 'review') return render(await reviewCommand(args));
120
- if (command === 'scheduler') return render(await schedulerCommand(args));
121
- if (command === 'wrap') return render(await wrapCommand(args));
122
- if (command === 'mcp') return startMcpServer(args);
79
+ const handler = COMMAND_HANDLERS[command];
80
+ if (handler) return render(await handler(args));
81
+ if (command === 'mcp') {
82
+ const { startMcpServer } = await import('./mcp/server.mjs');
83
+ return startMcpServer(args);
84
+ }
123
85
  if (command === 'selftest') return runSelftest();
124
86
 
125
87
  throw new Error(`Unknown command: ${command}\n\n${helpText()}`);
@@ -184,6 +146,8 @@ Usage:
184
146
  neurain onboard <folder> [--lang ko|en] [--host codex|claude|gemini|runtime] [--json]
185
147
  neurain adopt <folder> [--dry-run] [--apply --confirm "<N>건 저장 진행"] [--no-reindex]
186
148
  neurain adopt --rollback <receipt> [--root <folder>]
149
+ neurain organize <folder> [--apply --confirm "<N>건 저장 진행"] [--allow-secret] [--no-reindex] [--json]
150
+ neurain organize --rollback <receipt> --root <folder> [--json]
187
151
  neurain reindex <folder> [--dry-run] [--json]
188
152
  neurain doctor <folder> [--json]
189
153
  neurain search <folder> <query> [--top 10] [--json]
@@ -240,6 +204,9 @@ Usage:
240
204
  neurain hubs <folder> [--check | --apply] [--area name] [--json]
241
205
  neurain area-index <folder> [--build area | --refresh area | --register-curated area [--sensitivity private] | --detect [--fix] [--restamp-curated]] [--force] [--dry-run] [--json]
242
206
  neurain lint <folder> [--json]
207
+ neurain tidy <folder> [--json]
208
+ neurain structure-audit <folder> [--json]
209
+ neurain file <folder> [--apply --confirm "<N>건 저장 진행"] [--allow-secret] [--json]
243
210
  neurain session-pulse <folder> --session-id id --summary "..." [--focus t] [--next t] [--max-notes 8] [--no-area-brief] [--queue ...] [--now ts] [--dry-run] [--allow-secret] [--json]
244
211
  neurain sync <folder> --session-id id [--summary "..."] [--level light|standard|full] [--no-pulse] [--no-area-brief] [--queue ...] [--now ts] [--dry-run] [--json]
245
212
  neurain health <folder> [--fix] [--now ts] [--json]
@@ -1,9 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { absPath, compactStamp, ensureDir, exists, generatedPath, isTextFile, relPath, safeResolve, sha256, slug, timestamp, writeFileNoOverwrite } from './fs.mjs';
4
+ import { absPath, compactStamp, ensureDir, exists, generatedPath, isTextFile, relPath, safeResolve, sha256, timestamp, writeFileNoOverwrite } from './fs.mjs';
5
5
  import { inferSensitivityFromPath, secretLike } from './safety.mjs';
6
6
  import { reindexCommand } from './reindex.mjs';
7
+ import { slugify } from './envelope.mjs';
7
8
 
8
9
  const maxFileBytes = 20 * 1024 * 1024;
9
10
  const maxFiles = 5000;
@@ -119,7 +120,10 @@ export function scanAdoption(root, { dryRun = true } = {}) {
119
120
  : generatedRatio > 0.35
120
121
  ? 'hybrid'
121
122
  : 'in-place';
122
- const areaSlug = slug(path.basename(root), 'adopted-area');
123
+ // Hangul-preserving slug so a Korean folder name (e.g. 내폴더) becomes the area
124
+ // name `_내폴더` instead of being stripped to the generic `_adopted-area`. Area
125
+ // ids flow through `[^/]+` matchers and dir-listing discovery, so non-ASCII is safe.
126
+ const areaSlug = slugify(path.basename(root), 'adopted-area');
123
127
  const fingerprint = sha256(Buffer.from(files.map((file) => `${file.rel}:${file.sha256}`).sort().join('\n')));
124
128
  const required = [
125
129
  `10_areas/_${areaSlug}/_area.md`,
@@ -18,6 +18,7 @@ import path from 'node:path';
18
18
  import { absPath, ensureDir, timestamp } from './fs.mjs';
19
19
  import { vaultConfig } from './config.mjs';
20
20
  import { firstLineTitle, inferFlushLevelFromEnvelope, inferTargetLayerFromIntent } from './classify.mjs';
21
+ import { resolveCanonicalPath } from './resolve_target.mjs';
21
22
  import { folderForSourceType, makeEnvelope, readArgInput, renderCaptureMarkdown, slugify } from './envelope.mjs';
22
23
  import { withFileLock, atomicWriteJson } from './durable.mjs';
23
24
  import { stageAndPromote } from './stage.mjs';
@@ -151,7 +152,7 @@ export async function captureCommand(args) {
151
152
  const { envelope, assetPath, numericConflicts } = buildFullEnvelope(`raw-${timestamp().slice(0, 10).replace(/-/g, '')}-DRYRUN`, 'planned');
152
153
  const flushLevel = args['flush-level'] || inferFlushLevelFromEnvelope(envelope);
153
154
  const targetLayer = args['target-layer'] || inferTargetLayerFromIntent(envelope.write_intent);
154
- const queueItem = buildQueueItem(envelope, { sourceId: envelope.source_id, rawPath: envelope.raw_path, assetPath, title, numericConflictCount: numericConflicts.length, session, sessionId, flushLevel, targetLayer, targetPath: args['target-path'] || '', conflictPolicy: args['conflict-policy'] || 'queue_first', handoffPath: handoffPathFor(vaultCfg, sessionId, session) });
155
+ const queueItem = buildQueueItem(envelope, { sourceId: envelope.source_id, rawPath: envelope.raw_path, assetPath, title, numericConflictCount: numericConflicts.length, session, sessionId, flushLevel, targetLayer, targetPath: args['target-path'] || resolveCanonicalPath(root, vaultCfg, { area_candidates: envelope.area_candidates, write_intent: envelope.write_intent, sensitivity: envelope.sensitivity, title: envelope.title, target_layer: targetLayer, requires_user_decision: envelope.requires_user_decision }).target_path, conflictPolicy: args['conflict-policy'] || 'queue_first', handoffPath: handoffPathFor(vaultCfg, sessionId, session) });
155
156
  return done(args, {
156
157
  ok: true, command: 'capture', durable_write: false, dry_run: true,
157
158
  envelope, queue_item: queueItem, duplicate_of: duplicateOf || undefined,
@@ -202,7 +203,7 @@ export async function captureCommand(args) {
202
203
 
203
204
  const flushLevel = args['flush-level'] || inferFlushLevelFromEnvelope(envelope);
204
205
  const targetLayer = args['target-layer'] || inferTargetLayerFromIntent(envelope.write_intent);
205
- const queueItem = buildQueueItem(envelope, { sourceId, rawPath, assetPath, title, numericConflictCount: numericConflicts.length, session, sessionId, flushLevel, targetLayer, targetPath: args['target-path'] || '', conflictPolicy: args['conflict-policy'] || 'queue_first', handoffPath: handoffPathFor(vaultCfg, sessionId, session) });
206
+ const queueItem = buildQueueItem(envelope, { sourceId, rawPath, assetPath, title, numericConflictCount: numericConflicts.length, session, sessionId, flushLevel, targetLayer, targetPath: args['target-path'] || resolveCanonicalPath(root, vaultCfg, { area_candidates: envelope.area_candidates, write_intent: envelope.write_intent, sensitivity: envelope.sensitivity, title: envelope.title, target_layer: targetLayer, requires_user_decision: envelope.requires_user_decision }).target_path, conflictPolicy: args['conflict-policy'] || 'queue_first', handoffPath: handoffPathFor(vaultCfg, sessionId, session) });
206
207
  fs.appendFileSync(queueAbs, `${JSON.stringify(queueItem)}\n`); // direct: already holding the queue lock
207
208
  const pendingCount = pendingCountForSession(root, vaultCfg, sessionId); // after append
208
209
  return { refused: false, sourceId, envelope, rawPath, assetPath, pendingCount };
@@ -8,6 +8,7 @@
8
8
  import path from 'node:path';
9
9
  import { absPath, timestamp } from './fs.mjs';
10
10
  import { vaultConfig } from './config.mjs';
11
+ import { resolveCanonicalPath } from './resolve_target.mjs';
11
12
  import { readJsonSafe, readJsonl } from './vault_state.mjs';
12
13
 
13
14
  function manifestEntries(value) {
@@ -51,13 +52,29 @@ function reviewReasons(item) {
51
52
  return [...new Set(reasons)];
52
53
  }
53
54
 
54
- function summarizeQueueItem(item, reasons = []) {
55
+ function summarizeQueueItem(item, reasons = [], ctx = null) {
56
+ // Fill the canonical destination the pipeline historically left blank: when the
57
+ // queue row carries no explicit target_path, derive it deterministically from
58
+ // area + write_intent + sensitivity so every candidate shows a real save target
59
+ // instead of an empty string (the wiki/area/output drift fix). ctx carries the
60
+ // root + vaultCfg needed to build the path; without it we keep the old behavior.
61
+ let targetPath = item.target_path || '';
62
+ if (!targetPath && ctx && ctx.root && ctx.vaultCfg) {
63
+ targetPath = resolveCanonicalPath(ctx.root, ctx.vaultCfg, {
64
+ area_candidates: item.area_candidates,
65
+ write_intent: item.write_intent,
66
+ sensitivity: item.sensitivity,
67
+ title: item.title,
68
+ target_layer: item.target_layer,
69
+ requires_user_decision: item.requires_user_decision,
70
+ }).target_path;
71
+ }
55
72
  return {
56
73
  source_id: item.source_id || '',
57
74
  title: item.title || '',
58
75
  raw_path: item.raw_path || '',
59
76
  target_layer: item.target_layer || inferTargetLayer(item),
60
- target_path: item.target_path || '',
77
+ target_path: targetPath,
61
78
  sensitivity: item.sensitivity || 'internal',
62
79
  flush_level: item.flush_level || inferFlushLevel(item),
63
80
  reasons,
@@ -127,13 +144,13 @@ function summarizeManifestTarget(entry, selectedBy) {
127
144
  };
128
145
  }
129
146
 
130
- function selectTarget(query, value, queueItems, rawDir) {
147
+ function selectTarget(query, value, queueItems, rawDir, ctx = null) {
131
148
  const lower = String(query || '').toLowerCase();
132
149
  const sourceIdMatch = String(query || '').match(/\b(?:raw-\d{8}-\d{3}|clipper-\d{8}-\d{6})\b/);
133
150
  if (sourceIdMatch) {
134
151
  const sourceId = sourceIdMatch[0];
135
152
  const queueMatch = queueItems.find((item) => item.source_id === sourceId);
136
- if (queueMatch) return { kind: 'queue_item', ...summarizeQueueItem(queueMatch, reviewReasons(queueMatch)) };
153
+ if (queueMatch) return { kind: 'queue_item', ...summarizeQueueItem(queueMatch, reviewReasons(queueMatch), ctx) };
137
154
  const manifestMatch = manifestEntries(value).find((entry) => entry.source_id === sourceId);
138
155
  if (manifestMatch) return summarizeManifestTarget(manifestMatch, 'source_id');
139
156
  }
@@ -156,10 +173,10 @@ function selectTarget(query, value, queueItems, rawDir) {
156
173
  return null;
157
174
  }
158
175
 
159
- function selectAutoTarget({ safe: safeItems, needsConfirmation: confirmationItems, manifest: value, sessionId: targetSessionId }) {
176
+ function selectAutoTarget({ safe: safeItems, needsConfirmation: confirmationItems, manifest: value, sessionId: targetSessionId, ctx = null }) {
160
177
  if (safeItems.length > 0) {
161
178
  const item = safeItems[0];
162
- return { auto_selected: true, selected_by: 'safe_queue', kind: 'queue_item', ...summarizeQueueItem(item, []) };
179
+ return { auto_selected: true, selected_by: 'safe_queue', kind: 'queue_item', ...summarizeQueueItem(item, [], ctx) };
163
180
  }
164
181
  if (confirmationItems.length > 0) return null;
165
182
 
@@ -217,7 +234,7 @@ export async function compileCommand(args) {
217
234
  const needsConfirmation = pending
218
235
  .filter((item) => reviewReasons(item).length > 0)
219
236
  .map((item) => {
220
- const s = summarizeQueueItem(item, reviewReasons(item));
237
+ const s = summarizeQueueItem(item, reviewReasons(item), { root, vaultCfg });
221
238
  // Never expose a private item's file paths in output (R3-#1).
222
239
  if (s.sensitivity === 'private') { s.raw_path = '[redacted:private]'; s.target_path = ''; }
223
240
  return s;
@@ -236,10 +253,10 @@ export async function compileCommand(args) {
236
253
  const wantAuto = Boolean(args.auto);
237
254
  const deskOnly = Boolean(args.desk || args['no-auto-select']) || wantList || (!detail && !wantAuto);
238
255
  const selected = detail
239
- ? selectTarget(detail, manifest, pending, rawDir)
256
+ ? selectTarget(detail, manifest, pending, rawDir, { root, vaultCfg })
240
257
  : deskOnly
241
258
  ? null
242
- : selectAutoTarget({ safe, needsConfirmation, manifest, sessionId });
259
+ : selectAutoTarget({ safe, needsConfirmation, manifest, sessionId, ctx: { root, vaultCfg } });
243
260
  const mode = selected
244
261
  ? (selected.auto_selected ? 'auto_selected_compile_plan' : 'selected_compile_plan')
245
262
  : deskOnly
@@ -267,7 +284,7 @@ export async function compileCommand(args) {
267
284
  pending_count: pending.length,
268
285
  safe_count: safe.length,
269
286
  needs_confirmation_count: needsConfirmation.length,
270
- safe: safe.slice(0, top).map((item) => summarizeQueueItem(item, [])),
287
+ safe: safe.slice(0, top).map((item) => summarizeQueueItem(item, [], { root, vaultCfg })),
271
288
  needs_confirmation: needsConfirmation.slice(0, top),
272
289
  },
273
290
  source_digest: {