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 +15 -0
- package/README.md +17 -1
- package/docs/development-status.en.md +16 -5
- package/docs/development-status.kr.md +16 -5
- package/package.json +1 -1
- package/src/cli.mjs +69 -102
- package/src/core/adopt.mjs +6 -2
- package/src/core/capture_durable.mjs +3 -2
- package/src/core/compile_desk.mjs +27 -10
- package/src/core/file_loose.mjs +204 -0
- package/src/core/lint.mjs +4 -0
- package/src/core/onboard.mjs +3 -3
- package/src/core/organize.mjs +297 -0
- package/src/core/resolve_target.mjs +92 -0
- package/src/core/structure_audit.mjs +107 -0
- package/src/core/tidy.mjs +167 -0
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.
|
|
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-
|
|
5
|
-
Package: `neurain@0.1.0-alpha.
|
|
6
|
-
Latest documented commit: `
|
|
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
|
-
-
|
|
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-
|
|
5
|
-
Package: `neurain@0.1.0-alpha.
|
|
6
|
-
Latest documented commit: `
|
|
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
|
-
-
|
|
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
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
75
|
-
if (
|
|
76
|
-
if (command === '
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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]
|
package/src/core/adopt.mjs
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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'] ||
|
|
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'] ||
|
|
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:
|
|
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: {
|