@vs4vijay/piverse 0.1.0 → 0.6.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/.agents/skills/pi-extension/SKILL.md +229 -0
- package/.github/workflows/ci.yml +18 -0
- package/.github/workflows/release.yml +106 -0
- package/LICENSE +21 -0
- package/README.md +70 -27
- package/extensions/pi-notes/README.md +152 -0
- package/extensions/pi-notes/package.json +31 -0
- package/extensions/pi-notes/plan.md +151 -0
- package/extensions/pi-notes/src/index.ts +369 -0
- package/extensions/pi-notes/src/store.ts +229 -0
- package/extensions/pi-notes/src/tui.ts +426 -0
- package/extensions/pi-notes/src/types.ts +60 -0
- package/extensions/pi-queue/README.md +178 -0
- package/extensions/pi-queue/package.json +14 -1
- package/extensions/pi-queue/plan.md +79 -0
- package/extensions/pi-queue/src/index.ts +136 -28
- package/package.json +21 -5
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vs4vijay/pi-notes",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Project-level notes for the Pi coding agent — /notes command with CLI and TUI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": ["pi-package"],
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/vs4vijay/piverse.git"
|
|
11
|
+
},
|
|
12
|
+
"author": "vs4vijay",
|
|
13
|
+
"homepage": "https://github.com/vs4vijay/piverse",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/vs4vijay/piverse/issues"
|
|
16
|
+
},
|
|
17
|
+
"pi": {
|
|
18
|
+
"extensions": ["./src/index.ts"]
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"typecheck": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
25
|
+
"@earendil-works/pi-tui": "*",
|
|
26
|
+
"typebox": "*"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# pi-notes — Feature Plan
|
|
2
|
+
|
|
3
|
+
## Current State
|
|
4
|
+
|
|
5
|
+
Project-level note-taking extension for Pi coding agent. Persists notes as JSON in `.pi/notes/notes.json`. Primary interface is a TUI opened via `/notes`, with CLI fallbacks for common operations.
|
|
6
|
+
|
|
7
|
+
**Implemented:** MVP (store, core commands, TUI), CLI + TUI search, tags, pinning, external-editor title/content editing (`edit`), and JSON export/import. Stores are cached per-project so multi-project sessions don't collide.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Planned Features
|
|
12
|
+
|
|
13
|
+
### Phase 1: Foundation (MVP)
|
|
14
|
+
|
|
15
|
+
#### 1.1 Note Store & Persistence
|
|
16
|
+
- Load notes from `.pi/notes/notes.json` on `session_start`
|
|
17
|
+
- Save notes after every mutation (create/update/delete)
|
|
18
|
+
- Also save on `session_shutdown` (safety net)
|
|
19
|
+
- Create directory `.pi/notes/` if missing
|
|
20
|
+
- Note schema: `{ id: string, title: string, content: string, createdAt: ISO, updatedAt: ISO }`
|
|
21
|
+
- ID generation: `crypto.randomUUID()`
|
|
22
|
+
|
|
23
|
+
#### 1.2 Core Commands
|
|
24
|
+
| Command | Behavior |
|
|
25
|
+
|---------|----------|
|
|
26
|
+
| `/notes` | Open TUI (list/view/edit/create) |
|
|
27
|
+
| `/notes <title>` | Quick-add note with title (opens editor for content) |
|
|
28
|
+
| `/notes list` | CLI: list all notes with index, title, preview |
|
|
29
|
+
| `/notes show <id>` | CLI: print full note content (markdown) |
|
|
30
|
+
| `/notes rm <id>` | CLI: delete note by ID (with confirmation) |
|
|
31
|
+
|
|
32
|
+
#### 1.3 TUI — List View
|
|
33
|
+
- Full-screen overlay via `ctx.ui.custom()`
|
|
34
|
+
- Left pane: `SelectList` of notes (title + first 60 chars of content)
|
|
35
|
+
- Right pane: `Markdown` preview of selected note
|
|
36
|
+
- Keys: `↑/↓` navigate, `Enter` view full, `n` new, `e` edit, `d` delete, `q` quit
|
|
37
|
+
- Header: "Pi Notes [n] new [q] quit"
|
|
38
|
+
- Fuzzy filter on type (like session selector)
|
|
39
|
+
|
|
40
|
+
#### 1.4 TUI — View Mode
|
|
41
|
+
- Full-screen markdown render of selected note
|
|
42
|
+
- Keys: `q`/`Esc` back to list, `e` edit
|
|
43
|
+
|
|
44
|
+
#### 1.5 TUI — Create/Edit Mode
|
|
45
|
+
- `ctx.ui.editor(title, prefill)` for title + content
|
|
46
|
+
- On save: persist, return to list with new/updated note selected
|
|
47
|
+
- On cancel: return to list unchanged
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### Phase 2: Quality of Life
|
|
52
|
+
|
|
53
|
+
#### 2.1 Search & Filter
|
|
54
|
+
- `/notes search <query>` — CLI: filter notes by title/content match
|
|
55
|
+
- TUI: live filter as you type (`/` to start, type to filter, `Esc`/`Enter` to finish)
|
|
56
|
+
- Show match count in header
|
|
57
|
+
|
|
58
|
+
#### 2.2 Tags (Frontmatter Extension)
|
|
59
|
+
- Optional tags in note frontmatter: `tags: ["architecture", "decision"]`
|
|
60
|
+
- `/notes tag <id> <tag>` — add tag
|
|
61
|
+
- `/notes untag <id> <tag>` — remove tag
|
|
62
|
+
- TUI: filter by tag (`t` key cycles tag filters)
|
|
63
|
+
- CLI: `/notes list --tag <tag>`
|
|
64
|
+
|
|
65
|
+
#### 2.3 Pinning
|
|
66
|
+
- `/notes pin <id>` / `/notes unpin <id>` — pin to top of list
|
|
67
|
+
- TUI: `p` toggles pin, pinned notes show 📌 prefix
|
|
68
|
+
- Pinned notes sort above unpinned (both by updatedAt desc)
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### Phase 3: Power Features
|
|
73
|
+
|
|
74
|
+
#### 3.1 Linking & References
|
|
75
|
+
- `[[note-id]]` or `[[note-title]]` syntax in content
|
|
76
|
+
- TUI: `Enter` on link navigates to linked note
|
|
77
|
+
- CLI: `/notes links <id>` — show backlinks
|
|
78
|
+
|
|
79
|
+
#### 3.2 Templates
|
|
80
|
+
- `/notes template <name>` — create note from template
|
|
81
|
+
- Built-in templates: `decision`, `meeting`, `todo`, `snippet`
|
|
82
|
+
- Templates stored in `.pi/notes/templates/*.md`
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### Completed
|
|
87
|
+
|
|
88
|
+
#### Export/Import (CLI)
|
|
89
|
+
- `/notes export [path]` — export all notes as JSON (default `./notes.json` in cwd)
|
|
90
|
+
- `/notes import <path>` — import and merge from JSON; incoming notes whose `id` already exists are skipped (existing notes win)
|
|
91
|
+
- Reuses the atomic (temp + rename) write; validates the imported file shape
|
|
92
|
+
|
|
93
|
+
#### Title/Content Editing (CLI)
|
|
94
|
+
- `/notes edit <id>` — edit a note's title/content via external editor, prefilled with current values
|
|
95
|
+
|
|
96
|
+
#### Per-Project Store Caching
|
|
97
|
+
- `getNoteStore` caches one `NoteStore` per `cwd` (was a single global instance), so notes for different projects in one session don't collide
|
|
98
|
+
- `getNoteStore()` (no arg) defaults to `process.cwd()`
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Implementation Priority
|
|
103
|
+
|
|
104
|
+
| Phase | Features | Why |
|
|
105
|
+
|-------|----------|-----|
|
|
106
|
+
| 1 | Store, Core Commands, TUI (List/View/Edit) | Working note system end-to-end |
|
|
107
|
+
| 2 | Search, Tags, Pinning | Daily workflow improvements |
|
|
108
|
+
| 3 | Linking, Templates, Export | Advanced organization |
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Technical Notes
|
|
113
|
+
|
|
114
|
+
- All commands register via `pi.registerCommand("notes", ...)`
|
|
115
|
+
- Subcommands parsed from `args` string in handler
|
|
116
|
+
- TUI uses pi-tui: `SelectList`, `Markdown`, `Editor`, `VStack`, `HStack`, `Box`, `Text`
|
|
117
|
+
- Persistence: JSON file, atomic write (write temp → rename)
|
|
118
|
+
- Error handling: notify user via `ctx.ui.notify()`, never throw
|
|
119
|
+
- Follows Pi Queue patterns for `session_start`/`session_shutdown`
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## File Structure
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
extensions/pi-notes/
|
|
127
|
+
├── src/
|
|
128
|
+
│ ├── index.ts # Extension entry, command registration, hooks
|
|
129
|
+
│ ├── store.ts # NoteStore class: load/save/CRUD
|
|
130
|
+
│ ├── tui.ts # TUI components: ListView, ViewMode, EditorMode
|
|
131
|
+
│ └── types.ts # Note interface, CommandArgs types
|
|
132
|
+
├── package.json
|
|
133
|
+
├── README.md
|
|
134
|
+
└── plan.md # This file
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Acceptance Criteria (Phase 1)
|
|
140
|
+
|
|
141
|
+
- [ ] `/notes` opens TUI with empty list (first run)
|
|
142
|
+
- [ ] `/notes "My Title"` creates note, opens editor for content
|
|
143
|
+
- [ ] TUI: list shows notes, preview updates on selection
|
|
144
|
+
- [ ] TUI: `n` → editor → save → note appears in list
|
|
145
|
+
- [ ] TUI: `e` on selected → editor → save → updates note
|
|
146
|
+
- [ ] TUI: `d` on selected → confirm → removes note
|
|
147
|
+
- [ ] `/notes list` prints formatted table in CLI
|
|
148
|
+
- [ ] `/notes show <id>` prints full markdown
|
|
149
|
+
- [ ] `/notes rm <id>` confirms then deletes
|
|
150
|
+
- [ ] Notes persist across `/reload` and session restart
|
|
151
|
+
- [ ] TypeScript compiles without errors
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-notes — Project-level notes for Pi coding agent
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { getNoteStore, resetNoteStore } from "./store.js";
|
|
7
|
+
import type { Note } from "./types.js";
|
|
8
|
+
import { parseNotesCommand, type ParsedCommand } from "./types.js";
|
|
9
|
+
import { createNotesTUI } from "./tui.js";
|
|
10
|
+
import { isAbsolute, join } from "path";
|
|
11
|
+
|
|
12
|
+
let tuiHandle: (() => void) | null = null;
|
|
13
|
+
|
|
14
|
+
export default async function (pi: ExtensionAPI) {
|
|
15
|
+
// Initialize store on session start
|
|
16
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
17
|
+
resetNoteStore(); // Ensure fresh instance per session
|
|
18
|
+
const store = getNoteStore(ctx.cwd);
|
|
19
|
+
await store.load();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Save on shutdown
|
|
23
|
+
pi.on("session_shutdown", async () => {
|
|
24
|
+
// Store auto-saves on mutations, but ensure final flush
|
|
25
|
+
const store = getNoteStore();
|
|
26
|
+
const notes = await store.getAll();
|
|
27
|
+
await store.save(notes);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Main /notes command
|
|
31
|
+
pi.registerCommand("notes", {
|
|
32
|
+
description: "Project notes — TUI (no args) or quick-add: /notes <title>",
|
|
33
|
+
handler: async (args, ctx) => {
|
|
34
|
+
const parsed = parseNotesCommand(args);
|
|
35
|
+
const store = getNoteStore(ctx.cwd);
|
|
36
|
+
const notes = await store.getAll();
|
|
37
|
+
|
|
38
|
+
// Handle subcommands
|
|
39
|
+
switch (parsed.subcommand) {
|
|
40
|
+
case "list":
|
|
41
|
+
await handleList(notes, ctx);
|
|
42
|
+
return;
|
|
43
|
+
case "show":
|
|
44
|
+
await handleShow(parsed.args, notes, ctx);
|
|
45
|
+
return;
|
|
46
|
+
case "rm":
|
|
47
|
+
await handleRemove(parsed.args, store, ctx);
|
|
48
|
+
return;
|
|
49
|
+
case "edit":
|
|
50
|
+
await handleEdit(parsed.args, store, ctx);
|
|
51
|
+
return;
|
|
52
|
+
case "search":
|
|
53
|
+
await handleSearch(parsed.args, notes, ctx);
|
|
54
|
+
return;
|
|
55
|
+
case "tag":
|
|
56
|
+
await handleTag(parsed.args, store, ctx);
|
|
57
|
+
return;
|
|
58
|
+
case "untag":
|
|
59
|
+
await handleUntag(parsed.args, store, ctx);
|
|
60
|
+
return;
|
|
61
|
+
case "pin":
|
|
62
|
+
await handlePin(parsed.args, store, ctx);
|
|
63
|
+
return;
|
|
64
|
+
case "unpin":
|
|
65
|
+
await handleUnpin(parsed.args, store, ctx);
|
|
66
|
+
return;
|
|
67
|
+
case "export":
|
|
68
|
+
await handleExport(parsed.args, store, ctx);
|
|
69
|
+
return;
|
|
70
|
+
case "import":
|
|
71
|
+
await handleImport(parsed.args, store, ctx);
|
|
72
|
+
return;
|
|
73
|
+
default:
|
|
74
|
+
// Quick-add or open TUI
|
|
75
|
+
if (parsed.args.length > 0) {
|
|
76
|
+
await handleQuickAdd(parsed.args.join(" "), store, ctx);
|
|
77
|
+
} else {
|
|
78
|
+
await openTUI(ctx, notes);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function handleList(notes: Note[], ctx: ExtensionCommandContext): Promise<void> {
|
|
86
|
+
if (notes.length === 0) {
|
|
87
|
+
ctx.ui.notify("No notes yet. Use /notes <title> to create one.", "info");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const lines = notes.map((n, i) => {
|
|
92
|
+
const pin = n.pinned ? "📌 " : "";
|
|
93
|
+
const tags = n.tags?.length ? ` [${n.tags.join(", ")}]` : "";
|
|
94
|
+
const preview = n.content.slice(0, 50).replace(/\n/g, " ");
|
|
95
|
+
return `${i + 1}. ${pin}${n.title}${tags} — ${preview}`;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
ctx.ui.notify(`Notes (${notes.length}):\n${lines.join("\n")}`, "info");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function handleShow(args: string[], notes: Note[], ctx: ExtensionCommandContext): Promise<void> {
|
|
102
|
+
if (args.length === 0) {
|
|
103
|
+
ctx.ui.notify("Usage: /notes show <id>", "error");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const id = args[0];
|
|
108
|
+
const note = notes.find((n) => n.id === id || n.id.startsWith(id));
|
|
109
|
+
|
|
110
|
+
if (!note) {
|
|
111
|
+
ctx.ui.notify(`Note not found: ${id}`, "error");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const meta = `📌 ${note.pinned ? "pinned" : ""} | Tags: ${note.tags?.join(", ") || "none"} | Created: ${note.createdAt} | Updated: ${note.updatedAt}`;
|
|
116
|
+
ctx.ui.notify(`# ${note.title}\n${meta}\n\n${note.content || "*(empty)*"}`, "info");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function handleRemove(args: string[], store: ReturnType<typeof getNoteStore>, ctx: ExtensionCommandContext): Promise<void> {
|
|
120
|
+
if (args.length === 0) {
|
|
121
|
+
ctx.ui.notify("Usage: /notes rm <id>", "error");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const id = args[0];
|
|
126
|
+
const notes = await store.getAll();
|
|
127
|
+
const note = notes.find((n) => n.id === id || n.id.startsWith(id));
|
|
128
|
+
|
|
129
|
+
if (!note) {
|
|
130
|
+
ctx.ui.notify(`Note not found: ${id}`, "error");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Confirm via TUI confirm dialog
|
|
135
|
+
const confirmed = await ctx.ui.confirm(
|
|
136
|
+
"Delete note",
|
|
137
|
+
`Delete "${note.title}"? This cannot be undone.`
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (confirmed) {
|
|
141
|
+
const deleted = await store.delete(id);
|
|
142
|
+
if (deleted) {
|
|
143
|
+
ctx.ui.notify(`Deleted note: ${note.title}`, "info");
|
|
144
|
+
} else {
|
|
145
|
+
ctx.ui.notify("Delete failed", "error");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function handleEdit(args: string[], store: ReturnType<typeof getNoteStore>, ctx: ExtensionCommandContext): Promise<void> {
|
|
151
|
+
if (args.length === 0) {
|
|
152
|
+
ctx.ui.notify("Usage: /notes edit <id>", "error");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const id = args[0];
|
|
157
|
+
const notes = await store.getAll();
|
|
158
|
+
const note = notes.find((n) => n.id === id || n.id.startsWith(id));
|
|
159
|
+
|
|
160
|
+
if (!note) {
|
|
161
|
+
ctx.ui.notify(`Note not found: ${id}`, "error");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const title = await ctx.ui.editor(`Edit: ${note.title}`, note.title);
|
|
166
|
+
if (title === undefined) {
|
|
167
|
+
ctx.ui.notify("Cancelled", "info");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const content = await ctx.ui.editor(`Content for: ${title}`, note.content);
|
|
172
|
+
if (content === undefined) {
|
|
173
|
+
ctx.ui.notify("Cancelled", "info");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await store.update(note.id, { title, content });
|
|
178
|
+
ctx.ui.notify(`Updated note: ${title}`, "info");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function handleSearch(args: string[], notes: Note[], ctx: ExtensionCommandContext): Promise<void> {
|
|
182
|
+
if (args.length === 0) {
|
|
183
|
+
ctx.ui.notify("Usage: /notes search <query>", "error");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const query = args.join(" ").toLowerCase();
|
|
188
|
+
const results = notes.filter(
|
|
189
|
+
(n) => n.title.toLowerCase().includes(query) || n.content.toLowerCase().includes(query)
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (results.length === 0) {
|
|
193
|
+
ctx.ui.notify(`No notes matching: ${query}`, "info");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const lines = results.map((n, i) => {
|
|
198
|
+
const pin = n.pinned ? "📌 " : "";
|
|
199
|
+
const preview = n.content.slice(0, 50).replace(/\n/g, " ");
|
|
200
|
+
return `${i + 1}. ${pin}${n.title} — ${preview}`;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
ctx.ui.notify(`Search results (${results.length}):\n${lines.join("\n")}`, "info");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function handleTag(args: string[], store: ReturnType<typeof getNoteStore>, ctx: ExtensionCommandContext): Promise<void> {
|
|
207
|
+
if (args.length < 2) {
|
|
208
|
+
ctx.ui.notify("Usage: /notes tag <id> <tag>", "error");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const [id, tag] = args;
|
|
213
|
+
const note = await store.getById(id);
|
|
214
|
+
|
|
215
|
+
if (!note) {
|
|
216
|
+
ctx.ui.notify(`Note not found: ${id}`, "error");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const tags = [...new Set([...(note.tags || []), tag])];
|
|
221
|
+
await store.update(id, { tags });
|
|
222
|
+
ctx.ui.notify(`Added tag "${tag}" to "${note.title}"`, "info");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function handleUntag(args: string[], store: ReturnType<typeof getNoteStore>, ctx: ExtensionCommandContext): Promise<void> {
|
|
226
|
+
if (args.length < 2) {
|
|
227
|
+
ctx.ui.notify("Usage: /notes untag <id> <tag>", "error");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const [id, tag] = args;
|
|
232
|
+
const note = await store.getById(id);
|
|
233
|
+
|
|
234
|
+
if (!note) {
|
|
235
|
+
ctx.ui.notify(`Note not found: ${id}`, "error");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const tags = (note.tags || []).filter((t) => t !== tag);
|
|
240
|
+
await store.update(id, { tags });
|
|
241
|
+
ctx.ui.notify(`Removed tag "${tag}" from "${note.title}"`, "info");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function handlePin(args: string[], store: ReturnType<typeof getNoteStore>, ctx: ExtensionCommandContext): Promise<void> {
|
|
245
|
+
if (args.length === 0) {
|
|
246
|
+
ctx.ui.notify("Usage: /notes pin <id>", "error");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const id = args[0];
|
|
251
|
+
const note = await store.getById(id);
|
|
252
|
+
|
|
253
|
+
if (!note) {
|
|
254
|
+
ctx.ui.notify(`Note not found: ${id}`, "error");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await store.update(id, { pinned: true });
|
|
259
|
+
ctx.ui.notify(`Pinned "${note.title}"`, "info");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function handleUnpin(args: string[], store: ReturnType<typeof getNoteStore>, ctx: ExtensionCommandContext): Promise<void> {
|
|
263
|
+
if (args.length === 0) {
|
|
264
|
+
ctx.ui.notify("Usage: /notes unpin <id>", "error");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const id = args[0];
|
|
269
|
+
const note = await store.getById(id);
|
|
270
|
+
|
|
271
|
+
if (!note) {
|
|
272
|
+
ctx.ui.notify(`Note not found: ${id}`, "error");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await store.update(id, { pinned: false });
|
|
277
|
+
ctx.ui.notify(`Unpinned "${note.title}"`, "info");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function handleQuickAdd(title: string, store: ReturnType<typeof getNoteStore>, ctx: ExtensionCommandContext): Promise<void> {
|
|
281
|
+
// Strip surrounding quotes from a quoted title
|
|
282
|
+
title = title.replace(/^(['"])(.*)\1$/, "$2");
|
|
283
|
+
|
|
284
|
+
// Open external editor for content
|
|
285
|
+
const content = await ctx.ui.editor(`New note: ${title}`, "");
|
|
286
|
+
|
|
287
|
+
if (content === undefined) {
|
|
288
|
+
ctx.ui.notify("Cancelled", "info");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const note = await store.create(title, content);
|
|
293
|
+
ctx.ui.notify(`Created note: ${note.title}`, "info");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function handleExport(args: string[], store: ReturnType<typeof getNoteStore>, ctx: ExtensionCommandContext): Promise<void> {
|
|
297
|
+
const filePath = resolvePath(ctx.cwd, args[0] ?? "notes.json");
|
|
298
|
+
const count = await store.exportTo(filePath);
|
|
299
|
+
ctx.ui.notify(`Exported ${count} notes to ${filePath}`, "info");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function handleImport(args: string[], store: ReturnType<typeof getNoteStore>, ctx: ExtensionCommandContext): Promise<void> {
|
|
303
|
+
if (args.length === 0) {
|
|
304
|
+
ctx.ui.notify("Usage: /notes import <path>", "error");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const filePath = resolvePath(ctx.cwd, args[0]);
|
|
309
|
+
try {
|
|
310
|
+
const { imported, skipped } = await store.importFrom(filePath);
|
|
311
|
+
const skippedText = skipped > 0 ? ` (${skipped} already present, skipped)` : "";
|
|
312
|
+
ctx.ui.notify(`Imported ${imported} note(s) from ${filePath}${skippedText}`, "info");
|
|
313
|
+
} catch (err) {
|
|
314
|
+
ctx.ui.notify(`Import failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function resolvePath(cwd: string, p: string): string {
|
|
319
|
+
return isAbsolute(p) ? p : join(cwd, p);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function openTUI(ctx: ExtensionCommandContext, initialNotes: Note[]): Promise<void> {
|
|
323
|
+
const store = getNoteStore(ctx.cwd);
|
|
324
|
+
|
|
325
|
+
const component = createNotesTUI(
|
|
326
|
+
initialNotes,
|
|
327
|
+
ctx.ui.theme,
|
|
328
|
+
() => {
|
|
329
|
+
// onClose
|
|
330
|
+
tuiHandle?.();
|
|
331
|
+
tuiHandle = null;
|
|
332
|
+
},
|
|
333
|
+
async (note?: Note): Promise<Note[] | undefined> => {
|
|
334
|
+
// External editor: create or edit a note
|
|
335
|
+
if (note) {
|
|
336
|
+
const title = await ctx.ui.editor(`Edit: ${note.title}`, note.title);
|
|
337
|
+
if (title === undefined) return undefined;
|
|
338
|
+
const content = await ctx.ui.editor(`Content for: ${title}`, note.content);
|
|
339
|
+
if (content === undefined) return undefined;
|
|
340
|
+
await store.update(note.id, { title, content });
|
|
341
|
+
} else {
|
|
342
|
+
const title = await ctx.ui.editor("New Note", "");
|
|
343
|
+
if (title === undefined) return undefined;
|
|
344
|
+
const content = await ctx.ui.editor(`Content for: ${title}`, "");
|
|
345
|
+
if (content === undefined) return undefined;
|
|
346
|
+
await store.create(title, content);
|
|
347
|
+
}
|
|
348
|
+
return store.getAll();
|
|
349
|
+
},
|
|
350
|
+
async (id: string): Promise<Note[]> => {
|
|
351
|
+
await store.delete(id);
|
|
352
|
+
return store.getAll();
|
|
353
|
+
},
|
|
354
|
+
async (note: Note): Promise<Note[]> => {
|
|
355
|
+
await store.update(note.id, { pinned: !note.pinned });
|
|
356
|
+
return store.getAll();
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Show custom overlay
|
|
361
|
+
await ctx.ui.custom(
|
|
362
|
+
(tui, theme, keybindings, done) => {
|
|
363
|
+
// The component is already a Focusable Component
|
|
364
|
+
tuiHandle = () => done(undefined);
|
|
365
|
+
return component;
|
|
366
|
+
},
|
|
367
|
+
{ overlay: true, overlayOptions: { width: "100%", maxHeight: "100%" } }
|
|
368
|
+
);
|
|
369
|
+
}
|