@drewpayment/mink 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 +191 -8
- package/dist/cli.js +87465 -0
- package/package.json +7 -3
- package/skills/mink-note/SKILL.md +131 -0
- package/src/cli.ts +46 -0
- 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/note-index.ts +262 -0
- package/src/core/note-linker.ts +161 -0
- package/src/core/note-writer.ts +203 -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
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drewpayment/mink",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A hidden presence that moves alongside the developer — token efficiency and cross-project wiki for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"mink": "./
|
|
7
|
+
"mink": "./dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
+
"build": "bun build src/cli.ts --outfile dist/cli.js --target node --format esm && node -e \"const f=require('fs');let c=f.readFileSync('dist/cli.js','utf8');c=c.replace(/^#!.*\\n/,'');f.writeFileSync('dist/cli.js','#!/usr/bin/env node\\n'+c);f.chmodSync('dist/cli.js',0o755)\"",
|
|
11
|
+
"postinstall": "bun run build 2>/dev/null || true",
|
|
10
12
|
"typecheck": "bunx tsc --noEmit",
|
|
11
13
|
"test": "bun test",
|
|
12
14
|
"test:watch": "bun test --watch",
|
|
@@ -14,7 +16,9 @@
|
|
|
14
16
|
"dashboard:build": "cd dashboard && bun run build"
|
|
15
17
|
},
|
|
16
18
|
"files": [
|
|
17
|
-
"src/**/*.ts"
|
|
19
|
+
"src/**/*.ts",
|
|
20
|
+
"dist/cli.js",
|
|
21
|
+
"skills/**/*"
|
|
18
22
|
],
|
|
19
23
|
"publishConfig": {
|
|
20
24
|
"access": "public"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mink-note
|
|
3
|
+
description: Capture and organize notes in your Mink knowledge vault. Use when the user wants to save a note, log a thought, record a meeting, or capture any knowledge.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /mink:note — Intelligent Note Capture
|
|
7
|
+
|
|
8
|
+
You are an intelligent note-taking assistant powered by [Mink](https://github.com/drewpayment/mink). When this skill is invoked, you help the user capture, categorize, and connect notes in their Mink wiki vault.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
Mink must be installed and a vault must be initialized:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Install Mink
|
|
16
|
+
bun add -g @drewpayment/mink
|
|
17
|
+
|
|
18
|
+
# Initialize the vault (once)
|
|
19
|
+
mink wiki init
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Your Role
|
|
23
|
+
|
|
24
|
+
You are the **smart orchestrator**. The `mink note` CLI is a dumb writer — it takes explicit flags and writes files. Your job is to:
|
|
25
|
+
|
|
26
|
+
1. Understand what the user wants to capture
|
|
27
|
+
2. Analyze the vault context to make smart decisions
|
|
28
|
+
3. Call `mink note` with the right flags
|
|
29
|
+
4. Optionally update related notes with backlinks
|
|
30
|
+
|
|
31
|
+
## Workflow
|
|
32
|
+
|
|
33
|
+
### Step 1: Understand the Note
|
|
34
|
+
|
|
35
|
+
If the user provided text after `/mink:note`, use that as the note content. Otherwise, ask what they'd like to capture.
|
|
36
|
+
|
|
37
|
+
### Step 2: Gather Vault Context
|
|
38
|
+
|
|
39
|
+
Run these commands to understand the current vault state:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
mink note list --recent 10
|
|
43
|
+
mink wiki status
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Also read the vault index for tag vocabulary:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cat "$(mink config wiki.path)/.mink-index.json" 2>/dev/null | head -100
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Step 3: Analyze and Categorize
|
|
53
|
+
|
|
54
|
+
Based on the note content and vault context, determine:
|
|
55
|
+
|
|
56
|
+
- **Title**: A clear, descriptive title (not the raw text)
|
|
57
|
+
- **Category**: One of `inbox`, `projects`, `areas`, `resources`, `archives`
|
|
58
|
+
- `projects` — Has a deadline, milestone, or deliverable. Use `--project <slug>` if it relates to a known Mink project.
|
|
59
|
+
- `areas` — Ongoing responsibility, standard, or recurring concern
|
|
60
|
+
- `resources` — Reference material, how-to, guide, or knowledge to look up later
|
|
61
|
+
- `archives` — Completed work, historical record
|
|
62
|
+
- `inbox` — Only if genuinely unclear
|
|
63
|
+
- **Tags**: 1-5 relevant tags from the existing tag vocabulary when possible, new tags when necessary. Use lowercase, hyphenated format.
|
|
64
|
+
- **Wikilinks**: If the note mentions people, projects, or concepts that exist as notes in the vault, include `[[wikilinks]]` in the body text.
|
|
65
|
+
|
|
66
|
+
### Step 4: Create the Note
|
|
67
|
+
|
|
68
|
+
Run the `mink note` command with all determined flags:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
mink note --title "Title Here" \
|
|
72
|
+
--body "Note body with [[wikilinks]] to related notes..." \
|
|
73
|
+
--category <category> \
|
|
74
|
+
--tags "tag1,tag2,tag3" \
|
|
75
|
+
--project <project-slug> # only if project-linked
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Step 5: Report Back
|
|
79
|
+
|
|
80
|
+
Tell the user:
|
|
81
|
+
- Where the note was saved
|
|
82
|
+
- What category and tags were applied
|
|
83
|
+
- Any wikilinks that were added
|
|
84
|
+
- Suggest related notes they might want to update
|
|
85
|
+
|
|
86
|
+
## Special Modes
|
|
87
|
+
|
|
88
|
+
### Daily Note
|
|
89
|
+
If the user says something like "add to my daily" or "daily note":
|
|
90
|
+
```bash
|
|
91
|
+
mink note --daily "The content to append"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Meeting Note
|
|
95
|
+
If the user describes a meeting:
|
|
96
|
+
```bash
|
|
97
|
+
mink note --template meeting --title "Meeting: Topic" --body "..." --category areas --tags "meeting,..."
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### File Ingestion
|
|
101
|
+
If the user wants to add an existing file to the vault:
|
|
102
|
+
```bash
|
|
103
|
+
mink note --file ./path/to/file.md --category resources --tags "..."
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## CLI Reference
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
mink note "Quick thought" # Quick capture to inbox
|
|
110
|
+
mink note --title "Title" --body "Content" # Structured note
|
|
111
|
+
mink note --title "T" --body "B" --category areas # Explicit category
|
|
112
|
+
mink note --title "T" --body "B" --tags "a,b,c" # With tags
|
|
113
|
+
mink note --project my-api --title "T" --body "B" # Project-linked
|
|
114
|
+
mink note --daily "Today's insight" # Append to daily note
|
|
115
|
+
mink note --daily # Create today's daily
|
|
116
|
+
mink note --template meeting --title "Sprint Planning" # From template
|
|
117
|
+
mink note --file ./scratch.md # Ingest external file
|
|
118
|
+
mink note list [--category X] [--tag X] [--recent N] # List notes
|
|
119
|
+
mink note search <term> # Full-text search
|
|
120
|
+
mink wiki status # Vault statistics
|
|
121
|
+
mink wiki rebuild-index # Rescan vault
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Guidelines
|
|
125
|
+
|
|
126
|
+
- Always prefer existing tags over inventing new ones (check the vault index)
|
|
127
|
+
- Use `[[wikilinks]]` for any person, project, or concept that has a note in the vault
|
|
128
|
+
- Keep titles concise but descriptive — they become filenames
|
|
129
|
+
- When in doubt about category, use `inbox` — the user can recategorize later
|
|
130
|
+
- If the note relates to the current working directory's Mink project, use `--project`
|
|
131
|
+
- Don't over-tag. 2-3 tags is usually right.
|
package/src/cli.ts
CHANGED
|
@@ -119,6 +119,24 @@ switch (command) {
|
|
|
119
119
|
break;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
case "wiki": {
|
|
123
|
+
const { wiki } = await import("./commands/wiki");
|
|
124
|
+
await wiki(cwd, process.argv.slice(3));
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "note": {
|
|
129
|
+
const { note } = await import("./commands/note");
|
|
130
|
+
await note(cwd, process.argv.slice(3));
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case "skill": {
|
|
135
|
+
const { skill } = await import("./commands/skill");
|
|
136
|
+
await skill(process.argv.slice(3));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
122
140
|
case "bug-search": {
|
|
123
141
|
const { bugSearch } = await import("./commands/bug-search");
|
|
124
142
|
bugSearch(cwd, process.argv.slice(3).join(" "));
|
|
@@ -139,6 +157,24 @@ switch (command) {
|
|
|
139
157
|
break;
|
|
140
158
|
}
|
|
141
159
|
|
|
160
|
+
case "version":
|
|
161
|
+
case "--version":
|
|
162
|
+
case "-v": {
|
|
163
|
+
const { resolve, dirname } = await import("path");
|
|
164
|
+
const cliPath = resolve(dirname(new URL(import.meta.url).pathname));
|
|
165
|
+
const { readFileSync } = await import("fs");
|
|
166
|
+
try {
|
|
167
|
+
const pkg = JSON.parse(
|
|
168
|
+
readFileSync(resolve(cliPath, "../package.json"), "utf-8")
|
|
169
|
+
);
|
|
170
|
+
console.log(`mink ${pkg.version}`);
|
|
171
|
+
} catch {
|
|
172
|
+
console.log("mink (unknown version)");
|
|
173
|
+
}
|
|
174
|
+
console.log(` location: ${cliPath}`);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
142
178
|
case "help":
|
|
143
179
|
case "--help":
|
|
144
180
|
case "-h":
|
|
@@ -151,6 +187,16 @@ switch (command) {
|
|
|
151
187
|
console.log(" status Display project health at a glance");
|
|
152
188
|
console.log(" scan [--check] Force a full file index rescan");
|
|
153
189
|
console.log(" config [key] [value] Manage global user settings");
|
|
190
|
+
console.log();
|
|
191
|
+
console.log("Notes & Wiki:");
|
|
192
|
+
console.log(" wiki <cmd> Manage the notes/wiki vault (init|status|rebuild-index|organize)");
|
|
193
|
+
console.log(" note \"text\" Capture a note to the vault");
|
|
194
|
+
console.log(" note --daily [text] Create or append to today's daily note");
|
|
195
|
+
console.log(" note list [filters] List notes (--category, --tag, --recent)");
|
|
196
|
+
console.log(" note search <term> Full-text search across the vault");
|
|
197
|
+
console.log(" skill install Install /mink:note skill for Claude Code");
|
|
198
|
+
console.log();
|
|
199
|
+
console.log("Automation & Analysis:");
|
|
154
200
|
console.log(" dashboard [--port=N] Open the real-time web dashboard");
|
|
155
201
|
console.log(" daemon <cmd> Manage the background daemon (start|stop|restart|logs)");
|
|
156
202
|
console.log(" cron <cmd> [id] Manage scheduled tasks (list|run|retry)");
|
package/src/commands/init.ts
CHANGED
|
@@ -3,7 +3,13 @@ import { mkdirSync, existsSync } from "fs";
|
|
|
3
3
|
import { resolve, dirname, basename, join } from "path";
|
|
4
4
|
import { projectDir, projectMetaPath } from "../core/paths";
|
|
5
5
|
import { generateProjectId } from "../core/project-id";
|
|
6
|
-
import { atomicWriteJson, safeReadJson } from "../core/fs-utils";
|
|
6
|
+
import { atomicWriteJson, atomicWriteText, safeReadJson } from "../core/fs-utils";
|
|
7
|
+
import {
|
|
8
|
+
isWikiEnabled,
|
|
9
|
+
isVaultInitialized,
|
|
10
|
+
isInsideVault,
|
|
11
|
+
vaultProjects,
|
|
12
|
+
} from "../core/vault";
|
|
7
13
|
|
|
8
14
|
interface HookCommand {
|
|
9
15
|
type: "command";
|
|
@@ -26,11 +32,39 @@ export function detectRuntime(): "bun" | "node" {
|
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
export function resolveCliPath(): string {
|
|
36
|
+
// When running from the compiled bundle, import.meta.url points to dist/cli.js
|
|
37
|
+
// When running from source, it points to src/commands/init.ts
|
|
38
|
+
const selfPath = new URL(import.meta.url).pathname;
|
|
39
|
+
const selfDir = dirname(selfPath);
|
|
40
|
+
|
|
41
|
+
// If we're running from dist/cli.js, use that directly
|
|
42
|
+
if (selfPath.endsWith("dist/cli.js")) {
|
|
43
|
+
return selfPath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check for compiled dist/cli.js relative to project root
|
|
47
|
+
// From src/commands/ go up two levels to project root
|
|
48
|
+
const projectRoot = resolve(selfDir, "../..");
|
|
49
|
+
const distPath = join(projectRoot, "dist", "cli.js");
|
|
50
|
+
if (existsSync(distPath)) return distPath;
|
|
51
|
+
|
|
52
|
+
// Fall back to src/cli.ts (requires bun)
|
|
53
|
+
return resolve(selfDir, "../cli.ts");
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
export function buildHooksConfig(
|
|
30
57
|
runtime: "bun" | "node",
|
|
31
58
|
cliPath: string
|
|
32
59
|
): HooksConfig {
|
|
33
|
-
|
|
60
|
+
// If using compiled JS, always use node (universally available)
|
|
61
|
+
// If using .ts source, must use bun
|
|
62
|
+
const isTsSource = cliPath.endsWith(".ts");
|
|
63
|
+
const prefix = isTsSource
|
|
64
|
+
? `bun run ${cliPath}`
|
|
65
|
+
: runtime === "bun"
|
|
66
|
+
? `bun run ${cliPath}`
|
|
67
|
+
: `node ${cliPath}`;
|
|
34
68
|
const hook = (cmd: string): HookCommand[] => [{ type: "command", command: cmd }];
|
|
35
69
|
return {
|
|
36
70
|
SessionStart: [{ matcher: "", hooks: hook(`${prefix} session-start`) }],
|
|
@@ -100,7 +134,7 @@ function isExistingInstallation(cwd: string): boolean {
|
|
|
100
134
|
|
|
101
135
|
export async function init(cwd: string): Promise<void> {
|
|
102
136
|
const runtime = detectRuntime();
|
|
103
|
-
const cliPath =
|
|
137
|
+
const cliPath = resolveCliPath();
|
|
104
138
|
const hooks = buildHooksConfig(runtime, cliPath);
|
|
105
139
|
const settingsPath = resolve(cwd, ".claude", "settings.json");
|
|
106
140
|
const dir = projectDir(cwd);
|
|
@@ -119,6 +153,10 @@ export async function init(cwd: string): Promise<void> {
|
|
|
119
153
|
|
|
120
154
|
const projectId = generateProjectId(cwd);
|
|
121
155
|
|
|
156
|
+
// Detect notes project type
|
|
157
|
+
const isNotesProject =
|
|
158
|
+
isWikiEnabled() && isVaultInitialized() && isInsideVault(cwd);
|
|
159
|
+
|
|
122
160
|
// Write project metadata
|
|
123
161
|
const metaPath = projectMetaPath(cwd);
|
|
124
162
|
const existingMeta = safeReadJson(metaPath) as Record<string, unknown> | null;
|
|
@@ -128,6 +166,7 @@ export async function init(cwd: string): Promise<void> {
|
|
|
128
166
|
name: basename(cwd),
|
|
129
167
|
initTimestamp: existingMeta?.initTimestamp ?? new Date().toISOString(),
|
|
130
168
|
version: "0.1.0",
|
|
169
|
+
...(isNotesProject ? { projectType: "notes" } : {}),
|
|
131
170
|
});
|
|
132
171
|
|
|
133
172
|
if (upgrading) {
|
|
@@ -152,8 +191,42 @@ export async function init(cwd: string): Promise<void> {
|
|
|
152
191
|
if (!existsSync(memPath)) {
|
|
153
192
|
const { seedLearningMemory } = await import("../core/seed");
|
|
154
193
|
const { serializeLearningMemory } = await import("../core/learning-memory");
|
|
155
|
-
const { atomicWriteText } = await import("../core/fs-utils");
|
|
156
194
|
const mem = seedLearningMemory(cwd);
|
|
157
195
|
atomicWriteText(memPath, serializeLearningMemory(mem));
|
|
158
196
|
}
|
|
197
|
+
|
|
198
|
+
// Create wiki project overview if wiki is enabled
|
|
199
|
+
if (isWikiEnabled() && isVaultInitialized() && !isNotesProject) {
|
|
200
|
+
try {
|
|
201
|
+
const projectSlug = basename(cwd);
|
|
202
|
+
const overviewPath = join(vaultProjects(projectSlug), "overview.md");
|
|
203
|
+
if (!existsSync(overviewPath)) {
|
|
204
|
+
const now = new Date().toISOString();
|
|
205
|
+
const overview = [
|
|
206
|
+
`---`,
|
|
207
|
+
`created: "${now}"`,
|
|
208
|
+
`updated: "${now}"`,
|
|
209
|
+
`tags: [project, ${projectSlug}]`,
|
|
210
|
+
`category: projects`,
|
|
211
|
+
`---`,
|
|
212
|
+
``,
|
|
213
|
+
`# ${projectSlug}`,
|
|
214
|
+
``,
|
|
215
|
+
`**Path**: \`${cwd}\``,
|
|
216
|
+
`**Initialized**: ${now.split("T")[0]}`,
|
|
217
|
+
``,
|
|
218
|
+
`## Overview`,
|
|
219
|
+
``,
|
|
220
|
+
`## Key Decisions`,
|
|
221
|
+
``,
|
|
222
|
+
`## Links`,
|
|
223
|
+
``,
|
|
224
|
+
].join("\n");
|
|
225
|
+
atomicWriteText(overviewPath, overview);
|
|
226
|
+
console.log(` wiki: ${overviewPath}`);
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
// Non-critical — don't fail init
|
|
230
|
+
}
|
|
231
|
+
}
|
|
159
232
|
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { resolve } from "path";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import {
|
|
4
|
+
isVaultInitialized,
|
|
5
|
+
isWikiEnabled,
|
|
6
|
+
resolveVaultPath,
|
|
7
|
+
} from "../core/vault";
|
|
8
|
+
import { resolveConfigValue } from "../core/global-config";
|
|
9
|
+
import { generateProjectId } from "../core/project-id";
|
|
10
|
+
import {
|
|
11
|
+
createNote,
|
|
12
|
+
appendToDaily,
|
|
13
|
+
ingestFile,
|
|
14
|
+
slugifyTitle,
|
|
15
|
+
} from "../core/note-writer";
|
|
16
|
+
import {
|
|
17
|
+
updateVaultIndexForFile,
|
|
18
|
+
searchVaultIndex,
|
|
19
|
+
getRecentNotes,
|
|
20
|
+
loadVaultIndex,
|
|
21
|
+
} from "../core/note-index";
|
|
22
|
+
import { updateMasterIndex } from "../core/note-linker";
|
|
23
|
+
import type { NoteCategory, NoteMetadata } from "../types/note";
|
|
24
|
+
|
|
25
|
+
export async function note(
|
|
26
|
+
cwd: string,
|
|
27
|
+
args: string[]
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
if (!isWikiEnabled()) {
|
|
30
|
+
console.error("[mink] wiki feature is disabled");
|
|
31
|
+
console.error(" Enable with: mink config wiki.enabled true");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!isVaultInitialized()) {
|
|
36
|
+
console.error("[mink] vault not initialized");
|
|
37
|
+
console.error(" Run 'mink wiki init' first.");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Handle subcommands
|
|
42
|
+
if (args[0] === "list") {
|
|
43
|
+
noteList(args.slice(1));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (args[0] === "search") {
|
|
47
|
+
noteSearch(args.slice(1).join(" "));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Parse flags
|
|
52
|
+
const parsed = parseNoteArgs(args);
|
|
53
|
+
|
|
54
|
+
// Handle daily note
|
|
55
|
+
if (parsed.daily) {
|
|
56
|
+
const date = new Date().toISOString().split("T")[0];
|
|
57
|
+
const content = parsed.positional || parsed.body || "";
|
|
58
|
+
const filePath = appendToDaily(date, content);
|
|
59
|
+
updateVaultIndexForFile(
|
|
60
|
+
filePath,
|
|
61
|
+
readFileSync(filePath, "utf-8")
|
|
62
|
+
);
|
|
63
|
+
console.log(`[mink] daily note: ${filePath}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Handle file ingestion
|
|
68
|
+
if (parsed.file) {
|
|
69
|
+
const sourcePath = resolve(cwd, parsed.file);
|
|
70
|
+
if (!existsSync(sourcePath)) {
|
|
71
|
+
console.error(`[mink] file not found: ${sourcePath}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const result = ingestFile(sourcePath, {
|
|
75
|
+
category: parsed.category,
|
|
76
|
+
tags: parsed.tags,
|
|
77
|
+
projectSlug: parsed.project,
|
|
78
|
+
sourceProject: detectSourceProject(cwd),
|
|
79
|
+
});
|
|
80
|
+
updateVaultIndexForFile(result.filePath, result.content);
|
|
81
|
+
console.log(`[mink] ingested: ${result.filePath}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Regular note creation
|
|
86
|
+
const title =
|
|
87
|
+
parsed.title ||
|
|
88
|
+
(parsed.positional
|
|
89
|
+
? parsed.positional.split("\n")[0].slice(0, 80)
|
|
90
|
+
: `note-${Date.now()}`);
|
|
91
|
+
|
|
92
|
+
const body = parsed.body || parsed.positional || "";
|
|
93
|
+
|
|
94
|
+
const meta: NoteMetadata = {
|
|
95
|
+
title,
|
|
96
|
+
category: parsed.category,
|
|
97
|
+
tags: parsed.tags,
|
|
98
|
+
created: new Date().toISOString(),
|
|
99
|
+
updated: new Date().toISOString(),
|
|
100
|
+
template: parsed.template,
|
|
101
|
+
projectSlug: parsed.project,
|
|
102
|
+
sourceProject: detectSourceProject(cwd),
|
|
103
|
+
body,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const result = createNote(meta);
|
|
107
|
+
updateVaultIndexForFile(result.filePath, result.content);
|
|
108
|
+
updateMasterIndex(resolveVaultPath());
|
|
109
|
+
|
|
110
|
+
const tagsStr =
|
|
111
|
+
meta.tags.length > 0 ? ` [${meta.tags.join(", ")}]` : "";
|
|
112
|
+
console.log(
|
|
113
|
+
`[mink] note saved: ${result.filePath}${tagsStr}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface ParsedNoteArgs {
|
|
118
|
+
title: string;
|
|
119
|
+
body: string;
|
|
120
|
+
category: NoteCategory;
|
|
121
|
+
tags: string[];
|
|
122
|
+
project: string;
|
|
123
|
+
template: string;
|
|
124
|
+
daily: boolean;
|
|
125
|
+
file: string;
|
|
126
|
+
positional: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseNoteArgs(args: string[]): ParsedNoteArgs {
|
|
130
|
+
const result: ParsedNoteArgs = {
|
|
131
|
+
title: "",
|
|
132
|
+
body: "",
|
|
133
|
+
category: resolveConfigValue("notes.default-category").value as NoteCategory,
|
|
134
|
+
tags: [],
|
|
135
|
+
project: "",
|
|
136
|
+
template: "",
|
|
137
|
+
daily: false,
|
|
138
|
+
file: "",
|
|
139
|
+
positional: "",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const positionalParts: string[] = [];
|
|
143
|
+
let i = 0;
|
|
144
|
+
|
|
145
|
+
while (i < args.length) {
|
|
146
|
+
const arg = args[i];
|
|
147
|
+
|
|
148
|
+
if (arg === "--title" && i + 1 < args.length) {
|
|
149
|
+
result.title = args[++i];
|
|
150
|
+
} else if (arg === "--body" && i + 1 < args.length) {
|
|
151
|
+
result.body = args[++i];
|
|
152
|
+
} else if (arg === "--category" && i + 1 < args.length) {
|
|
153
|
+
result.category = args[++i] as NoteCategory;
|
|
154
|
+
} else if (arg === "--tags" && i + 1 < args.length) {
|
|
155
|
+
result.tags = args[++i].split(",").map((t) => t.trim()).filter(Boolean);
|
|
156
|
+
} else if (arg === "--project" && i + 1 < args.length) {
|
|
157
|
+
result.project = args[++i];
|
|
158
|
+
if (result.category === "inbox") {
|
|
159
|
+
result.category = "projects";
|
|
160
|
+
}
|
|
161
|
+
} else if (arg === "--template" && i + 1 < args.length) {
|
|
162
|
+
result.template = args[++i];
|
|
163
|
+
} else if (arg === "--daily") {
|
|
164
|
+
result.daily = true;
|
|
165
|
+
} else if (arg === "--file" && i + 1 < args.length) {
|
|
166
|
+
result.file = args[++i];
|
|
167
|
+
} else if (!arg.startsWith("--")) {
|
|
168
|
+
positionalParts.push(arg);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
i++;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
result.positional = positionalParts.join(" ");
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function detectSourceProject(cwd: string): string | undefined {
|
|
179
|
+
try {
|
|
180
|
+
const vaultPath = resolveVaultPath();
|
|
181
|
+
// Don't mark notes created from within the vault itself
|
|
182
|
+
if (cwd.startsWith(vaultPath)) return undefined;
|
|
183
|
+
return generateProjectId(cwd);
|
|
184
|
+
} catch {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function noteList(args: string[]): void {
|
|
190
|
+
const index = loadVaultIndex();
|
|
191
|
+
let entries = Object.values(index.entries);
|
|
192
|
+
|
|
193
|
+
// Parse filters
|
|
194
|
+
let categoryFilter = "";
|
|
195
|
+
let tagFilter = "";
|
|
196
|
+
let recent = 20;
|
|
197
|
+
|
|
198
|
+
for (let i = 0; i < args.length; i++) {
|
|
199
|
+
if (args[i] === "--category" && i + 1 < args.length) {
|
|
200
|
+
categoryFilter = args[++i];
|
|
201
|
+
} else if (args[i] === "--tag" && i + 1 < args.length) {
|
|
202
|
+
tagFilter = args[++i];
|
|
203
|
+
} else if (args[i] === "--recent" && i + 1 < args.length) {
|
|
204
|
+
recent = parseInt(args[++i], 10) || 20;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (categoryFilter) {
|
|
209
|
+
entries = entries.filter((e) => e.category === categoryFilter);
|
|
210
|
+
}
|
|
211
|
+
if (tagFilter) {
|
|
212
|
+
entries = entries.filter((e) =>
|
|
213
|
+
e.tags.some((t) => t.toLowerCase().includes(tagFilter.toLowerCase()))
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Sort by last modified, most recent first
|
|
218
|
+
entries.sort((a, b) => b.lastModified.localeCompare(a.lastModified));
|
|
219
|
+
entries = entries.slice(0, recent);
|
|
220
|
+
|
|
221
|
+
if (entries.length === 0) {
|
|
222
|
+
console.log("[mink] no notes found");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log(`[mink] ${entries.length} notes:`);
|
|
227
|
+
console.log();
|
|
228
|
+
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
const tags =
|
|
231
|
+
entry.tags.length > 0 ? ` [${entry.tags.join(", ")}]` : "";
|
|
232
|
+
console.log(` ${entry.category.padEnd(10)} ${entry.title}${tags}`);
|
|
233
|
+
console.log(` ${entry.filePath}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function noteSearch(term: string): void {
|
|
238
|
+
if (!term.trim()) {
|
|
239
|
+
console.error("Usage: mink note search <term>");
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const results = searchVaultIndex(term);
|
|
244
|
+
|
|
245
|
+
if (results.length === 0) {
|
|
246
|
+
console.log(`[mink] no notes matching "${term}"`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log(`[mink] ${results.length} notes matching "${term}":`);
|
|
251
|
+
console.log();
|
|
252
|
+
|
|
253
|
+
for (const entry of results.slice(0, 20)) {
|
|
254
|
+
const tags =
|
|
255
|
+
entry.tags.length > 0 ? ` [${entry.tags.join(", ")}]` : "";
|
|
256
|
+
console.log(` ${entry.category.padEnd(10)} ${entry.title}${tags}`);
|
|
257
|
+
if (entry.description) {
|
|
258
|
+
console.log(` ${entry.description}`);
|
|
259
|
+
}
|
|
260
|
+
console.log(` ${entry.filePath}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (results.length > 20) {
|
|
264
|
+
console.log();
|
|
265
|
+
console.log(` ... and ${results.length - 20} more results`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -3,6 +3,8 @@ import { createSessionState } from "../core/session";
|
|
|
3
3
|
import { projectDir, sessionPath, actionLogPath } from "../core/paths";
|
|
4
4
|
import { atomicWriteJson } from "../core/fs-utils";
|
|
5
5
|
import { createActionLogWriter } from "../core/action-log";
|
|
6
|
+
import { isWikiEnabled, isVaultInitialized, isInsideVault } from "../core/vault";
|
|
7
|
+
import { loadVaultIndex } from "../core/note-index";
|
|
6
8
|
|
|
7
9
|
export function sessionStart(cwd: string): void {
|
|
8
10
|
const dir = projectDir(cwd);
|
|
@@ -18,4 +20,28 @@ export function sessionStart(cwd: string): void {
|
|
|
18
20
|
} catch {
|
|
19
21
|
// Never crash hooks
|
|
20
22
|
}
|
|
23
|
+
|
|
24
|
+
// Emit vault context if wiki is enabled
|
|
25
|
+
try {
|
|
26
|
+
if (isWikiEnabled() && isVaultInitialized()) {
|
|
27
|
+
const index = loadVaultIndex();
|
|
28
|
+
const inboxCount = Object.values(index.entries).filter(
|
|
29
|
+
(e) => e.category === "inbox"
|
|
30
|
+
).length;
|
|
31
|
+
|
|
32
|
+
if (inboxCount > 0) {
|
|
33
|
+
console.error(
|
|
34
|
+
`[mink] vault: ${inboxCount} notes in inbox need categorization`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isInsideVault(cwd)) {
|
|
39
|
+
console.error(
|
|
40
|
+
`[mink] notes project detected — vault has ${index.totalNotes} notes`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Never crash hooks
|
|
46
|
+
}
|
|
21
47
|
}
|