@drewpayment/mink 0.1.0 → 0.2.1
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 +191 -8
- package/dist/cli.js +87605 -0
- package/package.json +7 -3
- package/skills/mink-note/SKILL.md +131 -0
- package/src/cli.ts +46 -0
- package/src/commands/dashboard.ts +1 -1
- package/src/commands/init.ts +77 -4
- package/src/commands/note.ts +267 -0
- package/src/commands/session-start.ts +26 -0
- package/src/commands/session-stop.ts +148 -2
- package/src/commands/skill.ts +186 -0
- package/src/commands/wiki.ts +250 -0
- package/src/core/daemon.ts +2 -1
- package/src/core/dashboard-server.ts +47 -48
- package/src/core/note-index.ts +262 -0
- package/src/core/note-linker.ts +161 -0
- package/src/core/note-writer.ts +203 -0
- package/src/core/runtime.ts +214 -0
- package/src/core/vault-templates.ts +179 -0
- package/src/core/vault.ts +132 -0
- package/src/types/config.ts +7 -0
- package/src/types/note.ts +60 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { atomicWriteText } from "./fs-utils";
|
|
4
|
+
import { categoryToDir, vaultDailyDir, vaultTemplates } from "./vault";
|
|
5
|
+
import { loadTemplate } from "./vault-templates";
|
|
6
|
+
import type { NoteMetadata, NoteFrontmatter, NoteCategory } from "../types/note";
|
|
7
|
+
|
|
8
|
+
export function slugifyTitle(title: string): string {
|
|
9
|
+
return title
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
12
|
+
.replace(/\s+/g, "-")
|
|
13
|
+
.replace(/-+/g, "-")
|
|
14
|
+
.replace(/^-|-$/g, "")
|
|
15
|
+
.slice(0, 80);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function generateFrontmatter(meta: {
|
|
19
|
+
created: string;
|
|
20
|
+
updated: string;
|
|
21
|
+
tags: string[];
|
|
22
|
+
category: NoteCategory;
|
|
23
|
+
sourceProject?: string;
|
|
24
|
+
aliases?: string[];
|
|
25
|
+
extra?: Record<string, unknown>;
|
|
26
|
+
}): string {
|
|
27
|
+
const lines: string[] = ["---"];
|
|
28
|
+
lines.push(`created: "${meta.created}"`);
|
|
29
|
+
lines.push(`updated: "${meta.updated}"`);
|
|
30
|
+
|
|
31
|
+
if (meta.tags.length > 0) {
|
|
32
|
+
lines.push(`tags: [${meta.tags.join(", ")}]`);
|
|
33
|
+
} else {
|
|
34
|
+
lines.push("tags: []");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
lines.push(`category: ${meta.category}`);
|
|
38
|
+
|
|
39
|
+
if (meta.sourceProject) {
|
|
40
|
+
lines.push(`source_project: ${meta.sourceProject}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (meta.aliases && meta.aliases.length > 0) {
|
|
44
|
+
lines.push(`aliases: [${meta.aliases.join(", ")}]`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (meta.extra) {
|
|
48
|
+
for (const [key, value] of Object.entries(meta.extra)) {
|
|
49
|
+
lines.push(`${key}: ${JSON.stringify(value)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lines.push("---");
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createNote(meta: NoteMetadata): {
|
|
58
|
+
filePath: string;
|
|
59
|
+
content: string;
|
|
60
|
+
} {
|
|
61
|
+
const now = meta.created || new Date().toISOString();
|
|
62
|
+
const slug = slugifyTitle(meta.title);
|
|
63
|
+
const dir = categoryToDir(meta.category, meta.projectSlug);
|
|
64
|
+
const filePath = join(dir, `${slug}.md`);
|
|
65
|
+
|
|
66
|
+
let content: string;
|
|
67
|
+
|
|
68
|
+
if (meta.template) {
|
|
69
|
+
const rendered = loadTemplate(vaultTemplates(), meta.template, {
|
|
70
|
+
title: meta.title,
|
|
71
|
+
body: meta.body,
|
|
72
|
+
created: now,
|
|
73
|
+
updated: now,
|
|
74
|
+
date: now.split("T")[0],
|
|
75
|
+
});
|
|
76
|
+
content = rendered ?? buildNoteContent(meta, now);
|
|
77
|
+
} else {
|
|
78
|
+
content = buildNoteContent(meta, now);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
atomicWriteText(filePath, content);
|
|
82
|
+
return { filePath, content };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildNoteContent(meta: NoteMetadata, now: string): string {
|
|
86
|
+
const frontmatter = generateFrontmatter({
|
|
87
|
+
created: now,
|
|
88
|
+
updated: now,
|
|
89
|
+
tags: meta.tags,
|
|
90
|
+
category: meta.category,
|
|
91
|
+
sourceProject: meta.sourceProject,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return `${frontmatter}
|
|
95
|
+
|
|
96
|
+
# ${meta.title}
|
|
97
|
+
|
|
98
|
+
${meta.body}
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function appendToDaily(date: string, content: string): string {
|
|
103
|
+
const dir = vaultDailyDir();
|
|
104
|
+
const filePath = join(dir, `${date}.md`);
|
|
105
|
+
|
|
106
|
+
if (existsSync(filePath)) {
|
|
107
|
+
const existing = readFileSync(filePath, "utf-8");
|
|
108
|
+
const timestamp = new Date().toLocaleTimeString("en-US", {
|
|
109
|
+
hour: "2-digit",
|
|
110
|
+
minute: "2-digit",
|
|
111
|
+
hour12: false,
|
|
112
|
+
});
|
|
113
|
+
const updated = `${existing.trimEnd()}\n\n## ${timestamp}\n\n${content}\n`;
|
|
114
|
+
atomicWriteText(filePath, updated);
|
|
115
|
+
} else {
|
|
116
|
+
const now = new Date().toISOString();
|
|
117
|
+
const rendered = loadTemplate(vaultTemplates(), "daily-note", {
|
|
118
|
+
title: date,
|
|
119
|
+
date,
|
|
120
|
+
body: content,
|
|
121
|
+
created: now,
|
|
122
|
+
updated: now,
|
|
123
|
+
});
|
|
124
|
+
const noteContent =
|
|
125
|
+
rendered ??
|
|
126
|
+
`---
|
|
127
|
+
created: "${now}"
|
|
128
|
+
updated: "${now}"
|
|
129
|
+
tags: [daily]
|
|
130
|
+
category: areas
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
# ${date}
|
|
134
|
+
|
|
135
|
+
${content}
|
|
136
|
+
`;
|
|
137
|
+
atomicWriteText(filePath, noteContent);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return filePath;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function ingestFile(
|
|
144
|
+
sourcePath: string,
|
|
145
|
+
meta: {
|
|
146
|
+
category: NoteCategory;
|
|
147
|
+
tags?: string[];
|
|
148
|
+
projectSlug?: string;
|
|
149
|
+
sourceProject?: string;
|
|
150
|
+
}
|
|
151
|
+
): { filePath: string; content: string } {
|
|
152
|
+
const raw = readFileSync(sourcePath, "utf-8");
|
|
153
|
+
const now = new Date().toISOString();
|
|
154
|
+
|
|
155
|
+
// Extract title from first heading or filename
|
|
156
|
+
const headingMatch = raw.match(/^#\s+(.+)$/m);
|
|
157
|
+
const title =
|
|
158
|
+
headingMatch?.[1] ??
|
|
159
|
+
sourcePath
|
|
160
|
+
.split("/")
|
|
161
|
+
.pop()!
|
|
162
|
+
.replace(/\.md$/, "");
|
|
163
|
+
|
|
164
|
+
// Check if file already has frontmatter
|
|
165
|
+
const hasFrontmatter = raw.startsWith("---");
|
|
166
|
+
let content: string;
|
|
167
|
+
|
|
168
|
+
if (hasFrontmatter) {
|
|
169
|
+
// Preserve existing frontmatter, add missing fields
|
|
170
|
+
const endIdx = raw.indexOf("---", 3);
|
|
171
|
+
if (endIdx !== -1) {
|
|
172
|
+
const existingFm = raw.slice(0, endIdx + 3);
|
|
173
|
+
const body = raw.slice(endIdx + 3).trim();
|
|
174
|
+
// Add category if missing
|
|
175
|
+
if (!existingFm.includes("category:")) {
|
|
176
|
+
const updatedFm = existingFm.replace(
|
|
177
|
+
/---$/,
|
|
178
|
+
`category: ${meta.category}\n---`
|
|
179
|
+
);
|
|
180
|
+
content = `${updatedFm}\n\n${body}\n`;
|
|
181
|
+
} else {
|
|
182
|
+
content = raw;
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
content = raw;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
const frontmatter = generateFrontmatter({
|
|
189
|
+
created: now,
|
|
190
|
+
updated: now,
|
|
191
|
+
tags: meta.tags ?? [],
|
|
192
|
+
category: meta.category,
|
|
193
|
+
sourceProject: meta.sourceProject,
|
|
194
|
+
});
|
|
195
|
+
content = `${frontmatter}\n\n${raw}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const slug = slugifyTitle(title);
|
|
199
|
+
const dir = categoryToDir(meta.category, meta.projectSlug);
|
|
200
|
+
const filePath = join(dir, `${slug}.md`);
|
|
201
|
+
atomicWriteText(filePath, content);
|
|
202
|
+
return { filePath, content };
|
|
203
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-runtime utilities — Bun-first with Node.js fallbacks.
|
|
3
|
+
*
|
|
4
|
+
* Detects the runtime once at import time and exports helpers that
|
|
5
|
+
* abstract over Bun.serve / node:http, Bun.file / fs, and Bun.spawn / child_process.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile, stat } from "fs/promises";
|
|
9
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
10
|
+
|
|
11
|
+
export const isBun = typeof globalThis.Bun !== "undefined";
|
|
12
|
+
|
|
13
|
+
// ── File helpers ──────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface RuntimeFile {
|
|
16
|
+
exists(): Promise<boolean>;
|
|
17
|
+
bytes(): Promise<Uint8Array>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns a lightweight file handle with `.exists()` and `.bytes()`.
|
|
22
|
+
* Uses Bun.file when available, otherwise falls back to fs.
|
|
23
|
+
*/
|
|
24
|
+
export function runtimeFile(path: string): RuntimeFile {
|
|
25
|
+
if (isBun) {
|
|
26
|
+
const f = Bun.file(path);
|
|
27
|
+
return {
|
|
28
|
+
exists: () => f.exists(),
|
|
29
|
+
bytes: () => f.arrayBuffer().then((ab) => new Uint8Array(ab)),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
async exists() {
|
|
34
|
+
try {
|
|
35
|
+
await stat(path);
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
async bytes() {
|
|
42
|
+
return readFile(path);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Spawn helper ──────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export interface SpawnOptions {
|
|
50
|
+
cwd?: string;
|
|
51
|
+
env?: Record<string, string | undefined>;
|
|
52
|
+
stdout?: "ignore" | "pipe";
|
|
53
|
+
stderr?: "ignore" | "pipe";
|
|
54
|
+
stdin?: "ignore";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SpawnedProcess {
|
|
58
|
+
unref(): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fire-and-forget process spawning. Uses Bun.spawn when available,
|
|
63
|
+
* otherwise child_process.spawn with detached + unref.
|
|
64
|
+
*/
|
|
65
|
+
export function runtimeSpawn(
|
|
66
|
+
cmd: string[],
|
|
67
|
+
opts: SpawnOptions = {}
|
|
68
|
+
): SpawnedProcess {
|
|
69
|
+
if (isBun) {
|
|
70
|
+
const proc = Bun.spawn(cmd, {
|
|
71
|
+
cwd: opts.cwd,
|
|
72
|
+
env: opts.env,
|
|
73
|
+
stdout: opts.stdout ?? "ignore",
|
|
74
|
+
stderr: opts.stderr ?? "ignore",
|
|
75
|
+
stdin: opts.stdin ?? "ignore",
|
|
76
|
+
});
|
|
77
|
+
return { unref: () => proc.unref() };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const [bin, ...args] = cmd;
|
|
81
|
+
const proc = nodeSpawn(bin, args, {
|
|
82
|
+
cwd: opts.cwd,
|
|
83
|
+
env: opts.env as NodeJS.ProcessEnv,
|
|
84
|
+
stdio: [
|
|
85
|
+
opts.stdin ?? "ignore",
|
|
86
|
+
opts.stdout ?? "ignore",
|
|
87
|
+
opts.stderr ?? "ignore",
|
|
88
|
+
],
|
|
89
|
+
detached: true,
|
|
90
|
+
});
|
|
91
|
+
proc.unref();
|
|
92
|
+
return { unref: () => {} };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── HTTP Server ───────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export interface RuntimeServer {
|
|
98
|
+
port: number;
|
|
99
|
+
stop(closeConnections?: boolean): void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type FetchHandler = (req: Request) => Response | Promise<Response>;
|
|
103
|
+
|
|
104
|
+
interface ServeOptions {
|
|
105
|
+
port: number;
|
|
106
|
+
hostname: string;
|
|
107
|
+
idleTimeout?: number;
|
|
108
|
+
fetch: FetchHandler;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Start an HTTP server using Bun.serve or node:http.
|
|
113
|
+
* The fetch handler uses standard Web API Request/Response in both runtimes.
|
|
114
|
+
*/
|
|
115
|
+
export async function runtimeServe(opts: ServeOptions): Promise<RuntimeServer> {
|
|
116
|
+
if (isBun) {
|
|
117
|
+
const server = Bun.serve({
|
|
118
|
+
port: opts.port,
|
|
119
|
+
hostname: opts.hostname,
|
|
120
|
+
idleTimeout: opts.idleTimeout ?? 0,
|
|
121
|
+
fetch: opts.fetch,
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
port: server.port as number,
|
|
125
|
+
stop: (close) => server.stop(close),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Node.js fallback using node:http
|
|
130
|
+
const { createServer } = await import("node:http");
|
|
131
|
+
const { Readable } = await import("node:stream");
|
|
132
|
+
|
|
133
|
+
return new Promise<RuntimeServer>((resolve) => {
|
|
134
|
+
const httpServer = createServer(async (req, res) => {
|
|
135
|
+
// Build a Web API Request from the Node IncomingMessage
|
|
136
|
+
const url = `http://${opts.hostname}:${opts.port}${req.url ?? "/"}`;
|
|
137
|
+
const headers = new Headers();
|
|
138
|
+
for (const [key, val] of Object.entries(req.headers)) {
|
|
139
|
+
if (val) headers.set(key, Array.isArray(val) ? val.join(", ") : val);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let body: BodyInit | null = null;
|
|
143
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
144
|
+
const chunks: Buffer[] = [];
|
|
145
|
+
for await (const chunk of req) {
|
|
146
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
147
|
+
}
|
|
148
|
+
body = Buffer.concat(chunks);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const request = new Request(url, {
|
|
152
|
+
method: req.method,
|
|
153
|
+
headers,
|
|
154
|
+
body,
|
|
155
|
+
// @ts-expect-error -- Node 18+ supports duplex on Request
|
|
156
|
+
duplex: body ? "half" : undefined,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const response = await opts.fetch(request);
|
|
161
|
+
|
|
162
|
+
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
163
|
+
|
|
164
|
+
if (!response.body) {
|
|
165
|
+
res.end();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Stream the response body
|
|
170
|
+
const reader = response.body.getReader();
|
|
171
|
+
const nodeStream = new Readable({
|
|
172
|
+
async read() {
|
|
173
|
+
try {
|
|
174
|
+
const { done, value } = await reader.read();
|
|
175
|
+
if (done) {
|
|
176
|
+
this.push(null);
|
|
177
|
+
} else {
|
|
178
|
+
this.push(Buffer.from(value));
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
this.push(null);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Clean up SSE streams when client disconnects
|
|
187
|
+
res.on("close", () => {
|
|
188
|
+
reader.cancel().catch(() => {});
|
|
189
|
+
nodeStream.destroy();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
nodeStream.pipe(res);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (!res.headersSent) {
|
|
195
|
+
res.writeHead(500);
|
|
196
|
+
res.end(String(err));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
httpServer.listen(opts.port, opts.hostname, () => {
|
|
202
|
+
const addr = httpServer.address();
|
|
203
|
+
const boundPort =
|
|
204
|
+
typeof addr === "object" && addr ? addr.port : opts.port;
|
|
205
|
+
resolve({
|
|
206
|
+
port: boundPort,
|
|
207
|
+
stop: (close) => {
|
|
208
|
+
if (close) httpServer.closeAllConnections();
|
|
209
|
+
httpServer.close();
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { existsSync, writeFileSync, readFileSync, mkdirSync } from "fs";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_TEMPLATES: Record<string, string> = {
|
|
5
|
+
"quick-capture": `---
|
|
6
|
+
created: "{{created}}"
|
|
7
|
+
updated: "{{updated}}"
|
|
8
|
+
tags: []
|
|
9
|
+
category: inbox
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# {{title}}
|
|
13
|
+
|
|
14
|
+
{{body}}
|
|
15
|
+
`,
|
|
16
|
+
|
|
17
|
+
"daily-note": `---
|
|
18
|
+
created: "{{created}}"
|
|
19
|
+
updated: "{{updated}}"
|
|
20
|
+
tags: [daily]
|
|
21
|
+
category: areas
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# {{date}}
|
|
25
|
+
|
|
26
|
+
## Focus
|
|
27
|
+
|
|
28
|
+
-
|
|
29
|
+
|
|
30
|
+
## Notes
|
|
31
|
+
|
|
32
|
+
-
|
|
33
|
+
|
|
34
|
+
## Tasks
|
|
35
|
+
|
|
36
|
+
- [ ]
|
|
37
|
+
|
|
38
|
+
## Reflections
|
|
39
|
+
|
|
40
|
+
`,
|
|
41
|
+
|
|
42
|
+
meeting: `---
|
|
43
|
+
created: "{{created}}"
|
|
44
|
+
updated: "{{updated}}"
|
|
45
|
+
tags: [meeting]
|
|
46
|
+
category: areas
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
# {{title}}
|
|
50
|
+
|
|
51
|
+
**Date**: {{date}}
|
|
52
|
+
**Attendees**:
|
|
53
|
+
|
|
54
|
+
## Agenda
|
|
55
|
+
|
|
56
|
+
-
|
|
57
|
+
|
|
58
|
+
## Discussion
|
|
59
|
+
|
|
60
|
+
-
|
|
61
|
+
|
|
62
|
+
## Decisions
|
|
63
|
+
|
|
64
|
+
-
|
|
65
|
+
|
|
66
|
+
## Action Items
|
|
67
|
+
|
|
68
|
+
- [ ]
|
|
69
|
+
`,
|
|
70
|
+
|
|
71
|
+
project: `---
|
|
72
|
+
created: "{{created}}"
|
|
73
|
+
updated: "{{updated}}"
|
|
74
|
+
tags: [project]
|
|
75
|
+
category: projects
|
|
76
|
+
status: active
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
# {{title}}
|
|
80
|
+
|
|
81
|
+
## Overview
|
|
82
|
+
|
|
83
|
+
{{body}}
|
|
84
|
+
|
|
85
|
+
## Goals
|
|
86
|
+
|
|
87
|
+
-
|
|
88
|
+
|
|
89
|
+
## Key Decisions
|
|
90
|
+
|
|
91
|
+
-
|
|
92
|
+
|
|
93
|
+
## Links
|
|
94
|
+
|
|
95
|
+
-
|
|
96
|
+
`,
|
|
97
|
+
|
|
98
|
+
area: `---
|
|
99
|
+
created: "{{created}}"
|
|
100
|
+
updated: "{{updated}}"
|
|
101
|
+
tags: [area]
|
|
102
|
+
category: areas
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
# {{title}}
|
|
106
|
+
|
|
107
|
+
## Purpose
|
|
108
|
+
|
|
109
|
+
{{body}}
|
|
110
|
+
|
|
111
|
+
## Standards
|
|
112
|
+
|
|
113
|
+
-
|
|
114
|
+
|
|
115
|
+
## Key Resources
|
|
116
|
+
|
|
117
|
+
-
|
|
118
|
+
`,
|
|
119
|
+
|
|
120
|
+
person: `---
|
|
121
|
+
created: "{{created}}"
|
|
122
|
+
updated: "{{updated}}"
|
|
123
|
+
tags: [person]
|
|
124
|
+
category: resources
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
# {{title}}
|
|
128
|
+
|
|
129
|
+
## Role
|
|
130
|
+
|
|
131
|
+
## Context
|
|
132
|
+
|
|
133
|
+
## 1:1 Notes
|
|
134
|
+
|
|
135
|
+
-
|
|
136
|
+
|
|
137
|
+
## Key Projects
|
|
138
|
+
|
|
139
|
+
-
|
|
140
|
+
`,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export function seedTemplates(templatesDir: string): void {
|
|
144
|
+
mkdirSync(templatesDir, { recursive: true });
|
|
145
|
+
for (const [name, content] of Object.entries(DEFAULT_TEMPLATES)) {
|
|
146
|
+
const filePath = join(templatesDir, `${name}.md`);
|
|
147
|
+
if (!existsSync(filePath)) {
|
|
148
|
+
writeFileSync(filePath, content);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function loadTemplate(
|
|
154
|
+
templatesDir: string,
|
|
155
|
+
templateName: string,
|
|
156
|
+
vars: Record<string, string>
|
|
157
|
+
): string | null {
|
|
158
|
+
const filePath = join(templatesDir, `${templateName}.md`);
|
|
159
|
+
let content: string;
|
|
160
|
+
if (existsSync(filePath)) {
|
|
161
|
+
content = readFileSync(filePath, "utf-8");
|
|
162
|
+
} else if (DEFAULT_TEMPLATES[templateName]) {
|
|
163
|
+
content = DEFAULT_TEMPLATES[templateName];
|
|
164
|
+
} else {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return fillTemplate(content, vars);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function fillTemplate(
|
|
171
|
+
template: string,
|
|
172
|
+
vars: Record<string, string>
|
|
173
|
+
): string {
|
|
174
|
+
let result = template;
|
|
175
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
176
|
+
result = result.replaceAll(`{{${key}}}`, value);
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { existsSync, mkdirSync } from "fs";
|
|
4
|
+
import { resolveConfigValue } from "./global-config";
|
|
5
|
+
import { safeReadJson } from "./fs-utils";
|
|
6
|
+
import type { VaultManifest } from "../types/note";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_VAULT_PATH = join(homedir(), ".mink", "wiki");
|
|
9
|
+
|
|
10
|
+
export function resolveVaultPath(): string {
|
|
11
|
+
const resolved = resolveConfigValue("wiki.path");
|
|
12
|
+
const raw = resolved.value;
|
|
13
|
+
if (raw.startsWith("~/")) {
|
|
14
|
+
return join(homedir(), raw.slice(2));
|
|
15
|
+
}
|
|
16
|
+
return raw;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function vaultRoot(): string {
|
|
20
|
+
return resolveVaultPath();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function vaultInbox(): string {
|
|
24
|
+
return join(resolveVaultPath(), "inbox");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function vaultProjects(slug?: string): string {
|
|
28
|
+
const base = join(resolveVaultPath(), "projects");
|
|
29
|
+
return slug ? join(base, slug) : base;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function vaultAreas(): string {
|
|
33
|
+
return join(resolveVaultPath(), "areas");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function vaultDailyDir(): string {
|
|
37
|
+
return join(resolveVaultPath(), "areas", "daily");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function vaultResources(): string {
|
|
41
|
+
return join(resolveVaultPath(), "resources");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function vaultArchives(): string {
|
|
45
|
+
return join(resolveVaultPath(), "archives");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function vaultTemplates(): string {
|
|
49
|
+
return join(resolveVaultPath(), "templates");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function vaultPatterns(): string {
|
|
53
|
+
return join(resolveVaultPath(), "patterns");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function vaultManifestPath(): string {
|
|
57
|
+
return join(resolveVaultPath(), ".mink-vault.json");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function vaultIndexPath(): string {
|
|
61
|
+
return join(resolveVaultPath(), ".mink-index.json");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function vaultMasterIndexPath(): string {
|
|
65
|
+
return join(resolveVaultPath(), "_index.md");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isVaultInitialized(): boolean {
|
|
69
|
+
return existsSync(vaultManifestPath());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isInsideVault(cwd: string): boolean {
|
|
73
|
+
const vault = resolveVaultPath();
|
|
74
|
+
const normalizedCwd = cwd.replace(/\/+$/, "");
|
|
75
|
+
const normalizedVault = vault.replace(/\/+$/, "");
|
|
76
|
+
return (
|
|
77
|
+
normalizedCwd === normalizedVault ||
|
|
78
|
+
normalizedCwd.startsWith(normalizedVault + "/")
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isWikiEnabled(): boolean {
|
|
83
|
+
const resolved = resolveConfigValue("wiki.enabled");
|
|
84
|
+
return resolved.value === "true";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function loadVaultManifest(): VaultManifest | null {
|
|
88
|
+
const raw = safeReadJson(vaultManifestPath());
|
|
89
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
90
|
+
return raw as VaultManifest;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const VAULT_DIRS = [
|
|
94
|
+
"",
|
|
95
|
+
"inbox",
|
|
96
|
+
"projects",
|
|
97
|
+
"areas",
|
|
98
|
+
"areas/daily",
|
|
99
|
+
"resources",
|
|
100
|
+
"archives",
|
|
101
|
+
"templates",
|
|
102
|
+
"patterns",
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
export function ensureVaultStructure(): void {
|
|
106
|
+
const root = resolveVaultPath();
|
|
107
|
+
for (const dir of VAULT_DIRS) {
|
|
108
|
+
mkdirSync(join(root, dir), { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function categoryToDir(
|
|
113
|
+
category: string,
|
|
114
|
+
projectSlug?: string
|
|
115
|
+
): string {
|
|
116
|
+
const root = resolveVaultPath();
|
|
117
|
+
switch (category) {
|
|
118
|
+
case "projects":
|
|
119
|
+
return projectSlug
|
|
120
|
+
? join(root, "projects", projectSlug)
|
|
121
|
+
: join(root, "projects");
|
|
122
|
+
case "areas":
|
|
123
|
+
return join(root, "areas");
|
|
124
|
+
case "resources":
|
|
125
|
+
return join(root, "resources");
|
|
126
|
+
case "archives":
|
|
127
|
+
return join(root, "archives");
|
|
128
|
+
case "inbox":
|
|
129
|
+
default:
|
|
130
|
+
return join(root, "inbox");
|
|
131
|
+
}
|
|
132
|
+
}
|