@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
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import { statSync, existsSync } from "fs";
|
|
1
|
+
import { statSync, existsSync, readFileSync } from "fs";
|
|
2
2
|
import { join, dirname } from "path";
|
|
3
|
-
import {
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { safeReadJson, atomicWriteJson, atomicWriteText } from "../core/fs-utils";
|
|
4
5
|
import { isSessionState, buildSummary } from "../core/session";
|
|
5
6
|
import { reflect } from "./reflect";
|
|
6
7
|
import { createLedgerFinalizer } from "../core/token-ledger";
|
|
7
8
|
import { loadBugMemory, hasBugForFileInSession } from "../core/bug-memory";
|
|
8
9
|
import { createActionLogWriter, consolidateLog } from "../core/action-log";
|
|
10
|
+
import {
|
|
11
|
+
isWikiEnabled,
|
|
12
|
+
isVaultInitialized,
|
|
13
|
+
resolveVaultPath,
|
|
14
|
+
vaultProjects,
|
|
15
|
+
} from "../core/vault";
|
|
16
|
+
import { resolveConfigValue } from "../core/global-config";
|
|
9
17
|
import type { SessionState, SessionFinalizer } from "../types/session";
|
|
10
18
|
import type { ProjectConfig } from "../types/file-index";
|
|
11
19
|
|
|
@@ -111,5 +119,143 @@ export function sessionStop(
|
|
|
111
119
|
);
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
// Write session summary to wiki vault
|
|
123
|
+
try {
|
|
124
|
+
if (isWikiEnabled() && isVaultInitialized() && hasActivity(state)) {
|
|
125
|
+
writeSessionToWiki(state, projDir);
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Never crash hooks
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Git backup for wiki vault
|
|
132
|
+
try {
|
|
133
|
+
if (isWikiEnabled() && isVaultInitialized()) {
|
|
134
|
+
const gitBackup = resolveConfigValue("wiki.git-backup");
|
|
135
|
+
if (gitBackup.value === "true") {
|
|
136
|
+
gitBackupVault(onReminder);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Never crash hooks
|
|
141
|
+
}
|
|
142
|
+
|
|
114
143
|
atomicWriteJson(sessionFile, state);
|
|
115
144
|
}
|
|
145
|
+
|
|
146
|
+
function writeSessionToWiki(
|
|
147
|
+
state: SessionState,
|
|
148
|
+
projDir: string
|
|
149
|
+
): void {
|
|
150
|
+
const metaRaw = safeReadJson(join(projDir, "project-meta.json")) as Record<
|
|
151
|
+
string,
|
|
152
|
+
unknown
|
|
153
|
+
> | null;
|
|
154
|
+
const projectName = (metaRaw?.name as string) ?? "unknown";
|
|
155
|
+
|
|
156
|
+
const date = new Date().toISOString().split("T")[0];
|
|
157
|
+
const readCount = Object.keys(state.reads).length;
|
|
158
|
+
const writeCount = state.writes.length;
|
|
159
|
+
|
|
160
|
+
const sessionDir = join(vaultProjects(projectName), "sessions");
|
|
161
|
+
const sessionFile = join(sessionDir, `${date}.md`);
|
|
162
|
+
|
|
163
|
+
const timestamp = new Date().toLocaleTimeString("en-US", {
|
|
164
|
+
hour: "2-digit",
|
|
165
|
+
minute: "2-digit",
|
|
166
|
+
hour12: false,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const entry = [
|
|
170
|
+
``,
|
|
171
|
+
`### Session ${timestamp}`,
|
|
172
|
+
``,
|
|
173
|
+
`- Reads: ${readCount}`,
|
|
174
|
+
`- Writes: ${writeCount}`,
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
// Top edited files
|
|
178
|
+
const editCounts: Record<string, number> = {};
|
|
179
|
+
for (const w of state.writes) {
|
|
180
|
+
editCounts[w.filePath] = (editCounts[w.filePath] || 0) + 1;
|
|
181
|
+
}
|
|
182
|
+
const topEdits = Object.entries(editCounts)
|
|
183
|
+
.sort((a, b) => b[1] - a[1])
|
|
184
|
+
.slice(0, 5);
|
|
185
|
+
if (topEdits.length > 0) {
|
|
186
|
+
entry.push(`- Key files:`);
|
|
187
|
+
for (const [file, count] of topEdits) {
|
|
188
|
+
entry.push(` - \`${file}\` (${count} edits)`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
entry.push("");
|
|
193
|
+
|
|
194
|
+
if (existsSync(sessionFile)) {
|
|
195
|
+
const existing = readFileSync(sessionFile, "utf-8");
|
|
196
|
+
atomicWriteText(sessionFile, existing.trimEnd() + "\n" + entry.join("\n"));
|
|
197
|
+
} else {
|
|
198
|
+
const header = [
|
|
199
|
+
`---`,
|
|
200
|
+
`created: "${new Date().toISOString()}"`,
|
|
201
|
+
`updated: "${new Date().toISOString()}"`,
|
|
202
|
+
`tags: [session, ${projectName}]`,
|
|
203
|
+
`category: projects`,
|
|
204
|
+
`---`,
|
|
205
|
+
``,
|
|
206
|
+
`# Sessions — ${projectName} — ${date}`,
|
|
207
|
+
].join("\n");
|
|
208
|
+
atomicWriteText(sessionFile, header + "\n" + entry.join("\n"));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function gitBackupVault(
|
|
213
|
+
onReminder: (msg: string) => void
|
|
214
|
+
): void {
|
|
215
|
+
const vaultPath = resolveVaultPath();
|
|
216
|
+
|
|
217
|
+
// Check if vault is a git repo
|
|
218
|
+
const gitDir = join(vaultPath, ".git");
|
|
219
|
+
if (!existsSync(gitDir)) {
|
|
220
|
+
onReminder(
|
|
221
|
+
"[mink] wiki git-backup enabled but vault is not a git repo — run 'git init' in " +
|
|
222
|
+
vaultPath
|
|
223
|
+
);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
// Check for changes
|
|
229
|
+
const status = execSync("git status --porcelain", {
|
|
230
|
+
cwd: vaultPath,
|
|
231
|
+
timeout: 5000,
|
|
232
|
+
}).toString();
|
|
233
|
+
|
|
234
|
+
if (!status.trim()) return; // Nothing to commit
|
|
235
|
+
|
|
236
|
+
// Stage and commit
|
|
237
|
+
execSync("git add -A", { cwd: vaultPath, timeout: 5000 });
|
|
238
|
+
const msg = `mink: vault update ${new Date().toISOString().split("T")[0]}`;
|
|
239
|
+
execSync(`git commit -m "${msg}"`, {
|
|
240
|
+
cwd: vaultPath,
|
|
241
|
+
timeout: 5000,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Push (best-effort with timeout)
|
|
245
|
+
const remote = resolveConfigValue("wiki.git-remote").value;
|
|
246
|
+
try {
|
|
247
|
+
execSync(`git push ${remote}`, {
|
|
248
|
+
cwd: vaultPath,
|
|
249
|
+
timeout: 10000,
|
|
250
|
+
});
|
|
251
|
+
} catch {
|
|
252
|
+
onReminder(
|
|
253
|
+
`[mink] wiki git push to '${remote}' failed — local commit preserved, will retry next session`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
} catch (err) {
|
|
257
|
+
onReminder(
|
|
258
|
+
`[mink] wiki git backup error: ${err instanceof Error ? err.message : String(err)}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { join, resolve, dirname } from "path";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
copyFileSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
symlinkSync,
|
|
11
|
+
lstatSync,
|
|
12
|
+
} from "fs";
|
|
13
|
+
|
|
14
|
+
// Standard skills directory used by the skills CLI ecosystem
|
|
15
|
+
const AGENTS_SKILLS_DIR = join(homedir(), ".agents", "skills");
|
|
16
|
+
// Claude Code looks for skills via symlinks here
|
|
17
|
+
const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
18
|
+
|
|
19
|
+
function getSkillsSourceDir(): string {
|
|
20
|
+
// Skills live at repo-root/skills/ (the standard skills/{name}/SKILL.md layout)
|
|
21
|
+
return resolve(
|
|
22
|
+
dirname(new URL(import.meta.url).pathname),
|
|
23
|
+
"../../skills"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getAvailableSkills(): string[] {
|
|
28
|
+
const dir = getSkillsSourceDir();
|
|
29
|
+
if (!existsSync(dir)) return [];
|
|
30
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
31
|
+
.filter(
|
|
32
|
+
(d) => d.isDirectory() && existsSync(join(dir, d.name, "SKILL.md"))
|
|
33
|
+
)
|
|
34
|
+
.map((d) => d.name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isInstalled(skillName: string): boolean {
|
|
38
|
+
return existsSync(join(AGENTS_SKILLS_DIR, skillName, "SKILL.md"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function skill(args: string[]): Promise<void> {
|
|
42
|
+
const sub = args[0];
|
|
43
|
+
|
|
44
|
+
switch (sub) {
|
|
45
|
+
case "install":
|
|
46
|
+
skillInstall(args[1]);
|
|
47
|
+
break;
|
|
48
|
+
case "uninstall":
|
|
49
|
+
skillUninstall(args[1]);
|
|
50
|
+
break;
|
|
51
|
+
case "list":
|
|
52
|
+
skillList();
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
console.log("Usage: mink skill <install|uninstall|list> [name]");
|
|
56
|
+
console.log();
|
|
57
|
+
console.log(" install [name] Install Mink skills to ~/.agents/skills/");
|
|
58
|
+
console.log(" uninstall [name] Remove installed Mink skills");
|
|
59
|
+
console.log(" list Show available and installed skills");
|
|
60
|
+
console.log();
|
|
61
|
+
console.log("Or install via the skills CLI:");
|
|
62
|
+
console.log(" npx skills add drewpayment/mink");
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function skillInstall(name?: string): void {
|
|
68
|
+
const sourceDir = getSkillsSourceDir();
|
|
69
|
+
const skills = name ? [name] : getAvailableSkills();
|
|
70
|
+
|
|
71
|
+
if (skills.length === 0) {
|
|
72
|
+
console.error("[mink] no skills found to install");
|
|
73
|
+
console.error(" Expected skills at: " + sourceDir);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
mkdirSync(AGENTS_SKILLS_DIR, { recursive: true });
|
|
78
|
+
|
|
79
|
+
for (const skillName of skills) {
|
|
80
|
+
const srcDir = join(sourceDir, skillName);
|
|
81
|
+
const srcFile = join(srcDir, "SKILL.md");
|
|
82
|
+
const destDir = join(AGENTS_SKILLS_DIR, skillName);
|
|
83
|
+
|
|
84
|
+
if (!existsSync(srcFile)) {
|
|
85
|
+
console.error(`[mink] skill not found: ${skillName}`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Copy the entire skill directory (SKILL.md + any reference files)
|
|
90
|
+
mkdirSync(destDir, { recursive: true });
|
|
91
|
+
copyDirRecursive(srcDir, destDir);
|
|
92
|
+
|
|
93
|
+
// Create symlink in ~/.claude/skills/ (how Claude Code discovers skills)
|
|
94
|
+
mkdirSync(CLAUDE_SKILLS_DIR, { recursive: true });
|
|
95
|
+
const symlink = join(CLAUDE_SKILLS_DIR, skillName);
|
|
96
|
+
try {
|
|
97
|
+
if (existsSync(symlink)) {
|
|
98
|
+
// Remove old file or symlink
|
|
99
|
+
if (lstatSync(symlink).isSymbolicLink() || lstatSync(symlink).isFile()) {
|
|
100
|
+
unlinkSync(symlink);
|
|
101
|
+
} else {
|
|
102
|
+
rmSync(symlink, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const relativeTarget = join("..", "..", ".agents", "skills", skillName);
|
|
106
|
+
symlinkSync(relativeTarget, symlink);
|
|
107
|
+
} catch {
|
|
108
|
+
// Non-critical — skill still works from ~/.agents/skills/
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`[mink] installed: ${skillName} -> ${destDir}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log();
|
|
115
|
+
console.log(" Restart your Claude Code session to use the new skills.");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function skillUninstall(name?: string): void {
|
|
119
|
+
const skills = name ? [name] : getAvailableSkills();
|
|
120
|
+
|
|
121
|
+
for (const skillName of skills) {
|
|
122
|
+
const destDir = join(AGENTS_SKILLS_DIR, skillName);
|
|
123
|
+
|
|
124
|
+
if (!existsSync(destDir)) {
|
|
125
|
+
console.log(`[mink] not installed: ${skillName}`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
rmSync(destDir, { recursive: true, force: true });
|
|
130
|
+
|
|
131
|
+
// Remove symlink from ~/.claude/skills/
|
|
132
|
+
const symlink = join(CLAUDE_SKILLS_DIR, skillName);
|
|
133
|
+
try {
|
|
134
|
+
if (existsSync(symlink)) unlinkSync(symlink);
|
|
135
|
+
} catch {
|
|
136
|
+
// Non-critical
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(`[mink] uninstalled: ${skillName}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function skillList(): void {
|
|
144
|
+
const available = getAvailableSkills();
|
|
145
|
+
const installed = available.filter(isInstalled);
|
|
146
|
+
const notInstalled = available.filter((s) => !installed.includes(s));
|
|
147
|
+
|
|
148
|
+
console.log("[mink] skills:");
|
|
149
|
+
console.log();
|
|
150
|
+
|
|
151
|
+
if (installed.length > 0) {
|
|
152
|
+
console.log(" Installed:");
|
|
153
|
+
for (const s of installed) {
|
|
154
|
+
console.log(` ${s} (${join(AGENTS_SKILLS_DIR, s)})`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (notInstalled.length > 0) {
|
|
159
|
+
console.log(" Available:");
|
|
160
|
+
for (const s of notInstalled) {
|
|
161
|
+
console.log(` ${s}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (available.length === 0) {
|
|
166
|
+
console.log(" No skills available.");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log();
|
|
170
|
+
console.log(" Install with: mink skill install");
|
|
171
|
+
console.log(" Or via skills CLI: npx skills add drewpayment/mink");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function copyDirRecursive(src: string, dest: string): void {
|
|
175
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
const srcPath = join(src, entry.name);
|
|
178
|
+
const destPath = join(dest, entry.name);
|
|
179
|
+
if (entry.isDirectory()) {
|
|
180
|
+
mkdirSync(destPath, { recursive: true });
|
|
181
|
+
copyDirRecursive(srcPath, destPath);
|
|
182
|
+
} else {
|
|
183
|
+
copyFileSync(srcPath, destPath);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { existsSync, statSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import {
|
|
5
|
+
resolveVaultPath,
|
|
6
|
+
ensureVaultStructure,
|
|
7
|
+
isVaultInitialized,
|
|
8
|
+
vaultManifestPath,
|
|
9
|
+
vaultTemplates,
|
|
10
|
+
} from "../core/vault";
|
|
11
|
+
import { atomicWriteJson } from "../core/fs-utils";
|
|
12
|
+
import { setConfigValue } from "../core/global-config";
|
|
13
|
+
import { seedTemplates } from "../core/vault-templates";
|
|
14
|
+
import { rebuildVaultIndex, loadVaultIndex } from "../core/note-index";
|
|
15
|
+
import { updateMasterIndex } from "../core/note-linker";
|
|
16
|
+
import type { VaultManifest, NoteCategory } from "../types/note";
|
|
17
|
+
|
|
18
|
+
export async function wiki(
|
|
19
|
+
_cwd: string,
|
|
20
|
+
args: string[]
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const sub = args[0];
|
|
23
|
+
|
|
24
|
+
switch (sub) {
|
|
25
|
+
case "init":
|
|
26
|
+
await wikiInit(args.slice(1));
|
|
27
|
+
break;
|
|
28
|
+
case "status":
|
|
29
|
+
wikiStatus();
|
|
30
|
+
break;
|
|
31
|
+
case "rebuild-index":
|
|
32
|
+
wikiRebuildIndex();
|
|
33
|
+
break;
|
|
34
|
+
case "organize":
|
|
35
|
+
wikiOrganize();
|
|
36
|
+
break;
|
|
37
|
+
default:
|
|
38
|
+
console.log("Usage: mink wiki <init|status|rebuild-index|organize>");
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(" init Initialize the notes/wiki vault");
|
|
41
|
+
console.log(" status Show vault statistics");
|
|
42
|
+
console.log(" rebuild-index Full rescan and reindex of vault");
|
|
43
|
+
console.log(" organize List inbox notes needing categorization");
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function wikiInit(args: string[]): Promise<void> {
|
|
49
|
+
const pathArg = args[0];
|
|
50
|
+
|
|
51
|
+
if (isVaultInitialized()) {
|
|
52
|
+
const vaultPath = resolveVaultPath();
|
|
53
|
+
console.log(`[mink] vault already initialized at ${vaultPath}`);
|
|
54
|
+
console.log(" Run 'mink wiki rebuild-index' to refresh the index.");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let targetPath: string;
|
|
59
|
+
|
|
60
|
+
if (pathArg) {
|
|
61
|
+
targetPath = expandPath(pathArg);
|
|
62
|
+
} else {
|
|
63
|
+
targetPath = resolveVaultPath();
|
|
64
|
+
console.log(`[mink] initializing vault at ${targetPath}`);
|
|
65
|
+
console.log(
|
|
66
|
+
" (set a custom path with: mink wiki init /path/to/vault)"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isExisting =
|
|
71
|
+
existsSync(targetPath) && statSync(targetPath).isDirectory();
|
|
72
|
+
|
|
73
|
+
// Set the config value
|
|
74
|
+
setConfigValue("wiki.path", targetPath);
|
|
75
|
+
|
|
76
|
+
// Create vault structure
|
|
77
|
+
ensureVaultStructure();
|
|
78
|
+
|
|
79
|
+
// Seed templates
|
|
80
|
+
seedTemplates(vaultTemplates());
|
|
81
|
+
|
|
82
|
+
// Create vault manifest
|
|
83
|
+
const manifest: VaultManifest = {
|
|
84
|
+
version: 1,
|
|
85
|
+
createdAt: new Date().toISOString(),
|
|
86
|
+
totalNotes: 0,
|
|
87
|
+
categories: {
|
|
88
|
+
inbox: 0,
|
|
89
|
+
projects: 0,
|
|
90
|
+
areas: 0,
|
|
91
|
+
resources: 0,
|
|
92
|
+
archives: 0,
|
|
93
|
+
},
|
|
94
|
+
lastOrganized: "",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (isExisting) {
|
|
98
|
+
console.log(`[mink] scanning existing directory: ${targetPath}`);
|
|
99
|
+
const index = rebuildVaultIndex();
|
|
100
|
+
manifest.totalNotes = index.totalNotes;
|
|
101
|
+
|
|
102
|
+
// Count by category
|
|
103
|
+
for (const entry of Object.values(index.entries)) {
|
|
104
|
+
if (manifest.categories[entry.category] !== undefined) {
|
|
105
|
+
manifest.categories[entry.category]++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Flag potential garbage
|
|
110
|
+
const suspicious = Object.values(index.entries).filter((e) => {
|
|
111
|
+
const name = e.filePath.split("/").pop() ?? "";
|
|
112
|
+
return (
|
|
113
|
+
e.estimatedTokens < 15 ||
|
|
114
|
+
name.length > 30 && /[A-Za-z0-9_-]{20,}/.test(name.replace(/\.md$/, ""))
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (suspicious.length > 0) {
|
|
119
|
+
console.log();
|
|
120
|
+
console.log(
|
|
121
|
+
` [review] ${suspicious.length} files may need attention:`
|
|
122
|
+
);
|
|
123
|
+
for (const s of suspicious.slice(0, 10)) {
|
|
124
|
+
console.log(` - ${s.filePath} (${s.estimatedTokens} tokens)`);
|
|
125
|
+
}
|
|
126
|
+
if (suspicious.length > 10) {
|
|
127
|
+
console.log(` ... and ${suspicious.length - 10} more`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(
|
|
133
|
+
` indexed ${index.totalNotes} notes across the vault`
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
console.log(`[mink] created new vault at ${targetPath}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
atomicWriteJson(vaultManifestPath(), manifest);
|
|
140
|
+
updateMasterIndex(targetPath);
|
|
141
|
+
|
|
142
|
+
console.log();
|
|
143
|
+
console.log(`[mink] vault initialized`);
|
|
144
|
+
console.log(` path: ${targetPath}`);
|
|
145
|
+
console.log(` templates: ${vaultTemplates()}`);
|
|
146
|
+
console.log(` manifest: ${vaultManifestPath()}`);
|
|
147
|
+
console.log();
|
|
148
|
+
console.log(" Next steps:");
|
|
149
|
+
console.log(" mink note \"your first note\"");
|
|
150
|
+
console.log(" mink note --daily");
|
|
151
|
+
console.log(" mink skill install # install /mink:note skill for Claude Code");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function wikiStatus(): void {
|
|
155
|
+
if (!isVaultInitialized()) {
|
|
156
|
+
console.log("[mink] no vault initialized");
|
|
157
|
+
console.log(" Run 'mink wiki init' to get started.");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const vaultPath = resolveVaultPath();
|
|
162
|
+
const index = loadVaultIndex();
|
|
163
|
+
|
|
164
|
+
const categoryCounts: Record<string, number> = {
|
|
165
|
+
inbox: 0,
|
|
166
|
+
projects: 0,
|
|
167
|
+
areas: 0,
|
|
168
|
+
resources: 0,
|
|
169
|
+
archives: 0,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
for (const entry of Object.values(index.entries)) {
|
|
173
|
+
if (categoryCounts[entry.category] !== undefined) {
|
|
174
|
+
categoryCounts[entry.category]++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log("[mink] vault status");
|
|
179
|
+
console.log(` path: ${vaultPath}`);
|
|
180
|
+
console.log(` notes: ${index.totalNotes}`);
|
|
181
|
+
console.log();
|
|
182
|
+
console.log(" Categories:");
|
|
183
|
+
for (const [cat, count] of Object.entries(categoryCounts)) {
|
|
184
|
+
console.log(` ${cat.padEnd(12)} ${count}`);
|
|
185
|
+
}
|
|
186
|
+
console.log();
|
|
187
|
+
console.log(
|
|
188
|
+
` last indexed: ${index.lastScanTimestamp || "never"}`
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (categoryCounts.inbox > 0) {
|
|
192
|
+
console.log();
|
|
193
|
+
console.log(
|
|
194
|
+
` ${categoryCounts.inbox} notes in inbox need categorization`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function wikiRebuildIndex(): void {
|
|
200
|
+
if (!isVaultInitialized()) {
|
|
201
|
+
console.log("[mink] no vault initialized");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log("[mink] rebuilding vault index...");
|
|
206
|
+
const index = rebuildVaultIndex();
|
|
207
|
+
updateMasterIndex(resolveVaultPath());
|
|
208
|
+
console.log(` indexed ${index.totalNotes} notes`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function wikiOrganize(): void {
|
|
212
|
+
if (!isVaultInitialized()) {
|
|
213
|
+
console.log("[mink] no vault initialized");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const index = loadVaultIndex();
|
|
218
|
+
const inboxNotes = Object.values(index.entries).filter(
|
|
219
|
+
(e) => e.category === "inbox"
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (inboxNotes.length === 0) {
|
|
223
|
+
console.log("[mink] inbox is empty — nothing to organize");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(`[mink] ${inboxNotes.length} notes in inbox:`);
|
|
228
|
+
console.log();
|
|
229
|
+
|
|
230
|
+
for (const note of inboxNotes) {
|
|
231
|
+
const tags = note.tags.length > 0 ? ` [${note.tags.join(", ")}]` : "";
|
|
232
|
+
console.log(` ${note.filePath}`);
|
|
233
|
+
console.log(` ${note.title}${tags}`);
|
|
234
|
+
if (note.description) {
|
|
235
|
+
console.log(` ${note.description}`);
|
|
236
|
+
}
|
|
237
|
+
console.log();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log(
|
|
241
|
+
"Use '/mink:note' in Claude Code to intelligently categorize these notes."
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function expandPath(raw: string): string {
|
|
246
|
+
if (raw.startsWith("~/")) {
|
|
247
|
+
return resolve(homedir(), raw.slice(2));
|
|
248
|
+
}
|
|
249
|
+
return resolve(raw);
|
|
250
|
+
}
|
package/src/core/daemon.ts
CHANGED
|
@@ -64,7 +64,8 @@ export function startDaemon(cwd: string): void {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Resolve the CLI entry point
|
|
67
|
-
const
|
|
67
|
+
const __dir = dirname(new URL(import.meta.url).pathname);
|
|
68
|
+
const cliPath = resolve(__dir, "../cli.ts");
|
|
68
69
|
|
|
69
70
|
// Ensure log directory exists
|
|
70
71
|
const logPath = schedulerLogPath();
|