neurain 0.1.0-alpha.0

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 (89) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +57 -0
  3. package/README.md +205 -0
  4. package/SECURITY.md +22 -0
  5. package/bin/neurain.mjs +7 -0
  6. package/docs/comparison-mem0.en.md +22 -0
  7. package/docs/connect-claude.en.md +48 -0
  8. package/docs/connect-claude.kr.md +51 -0
  9. package/docs/connect-codex.en.md +38 -0
  10. package/docs/connect-codex.kr.md +40 -0
  11. package/docs/connect-gemini.en.md +71 -0
  12. package/docs/connect-gemini.kr.md +71 -0
  13. package/docs/connect-runtime.en.md +61 -0
  14. package/docs/connect-runtime.kr.md +61 -0
  15. package/docs/development-status.en.md +157 -0
  16. package/docs/development-status.kr.md +157 -0
  17. package/docs/knowledge-os.en.md +105 -0
  18. package/docs/knowledge-os.kr.md +106 -0
  19. package/docs/pricing.en.md +14 -0
  20. package/docs/privacy-and-data-flow.en.md +25 -0
  21. package/docs/public-saas-readiness.en.md +39 -0
  22. package/docs/quickstart.en.md +64 -0
  23. package/docs/quickstart.kr.md +64 -0
  24. package/docs/release-checklist.en.md +38 -0
  25. package/docs/safety.en.md +36 -0
  26. package/docs/self-improvement-90-roadmap.en.md +429 -0
  27. package/docs/self-improvement-90-roadmap.kr.md +429 -0
  28. package/docs/self-improving-workflows.en.md +163 -0
  29. package/docs/self-improving-workflows.kr.md +163 -0
  30. package/docs/support.en.md +17 -0
  31. package/docs/troubleshooting.en.md +35 -0
  32. package/package.json +36 -0
  33. package/src/cli.mjs +261 -0
  34. package/src/core/adopt.mjs +304 -0
  35. package/src/core/answer_eval.mjs +450 -0
  36. package/src/core/capabilities.mjs +217 -0
  37. package/src/core/capture_durable.mjs +181 -0
  38. package/src/core/classify.mjs +237 -0
  39. package/src/core/compile_desk.mjs +324 -0
  40. package/src/core/complete.mjs +108 -0
  41. package/src/core/config.mjs +142 -0
  42. package/src/core/connect.mjs +355 -0
  43. package/src/core/curator.mjs +351 -0
  44. package/src/core/daemon.mjs +536 -0
  45. package/src/core/digest.mjs +155 -0
  46. package/src/core/doctor.mjs +115 -0
  47. package/src/core/durable.mjs +96 -0
  48. package/src/core/envelope.mjs +97 -0
  49. package/src/core/flush.mjs +190 -0
  50. package/src/core/fs.mjs +121 -0
  51. package/src/core/init.mjs +194 -0
  52. package/src/core/journal.mjs +269 -0
  53. package/src/core/labels.mjs +117 -0
  54. package/src/core/lessons.mjs +793 -0
  55. package/src/core/lifecycle.mjs +1138 -0
  56. package/src/core/link_check.mjs +180 -0
  57. package/src/core/live_cases.mjs +221 -0
  58. package/src/core/onboard.mjs +175 -0
  59. package/src/core/plan_receipt.mjs +177 -0
  60. package/src/core/plan_writeback.mjs +176 -0
  61. package/src/core/queue.mjs +62 -0
  62. package/src/core/queue_archive.mjs +87 -0
  63. package/src/core/queue_model.mjs +161 -0
  64. package/src/core/queue_write.mjs +28 -0
  65. package/src/core/recall.mjs +1802 -0
  66. package/src/core/recall_bench.mjs +275 -0
  67. package/src/core/recall_corpus.mjs +152 -0
  68. package/src/core/recall_facts.mjs +233 -0
  69. package/src/core/recall_intel.mjs +233 -0
  70. package/src/core/recall_lexical.mjs +269 -0
  71. package/src/core/recap.mjs +78 -0
  72. package/src/core/review_queue.mjs +131 -0
  73. package/src/core/review_worker.mjs +284 -0
  74. package/src/core/route.mjs +73 -0
  75. package/src/core/safety.mjs +57 -0
  76. package/src/core/scheduler.mjs +697 -0
  77. package/src/core/search.mjs +54 -0
  78. package/src/core/secret_scan.mjs +143 -0
  79. package/src/core/semantic.mjs +187 -0
  80. package/src/core/source_digest.mjs +56 -0
  81. package/src/core/source_digest_gen.mjs +311 -0
  82. package/src/core/stage.mjs +105 -0
  83. package/src/core/status.mjs +175 -0
  84. package/src/core/vault_state.mjs +115 -0
  85. package/src/core/watch.mjs +282 -0
  86. package/src/core/wiki_log.mjs +29 -0
  87. package/src/core/wrap.mjs +62 -0
  88. package/src/mcp/server.mjs +865 -0
  89. package/templates/starter-vault/README.md +9 -0
@@ -0,0 +1,304 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { absPath, compactStamp, ensureDir, exists, generatedPath, isTextFile, relPath, safeResolve, sha256, slug, timestamp, writeFileNoOverwrite } from './fs.mjs';
5
+ import { inferSensitivityFromPath, secretLike } from './safety.mjs';
6
+
7
+ const maxFileBytes = 20 * 1024 * 1024;
8
+ const maxFiles = 5000;
9
+
10
+ export async function adoptCommand(args) {
11
+ const target = absPath(args._[0] || args.folder);
12
+ const dryRun = !args.apply;
13
+ const scan = scanAdoption(target, { dryRun });
14
+ if (args.apply) {
15
+ const expected = scan.confirmation_required;
16
+ if (String(args.confirm || '') !== expected) {
17
+ throw new Error(`Apply requires --confirm "${expected}".`);
18
+ }
19
+ scan.apply = applyAdoption(scan, { root: target });
20
+ }
21
+ if (args.json) return { json: true, payload: scan };
22
+ return {
23
+ text: [
24
+ '# Neurain adopt scan',
25
+ '',
26
+ `- Target: ${scan.target}`,
27
+ `- Dry run: ${scan.dry_run ? 'yes' : 'no'}`,
28
+ `- Proposed area: ${scan.proposed_area}`,
29
+ `- Recommended mode: ${scan.summary.recommended_mode}`,
30
+ `- Scanned files: ${scan.summary.scanned_files}`,
31
+ `- Excluded entries: ${scan.summary.excluded_entries}`,
32
+ `- Risks: ${scan.summary.risk_count}`,
33
+ `- Confirmation required: ${scan.confirmation_required}`,
34
+ scan.apply ? `- Apply receipt: ${scan.apply.receipt_path}` : '',
35
+ ].filter(Boolean).join('\n'),
36
+ };
37
+ }
38
+
39
+ export function scanAdoption(root, { dryRun = true } = {}) {
40
+ if (!exists(root)) throw new Error(`Folder does not exist: ${root}`);
41
+ if (!fs.statSync(root).isDirectory()) throw new Error(`Target is not a directory: ${root}`);
42
+
43
+ const rootReal = fs.realpathSync(root);
44
+ const files = [];
45
+ const excluded = [];
46
+ const risks = [];
47
+ let totalBytes = 0;
48
+ let truncated = false;
49
+
50
+ function rec(dir) {
51
+ if (files.length + excluded.length >= maxFiles) {
52
+ truncated = true;
53
+ return;
54
+ }
55
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
56
+ const abs = path.join(dir, entry.name);
57
+ const rel = relPath(root, abs);
58
+ if (generatedPath(rel)) {
59
+ excluded.push({ rel, reason: 'generated/cache artifact' });
60
+ continue;
61
+ }
62
+ if (entry.isSymbolicLink()) {
63
+ excluded.push({ rel, reason: 'symlink excluded' });
64
+ risks.push({ rel, severity: 'high', reason: 'symlink requires explicit handling' });
65
+ continue;
66
+ }
67
+ if (entry.isDirectory()) {
68
+ const real = fs.realpathSync(abs);
69
+ if (real !== rootReal && !real.startsWith(`${rootReal}${path.sep}`)) {
70
+ excluded.push({ rel, reason: 'external directory excluded' });
71
+ risks.push({ rel, severity: 'high', reason: 'path traversal or external directory' });
72
+ continue;
73
+ }
74
+ rec(abs);
75
+ continue;
76
+ }
77
+ if (!entry.isFile()) {
78
+ excluded.push({ rel, reason: 'not a regular file' });
79
+ continue;
80
+ }
81
+ const stat = fs.statSync(abs);
82
+ totalBytes += stat.size;
83
+ if (stat.size > maxFileBytes) {
84
+ excluded.push({ rel, reason: 'large file excluded from default adoption' });
85
+ risks.push({ rel, severity: 'medium', reason: `large file over ${maxFileBytes} bytes` });
86
+ continue;
87
+ }
88
+ const text = isTextFile(rel) ? safeReadText(abs) : '';
89
+ const secret = text ? secretLike(text) : '';
90
+ if (secret) risks.push({ rel, severity: 'high', reason: `secret-like content: ${secret}` });
91
+ files.push({
92
+ rel,
93
+ bytes: stat.size,
94
+ kind: classify(rel, text),
95
+ sensitivity: secret || inferSensitivityFromPath(rel) === 'private' ? 'private' : 'internal',
96
+ sha256: sha256(fs.readFileSync(abs)),
97
+ });
98
+ }
99
+ }
100
+
101
+ rec(root);
102
+ const privateCount = files.filter((f) => f.sensitivity === 'private').length;
103
+ const generatedRatio = excluded.length / Math.max(1, files.length + excluded.length);
104
+ const recommendedMode = privateCount > 0 || risks.some((risk) => risk.severity === 'high')
105
+ ? 'copy'
106
+ : generatedRatio > 0.35
107
+ ? 'hybrid'
108
+ : 'in-place';
109
+ const areaSlug = slug(path.basename(root), 'adopted-area');
110
+ const fingerprint = sha256(Buffer.from(files.map((file) => `${file.rel}:${file.sha256}`).sort().join('\n')));
111
+ const required = [
112
+ `10_areas/_${areaSlug}/_area.md`,
113
+ `10_areas/_${areaSlug}/index.md`,
114
+ `10_areas/_${areaSlug}/sources_map.md`,
115
+ `10_areas/_${areaSlug}/log.md`,
116
+ ];
117
+ const optional = [
118
+ `10_areas/_${areaSlug}/current/${areaSlug}-area-brief.md`,
119
+ `10_areas/_${areaSlug}/search-index/entities.json`,
120
+ `10_areas/_${areaSlug}/search-index/domain-routing.json`,
121
+ ];
122
+
123
+ return {
124
+ ok: true,
125
+ command: 'adopt',
126
+ generated_at: timestamp(),
127
+ dry_run: dryRun,
128
+ target: root,
129
+ folder_fingerprint: fingerprint,
130
+ proposed_area: `_${areaSlug}`,
131
+ summary: {
132
+ scanned_files: files.length,
133
+ excluded_entries: excluded.length,
134
+ total_bytes: totalBytes,
135
+ private_or_secret_files: privateCount,
136
+ risk_count: risks.length,
137
+ truncated,
138
+ recommended_mode: recommendedMode,
139
+ },
140
+ write_plan_preview: {
141
+ required_writes: required,
142
+ optional_writes: optional,
143
+ storage_only: ['output/receipts/adoption/<timestamp>.json'],
144
+ },
145
+ files: files.slice(0, 200),
146
+ excluded: excluded.slice(0, 200),
147
+ risks: risks.slice(0, 200),
148
+ confirmation_required: `${required.length + optional.length}건 저장 진행`,
149
+ };
150
+ }
151
+
152
+ export function applyAdoption(scan, { root = scan.target } = {}) {
153
+ const planned = [...scan.write_plan_preview.required_writes, ...scan.write_plan_preview.optional_writes];
154
+ const collisions = planned.filter((rel) => exists(safeResolve(root, rel)));
155
+ if (collisions.length) {
156
+ return { ok: false, attempted: false, reason: 'target path collision', collision_files: collisions };
157
+ }
158
+
159
+ const stamp = compactStamp();
160
+ const areaSlug = scan.proposed_area.replace(/^_+/, '');
161
+ const receiptRel = `output/receipts/adoption/${stamp}-${areaSlug}.json`;
162
+ const intentRel = `output/receipts/adoption/${stamp}-${areaSlug}-intent.json`;
163
+ const created = [];
164
+ const skipped = [];
165
+ const hashes = {};
166
+
167
+ ensureDir(safeResolve(root, 'output/receipts/adoption'));
168
+ fs.writeFileSync(safeResolve(root, intentRel), `${JSON.stringify({
169
+ tool: 'neurain-adopt',
170
+ generated_at: timestamp(),
171
+ planned_writes: planned,
172
+ target: scan.target,
173
+ folder_fingerprint: scan.folder_fingerprint,
174
+ }, null, 2)}\n`);
175
+
176
+ for (const rel of planned) {
177
+ const abs = safeResolve(root, rel);
178
+ writeFileNoOverwrite(abs, adapterContent(rel, scan, areaSlug), { created, skipped });
179
+ if (exists(abs)) hashes[rel] = sha256(fs.readFileSync(abs));
180
+ }
181
+
182
+ const receipt = {
183
+ ok: true,
184
+ tool: 'neurain-adopt',
185
+ generated_at: timestamp(),
186
+ receipt_version: 1,
187
+ mode: scan.summary.recommended_mode,
188
+ target: scan.target,
189
+ proposed_area: scan.proposed_area,
190
+ folder_fingerprint: scan.folder_fingerprint,
191
+ intent_path: intentRel,
192
+ planned_writes: planned,
193
+ created_files: created.map((file) => relPath(root, file)),
194
+ created_file_hashes: hashes,
195
+ skipped_existing_files: skipped.map((file) => relPath(root, file)),
196
+ user_owned_files_modified: [],
197
+ rollback_command: `neurain adopt --rollback ${receiptRel} --root "${root}"`,
198
+ };
199
+ fs.writeFileSync(safeResolve(root, receiptRel), `${JSON.stringify(receipt, null, 2)}\n`);
200
+ return { ...receipt, receipt_path: receiptRel };
201
+ }
202
+
203
+ export async function rollbackAdoption(args) {
204
+ const root = absPath(args.root || process.cwd());
205
+ const receiptRel = String(args.rollback);
206
+ const receiptAbs = safeResolve(root, receiptRel);
207
+ if (!exists(receiptAbs)) throw new Error(`Receipt does not exist: ${receiptRel}`);
208
+ const receipt = JSON.parse(fs.readFileSync(receiptAbs, 'utf8'));
209
+ if (receipt.tool !== 'neurain-adopt') throw new Error(`Not an adoption receipt: ${receiptRel}`);
210
+ const removed = [];
211
+ const missing = [];
212
+ const changed = [];
213
+ for (const rel of receipt.created_files || []) {
214
+ const abs = safeResolve(root, rel);
215
+ if (!exists(abs)) {
216
+ missing.push(rel);
217
+ continue;
218
+ }
219
+ const expected = receipt.created_file_hashes?.[rel];
220
+ if (!expected || sha256(fs.readFileSync(abs)) !== expected) {
221
+ changed.push(rel);
222
+ continue;
223
+ }
224
+ fs.rmSync(abs);
225
+ removed.push(rel);
226
+ }
227
+ const result = {
228
+ ok: missing.length === 0 && changed.length === 0,
229
+ tool: 'neurain-adopt-rollback',
230
+ source_receipt_path: receiptRel,
231
+ removed_files: removed,
232
+ missing_files: missing,
233
+ changed_files: changed,
234
+ user_owned_files_modified: [],
235
+ };
236
+ const rollbackRel = `output/receipts/adoption/${compactStamp()}-rollback.json`;
237
+ ensureDir(safeResolve(root, 'output/receipts/adoption'));
238
+ fs.writeFileSync(safeResolve(root, rollbackRel), `${JSON.stringify(result, null, 2)}\n`);
239
+ if (args.json) return { json: true, payload: { ...result, receipt_path: rollbackRel } };
240
+ return {
241
+ text: [
242
+ '# Neurain adopt rollback',
243
+ '',
244
+ `- OK: ${result.ok ? 'yes' : 'no'}`,
245
+ `- Removed files: ${removed.length}`,
246
+ `- Missing files: ${missing.length}`,
247
+ `- Changed files: ${changed.length}`,
248
+ `- Receipt: ${rollbackRel}`,
249
+ ].join('\n'),
250
+ };
251
+ }
252
+
253
+ function adapterContent(rel, scan, areaSlug) {
254
+ const title = areaSlug.replace(/[-_]+/g, ' ');
255
+ if (rel.endsWith('/_area.md')) return `# ${title}\n\n- source status: adopted\n- sensitivity: internal\n- adoption mode: ${scan.summary.recommended_mode}\n`;
256
+ if (rel.endsWith('/index.md')) return `# ${title} Index\n\n- [_area.md](_area.md)\n- [sources_map.md](sources_map.md)\n`;
257
+ if (rel.endsWith('/sources_map.md')) return `# ${title} Sources Map\n\n- Source fingerprint: ${scan.folder_fingerprint}\n- Scanned files: ${scan.summary.scanned_files}\n- Recommended mode: ${scan.summary.recommended_mode}\n`;
258
+ if (rel.endsWith('/log.md')) return `# ${title} Log\n\n- ${timestamp()} adopted folder adapter created.\n`;
259
+ if (rel.includes('/current/')) return `# ${title} Area Brief\n\nInitial adopted area brief.\n`;
260
+ if (rel.endsWith('entities.json')) return '[]\n';
261
+ if (rel.endsWith('domain-routing.json')) return '[]\n';
262
+ return '';
263
+ }
264
+
265
+ function classify(rel, text) {
266
+ const lower = rel.toLowerCase();
267
+ if (lower.endsWith('.md') || lower.includes('readme')) return 'source_or_note';
268
+ if (/\.(json|ya?ml|toml)$/.test(lower)) return 'config_or_data';
269
+ if (/\.(js|mjs|ts|tsx|py|rb|go|rs)$/.test(lower)) return 'code';
270
+ if (text) return 'text';
271
+ return 'binary_or_asset';
272
+ }
273
+
274
+ export { secretLike };
275
+
276
+ function safeReadText(file) {
277
+ try {
278
+ return fs.readFileSync(file, 'utf8');
279
+ } catch {
280
+ return '';
281
+ }
282
+ }
283
+
284
+ export function seedAdoptionFixtures(root) {
285
+ const clean = path.join(root, 'clean');
286
+ const messy = path.join(root, 'messy');
287
+ const risky = path.join(root, 'risky');
288
+ fs.mkdirSync(path.join(clean, 'docs'), { recursive: true });
289
+ fs.writeFileSync(path.join(clean, 'README.md'), '# Clean\n');
290
+ fs.writeFileSync(path.join(clean, 'docs', 'plan.md'), 'Launch notes.\n');
291
+ fs.mkdirSync(path.join(messy, 'node_modules'), { recursive: true });
292
+ fs.writeFileSync(path.join(messy, 'README.md'), '# Messy\n');
293
+ fs.writeFileSync(path.join(messy, 'node_modules', 'cache.js'), 'generated');
294
+ fs.mkdirSync(path.join(messy, '.cache'), { recursive: true });
295
+ fs.writeFileSync(path.join(messy, '.cache', 'blob'), 'generated');
296
+ fs.mkdirSync(risky, { recursive: true });
297
+ fs.writeFileSync(path.join(risky, 'secret.env'), `${['API', '_KEY=abc123'].join('')}\n`);
298
+ try {
299
+ fs.symlinkSync(os.tmpdir(), path.join(risky, 'external-link'));
300
+ } catch {
301
+ fs.writeFileSync(path.join(risky, 'access-token.txt'), `${['access', '_token=abc123'].join('')}\n`);
302
+ }
303
+ return { clean, messy, risky };
304
+ }