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.
- package/CHANGELOG.md +19 -0
- package/LICENSE +57 -0
- package/README.md +205 -0
- package/SECURITY.md +22 -0
- package/bin/neurain.mjs +7 -0
- package/docs/comparison-mem0.en.md +22 -0
- package/docs/connect-claude.en.md +48 -0
- package/docs/connect-claude.kr.md +51 -0
- package/docs/connect-codex.en.md +38 -0
- package/docs/connect-codex.kr.md +40 -0
- package/docs/connect-gemini.en.md +71 -0
- package/docs/connect-gemini.kr.md +71 -0
- package/docs/connect-runtime.en.md +61 -0
- package/docs/connect-runtime.kr.md +61 -0
- package/docs/development-status.en.md +157 -0
- package/docs/development-status.kr.md +157 -0
- package/docs/knowledge-os.en.md +105 -0
- package/docs/knowledge-os.kr.md +106 -0
- package/docs/pricing.en.md +14 -0
- package/docs/privacy-and-data-flow.en.md +25 -0
- package/docs/public-saas-readiness.en.md +39 -0
- package/docs/quickstart.en.md +64 -0
- package/docs/quickstart.kr.md +64 -0
- package/docs/release-checklist.en.md +38 -0
- package/docs/safety.en.md +36 -0
- package/docs/self-improvement-90-roadmap.en.md +429 -0
- package/docs/self-improvement-90-roadmap.kr.md +429 -0
- package/docs/self-improving-workflows.en.md +163 -0
- package/docs/self-improving-workflows.kr.md +163 -0
- package/docs/support.en.md +17 -0
- package/docs/troubleshooting.en.md +35 -0
- package/package.json +36 -0
- package/src/cli.mjs +261 -0
- package/src/core/adopt.mjs +304 -0
- package/src/core/answer_eval.mjs +450 -0
- package/src/core/capabilities.mjs +217 -0
- package/src/core/capture_durable.mjs +181 -0
- package/src/core/classify.mjs +237 -0
- package/src/core/compile_desk.mjs +324 -0
- package/src/core/complete.mjs +108 -0
- package/src/core/config.mjs +142 -0
- package/src/core/connect.mjs +355 -0
- package/src/core/curator.mjs +351 -0
- package/src/core/daemon.mjs +536 -0
- package/src/core/digest.mjs +155 -0
- package/src/core/doctor.mjs +115 -0
- package/src/core/durable.mjs +96 -0
- package/src/core/envelope.mjs +97 -0
- package/src/core/flush.mjs +190 -0
- package/src/core/fs.mjs +121 -0
- package/src/core/init.mjs +194 -0
- package/src/core/journal.mjs +269 -0
- package/src/core/labels.mjs +117 -0
- package/src/core/lessons.mjs +793 -0
- package/src/core/lifecycle.mjs +1138 -0
- package/src/core/link_check.mjs +180 -0
- package/src/core/live_cases.mjs +221 -0
- package/src/core/onboard.mjs +175 -0
- package/src/core/plan_receipt.mjs +177 -0
- package/src/core/plan_writeback.mjs +176 -0
- package/src/core/queue.mjs +62 -0
- package/src/core/queue_archive.mjs +87 -0
- package/src/core/queue_model.mjs +161 -0
- package/src/core/queue_write.mjs +28 -0
- package/src/core/recall.mjs +1802 -0
- package/src/core/recall_bench.mjs +275 -0
- package/src/core/recall_corpus.mjs +152 -0
- package/src/core/recall_facts.mjs +233 -0
- package/src/core/recall_intel.mjs +233 -0
- package/src/core/recall_lexical.mjs +269 -0
- package/src/core/recap.mjs +78 -0
- package/src/core/review_queue.mjs +131 -0
- package/src/core/review_worker.mjs +284 -0
- package/src/core/route.mjs +73 -0
- package/src/core/safety.mjs +57 -0
- package/src/core/scheduler.mjs +697 -0
- package/src/core/search.mjs +54 -0
- package/src/core/secret_scan.mjs +143 -0
- package/src/core/semantic.mjs +187 -0
- package/src/core/source_digest.mjs +56 -0
- package/src/core/source_digest_gen.mjs +311 -0
- package/src/core/stage.mjs +105 -0
- package/src/core/status.mjs +175 -0
- package/src/core/vault_state.mjs +115 -0
- package/src/core/watch.mjs +282 -0
- package/src/core/wiki_log.mjs +29 -0
- package/src/core/wrap.mjs +62 -0
- package/src/mcp/server.mjs +865 -0
- package/templates/starter-vault/README.md +9 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// `link-check` command (W-C, read-only). Validates every markdown link `[..](..)`
|
|
2
|
+
// and wikilink `[[..]]` across the vault, reporting unresolved targets by file and
|
|
3
|
+
// area. Writes nothing. Faithful port of the vault neurain-link-check tool; the
|
|
4
|
+
// area buckets use the configured structural dirs (areas/wiki/system/raw) instead
|
|
5
|
+
// of hardcoded prefixes, so no vault identifier enters the engine.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { absPath, readText, relPath, walkFiles } from './fs.mjs';
|
|
9
|
+
import { vaultConfig } from './config.mjs';
|
|
10
|
+
|
|
11
|
+
function reEsc(s) {
|
|
12
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function linkCheckCommand(args) {
|
|
16
|
+
const root = absPath(args._[0] || args.root || process.cwd());
|
|
17
|
+
const vaultCfg = vaultConfig(root);
|
|
18
|
+
const top = Number(args.top || 20);
|
|
19
|
+
const scope = String(args.scope || '.');
|
|
20
|
+
const includeRaw = Boolean(args['include-raw']);
|
|
21
|
+
const includeArchive = Boolean(args['include-archive']);
|
|
22
|
+
const includeOutput = Boolean(args['include-output']);
|
|
23
|
+
|
|
24
|
+
const rawPrefix = `${vaultCfg.raw_dir}/`;
|
|
25
|
+
const outPrefix = `${vaultCfg.output_dir}/`;
|
|
26
|
+
const isArchivePath = (rel) =>
|
|
27
|
+
rel.startsWith(`${vaultCfg.archive_dir}/`) || rel.startsWith('_trash/') || rel.includes('/_archive/') || rel.includes('/archive/');
|
|
28
|
+
|
|
29
|
+
const files = walkFiles(path.join(root, scope), { includeRaw: true, maxFiles: 200000 })
|
|
30
|
+
.filter((abs) => abs.endsWith('.md'))
|
|
31
|
+
.map((abs) => ({ abs, rel: relPath(root, abs) }))
|
|
32
|
+
.filter(({ rel }) => includeRaw || !rel.startsWith(rawPrefix))
|
|
33
|
+
.filter(({ rel }) => includeOutput || (!rel.startsWith(outPrefix) && !rel.startsWith('outputs/')))
|
|
34
|
+
.filter(({ rel }) => includeArchive || !isArchivePath(rel));
|
|
35
|
+
|
|
36
|
+
const basenameIndex = new Set(files.map(({ rel }) => path.basename(rel, '.md')));
|
|
37
|
+
const areaRe = new RegExp(`^${reEsc(vaultCfg.areas_dir)}/([^/]+)/`);
|
|
38
|
+
|
|
39
|
+
const brokenMarkdownLinks = [];
|
|
40
|
+
const unresolvedWikiLinks = [];
|
|
41
|
+
let markdownLinksChecked = 0;
|
|
42
|
+
let wikiLinksChecked = 0;
|
|
43
|
+
|
|
44
|
+
for (const { abs, rel } of files) {
|
|
45
|
+
const text = stripCode(readText(abs, ''));
|
|
46
|
+
for (const target of markdownTargets(text)) {
|
|
47
|
+
if (isExternalOrAnchor(target)) continue;
|
|
48
|
+
markdownLinksChecked += 1;
|
|
49
|
+
if (!targetExists(root, abs, target, basenameIndex)) brokenMarkdownLinks.push({ file: rel, target });
|
|
50
|
+
}
|
|
51
|
+
for (const target of wikiTargets(text)) {
|
|
52
|
+
if (isExternalOrAnchor(target)) continue;
|
|
53
|
+
wikiLinksChecked += 1;
|
|
54
|
+
if (!targetExists(root, abs, target, basenameIndex)) unresolvedWikiLinks.push({ file: rel, target });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const payload = {
|
|
59
|
+
ok: brokenMarkdownLinks.length === 0,
|
|
60
|
+
command: 'link-check',
|
|
61
|
+
durable_write: false,
|
|
62
|
+
scope,
|
|
63
|
+
include_raw: includeRaw,
|
|
64
|
+
include_archive: includeArchive,
|
|
65
|
+
include_output: includeOutput,
|
|
66
|
+
markdown_files: files.length,
|
|
67
|
+
markdown_links_checked: markdownLinksChecked,
|
|
68
|
+
broken_markdown_links_count: brokenMarkdownLinks.length,
|
|
69
|
+
broken_markdown_by_area: countByArea(brokenMarkdownLinks, areaRe, vaultCfg),
|
|
70
|
+
broken_markdown_top_files: countByFile(brokenMarkdownLinks).slice(0, top),
|
|
71
|
+
broken_markdown_sample: brokenMarkdownLinks.slice(0, top),
|
|
72
|
+
wiki_links_checked: wikiLinksChecked,
|
|
73
|
+
unresolved_wiki_links_count: unresolvedWikiLinks.length,
|
|
74
|
+
unresolved_wiki_by_area: countByArea(unresolvedWikiLinks, areaRe, vaultCfg),
|
|
75
|
+
unresolved_wiki_top_files: countByFile(unresolvedWikiLinks).slice(0, top),
|
|
76
|
+
unresolved_wiki_top_targets: countByTarget(unresolvedWikiLinks).slice(0, top),
|
|
77
|
+
unresolved_wiki_sample: unresolvedWikiLinks.slice(0, top),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (args.json) return { json: true, payload };
|
|
81
|
+
return { text: render(payload) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function markdownTargets(text) {
|
|
85
|
+
const targets = [];
|
|
86
|
+
const re = /(?<!!)!?\[[^\]]*\]\(([^)]+)\)/g;
|
|
87
|
+
let match;
|
|
88
|
+
while ((match = re.exec(text))) {
|
|
89
|
+
const raw = match[1].split(/\s+"/)[0].trim();
|
|
90
|
+
if (raw) targets.push(raw);
|
|
91
|
+
}
|
|
92
|
+
return targets;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function wikiTargets(text) {
|
|
96
|
+
const targets = [];
|
|
97
|
+
const re = /\[\[([^\]]+)\]\]/g;
|
|
98
|
+
let match;
|
|
99
|
+
while ((match = re.exec(text))) {
|
|
100
|
+
const raw = match[1].split('|')[0].trim();
|
|
101
|
+
if (raw) targets.push(raw);
|
|
102
|
+
}
|
|
103
|
+
return targets;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function stripCode(text) {
|
|
107
|
+
return String(text || '').replace(/```[\s\S]*?```/g, ' ').replace(/`[^`]*`/g, ' ');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isExternalOrAnchor(rawTarget) {
|
|
111
|
+
const target = String(rawTarget || '').trim();
|
|
112
|
+
return !target || target.startsWith('#') || /^(https?:|mailto:|app:\/\/|obsidian:|data:)/i.test(target);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function targetExists(root, fromAbs, rawTarget, basenameIndex) {
|
|
116
|
+
let target = String(rawTarget || '').trim().replace(/^<|>$/g, '');
|
|
117
|
+
try { target = decodeURIComponent(target); } catch { /* keep original when not URI-encoded */ }
|
|
118
|
+
target = target.split('#')[0].trim();
|
|
119
|
+
if (!target) return true;
|
|
120
|
+
|
|
121
|
+
const candidates = [];
|
|
122
|
+
if (target.startsWith('/')) candidates.push(path.join(root, target.slice(1)));
|
|
123
|
+
candidates.push(path.resolve(path.dirname(fromAbs), target));
|
|
124
|
+
candidates.push(path.join(root, target));
|
|
125
|
+
if (!target.endsWith('.md')) {
|
|
126
|
+
candidates.push(path.resolve(path.dirname(fromAbs), `${target}.md`));
|
|
127
|
+
candidates.push(path.join(root, `${target}.md`));
|
|
128
|
+
}
|
|
129
|
+
for (const candidate of candidates) {
|
|
130
|
+
if (fs.existsSync(candidate)) return true;
|
|
131
|
+
}
|
|
132
|
+
if (!target.includes('/') && basenameIndex.has(target)) return true;
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function areaBucket(file, areaRe, vaultCfg) {
|
|
137
|
+
const m = String(file).match(areaRe);
|
|
138
|
+
if (m) return m[1];
|
|
139
|
+
if (file.startsWith(`${vaultCfg.wiki_dir}/`)) return vaultCfg.wiki_dir;
|
|
140
|
+
if (file.startsWith(`${vaultCfg.system_dir}/`)) return vaultCfg.system_dir;
|
|
141
|
+
if (file.startsWith(`${vaultCfg.raw_dir}/`)) return vaultCfg.raw_dir;
|
|
142
|
+
return file.split('/')[0] || '.';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sortedCounts(counts) {
|
|
146
|
+
return [...counts.entries()].map(([key, count]) => ({ key, count })).sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function countByArea(items, areaRe, vaultCfg) {
|
|
150
|
+
const counts = new Map();
|
|
151
|
+
for (const item of items) {
|
|
152
|
+
const key = areaBucket(item.file, areaRe, vaultCfg);
|
|
153
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
154
|
+
}
|
|
155
|
+
return sortedCounts(counts);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function countByFile(items) {
|
|
159
|
+
const counts = new Map();
|
|
160
|
+
for (const item of items) counts.set(item.file, (counts.get(item.file) || 0) + 1);
|
|
161
|
+
return sortedCounts(counts);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function countByTarget(items) {
|
|
165
|
+
const counts = new Map();
|
|
166
|
+
for (const item of items) counts.set(item.target, (counts.get(item.target) || 0) + 1);
|
|
167
|
+
return sortedCounts(counts);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function render(data) {
|
|
171
|
+
const lines = ['# Neurain Link Check', ''];
|
|
172
|
+
lines.push(`- OK: ${data.ok ? 'yes' : 'no'}`);
|
|
173
|
+
lines.push(`- Scope: ${data.scope}`);
|
|
174
|
+
lines.push(`- Markdown files: ${data.markdown_files}`);
|
|
175
|
+
lines.push(`- Strict Markdown links checked: ${data.markdown_links_checked}`);
|
|
176
|
+
lines.push(`- Broken strict Markdown links: ${data.broken_markdown_links_count}`);
|
|
177
|
+
lines.push(`- Wiki-style links checked: ${data.wiki_links_checked}`);
|
|
178
|
+
lines.push(`- Unresolved wiki-style links: ${data.unresolved_wiki_links_count}`);
|
|
179
|
+
return lines.join('\n');
|
|
180
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { absPath, compactStamp, ensureDir, isTextFile, readText, relPath, safeResolve, sha256, walkFiles } from './fs.mjs';
|
|
4
|
+
import { inferSensitivityFromPath, injectionLike, secretLike } from './safety.mjs';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SAMPLE_SIZE = 12;
|
|
7
|
+
const MAX_SAMPLE_SIZE = 50;
|
|
8
|
+
|
|
9
|
+
export async function liveCasesCommand(args) {
|
|
10
|
+
const [subcommand, ...rest] = args._;
|
|
11
|
+
const root = absPath(rest[0] || args.root || process.cwd());
|
|
12
|
+
if (!subcommand || subcommand === 'scaffold') return renderLiveCaseScaffold(root, args);
|
|
13
|
+
throw new Error('Unknown live-cases command. Use "live-cases scaffold".');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildLiveCaseScaffold(root, {
|
|
17
|
+
sampleSize = DEFAULT_SAMPLE_SIZE,
|
|
18
|
+
label = '',
|
|
19
|
+
output = '',
|
|
20
|
+
write = false,
|
|
21
|
+
confirm = '',
|
|
22
|
+
} = {}) {
|
|
23
|
+
const limit = Math.max(1, Math.min(Number(sampleSize || DEFAULT_SAMPLE_SIZE), MAX_SAMPLE_SIZE));
|
|
24
|
+
const samples = collectSourceSamples(root, limit);
|
|
25
|
+
const payload = {
|
|
26
|
+
ok: true,
|
|
27
|
+
command: 'live-cases scaffold',
|
|
28
|
+
durable_write: false,
|
|
29
|
+
model_calls: false,
|
|
30
|
+
external_tool_calls: false,
|
|
31
|
+
e23_live_case_pack_scaffolded: true,
|
|
32
|
+
redacted_live_case_pack: true,
|
|
33
|
+
reviewed_live_user_evidence: false,
|
|
34
|
+
human_judged: false,
|
|
35
|
+
reviewer_required: true,
|
|
36
|
+
generated_at: compactStamp(),
|
|
37
|
+
folder_label: sanitizeLabel(label || 'reviewed-folder-001'),
|
|
38
|
+
folder_hash: sha256(absPath(root)).slice(0, 16),
|
|
39
|
+
sample_size: limit,
|
|
40
|
+
source_sample_count: samples.length,
|
|
41
|
+
source_samples: samples,
|
|
42
|
+
storage_safety: storageSafety(samples),
|
|
43
|
+
case_templates: buildCaseTemplates(samples),
|
|
44
|
+
next_steps: [
|
|
45
|
+
'A human reviewer must fill safe, non-private recall queries and expected terms before claiming reviewed live evidence.',
|
|
46
|
+
'Run recall eval, answer eval, and lessons eval with the filled local case files.',
|
|
47
|
+
'Store only the redacted case pack or aggregate eval receipts. Do not store raw private source text in product docs.',
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
if (!write) return payload;
|
|
51
|
+
if (String(confirm || '') !== '1건 저장 진행') {
|
|
52
|
+
throw new Error('Writing a live case pack requires --confirm "1건 저장 진행".');
|
|
53
|
+
}
|
|
54
|
+
const rel = output
|
|
55
|
+
? String(output).replace(/\\/g, '/')
|
|
56
|
+
: `output/live-cases/${compactStamp()}-e23-live-case-pack.json`;
|
|
57
|
+
if (path.isAbsolute(rel)) throw new Error('Live case pack --output must be a relative path inside the target root.');
|
|
58
|
+
const abs = safeResolve(root, rel);
|
|
59
|
+
ensureDir(path.dirname(abs));
|
|
60
|
+
const writtenPayload = {
|
|
61
|
+
...payload,
|
|
62
|
+
durable_write: true,
|
|
63
|
+
output_path: rel,
|
|
64
|
+
};
|
|
65
|
+
fs.writeFileSync(abs, `${JSON.stringify(writtenPayload, null, 2)}\n`, 'utf8');
|
|
66
|
+
return writtenPayload;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderLiveCaseScaffold(root, args) {
|
|
70
|
+
const payload = buildLiveCaseScaffold(root, {
|
|
71
|
+
sampleSize: Number(args['sample-size'] || DEFAULT_SAMPLE_SIZE),
|
|
72
|
+
label: args.label || '',
|
|
73
|
+
output: args.output || '',
|
|
74
|
+
write: Boolean(args.write),
|
|
75
|
+
confirm: args.confirm || '',
|
|
76
|
+
});
|
|
77
|
+
if (args.json) return { json: true, payload };
|
|
78
|
+
return {
|
|
79
|
+
text: [
|
|
80
|
+
'# Neurain E23 live case scaffold',
|
|
81
|
+
'',
|
|
82
|
+
`- OK: ${payload.ok ? 'yes' : 'no'}`,
|
|
83
|
+
`- Sources sampled: ${payload.source_sample_count}`,
|
|
84
|
+
`- Reviewed live evidence: ${payload.reviewed_live_user_evidence ? 'yes' : 'no'}`,
|
|
85
|
+
`- Reviewer required: ${payload.reviewer_required ? 'yes' : 'no'}`,
|
|
86
|
+
`- Durable write: ${payload.durable_write ? 'yes' : 'no'}`,
|
|
87
|
+
payload.output_path ? `- Output: ${payload.output_path}` : '',
|
|
88
|
+
].filter(Boolean).join('\n'),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function collectSourceSamples(root, limit) {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const file of walkFiles(root, { includeRaw: false, maxFiles: 50000 })) {
|
|
95
|
+
if (out.length >= limit) break;
|
|
96
|
+
const rel = relPath(root, file);
|
|
97
|
+
if (!isTextFile(file)) continue;
|
|
98
|
+
if (!isLiveCaseSourcePath(rel)) continue;
|
|
99
|
+
const text = readText(file, '');
|
|
100
|
+
const flags = unsafeFlags(rel, text);
|
|
101
|
+
out.push({
|
|
102
|
+
source_ref: `source-${String(out.length + 1).padStart(3, '0')}`,
|
|
103
|
+
kind: kindForPath(rel),
|
|
104
|
+
path_hash: sha256(rel).slice(0, 16),
|
|
105
|
+
content_hash: sha256(text).slice(0, 16),
|
|
106
|
+
sensitivity: flags.private_path ? 'private' : 'internal',
|
|
107
|
+
redacted: flags.private_path || flags.secret_like || flags.injection_like,
|
|
108
|
+
unsafe_flags: flags,
|
|
109
|
+
raw_text_stored: false,
|
|
110
|
+
absolute_path_stored: false,
|
|
111
|
+
reviewer_note: 'Use this source only to create local reviewed cases. Do not copy private source text into product docs.',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isLiveCaseSourcePath(rel) {
|
|
118
|
+
if (/^raw\//.test(rel)) return false;
|
|
119
|
+
if (/^output\/receipts\//.test(rel)) return false;
|
|
120
|
+
if (/^output\/live-cases\//.test(rel)) return false;
|
|
121
|
+
if (/^node_modules\//.test(rel)) return false;
|
|
122
|
+
return [
|
|
123
|
+
/^README\.md$/,
|
|
124
|
+
/^index\.md$/,
|
|
125
|
+
/^wiki\//,
|
|
126
|
+
/^00_system\/sessions\/handoffs\/.+\.md$/,
|
|
127
|
+
/^10_areas\/[^/]+\/current\/.+\.md$/,
|
|
128
|
+
/^10_areas\/[^/]+\/product\/.+\.md$/,
|
|
129
|
+
/^10_areas\/[^/]+\/log\.md$/,
|
|
130
|
+
/^docs\/.+\.md$/,
|
|
131
|
+
].some((pattern) => pattern.test(rel));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function kindForPath(rel) {
|
|
135
|
+
if (rel.startsWith('docs/')) return 'docs';
|
|
136
|
+
if (rel.startsWith('wiki/')) return 'wiki';
|
|
137
|
+
if (rel.includes('/current/')) return 'current';
|
|
138
|
+
if (rel.includes('/product/')) return 'product';
|
|
139
|
+
if (rel.includes('/sessions/handoffs/')) return 'session';
|
|
140
|
+
if (rel.endsWith('log.md')) return 'log';
|
|
141
|
+
return 'markdown';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function unsafeFlags(rel, text) {
|
|
145
|
+
return {
|
|
146
|
+
private_path: inferSensitivityFromPath(rel) === 'private',
|
|
147
|
+
secret_like: Boolean(secretLike(text)),
|
|
148
|
+
injection_like: Boolean(injectionLike(text)),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function storageSafety(samples) {
|
|
153
|
+
return {
|
|
154
|
+
raw_source_text_stored: false,
|
|
155
|
+
absolute_paths_stored: false,
|
|
156
|
+
private_paths_stored: false,
|
|
157
|
+
secret_like_content_stored: false,
|
|
158
|
+
instruction_injection_content_stored: false,
|
|
159
|
+
redacted_source_count: samples.filter((item) => item.redacted).length,
|
|
160
|
+
hash_only_source_refs: true,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildCaseTemplates(samples) {
|
|
165
|
+
const refs = samples.slice(0, 3).map((sample) => sample.source_ref);
|
|
166
|
+
return {
|
|
167
|
+
recall_cases_template: refs.map((sourceRef, index) => ({
|
|
168
|
+
id: `reviewed-live-recall-${String(index + 1).padStart(3, '0')}`,
|
|
169
|
+
source_ref: sourceRef,
|
|
170
|
+
status: 'needs-human-review',
|
|
171
|
+
query: '<fill safe non-private query locally>',
|
|
172
|
+
host: '',
|
|
173
|
+
expected_path_contains: ['<fill non-private path marker or event id locally>'],
|
|
174
|
+
expected_terms: ['<fill safe support term locally>'],
|
|
175
|
+
wrong_hosts: [],
|
|
176
|
+
forbidden_path_contains: ['private', 'secret', 'credential', 'token'],
|
|
177
|
+
})),
|
|
178
|
+
answer_cases_template: [
|
|
179
|
+
{
|
|
180
|
+
id: 'reviewed-live-answer-supported-001',
|
|
181
|
+
type: 'supported',
|
|
182
|
+
status: 'needs-human-review',
|
|
183
|
+
sources: [{ id: 's1', source_ref: refs[0] || '', text: '<fill safe source excerpt locally>' }],
|
|
184
|
+
claims: [{ text: '<fill supported claim>', support_terms: ['<safe term>'], citations: ['s1'] }],
|
|
185
|
+
answer: { text: '<fill answer>', citations: [{ source_id: 's1', support_terms: ['<safe term>'] }] },
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'reviewed-live-answer-no-answer-001',
|
|
189
|
+
type: 'no_answer',
|
|
190
|
+
status: 'needs-human-review',
|
|
191
|
+
sources: [{ id: 's1', source_ref: refs[1] || '', text: '<fill safe nearby source excerpt locally>' }],
|
|
192
|
+
claims: [],
|
|
193
|
+
answer: { text: 'Not enough evidence in the provided sources.', abstained: true, citations: [] },
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
lesson_cases_template: [
|
|
197
|
+
{
|
|
198
|
+
id: 'reviewed-live-lesson-positive-001',
|
|
199
|
+
type: 'positive',
|
|
200
|
+
status: 'needs-human-review',
|
|
201
|
+
source_ref: refs[0] || '',
|
|
202
|
+
text: '<fill safe lesson candidate text locally>',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'reviewed-live-lesson-negative-001',
|
|
206
|
+
type: 'negative',
|
|
207
|
+
status: 'needs-human-review',
|
|
208
|
+
source_ref: refs[1] || '',
|
|
209
|
+
text: '<fill ordinary note that should not become a lesson locally>',
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function sanitizeLabel(value) {
|
|
216
|
+
return String(value || '')
|
|
217
|
+
.replace(/[^a-zA-Z0-9._ -]+/g, '-')
|
|
218
|
+
.replace(/\s+/g, ' ')
|
|
219
|
+
.trim()
|
|
220
|
+
.slice(0, 80) || 'reviewed-folder-001';
|
|
221
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { scanAdoption } from './adopt.mjs';
|
|
4
|
+
import { absPath, exists, safeResolve } from './fs.mjs';
|
|
5
|
+
|
|
6
|
+
export async function onboardCommand(args) {
|
|
7
|
+
const root = absPath(args._[0] || args.folder || process.cwd());
|
|
8
|
+
const lang = args.lang === 'en' ? 'en' : 'ko';
|
|
9
|
+
const host = normalizeHost(args.host || 'codex');
|
|
10
|
+
const profile = buildOnboardingProfile(root, { lang, host });
|
|
11
|
+
if (args.json) return { json: true, payload: profile };
|
|
12
|
+
return { text: renderOnboarding(profile) };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildOnboardingProfile(root, { lang = 'ko', host = 'codex' } = {}) {
|
|
16
|
+
const folderExists = exists(root);
|
|
17
|
+
const isDirectory = folderExists && fs.statSync(root).isDirectory();
|
|
18
|
+
const initialized = isDirectory && exists(safeResolve(root, 'neurain.config.json'));
|
|
19
|
+
const areaCount = initialized ? countAreaFolders(root) : 0;
|
|
20
|
+
const adoption = isDirectory && !initialized ? safeAdoptionSummary(root) : null;
|
|
21
|
+
const connectCommand = host === 'gemini'
|
|
22
|
+
? `npx neurain connect gemini "${root}" --dry-run`
|
|
23
|
+
: host === 'runtime'
|
|
24
|
+
? `npx neurain connect runtime "${root}" --dry-run`
|
|
25
|
+
: `npx neurain connect ${host} "${root}" --dry-run`;
|
|
26
|
+
|
|
27
|
+
const steps = nextSteps({ root, lang, host, folderExists, isDirectory, initialized, adoption, connectCommand });
|
|
28
|
+
return {
|
|
29
|
+
ok: folderExists ? isDirectory : true,
|
|
30
|
+
command: 'onboard',
|
|
31
|
+
generated_for: 'non_developer_first_run',
|
|
32
|
+
durable_write: false,
|
|
33
|
+
model_call: false,
|
|
34
|
+
external_call: false,
|
|
35
|
+
root,
|
|
36
|
+
lang,
|
|
37
|
+
host,
|
|
38
|
+
folder_exists: folderExists,
|
|
39
|
+
initialized,
|
|
40
|
+
area_count: areaCount,
|
|
41
|
+
adoption_scan_summary: adoption,
|
|
42
|
+
recommended_next_command: steps[0]?.command || '',
|
|
43
|
+
steps,
|
|
44
|
+
what_neurain_stores: [
|
|
45
|
+
'A local vault structure for sources, wiki, areas, sessions, and receipts.',
|
|
46
|
+
'Only files or receipts created through explicit CLI actions.',
|
|
47
|
+
'Redacted eval scaffolds that avoid raw source text when requested.',
|
|
48
|
+
],
|
|
49
|
+
what_neurain_does_not_store: [
|
|
50
|
+
'Cloud memory by default.',
|
|
51
|
+
'Prompt bodies, transcript paths, tool stdout, or tool stderr in lifecycle receipts.',
|
|
52
|
+
'Existing work-folder adapter files until adoption is explicitly confirmed.',
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function nextSteps({ root, lang, host, folderExists, isDirectory, initialized, adoption, connectCommand }) {
|
|
58
|
+
if (!folderExists) {
|
|
59
|
+
return [
|
|
60
|
+
{
|
|
61
|
+
title: text(lang, 'Create a local Neurain vault', '로컬 Neurain vault 만들기'),
|
|
62
|
+
why: text(lang, 'This creates the starter folders and rules in a new local folder.', '새 로컬 폴더에 시작 폴더와 기본 규칙을 만듭니다.'),
|
|
63
|
+
command: `npx neurain init "${root}" --lang ${lang}`,
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
if (!isDirectory) {
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
title: text(lang, 'Choose a folder', '폴더 선택하기'),
|
|
71
|
+
why: text(lang, 'Neurain starts from a folder, not a single file.', 'Neurain은 단일 파일이 아니라 폴더에서 시작합니다.'),
|
|
72
|
+
command: 'npx neurain onboard <folder>',
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
if (!initialized) {
|
|
77
|
+
const mode = adoption?.recommended_mode || 'copy';
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
title: text(lang, 'Scan before connecting', '연결 전에 먼저 스캔하기'),
|
|
81
|
+
why: text(lang, `This reads the folder and recommends ${mode} mode before any write.`, `쓰기 전에 폴더를 읽기 전용으로 확인하고 ${mode} 모드를 추천합니다.`),
|
|
82
|
+
command: `npx neurain adopt "${root}" --dry-run`,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
title: text(lang, 'Create a clean starter vault if this is a new Neurain folder', '새 Neurain 폴더라면 starter vault 만들기'),
|
|
86
|
+
why: text(lang, 'Use this only when the target should become a Neurain vault.', '대상 폴더 자체를 Neurain vault로 만들 때만 사용합니다.'),
|
|
87
|
+
command: `npx neurain init "${root}" --lang ${lang}`,
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
return [
|
|
92
|
+
{
|
|
93
|
+
title: text(lang, 'Check health', '상태 확인'),
|
|
94
|
+
why: text(lang, 'This confirms the local vault is readable and ready.', '로컬 vault가 읽히고 준비되었는지 확인합니다.'),
|
|
95
|
+
command: `npx neurain doctor "${root}"`,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
title: text(lang, `Preview ${host} connection`, `${host} 연결 미리보기`),
|
|
99
|
+
why: text(lang, 'Dry-run prints the host connection command without changing host config.', 'dry-run은 host 설정을 바꾸지 않고 연결 방법만 보여줍니다.'),
|
|
100
|
+
command: connectCommand,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
title: text(lang, 'Preview the working loop', '작업 루프 미리보기'),
|
|
104
|
+
why: text(lang, 'Wrap preview shows what future sessions can remember without writing durable knowledge.', 'wrap 미리보기는 durable knowledge를 쓰지 않고 다음 세션이 이어받을 내용을 보여줍니다.'),
|
|
105
|
+
command: `npx neurain wrap "${root}" --dry-run`,
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function safeAdoptionSummary(root) {
|
|
111
|
+
try {
|
|
112
|
+
const scan = scanAdoption(root, { dryRun: true });
|
|
113
|
+
return {
|
|
114
|
+
scanned_files: scan.summary.scanned_files,
|
|
115
|
+
excluded_entries: scan.summary.excluded_entries,
|
|
116
|
+
private_or_secret_files: scan.summary.private_or_secret_files,
|
|
117
|
+
risk_count: scan.summary.risk_count,
|
|
118
|
+
recommended_mode: scan.summary.recommended_mode,
|
|
119
|
+
write_required_before_apply: false,
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return {
|
|
123
|
+
scanned_files: 0,
|
|
124
|
+
excluded_entries: 0,
|
|
125
|
+
private_or_secret_files: 0,
|
|
126
|
+
risk_count: 1,
|
|
127
|
+
recommended_mode: 'copy',
|
|
128
|
+
write_required_before_apply: false,
|
|
129
|
+
scan_error: error.message || String(error),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function countAreaFolders(root) {
|
|
135
|
+
try {
|
|
136
|
+
const areasRoot = safeResolve(root, '10_areas');
|
|
137
|
+
if (!exists(areasRoot)) return 0;
|
|
138
|
+
return fs.readdirSync(areasRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length;
|
|
139
|
+
} catch {
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function renderOnboarding(profile) {
|
|
145
|
+
const lines = [
|
|
146
|
+
'# Neurain onboarding',
|
|
147
|
+
'',
|
|
148
|
+
`- Folder: ${profile.root}`,
|
|
149
|
+
`- Ready: ${profile.initialized ? 'yes' : 'not yet'}`,
|
|
150
|
+
`- Writes made: no`,
|
|
151
|
+
];
|
|
152
|
+
if (profile.adoption_scan_summary) {
|
|
153
|
+
lines.push(
|
|
154
|
+
`- Recommended adoption mode: ${profile.adoption_scan_summary.recommended_mode}`,
|
|
155
|
+
`- Risks found: ${profile.adoption_scan_summary.risk_count}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
lines.push('', 'Next steps:');
|
|
159
|
+
for (const [index, step] of profile.steps.entries()) {
|
|
160
|
+
lines.push('', `${index + 1}. ${step.title}`, ` ${step.why}`, ` ${step.command}`);
|
|
161
|
+
}
|
|
162
|
+
lines.push('', 'Stores:', ...profile.what_neurain_stores.map((item) => `- ${item}`));
|
|
163
|
+
lines.push('', 'Does not store:', ...profile.what_neurain_does_not_store.map((item) => `- ${item}`));
|
|
164
|
+
return lines.join('\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeHost(value) {
|
|
168
|
+
const host = String(value || 'codex').toLowerCase();
|
|
169
|
+
if (['codex', 'claude', 'gemini', 'runtime'].includes(host)) return host;
|
|
170
|
+
return 'codex';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function text(lang, en, ko) {
|
|
174
|
+
return lang === 'en' ? en : ko;
|
|
175
|
+
}
|