obsidian-second-brain 0.1.0 → 0.2.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/README.md +22 -0
- package/config/projects.example.json +10 -0
- package/dist/claude-runner.js +34 -0
- package/dist/cli-claude-backend.js +2 -18
- package/dist/cli.js +72 -22
- package/dist/code-boundaries.js +154 -0
- package/dist/code-capture.js +122 -0
- package/dist/code-classify.js +150 -0
- package/dist/code-evidence.js +113 -0
- package/dist/code-frontmatter.js +147 -0
- package/dist/code-ingest.js +289 -0
- package/dist/code-map-backend.js +70 -0
- package/dist/code-map-config.js +0 -0
- package/dist/code-map-prompt.js +84 -0
- package/dist/code-scan.js +168 -0
- package/dist/config.js +5 -0
- package/dist/consolidation.js +1 -1
- package/dist/managed-section.js +40 -19
- package/dist/map-command.js +191 -0
- package/dist/report.js +34 -0
- package/dist/shell.js +55 -1
- package/dist/vault-paths.js +23 -0
- package/package.json +2 -2
package/dist/managed-section.js
CHANGED
|
@@ -1,30 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// Historical strings — existing vaults already contain them; never reword.
|
|
2
|
+
export const INIT_MARKERS = {
|
|
3
|
+
begin: "<!-- BEGIN second-brain (managed by second-brain init — do not edit inside) -->",
|
|
4
|
+
end: "<!-- END second-brain -->"
|
|
5
|
+
};
|
|
6
|
+
export const CODE_MAPS_MARKERS = {
|
|
7
|
+
begin: "<!-- BEGIN second-brain code-maps (managed by second-brain — do not edit inside) -->",
|
|
8
|
+
end: "<!-- END second-brain code-maps -->"
|
|
9
|
+
};
|
|
10
|
+
export function hasManagedBlock(content, markers) {
|
|
11
|
+
return content.includes(markers.begin) && content.includes(markers.end);
|
|
5
12
|
}
|
|
6
13
|
// Idempotent: re-applying replaces the block in place, never duplicates
|
|
7
|
-
// (design 7.3).
|
|
14
|
+
// (design 7.3). `body` must NOT contain the markers — they are added here.
|
|
15
|
+
export function applyManagedBlock(existing, markers, body) {
|
|
16
|
+
const block = [markers.begin, body.trim(), markers.end].join("\n");
|
|
17
|
+
return applyBlockString(existing, markers, block);
|
|
18
|
+
}
|
|
19
|
+
export function removeManagedBlock(content, markers) {
|
|
20
|
+
if (!hasManagedBlock(content, markers)) {
|
|
21
|
+
return content;
|
|
22
|
+
}
|
|
23
|
+
const beginIndex = content.indexOf(markers.begin);
|
|
24
|
+
const endIndex = content.indexOf(markers.end) + markers.end.length;
|
|
25
|
+
const before = content.slice(0, beginIndex).replace(/\n+$/u, "");
|
|
26
|
+
const after = content.slice(endIndex).replace(/^\n+/u, "");
|
|
27
|
+
const joined = [before, after].filter((part) => part.length > 0).join("\n");
|
|
28
|
+
return joined.length > 0 ? `${joined}\n` : "";
|
|
29
|
+
}
|
|
30
|
+
// Legacy init API — the block argument already contains the INIT markers.
|
|
31
|
+
export function hasManagedSection(content) {
|
|
32
|
+
return hasManagedBlock(content, INIT_MARKERS);
|
|
33
|
+
}
|
|
8
34
|
export function applyManagedSection(existing, block) {
|
|
35
|
+
return applyBlockString(existing, INIT_MARKERS, block);
|
|
36
|
+
}
|
|
37
|
+
export function removeManagedSection(content) {
|
|
38
|
+
return removeManagedBlock(content, INIT_MARKERS);
|
|
39
|
+
}
|
|
40
|
+
function applyBlockString(existing, markers, block) {
|
|
9
41
|
const trimmedBlock = block.trim();
|
|
10
42
|
if (existing === undefined || existing.trim().length === 0) {
|
|
11
43
|
return `${trimmedBlock}\n`;
|
|
12
44
|
}
|
|
13
|
-
if (
|
|
14
|
-
const beginIndex = existing.indexOf(
|
|
15
|
-
const endIndex = existing.indexOf(
|
|
45
|
+
if (hasManagedBlock(existing, markers)) {
|
|
46
|
+
const beginIndex = existing.indexOf(markers.begin);
|
|
47
|
+
const endIndex = existing.indexOf(markers.end) + markers.end.length;
|
|
16
48
|
return `${existing.slice(0, beginIndex)}${trimmedBlock}${existing.slice(endIndex)}`;
|
|
17
49
|
}
|
|
18
50
|
return `${existing.replace(/\n+$/u, "")}\n\n${trimmedBlock}\n`;
|
|
19
51
|
}
|
|
20
|
-
export function removeManagedSection(content) {
|
|
21
|
-
if (!hasManagedSection(content)) {
|
|
22
|
-
return content;
|
|
23
|
-
}
|
|
24
|
-
const beginIndex = content.indexOf(BEGIN_MARKER);
|
|
25
|
-
const endIndex = content.indexOf(END_MARKER) + END_MARKER.length;
|
|
26
|
-
const before = content.slice(0, beginIndex).replace(/\n+$/u, "");
|
|
27
|
-
const after = content.slice(endIndex).replace(/^\n+/u, "");
|
|
28
|
-
const joined = [before, after].filter((part) => part.length > 0).join("\n");
|
|
29
|
-
return joined.length > 0 ? `${joined}\n` : "";
|
|
30
|
-
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import { captureRepo, defaultCaptureDependencies } from "./code-capture.js";
|
|
4
|
+
import { stripCodeManifestEntries } from "./code-classify.js";
|
|
5
|
+
import { runCodeMapIngest } from "./code-ingest.js";
|
|
6
|
+
import { createCliClaudeCodeMapBackend, resolveCodeMapBackend } from "./code-map-backend.js";
|
|
7
|
+
import { DEFAULT_MAX_MODULES } from "./code-map-config.js";
|
|
8
|
+
import { loadConfig } from "./config.js";
|
|
9
|
+
import { writeFileAtomic } from "./filesystem.js";
|
|
10
|
+
import { acquireLock } from "./lock.js";
|
|
11
|
+
import { loadManifest, saveManifest } from "./manifest.js";
|
|
12
|
+
import { createReportWriter } from "./report.js";
|
|
13
|
+
import { isSlug, slugify } from "./slug.js";
|
|
14
|
+
import { resolveVaultPaths } from "./vault-paths.js";
|
|
15
|
+
const USAGE = "Usage: second-brain map <repoPath> [--project <name>] [--path <subdir>] [--max-modules N] [--force]";
|
|
16
|
+
export function parseMapArgs(argv) {
|
|
17
|
+
let configPath;
|
|
18
|
+
let force = false;
|
|
19
|
+
let maxModules;
|
|
20
|
+
let project;
|
|
21
|
+
let repoPath;
|
|
22
|
+
let scope;
|
|
23
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
24
|
+
const currentArg = argv[index];
|
|
25
|
+
if (currentArg === "--force") {
|
|
26
|
+
force = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (currentArg === "--config" ||
|
|
30
|
+
currentArg === "--max-modules" ||
|
|
31
|
+
currentArg === "--path" ||
|
|
32
|
+
currentArg === "--project") {
|
|
33
|
+
const nextArg = argv[index + 1];
|
|
34
|
+
if (!nextArg) {
|
|
35
|
+
throw new Error(`Missing value for ${currentArg}`);
|
|
36
|
+
}
|
|
37
|
+
if (currentArg === "--config") {
|
|
38
|
+
configPath = nextArg;
|
|
39
|
+
}
|
|
40
|
+
else if (currentArg === "--project") {
|
|
41
|
+
project = nextArg;
|
|
42
|
+
}
|
|
43
|
+
else if (currentArg === "--path") {
|
|
44
|
+
scope = nextArg.replace(/\/+$/u, "");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const parsed = Number(nextArg);
|
|
48
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 64) {
|
|
49
|
+
throw new Error(`Invalid --max-modules: ${nextArg}`);
|
|
50
|
+
}
|
|
51
|
+
maxModules = parsed;
|
|
52
|
+
}
|
|
53
|
+
index += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (currentArg.startsWith("--")) {
|
|
57
|
+
throw new Error(`Unknown argument: ${currentArg}`);
|
|
58
|
+
}
|
|
59
|
+
if (repoPath !== undefined) {
|
|
60
|
+
throw new Error(`Unexpected argument: ${currentArg}`);
|
|
61
|
+
}
|
|
62
|
+
repoPath = currentArg;
|
|
63
|
+
}
|
|
64
|
+
if (repoPath === undefined) {
|
|
65
|
+
throw new Error(USAGE);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
...(configPath === undefined ? {} : { configPath }),
|
|
69
|
+
force,
|
|
70
|
+
...(maxModules === undefined ? {} : { maxModules }),
|
|
71
|
+
...(project === undefined ? {} : { project }),
|
|
72
|
+
repoPath,
|
|
73
|
+
...(scope === undefined ? {} : { scope })
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export async function runMapCommand(argv, dependencies) {
|
|
77
|
+
const flags = parseMapArgs(argv);
|
|
78
|
+
const configPath = resolve(flags.configPath ?? dependencies.defaultConfigPath);
|
|
79
|
+
const config = await loadConfig(configPath);
|
|
80
|
+
const repoPath = resolve(flags.repoPath);
|
|
81
|
+
const project = flags.project ?? slugify(basename(repoPath));
|
|
82
|
+
if (!isSlug(project)) {
|
|
83
|
+
throw new Error(`Project name does not slugify cleanly: ${flags.project ?? basename(repoPath)} — pass --project`);
|
|
84
|
+
}
|
|
85
|
+
const flagScope = flags.scope === undefined ? "" : flags.scope;
|
|
86
|
+
const byProject = config.codeMap?.repos.find((repo) => repo.project === project);
|
|
87
|
+
const byTarget = config.codeMap?.repos.find((repo) => repo.path === repoPath && (repo.scope ?? "") === flagScope);
|
|
88
|
+
if (byProject !== undefined && (byProject.path !== repoPath || (byProject.scope ?? "") !== flagScope)) {
|
|
89
|
+
throw new Error(`Project ${project} is already registered for ${byProject.path} — pass a different --project`);
|
|
90
|
+
}
|
|
91
|
+
if (byTarget !== undefined && byTarget.project !== project) {
|
|
92
|
+
throw new Error(`Repo is already registered as project ${byTarget.project}`);
|
|
93
|
+
}
|
|
94
|
+
const registered = byProject ?? byTarget;
|
|
95
|
+
const repo = {
|
|
96
|
+
...(flags.maxModules !== undefined
|
|
97
|
+
? { maxModules: flags.maxModules }
|
|
98
|
+
: registered?.maxModules === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: { maxModules: registered.maxModules }),
|
|
101
|
+
...(registered?.modules === undefined ? {} : { modules: registered.modules }),
|
|
102
|
+
path: repoPath,
|
|
103
|
+
project,
|
|
104
|
+
...(flags.scope !== undefined
|
|
105
|
+
? { scope: flags.scope }
|
|
106
|
+
: registered?.scope === undefined
|
|
107
|
+
? {}
|
|
108
|
+
: { scope: registered.scope })
|
|
109
|
+
};
|
|
110
|
+
const vaultPaths = resolveVaultPaths(config.vaultRoot);
|
|
111
|
+
// Same lock as the hourly job — map must never interleave with it.
|
|
112
|
+
const lock = await acquireLock(vaultPaths.lockPath);
|
|
113
|
+
try {
|
|
114
|
+
// 1. Capture. Read-before-write: a git failure aborts before any write,
|
|
115
|
+
// so a typo'd path leaves the vault byte-identical (design 6.1).
|
|
116
|
+
const captureSummary = await captureRepo({
|
|
117
|
+
date: dependencies.now().toISOString().slice(0, 10),
|
|
118
|
+
globalMaxModules: config.codeMap?.maxModules ?? DEFAULT_MAX_MODULES,
|
|
119
|
+
rawCodeRoot: vaultPaths.rawCodeRoot,
|
|
120
|
+
repo
|
|
121
|
+
}, defaultCaptureDependencies(repoPath));
|
|
122
|
+
dependencies.log(`captured ${repo.project}: written=${captureSummary.evidenceWritten}, unchanged=${captureSummary.evidenceUnchanged}, modules=${captureSummary.moduleCount}`);
|
|
123
|
+
// 2. Registration after a successful capture (design 7).
|
|
124
|
+
if (registered === undefined) {
|
|
125
|
+
await registerRepo(configPath, {
|
|
126
|
+
...(flags.maxModules === undefined ? {} : { maxModules: flags.maxModules }),
|
|
127
|
+
path: repoPath,
|
|
128
|
+
project,
|
|
129
|
+
...(flags.scope === undefined ? {} : { scope: flags.scope })
|
|
130
|
+
});
|
|
131
|
+
dependencies.log(`registered ${project} in ${configPath}`);
|
|
132
|
+
}
|
|
133
|
+
// 3. --force: clear this project's code:* manifest entries so every page
|
|
134
|
+
// re-renders (decoded-segment matching, design 7).
|
|
135
|
+
if (flags.force) {
|
|
136
|
+
const manifest = await loadManifest(vaultPaths.manifestPath);
|
|
137
|
+
await saveManifest(vaultPaths.manifestPath, stripCodeManifestEntries(manifest, project));
|
|
138
|
+
dependencies.log(`cleared code manifest entries for ${project}`);
|
|
139
|
+
}
|
|
140
|
+
// 4. Immediate ingest so pages exist right away.
|
|
141
|
+
const models = config.codeMap?.models ?? { pages: "haiku", structure: "sonnet" };
|
|
142
|
+
const backend = resolveCodeMapBackend(config.synthesis.backend, () => createCliClaudeCodeMapBackend({
|
|
143
|
+
...(config.synthesis.claudeBin === undefined
|
|
144
|
+
? {}
|
|
145
|
+
: { claudeBin: config.synthesis.claudeBin }),
|
|
146
|
+
logsRoot: config.logsRoot,
|
|
147
|
+
models
|
|
148
|
+
}));
|
|
149
|
+
if (backend === undefined) {
|
|
150
|
+
dependencies.log("synthesis backend is noop — evidence captured; pages will render once a backend is configured");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const reportWriter = createReportWriter({
|
|
154
|
+
now: dependencies.now,
|
|
155
|
+
reportsRoot: vaultPaths.reportsRoot,
|
|
156
|
+
runKind: "manual",
|
|
157
|
+
vaultRoot: vaultPaths.vaultDir
|
|
158
|
+
});
|
|
159
|
+
const summary = await runCodeMapIngest({
|
|
160
|
+
backend,
|
|
161
|
+
evidenceCaptured: captureSummary.evidenceWritten,
|
|
162
|
+
indexPath: vaultPaths.indexPath,
|
|
163
|
+
logPath: vaultPaths.wikiLogPath,
|
|
164
|
+
manifestPath: vaultPaths.manifestPath,
|
|
165
|
+
now: dependencies.now,
|
|
166
|
+
rawCodeRoot: vaultPaths.rawCodeRoot,
|
|
167
|
+
reportWriter,
|
|
168
|
+
wikiCodeRoot: vaultPaths.wikiCodeRoot
|
|
169
|
+
});
|
|
170
|
+
dependencies.log(`pages rendered: ${summary.pagesRendered.length}, pruned: ${summary.pagesPruned.length}, failures: ${summary.failures.length}`);
|
|
171
|
+
for (const failure of summary.failures) {
|
|
172
|
+
dependencies.log(`failed: ${failure.project} — ${failure.reason}`);
|
|
173
|
+
}
|
|
174
|
+
if (summary.failures.length > 0) {
|
|
175
|
+
process.exitCode = 1;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
await lock.release();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Registration edits the RAW config file (not the resolved view) so user
|
|
183
|
+
// formatting conventions like relative provider paths survive untouched.
|
|
184
|
+
async function registerRepo(configPath, entry) {
|
|
185
|
+
const raw = JSON.parse(await readFile(configPath, "utf8"));
|
|
186
|
+
const codeMap = typeof raw.codeMap === "object" && raw.codeMap !== null
|
|
187
|
+
? raw.codeMap
|
|
188
|
+
: {};
|
|
189
|
+
const repos = Array.isArray(codeMap.repos) ? codeMap.repos : [];
|
|
190
|
+
await writeFileAtomic(configPath, `${JSON.stringify({ ...raw, codeMap: { ...codeMap, repos: [...repos, entry] } }, null, 2)}\n`);
|
|
191
|
+
}
|
package/dist/report.js
CHANGED
|
@@ -21,6 +21,7 @@ export function createReportWriter(input) {
|
|
|
21
21
|
state.reportPath ??
|
|
22
22
|
(await claimReportPath(input.reportsRoot, createdAt, input.runKind));
|
|
23
23
|
const content = await buildReportContent({
|
|
24
|
+
codeMap: state.codeMap,
|
|
24
25
|
consolidation: state.consolidation,
|
|
25
26
|
createdAt,
|
|
26
27
|
ingest: state.ingest,
|
|
@@ -42,6 +43,10 @@ export function createReportWriter(input) {
|
|
|
42
43
|
};
|
|
43
44
|
};
|
|
44
45
|
return {
|
|
46
|
+
addCodeMap: async (data) => {
|
|
47
|
+
state.codeMap = data;
|
|
48
|
+
return write();
|
|
49
|
+
},
|
|
45
50
|
addConsolidation: async (data) => {
|
|
46
51
|
state.consolidation = data;
|
|
47
52
|
return write();
|
|
@@ -58,6 +63,9 @@ async function buildReportContent(input) {
|
|
|
58
63
|
if (input.ingest) {
|
|
59
64
|
lines.push(`transcripts_added: ${input.ingest.transcriptsAdded.length}`, `transcripts_changed: ${input.ingest.transcriptsChanged.length}`, `artifacts_ingested: ${input.ingest.artifacts.length}`, `episodes_created: ${input.ingest.episodes.length}`, `ingest_failures: ${input.ingest.failures.length}`, `artifacts_quarantined: ${input.ingest.quarantined.length}`);
|
|
60
65
|
}
|
|
66
|
+
if (input.codeMap) {
|
|
67
|
+
lines.push(`code_evidence_captured: ${input.codeMap.evidenceCaptured}`, `code_pages_rendered: ${input.codeMap.pagesRendered.length}`, `code_pages_pruned: ${input.codeMap.pagesPruned.length}`, `code_failures: ${input.codeMap.failures.length}`);
|
|
68
|
+
}
|
|
61
69
|
if (input.consolidation) {
|
|
62
70
|
lines.push(`pages_created: ${input.consolidation.pagesCreated.length}`, `pages_updated: ${input.consolidation.pagesUpdated.length}`, `episodes_consolidated: ${input.consolidation.episodesConsolidated}`, `consolidation_failures: ${input.consolidation.failures.length}`, `episodes_quarantined: ${input.consolidation.quarantined.length}`);
|
|
63
71
|
}
|
|
@@ -66,6 +74,9 @@ async function buildReportContent(input) {
|
|
|
66
74
|
if (input.ingest) {
|
|
67
75
|
lines.push("", ...(await buildIngestSection(input.ingest, input.vaultRoot)));
|
|
68
76
|
}
|
|
77
|
+
if (input.codeMap) {
|
|
78
|
+
lines.push("", ...buildCodeMapSection(input.codeMap));
|
|
79
|
+
}
|
|
69
80
|
if (input.consolidation) {
|
|
70
81
|
lines.push("", ...buildConsolidationSection(input.consolidation));
|
|
71
82
|
}
|
|
@@ -121,6 +132,29 @@ async function buildIngestSection(data, vaultRoot) {
|
|
|
121
132
|
}
|
|
122
133
|
return lines;
|
|
123
134
|
}
|
|
135
|
+
function buildCodeMapSection(data) {
|
|
136
|
+
const lines = ["## Code map", ""];
|
|
137
|
+
lines.push(`Evidence files captured: ${data.evidenceCaptured}`);
|
|
138
|
+
if (data.pagesRendered.length > 0) {
|
|
139
|
+
lines.push(`Pages rendered: ${data.pagesRendered.map((page) => `[[code/${page}]]`).join(", ")}`);
|
|
140
|
+
}
|
|
141
|
+
if (data.pagesPruned.length > 0) {
|
|
142
|
+
lines.push(`Pages pruned: ${data.pagesPruned.map((page) => `\`code/${page}\``).join(", ")}`);
|
|
143
|
+
}
|
|
144
|
+
if (data.failures.length > 0) {
|
|
145
|
+
lines.push("", "### Failures", "");
|
|
146
|
+
for (const failure of data.failures) {
|
|
147
|
+
lines.push(`- ${failure.project} — ${failure.reason}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (data.quarantined.length > 0) {
|
|
151
|
+
lines.push("", "### Quarantined evidence", "");
|
|
152
|
+
for (const entry of data.quarantined) {
|
|
153
|
+
lines.push(`- \`${entry.path}\` — ${entry.reason}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
124
158
|
function buildConsolidationSection(data) {
|
|
125
159
|
const lines = ["## Consolidation", ""];
|
|
126
160
|
if (data.pagesCreated.length > 0) {
|
package/dist/shell.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
const execFileAsync = promisify(execFile);
|
|
4
4
|
export async function runCommand(command, args, cwd, env) {
|
|
@@ -40,6 +40,60 @@ export async function runCommandAllowFailure(command, args, cwd) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
// execFile cannot feed stdin; the code-map backend pipes prompts that exceed
|
|
44
|
+
// OS argv limits, so this spawn-based variant exists alongside it.
|
|
45
|
+
export async function runCommandWithInput(command, args, cwd, input) {
|
|
46
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
47
|
+
const child = spawn(command, [...args], { cwd });
|
|
48
|
+
let stdout = "";
|
|
49
|
+
let stderr = "";
|
|
50
|
+
let settled = false;
|
|
51
|
+
const settle = (result) => {
|
|
52
|
+
if (settled) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
settled = true;
|
|
56
|
+
if (result instanceof Error) {
|
|
57
|
+
rejectPromise(result);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
resolvePromise(result);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
child.stdout.setEncoding("utf8");
|
|
64
|
+
child.stdout.on("data", (chunk) => {
|
|
65
|
+
stdout += chunk;
|
|
66
|
+
});
|
|
67
|
+
child.stderr.setEncoding("utf8");
|
|
68
|
+
child.stderr.on("data", (chunk) => {
|
|
69
|
+
stderr += chunk;
|
|
70
|
+
});
|
|
71
|
+
child.on("error", (error) => {
|
|
72
|
+
settle(new Error(buildCommandError(command, args, {
|
|
73
|
+
code: error.code,
|
|
74
|
+
message: error.message,
|
|
75
|
+
stderr,
|
|
76
|
+
stdout
|
|
77
|
+
})));
|
|
78
|
+
});
|
|
79
|
+
child.on("close", (code, signal) => {
|
|
80
|
+
if (code === 0) {
|
|
81
|
+
settle({ stderr, stdout });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
settle(new Error(buildCommandError(command, args, {
|
|
85
|
+
...(code === null ? {} : { code }),
|
|
86
|
+
...(signal === null ? {} : { signal }),
|
|
87
|
+
stderr,
|
|
88
|
+
stdout
|
|
89
|
+
})));
|
|
90
|
+
});
|
|
91
|
+
// A child that exits before draining stdin raises EPIPE here; the close
|
|
92
|
+
// handler already carries the real outcome.
|
|
93
|
+
child.stdin.on("error", () => undefined);
|
|
94
|
+
child.stdin.end(input);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
43
97
|
// Long prompts are passed as argv: echoing them whole made failure reasons
|
|
44
98
|
// unreadable, while the actual diagnostic (exit code, spawn error, stream
|
|
45
99
|
// tails) was missing entirely.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { join, resolve, sep } from "node:path";
|
|
2
|
+
export function resolveVaultPaths(vaultRoot) {
|
|
3
|
+
// Defense-in-depth (design 7.6): the prune scope must stay confined to
|
|
4
|
+
// raw/projects, so a hand-edited config cannot point ingest at a stray tree.
|
|
5
|
+
const normalized = resolve(vaultRoot);
|
|
6
|
+
if (!normalized.endsWith(join(`${sep}raw`, "projects"))) {
|
|
7
|
+
throw new Error(`vaultRoot must end in raw/projects to run ingest, got: ${vaultRoot}`);
|
|
8
|
+
}
|
|
9
|
+
const vault = resolve(normalized, "..", "..");
|
|
10
|
+
return {
|
|
11
|
+
artifactsRoot: join(vault, "raw", "artifacts"),
|
|
12
|
+
episodesRoot: join(vault, "wiki", "episodes"),
|
|
13
|
+
indexPath: join(vault, "wiki", "index.md"),
|
|
14
|
+
lockPath: join(vault, "wiki", ".ingest.lock"),
|
|
15
|
+
manifestPath: join(vault, "wiki", ".ingest-state.json"),
|
|
16
|
+
rawCodeRoot: join(vault, "raw", "code"),
|
|
17
|
+
reportsRoot: join(vault, "wiki", "reports"),
|
|
18
|
+
vaultDir: vault,
|
|
19
|
+
wikiCodeRoot: join(vault, "wiki", "code"),
|
|
20
|
+
wikiLogPath: join(vault, "wiki", "log.md"),
|
|
21
|
+
wikiRoot: join(vault, "wiki")
|
|
22
|
+
};
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obsidian-second-brain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Turn an Obsidian vault into a self-maintaining second brain for AI coding sessions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"sync": "tsx src/cli.ts",
|
|
29
29
|
"test": "tsx --test tests/**/*.test.ts",
|
|
30
30
|
"coverage": "tsx --test --experimental-test-coverage tests/**/*.test.ts",
|
|
31
|
-
"e2e": "npm run build && tsx --test tests/e2e/cli.e2e.ts tests/e2e/lifecycle.e2e.ts tests/e2e/claude-smoke.e2e.ts"
|
|
31
|
+
"e2e": "npm run build && tsx --test tests/e2e/cli.e2e.ts tests/e2e/lifecycle.e2e.ts tests/e2e/code-map.e2e.ts tests/e2e/claude-smoke.e2e.ts"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"zod": "^4.1.12"
|