vertex-notes 0.3.3 → 0.3.4
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 +130 -130
- package/bin/run.js +3 -3
- package/dist/{chunk-LXT34CD7.js → chunk-QQO4JMNC.js} +1 -1
- package/dist/{chunk-K5SQERJ7.js → chunk-XJVEK2OB.js} +475 -38
- package/dist/commands/capture.js +2 -2
- package/dist/commands/daily.js +2 -2
- package/dist/commands/delete.js +2 -2
- package/dist/commands/edit.js +2 -2
- package/dist/commands/export.js +2 -2
- package/dist/commands/hello.js +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/import.js +2 -2
- package/dist/commands/interactive.js +2 -2
- package/dist/commands/login.js +1 -1
- package/dist/commands/new.js +2 -2
- package/dist/commands/notes.js +2 -2
- package/dist/commands/restore.js +2 -2
- package/dist/commands/search.js +2 -2
- package/dist/commands/status.js +2 -2
- package/dist/commands/tags.js +2 -2
- package/dist/commands/today.js +2 -2
- package/dist/commands/trash/empty.js +2 -2
- package/dist/commands/trash/index.js +2 -2
- package/dist/commands/view.js +2 -2
- package/dist/lib/client.js +2 -2
- package/package.json +56 -54
package/README.md
CHANGED
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
# ▲ Vertex CLI
|
|
2
|
-
|
|
3
|
-
**Your knowledge, structured — from the terminal.**
|
|
4
|
-
|
|
5
|
-
Vertex is a keyboard-first knowledge workspace. This CLI gives you full access to your notes, todos, tags, and search from the command line.
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install -g vertex-notes
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Quick Start
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
vertex login # Log in with GitHub, Google, or Email
|
|
17
|
-
vertex notes # List all your notes
|
|
18
|
-
vertex today # Open daily note in interactive mode
|
|
19
|
-
vertex capture "idea" # Quick capture to Inbox
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Commands
|
|
23
|
-
|
|
24
|
-
### Auth
|
|
25
|
-
| Command | Description |
|
|
26
|
-
|---------|-------------|
|
|
27
|
-
| `vertex login` | Log in (GitHub, Google, or Email) |
|
|
28
|
-
| `vertex logout` | Log out and clear session |
|
|
29
|
-
| `vertex whoami` | Show logged-in user |
|
|
30
|
-
|
|
31
|
-
### Notes
|
|
32
|
-
| Command | Description |
|
|
33
|
-
|---------|-------------|
|
|
34
|
-
| `vertex notes` | List all notes |
|
|
35
|
-
| `vertex new [title]` | Create a new note |
|
|
36
|
-
| `vertex view <title>` | View note as markdown |
|
|
37
|
-
| `vertex edit <title>` | Edit in $EDITOR (vim/nano/code) |
|
|
38
|
-
| `vertex daily` | View or create today's daily note |
|
|
39
|
-
| `vertex daily --last` | View the most recent daily note |
|
|
40
|
-
|
|
41
|
-
### Interactive Mode
|
|
42
|
-
|
|
43
|
-
Open any note in a terminal UI with todo toggling:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
vertex today # Latest daily note
|
|
47
|
-
vertex interactive <title> # Any note
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
**Controls:**
|
|
51
|
-
|
|
52
|
-
| Key | Action |
|
|
53
|
-
|-----|--------|
|
|
54
|
-
| `↑ ↓` | Move between items |
|
|
55
|
-
| `Enter` | Toggle todo done/undone |
|
|
56
|
-
| `t` | Add new todo |
|
|
57
|
-
| `n` | Add new text line |
|
|
58
|
-
| `d` | Delete item at cursor |
|
|
59
|
-
| `s` | Save changes |
|
|
60
|
-
| `q` | Save and quit |
|
|
61
|
-
|
|
62
|
-
### Capture & Search
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
vertex capture "buy groceries" # Quick capture to Inbox
|
|
66
|
-
vertex search "keyword" # Full-text search
|
|
67
|
-
vertex search "#tag" # Search by tag
|
|
68
|
-
vertex search "type:todo status:open" # Open todos
|
|
69
|
-
vertex tags # List all tags
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Export
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
vertex export "Meeting Notes" # Print as markdown
|
|
76
|
-
vertex export "Meeting Notes" --json # Print as JSON
|
|
77
|
-
vertex export "Meeting Notes" -o out.md # Save to file
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Trash
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
vertex delete <title> # Move to trash
|
|
84
|
-
vertex restore <title> # Restore from trash
|
|
85
|
-
vertex trash # List trashed notes
|
|
86
|
-
vertex trash:empty # Permanently delete all
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### Info
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
vertex status # Storage usage, note count
|
|
93
|
-
vertex help # Full help with all commands
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Editor Syntax
|
|
97
|
-
|
|
98
|
-
When using `vertex edit`, you write in markdown:
|
|
99
|
-
|
|
100
|
-
```markdown
|
|
101
|
-
# Heading
|
|
102
|
-
**bold** *italic* `code` ~~strike~~
|
|
103
|
-
- Bullet list
|
|
104
|
-
1. Numbered list
|
|
105
|
-
- [x] Done task
|
|
106
|
-
- [ ] Open task
|
|
107
|
-
> Blockquote
|
|
108
|
-
[[Wiki Link]] #tagname
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Code blocks, math blocks (`$$ ... $$`), and standard markdown all work. Rich blocks (mermaid, callouts, file uploads) require the [web app](https://vertex-alu.pages.dev).
|
|
112
|
-
|
|
113
|
-
## Architecture
|
|
114
|
-
|
|
115
|
-
The CLI shares the same backend as the Vertex web app:
|
|
116
|
-
|
|
117
|
-
- **Database**: Supabase (PostgreSQL with RLS)
|
|
118
|
-
- **Auth**: Supabase Auth (GitHub, Google, Email)
|
|
119
|
-
- **Storage**: Cloudflare R2
|
|
120
|
-
|
|
121
|
-
Notes created in the CLI appear in the web app and vice versa.
|
|
122
|
-
|
|
123
|
-
## Requirements
|
|
124
|
-
|
|
125
|
-
- Node.js 18+
|
|
126
|
-
- A Vertex account (sign up via `vertex login`)
|
|
127
|
-
|
|
128
|
-
## License
|
|
129
|
-
|
|
130
|
-
MIT
|
|
1
|
+
# ▲ Vertex CLI
|
|
2
|
+
|
|
3
|
+
**Your knowledge, structured — from the terminal.**
|
|
4
|
+
|
|
5
|
+
Vertex is a keyboard-first knowledge workspace. This CLI gives you full access to your notes, todos, tags, and search from the command line.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g vertex-notes
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
vertex login # Log in with GitHub, Google, or Email
|
|
17
|
+
vertex notes # List all your notes
|
|
18
|
+
vertex today # Open daily note in interactive mode
|
|
19
|
+
vertex capture "idea" # Quick capture to Inbox
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
### Auth
|
|
25
|
+
| Command | Description |
|
|
26
|
+
|---------|-------------|
|
|
27
|
+
| `vertex login` | Log in (GitHub, Google, or Email) |
|
|
28
|
+
| `vertex logout` | Log out and clear session |
|
|
29
|
+
| `vertex whoami` | Show logged-in user |
|
|
30
|
+
|
|
31
|
+
### Notes
|
|
32
|
+
| Command | Description |
|
|
33
|
+
|---------|-------------|
|
|
34
|
+
| `vertex notes` | List all notes |
|
|
35
|
+
| `vertex new [title]` | Create a new note |
|
|
36
|
+
| `vertex view <title>` | View note as markdown |
|
|
37
|
+
| `vertex edit <title>` | Edit in $EDITOR (vim/nano/code) |
|
|
38
|
+
| `vertex daily` | View or create today's daily note |
|
|
39
|
+
| `vertex daily --last` | View the most recent daily note |
|
|
40
|
+
|
|
41
|
+
### Interactive Mode
|
|
42
|
+
|
|
43
|
+
Open any note in a terminal UI with todo toggling:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
vertex today # Latest daily note
|
|
47
|
+
vertex interactive <title> # Any note
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Controls:**
|
|
51
|
+
|
|
52
|
+
| Key | Action |
|
|
53
|
+
|-----|--------|
|
|
54
|
+
| `↑ ↓` | Move between items |
|
|
55
|
+
| `Enter` | Toggle todo done/undone |
|
|
56
|
+
| `t` | Add new todo |
|
|
57
|
+
| `n` | Add new text line |
|
|
58
|
+
| `d` | Delete item at cursor |
|
|
59
|
+
| `s` | Save changes |
|
|
60
|
+
| `q` | Save and quit |
|
|
61
|
+
|
|
62
|
+
### Capture & Search
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
vertex capture "buy groceries" # Quick capture to Inbox
|
|
66
|
+
vertex search "keyword" # Full-text search
|
|
67
|
+
vertex search "#tag" # Search by tag
|
|
68
|
+
vertex search "type:todo status:open" # Open todos
|
|
69
|
+
vertex tags # List all tags
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Export
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
vertex export "Meeting Notes" # Print as markdown
|
|
76
|
+
vertex export "Meeting Notes" --json # Print as JSON
|
|
77
|
+
vertex export "Meeting Notes" -o out.md # Save to file
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Trash
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
vertex delete <title> # Move to trash
|
|
84
|
+
vertex restore <title> # Restore from trash
|
|
85
|
+
vertex trash # List trashed notes
|
|
86
|
+
vertex trash:empty # Permanently delete all
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Info
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
vertex status # Storage usage, note count
|
|
93
|
+
vertex help # Full help with all commands
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Editor Syntax
|
|
97
|
+
|
|
98
|
+
When using `vertex edit`, you write in markdown:
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
# Heading
|
|
102
|
+
**bold** *italic* `code` ~~strike~~
|
|
103
|
+
- Bullet list
|
|
104
|
+
1. Numbered list
|
|
105
|
+
- [x] Done task
|
|
106
|
+
- [ ] Open task
|
|
107
|
+
> Blockquote
|
|
108
|
+
[[Wiki Link]] #tagname
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Code blocks, math blocks (`$$ ... $$`), and standard markdown all work. Rich blocks (mermaid, callouts, file uploads) require the [web app](https://vertex-alu.pages.dev).
|
|
112
|
+
|
|
113
|
+
## Architecture
|
|
114
|
+
|
|
115
|
+
The CLI shares the same backend as the Vertex web app:
|
|
116
|
+
|
|
117
|
+
- **Database**: Supabase (PostgreSQL with RLS)
|
|
118
|
+
- **Auth**: Supabase Auth (GitHub, Google, Email)
|
|
119
|
+
- **Storage**: Cloudflare R2
|
|
120
|
+
|
|
121
|
+
Notes created in the CLI appear in the web app and vice versa.
|
|
122
|
+
|
|
123
|
+
## Requirements
|
|
124
|
+
|
|
125
|
+
- Node.js 18+
|
|
126
|
+
- A Vertex account (sign up via `vertex login`)
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
package/bin/run.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { execute } from "@oclif/core";
|
|
4
|
-
await execute({ dir: import.meta.url });
|
|
2
|
+
|
|
3
|
+
import { execute } from "@oclif/core";
|
|
4
|
+
await execute({ dir: import.meta.url });
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// ../core/dist/index.js
|
|
2
2
|
import { createClient } from "@supabase/supabase-js";
|
|
3
|
+
import { WebSocket } from "ws";
|
|
3
4
|
function createSupabaseClient(url, anonKey) {
|
|
4
5
|
return createClient(url, anonKey, {
|
|
5
6
|
auth: {
|
|
6
7
|
autoRefreshToken: true,
|
|
7
8
|
persistSession: true
|
|
9
|
+
},
|
|
10
|
+
realtime: {
|
|
11
|
+
transport: WebSocket
|
|
8
12
|
}
|
|
9
13
|
});
|
|
10
14
|
}
|
|
@@ -198,11 +202,28 @@ async function getUserTags(client, userId) {
|
|
|
198
202
|
if (error) throw error;
|
|
199
203
|
return data ?? [];
|
|
200
204
|
}
|
|
205
|
+
async function deleteTag(client, tagId) {
|
|
206
|
+
const { error } = await client.from("tags").delete().eq("id", tagId);
|
|
207
|
+
if (error) throw error;
|
|
208
|
+
}
|
|
201
209
|
async function addTagEdge(client, userId, parentId, childId) {
|
|
202
210
|
const { data, error } = await client.from("tag_edges").insert({ parent_id: parentId, child_id: childId, user_id: userId }).select().single();
|
|
203
211
|
if (error) throw error;
|
|
204
212
|
return data;
|
|
205
213
|
}
|
|
214
|
+
async function removeTagEdge(client, parentId, childId) {
|
|
215
|
+
const { error } = await client.from("tag_edges").delete().eq("parent_id", parentId).eq("child_id", childId);
|
|
216
|
+
if (error) throw error;
|
|
217
|
+
}
|
|
218
|
+
async function getTagChildren(client, tagId) {
|
|
219
|
+
const { data, error } = await client.from("tag_edges").select("child_id").eq("parent_id", tagId);
|
|
220
|
+
if (error) throw error;
|
|
221
|
+
if (!data || data.length === 0) return [];
|
|
222
|
+
const childIds = data.map((e) => e.child_id);
|
|
223
|
+
const { data: tags, error: tagErr } = await client.from("tags").select("*").in("id", childIds);
|
|
224
|
+
if (tagErr) throw tagErr;
|
|
225
|
+
return tags ?? [];
|
|
226
|
+
}
|
|
206
227
|
async function getTagDescendants(client, tagId) {
|
|
207
228
|
const { data, error } = await client.rpc("get_tag_descendants", {
|
|
208
229
|
root_tag_id: tagId
|
|
@@ -252,6 +273,29 @@ async function getBlocksForNote(client, noteId) {
|
|
|
252
273
|
return data ?? [];
|
|
253
274
|
}
|
|
254
275
|
var saveLocks = /* @__PURE__ */ new Map();
|
|
276
|
+
var tagPathCache = /* @__PURE__ */ new Map();
|
|
277
|
+
var TAG_PATH_REGEX = /#([a-zA-Z][a-zA-Z0-9_-]*(?:\/[a-zA-Z][a-zA-Z0-9_-]*)*)/g;
|
|
278
|
+
function extractAllTagPaths(blockRows) {
|
|
279
|
+
const paths = /* @__PURE__ */ new Set();
|
|
280
|
+
for (const block of blockRows) {
|
|
281
|
+
let m;
|
|
282
|
+
while ((m = TAG_PATH_REGEX.exec(block.content)) !== null) {
|
|
283
|
+
paths.add(m[1].toLowerCase());
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return paths;
|
|
287
|
+
}
|
|
288
|
+
function setsAreEqual(a, b) {
|
|
289
|
+
if (a.size !== b.size) return false;
|
|
290
|
+
for (const item of a) {
|
|
291
|
+
if (!b.has(item)) return false;
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
async function getNoteTagPathsFromDb(client, noteId) {
|
|
296
|
+
const { data } = await client.from("note_tags").select("tag_path").eq("note_id", noteId);
|
|
297
|
+
return new Set((data ?? []).map((r) => r.tag_path));
|
|
298
|
+
}
|
|
255
299
|
async function saveNoteContent(client, noteId, userId, tiptapJson) {
|
|
256
300
|
const existing = saveLocks.get(noteId);
|
|
257
301
|
const doSave = async () => {
|
|
@@ -279,35 +323,73 @@ async function _saveNoteContentInner(client, noteId, userId, tiptapJson) {
|
|
|
279
323
|
metadata: { tiptap: node }
|
|
280
324
|
}));
|
|
281
325
|
if (blocksToInsert.length === 0) return;
|
|
282
|
-
const {
|
|
283
|
-
|
|
326
|
+
const { data: existingBlocks } = await client.from("blocks").select("id").eq("note_id", noteId);
|
|
327
|
+
const oldBlockIds = (existingBlocks ?? []).map((b) => b.id);
|
|
284
328
|
const { error: insertError } = await client.from("blocks").insert(blocksToInsert);
|
|
285
329
|
if (insertError) throw insertError;
|
|
330
|
+
if (oldBlockIds.length > 0) {
|
|
331
|
+
const { error: deleteError } = await client.from("blocks").delete().in("id", oldBlockIds);
|
|
332
|
+
if (deleteError) throw deleteError;
|
|
333
|
+
}
|
|
286
334
|
const { data: insertedBlocks } = await client.from("blocks").select("id, content, type").eq("note_id", noteId).order("position");
|
|
287
335
|
const blockRows = insertedBlocks ?? [];
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
for (const tagName of leafTags) {
|
|
296
|
-
const tag = await getOrCreateTag(client, userId, tagName);
|
|
297
|
-
await addBlockTag(client, block.id, tag.id).catch(() => {
|
|
298
|
-
});
|
|
299
|
-
noteTagIds.add(tag.id);
|
|
336
|
+
const currentPaths = extractAllTagPaths(blockRows);
|
|
337
|
+
const prevPaths = tagPathCache.get(noteId);
|
|
338
|
+
if (prevPaths && setsAreEqual(currentPaths, prevPaths)) {
|
|
339
|
+
} else {
|
|
340
|
+
let oldPathsForCleanup = prevPaths;
|
|
341
|
+
if (!oldPathsForCleanup) {
|
|
342
|
+
oldPathsForCleanup = await getNoteTagPathsFromDb(client, noteId);
|
|
300
343
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
344
|
+
await client.from("block_tags").delete().in("block_id", blockRows.map((b) => b.id));
|
|
345
|
+
await client.from("note_tags").delete().eq("note_id", noteId);
|
|
346
|
+
const SKIP_TAG_TYPES = /* @__PURE__ */ new Set(["code", "mermaid", "math", "image", "divider", "chart", "file", "summary"]);
|
|
347
|
+
const insertedNoteTags = [];
|
|
348
|
+
for (const block of blockRows) {
|
|
349
|
+
if (SKIP_TAG_TYPES.has(block.type)) continue;
|
|
350
|
+
const tagPaths = extractTagPaths(block.content);
|
|
351
|
+
for (const tagPath of tagPaths) {
|
|
352
|
+
const segments = tagPath.split("/");
|
|
353
|
+
const leafName = segments[segments.length - 1];
|
|
354
|
+
const tag = await getOrCreateTag(client, userId, leafName);
|
|
355
|
+
await addBlockTag(client, block.id, tag.id).catch(() => {
|
|
356
|
+
});
|
|
357
|
+
insertedNoteTags.push({ note_id: noteId, tag_id: tag.id, tag_path: tagPath });
|
|
358
|
+
}
|
|
359
|
+
const scopedTags = extractScopedTags(block.content);
|
|
360
|
+
for (const { parent, child } of scopedTags) {
|
|
361
|
+
const parentTag = await getOrCreateTag(client, userId, parent);
|
|
362
|
+
const childTag = await getOrCreateTag(client, userId, child);
|
|
363
|
+
await addTagEdge(client, userId, parentTag.id, childTag.id).catch(() => {
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const seen = /* @__PURE__ */ new Set();
|
|
368
|
+
for (const nt of insertedNoteTags) {
|
|
369
|
+
if (seen.has(nt.tag_path)) continue;
|
|
370
|
+
seen.add(nt.tag_path);
|
|
371
|
+
await client.from("note_tags").upsert({ note_id: nt.note_id, tag_id: nt.tag_id, tag_path: nt.tag_path }, { onConflict: "note_id,tag_path" });
|
|
372
|
+
}
|
|
373
|
+
tagPathCache.set(noteId, currentPaths);
|
|
374
|
+
if (oldPathsForCleanup) {
|
|
375
|
+
const oldEdges = extractEdgesFromPaths(oldPathsForCleanup);
|
|
376
|
+
const newEdges = extractEdgesFromPaths(currentPaths);
|
|
377
|
+
const edgesToRemove = oldEdges.filter(
|
|
378
|
+
(oe) => !newEdges.some((ne) => ne.parent === oe.parent && ne.child === oe.child)
|
|
379
|
+
);
|
|
380
|
+
for (const { parent: parentName, child: childName } of edgesToRemove) {
|
|
381
|
+
const parentTag = await getOrCreateTag(client, userId, parentName);
|
|
382
|
+
const childTag = await getOrCreateTag(client, userId, childName);
|
|
383
|
+
await removeTagEdge(client, parentTag.id, childTag.id).catch(() => {
|
|
384
|
+
});
|
|
385
|
+
const { count: parentNoteTagCount } = await client.from("note_tags").select("*", { count: "exact", head: true }).eq("tag_id", parentTag.id);
|
|
386
|
+
const children = await getTagChildren(client, parentTag.id);
|
|
387
|
+
if (parentNoteTagCount === 0 && children.length === 0) {
|
|
388
|
+
await deleteTag(client, parentTag.id).catch(() => {
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
307
392
|
}
|
|
308
|
-
}
|
|
309
|
-
for (const tagId of noteTagIds) {
|
|
310
|
-
await client.from("note_tags").upsert({ note_id: noteId, tag_id: tagId }, { onConflict: "note_id,tag_id" });
|
|
311
393
|
}
|
|
312
394
|
const fullText = blockRows.map((b) => b.content).join(" ");
|
|
313
395
|
const wikiLinkTitles = extractWikiLinks(fullText);
|
|
@@ -330,6 +412,7 @@ function mapTiptapTypeToBlockType(tiptapType) {
|
|
|
330
412
|
mathBlock: "math",
|
|
331
413
|
calloutBlock: "callout",
|
|
332
414
|
queryBlock: "query",
|
|
415
|
+
summaryBlock: "summary",
|
|
333
416
|
horizontalRule: "divider",
|
|
334
417
|
fileBlock: "file",
|
|
335
418
|
linkPreview: "text",
|
|
@@ -347,21 +430,26 @@ function extractPlainText(node) {
|
|
|
347
430
|
if (!content) return "";
|
|
348
431
|
return content.map(extractPlainText).join("");
|
|
349
432
|
}
|
|
350
|
-
function
|
|
433
|
+
function extractTagPaths(text) {
|
|
351
434
|
const matches = text.match(/#([a-zA-Z][a-zA-Z0-9_-]*(?:\/[a-zA-Z][a-zA-Z0-9_-]*)*)/g);
|
|
352
435
|
if (!matches) return [];
|
|
353
436
|
const tags = /* @__PURE__ */ new Set();
|
|
354
437
|
for (const m of matches) {
|
|
355
|
-
|
|
356
|
-
if (full.includes("/")) {
|
|
357
|
-
const parts = full.split("/");
|
|
358
|
-
tags.add(parts[parts.length - 1]);
|
|
359
|
-
} else {
|
|
360
|
-
tags.add(full);
|
|
361
|
-
}
|
|
438
|
+
tags.add(m.slice(1).toLowerCase());
|
|
362
439
|
}
|
|
363
440
|
return [...tags];
|
|
364
441
|
}
|
|
442
|
+
function extractEdgesFromPaths(paths) {
|
|
443
|
+
const edges = [];
|
|
444
|
+
for (const path of paths) {
|
|
445
|
+
if (!path.includes("/")) continue;
|
|
446
|
+
const segments = path.split("/");
|
|
447
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
448
|
+
edges.push({ parent: segments[i], child: segments[i + 1] });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return edges;
|
|
452
|
+
}
|
|
365
453
|
function extractScopedTags(text) {
|
|
366
454
|
const matches = text.match(/#([a-zA-Z][a-zA-Z0-9_-]*(?:\/[a-zA-Z][a-zA-Z0-9_-]*)+)/g);
|
|
367
455
|
if (!matches) return [];
|
|
@@ -393,11 +481,23 @@ async function fullTextSearch(client, userId, query, limit = 30) {
|
|
|
393
481
|
});
|
|
394
482
|
}
|
|
395
483
|
async function searchByTag(client, userId, tagName, includeDescendants = true) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
484
|
+
if (tagName.includes("/")) {
|
|
485
|
+
const matchPattern = includeDescendants ? `${tagName}%` : tagName;
|
|
486
|
+
const { data: noteTagRows } = await client.from("note_tags").select("note_id").ilike("tag_path", matchPattern);
|
|
487
|
+
if (!noteTagRows || noteTagRows.length === 0) return [];
|
|
488
|
+
const noteIds = [...new Set(noteTagRows.map((r) => r.note_id))];
|
|
489
|
+
const { data: blocks } = await client.from("blocks").select("*, notes!inner(id, title)").in("note_id", noteIds).eq("user_id", userId).is("deleted_at", null);
|
|
490
|
+
return (blocks ?? []).map((row) => {
|
|
491
|
+
const notes = row.notes;
|
|
492
|
+
return {
|
|
493
|
+
block: row,
|
|
494
|
+
noteId: notes.id,
|
|
495
|
+
noteTitle: notes.title,
|
|
496
|
+
matchType: "tag"
|
|
497
|
+
};
|
|
498
|
+
});
|
|
399
499
|
}
|
|
400
|
-
const { data: matchedTags } = await
|
|
500
|
+
const { data: matchedTags } = await client.from("tags").select("id").eq("user_id", userId).ilike("name", `${tagName}%`);
|
|
401
501
|
if (!matchedTags || matchedTags.length === 0) return [];
|
|
402
502
|
const allTagIds = /* @__PURE__ */ new Set();
|
|
403
503
|
for (const tag of matchedTags) {
|
|
@@ -442,8 +542,7 @@ function parseQuery(input) {
|
|
|
442
542
|
const textParts = [];
|
|
443
543
|
for (const part of parts) {
|
|
444
544
|
if (part.startsWith("#")) {
|
|
445
|
-
|
|
446
|
-
filter.tag = raw.includes("/") ? raw.split("/").pop() : raw;
|
|
545
|
+
filter.tag = part.slice(1).toLowerCase();
|
|
447
546
|
} else if (part.startsWith("type:")) {
|
|
448
547
|
filter.type = part.slice(5);
|
|
449
548
|
} else if (part.startsWith("status:")) {
|
|
@@ -481,6 +580,344 @@ async function executeQuery(client, userId, input, limit = 30) {
|
|
|
481
580
|
}
|
|
482
581
|
return results;
|
|
483
582
|
}
|
|
583
|
+
var HELP_SECTIONS = {
|
|
584
|
+
"shortcuts": `# Keyboard Shortcuts
|
|
585
|
+
|
|
586
|
+
## Global
|
|
587
|
+
- Cmd+B \u2014 Toggle sidebar
|
|
588
|
+
- Cmd+K \u2014 Command palette
|
|
589
|
+
- Cmd+P \u2014 Global search
|
|
590
|
+
- Cmd+G \u2014 Graph view
|
|
591
|
+
- Cmd+O \u2014 New note
|
|
592
|
+
- Cmd+S \u2014 Save snapshot (version checkpoint)
|
|
593
|
+
- Cmd+Shift+D \u2014 Open/create today's daily note
|
|
594
|
+
- F2 \u2014 Rename current note
|
|
595
|
+
- Cmd+Backspace \u2014 Delete current note (to Trash)
|
|
596
|
+
- Cmd+W \u2014 Close current tab
|
|
597
|
+
- Alt+Right \u2014 Next tab
|
|
598
|
+
- Alt+Left \u2014 Previous tab
|
|
599
|
+
- Cmd+\\ \u2014 Toggle focus mode
|
|
600
|
+
- Alt+Space \u2014 Quick capture to Inbox
|
|
601
|
+
- Cmd+/ \u2014 Open help
|
|
602
|
+
- Cmd+Shift+A \u2014 Toggle AI Chat panel
|
|
603
|
+
- Alt+Up \u2014 Move block up
|
|
604
|
+
- Alt+Down \u2014 Move block down
|
|
605
|
+
- Cmd+Z \u2014 Undo
|
|
606
|
+
- Cmd+Shift+Z \u2014 Redo
|
|
607
|
+
|
|
608
|
+
## Multi-Pane
|
|
609
|
+
- Cmd+D \u2014 Split right (side by side)
|
|
610
|
+
- Cmd+Shift+E \u2014 Split down (top/bottom)
|
|
611
|
+
- Cmd+J \u2014 Close active pane
|
|
612
|
+
- Cmd+] \u2014 Focus next pane
|
|
613
|
+
- Cmd+[ \u2014 Focus previous pane
|
|
614
|
+
|
|
615
|
+
## Text Formatting
|
|
616
|
+
- Cmd+B \u2014 Bold
|
|
617
|
+
- Cmd+I \u2014 Italic
|
|
618
|
+
- Cmd+E \u2014 Inline code
|
|
619
|
+
- Cmd+Shift+X \u2014 Strikethrough
|
|
620
|
+
- Cmd+Shift+M \u2014 Insert math block
|
|
621
|
+
|
|
622
|
+
## Block Navigation
|
|
623
|
+
- Cmd+Enter \u2014 Exit code/math/mermaid block \u2192 preview + new paragraph
|
|
624
|
+
- / \u2014 Open slash command menu
|
|
625
|
+
- # \u2014 Tag autocomplete (type to filter)
|
|
626
|
+
- [[ \u2014 Wiki link autocomplete (type to filter)
|
|
627
|
+
|
|
628
|
+
## Reminders
|
|
629
|
+
- Click clock icon on a todo \u2014 Set reminder
|
|
630
|
+
- Amber clock means a reminder is already set
|
|
631
|
+
- Sidebar \u2192 Reminders section \u2014 View all active reminders`,
|
|
632
|
+
"editor": `# Editor
|
|
633
|
+
|
|
634
|
+
## Markdown Shortcuts (type at start of line)
|
|
635
|
+
- # \u2014 Heading 1
|
|
636
|
+
- ## \u2014 Heading 2
|
|
637
|
+
- ### \u2014 Heading 3
|
|
638
|
+
- #### \u2014 Heading 4
|
|
639
|
+
- - \u2014 Bullet list
|
|
640
|
+
- 1. \u2014 Numbered list
|
|
641
|
+
- [ ] \u2014 Task/checkbox
|
|
642
|
+
- > \u2014 Blockquote
|
|
643
|
+
- --- \u2014 Horizontal divider
|
|
644
|
+
- \`\`\` \u2014 Code block
|
|
645
|
+
|
|
646
|
+
## Inline Syntax
|
|
647
|
+
- **text** \u2014 Bold
|
|
648
|
+
- *text* \u2014 Italic
|
|
649
|
+
- \`code\` \u2014 Inline code
|
|
650
|
+
- #tagname \u2014 Tag (renders as purple pill)
|
|
651
|
+
- #parent/child \u2014 Scoped tag (creates hierarchy)
|
|
652
|
+
- [[Note Name]] \u2014 Wiki link to another note
|
|
653
|
+
- $...$ \u2014 Inline math (LaTeX)
|
|
654
|
+
|
|
655
|
+
## Note Title
|
|
656
|
+
The first H1 heading in a note automatically becomes the note title. You can also rename via F2, double-click the title in the status bar, or Cmd+K \u2192 Rename Note.`,
|
|
657
|
+
"blocks": `# Block Types
|
|
658
|
+
|
|
659
|
+
## Slash Menu (type / at start of a line)
|
|
660
|
+
|
|
661
|
+
Available blocks:
|
|
662
|
+
- Text \u2014 Plain paragraph
|
|
663
|
+
- Heading 1\u20133 \u2014 Section headings
|
|
664
|
+
- Bullet List \u2014 Unordered list
|
|
665
|
+
- Numbered List \u2014 Ordered list
|
|
666
|
+
- Todo \u2014 Checkbox task item (supports nesting, has a clock icon for reminders)
|
|
667
|
+
- Code Block \u2014 Syntax-highlighted code with language selector and copy button
|
|
668
|
+
- Quote \u2014 Blockquote
|
|
669
|
+
- Divider \u2014 Horizontal rule
|
|
670
|
+
- Math \u2014 LaTeX equation. Edit the source, then press Cmd+Enter to preview the rendered formula (uses KaTeX).
|
|
671
|
+
- Callout \u2014 Info/Warning/Error/Tip box with a dropdown to switch type. Has colored backgrounds per variant.
|
|
672
|
+
- Mermaid \u2014 Diagram (flowchart, sequence, class, state, Gantt, pie, etc.). Edit the source, then press Cmd+Enter to preview.
|
|
673
|
+
- Table \u2014 Insert a 3x3 table with header row. Use Tab to navigate cells.
|
|
674
|
+
- Image \u2014 Upload, paste, or drag an image. Stored in Supabase Storage. Supports common formats.
|
|
675
|
+
- Excalidraw \u2014 Embedded whiteboard canvas. Opens a full drawing interface inline.
|
|
676
|
+
- File \u2014 PDF or audio file. Upload and view inline. Stored in R2 (Cloudflare).
|
|
677
|
+
- Summary \u2014 Collapsible preview block. Shows a snippet with a "Show more" toggle.
|
|
678
|
+
- Query \u2014 Live search block. Type a query and click Run to see matching results inline within the note.
|
|
679
|
+
|
|
680
|
+
## Query Block Syntax
|
|
681
|
+
- type:todo \u2014 All todo blocks
|
|
682
|
+
- type:todo status:open \u2014 Open todos only
|
|
683
|
+
- type:todo status:done \u2014 Completed todos
|
|
684
|
+
- type:code \u2014 All code blocks
|
|
685
|
+
- #tagname \u2014 Blocks tagged with that tag
|
|
686
|
+
- keyword \u2014 Full-text search
|
|
687
|
+
|
|
688
|
+
## Exiting Blocks
|
|
689
|
+
Press Cmd+Enter inside a code block, math block, or mermaid block to exit it, switch to preview mode, and create a new paragraph below.`,
|
|
690
|
+
"search": `# Search
|
|
691
|
+
|
|
692
|
+
## Global Search (Cmd+P)
|
|
693
|
+
Search across all your notes and blocks. Results show the block content with a link to the source note. Click a result to navigate.
|
|
694
|
+
|
|
695
|
+
### Filter Syntax
|
|
696
|
+
- keyword \u2014 Full-text search across all blocks
|
|
697
|
+
- #tagname \u2014 Find all blocks tagged with that tag (includes child tags via DAG)
|
|
698
|
+
- #parent/child \u2014 Searches by the leaf tag (e.g. #dsa/dp/tabular searches tabular)
|
|
699
|
+
- type:todo \u2014 All todo/checkbox blocks
|
|
700
|
+
- type:code \u2014 All code blocks
|
|
701
|
+
- type:heading \u2014 All headings
|
|
702
|
+
- status:open \u2014 Unchecked todos
|
|
703
|
+
- status:done \u2014 Checked todos
|
|
704
|
+
- Combine filters: type:todo status:open #work
|
|
705
|
+
|
|
706
|
+
## Command Palette (Cmd+K)
|
|
707
|
+
Quick access to actions and note switching. Type to fuzzy-search.
|
|
708
|
+
|
|
709
|
+
Available actions:
|
|
710
|
+
- New Note \u2014 Create a blank note
|
|
711
|
+
- Today's Daily Note \u2014 Open or create today's daily note
|
|
712
|
+
- Rename Note \u2014 Rename the current note
|
|
713
|
+
- Delete Note \u2014 Soft-delete to Trash
|
|
714
|
+
- Tag Extend \u2014 Set parent\u2192child relationship between tags
|
|
715
|
+
- Graph View \u2014 Open the visual knowledge graph
|
|
716
|
+
- [note titles] \u2014 Switch to any note by typing its name`,
|
|
717
|
+
"tags": `# Tags
|
|
718
|
+
|
|
719
|
+
Tags are a core organizational feature of Vertex. They work like folders/tags in other tools but are more flexible.
|
|
720
|
+
|
|
721
|
+
## Basic Usage
|
|
722
|
+
Type #tagname anywhere in your note text. Tags render as purple pills inline.
|
|
723
|
+
|
|
724
|
+
## Hierarchical / Scoped Tags
|
|
725
|
+
Use #parent/child to create hierarchical tags:
|
|
726
|
+
- #work/projects \u2014 creates a "projects" tag with "work" as its parent
|
|
727
|
+
- #work/projects/alpha \u2014 deeper nesting
|
|
728
|
+
- Multi-parent and multi-level are supported (it's a DAG, not a tree)
|
|
729
|
+
|
|
730
|
+
## Folder Structure
|
|
731
|
+
Tags create a folder-like hierarchy in the sidebar. Each tag path (e.g. "company/projects") gets its own folder node with correct note counts. This lets you organize notes like a file system \u2014 think of tags as folders:
|
|
732
|
+
- #design/mockups \u2014 all design mockup notes
|
|
733
|
+
- #engineering/frontend \u2014 frontend engineering notes
|
|
734
|
+
- #personal/finances \u2014 personal finance notes
|
|
735
|
+
|
|
736
|
+
## How Tags Work
|
|
737
|
+
- Tags are stored per-block (not per-note), so different blocks in the same note can have different tags
|
|
738
|
+
- The sidebar shows a "Tags" section listing all tags with color dots and lock toggles
|
|
739
|
+
- Tag autocomplete: type # and start typing to see suggestions from existing tags
|
|
740
|
+
- Tag Extend: Cmd+K \u2192 "Tag Extend" to manually create parent\u2192child edges between existing tags
|
|
741
|
+
- The tag DAG (directed acyclic graph) supports complex hierarchies \u2014 a child can have multiple parents
|
|
742
|
+
|
|
743
|
+
## Tag Locking
|
|
744
|
+
Lock any tag from the sidebar to prevent AI from reading notes under that tag. Locking walks the entire DAG \u2014 all descendant tags are also locked.
|
|
745
|
+
|
|
746
|
+
## Tag Colors
|
|
747
|
+
Each tag can have a custom color (shown as a dot in the sidebar).`,
|
|
748
|
+
"wiki-links": `# Wiki Links & Backlinks
|
|
749
|
+
|
|
750
|
+
## Creating Wiki Links
|
|
751
|
+
Type [[Note Name]] to create a link to another note. An autocomplete dropdown suggests existing notes as you type.
|
|
752
|
+
|
|
753
|
+
## Navigation
|
|
754
|
+
Click any wiki link to navigate to the linked note.
|
|
755
|
+
|
|
756
|
+
## Backlinks
|
|
757
|
+
The backlinks panel at the bottom of the editor shows all notes that link to the current note. Each backlink shows the source note title and you can click to navigate.`,
|
|
758
|
+
"version-history": `# Version History (Snapshots)
|
|
759
|
+
|
|
760
|
+
## Saving Snapshots
|
|
761
|
+
- Manual: Press Cmd+S to save a version checkpoint
|
|
762
|
+
- Auto: Snapshots are automatically created every 5 saves
|
|
763
|
+
|
|
764
|
+
## Viewing History
|
|
765
|
+
Click "Show version history" below the editor to see all snapshots. Each snapshot shows the timestamp.
|
|
766
|
+
|
|
767
|
+
## Comparing Versions
|
|
768
|
+
Select any two versions to see a side-by-side diff view (opens as a new tab). Additions and removals are highlighted at the block level.
|
|
769
|
+
|
|
770
|
+
## Rollback
|
|
771
|
+
Click "Restore" on any snapshot to rollback the note to that version with one click.`,
|
|
772
|
+
"graph-view": `# Graph View
|
|
773
|
+
|
|
774
|
+
Press Cmd+G to open the knowledge graph.
|
|
775
|
+
|
|
776
|
+
## What You See
|
|
777
|
+
- Gray dots represent notes
|
|
778
|
+
- Purple dots represent tags
|
|
779
|
+
- Lines show wiki link connections (between notes) and tag relationships
|
|
780
|
+
|
|
781
|
+
## Controls
|
|
782
|
+
- Hover over any dot to enlarge its label
|
|
783
|
+
- Click a note to navigate to it
|
|
784
|
+
- Mouse wheel to zoom
|
|
785
|
+
- Drag to pan
|
|
786
|
+
|
|
787
|
+
The graph uses ForceAtlas2 layout (powered by graphology + sigma.js) for natural-looking node positioning.`,
|
|
788
|
+
"quick-capture": `# Quick Capture
|
|
789
|
+
|
|
790
|
+
Press Alt+Space from anywhere in the app to open a quick capture input dialog.
|
|
791
|
+
|
|
792
|
+
Type anything and press Enter \u2014 the text is saved directly to your Inbox note. You stay on your current note (no navigation). This is useful for quickly jotting down ideas without context switching.`,
|
|
793
|
+
"multi-pane": `# Multi-Pane Layout
|
|
794
|
+
|
|
795
|
+
## Splitting
|
|
796
|
+
- Cmd+D \u2014 Split the editor side-by-side (horizontal split)
|
|
797
|
+
- Cmd+Shift+E \u2014 Split top/bottom (vertical split)
|
|
798
|
+
|
|
799
|
+
## Each Pane
|
|
800
|
+
Each pane has its own:
|
|
801
|
+
- Tab bar with open notes
|
|
802
|
+
- Editor with full functionality
|
|
803
|
+
- Status bar
|
|
804
|
+
|
|
805
|
+
## Controls
|
|
806
|
+
- Drag the purple divider to resize panes
|
|
807
|
+
- Cmd+J \u2014 Close the active pane
|
|
808
|
+
- Cmd+] \u2014 Focus the next pane
|
|
809
|
+
- Cmd+[ \u2014 Focus the previous pane
|
|
810
|
+
|
|
811
|
+
The active pane's editor is shared with the AI Chat panel, so the AI knows which note you're currently editing.`,
|
|
812
|
+
"reminders": `# Reminders & Notifications
|
|
813
|
+
|
|
814
|
+
## Setting Reminders
|
|
815
|
+
Every todo item (checkbox task) has a clock icon next to it. Click the clock to open the reminder picker. Set:
|
|
816
|
+
- Due date and time
|
|
817
|
+
- Repeat: once, daily, weekly, monthly, yearly, or custom
|
|
818
|
+
- "Custom" lets you pick specific days of the week (Mon, Tue, Wed, etc.) that fire every week, or specific dates of the month (1st\u201331st) that fire every month
|
|
819
|
+
- "Keep notifying until done": re-fires the reminder every N minutes until the task is checked off
|
|
820
|
+
|
|
821
|
+
## Visual Indicator
|
|
822
|
+
An amber-colored clock icon means a reminder is already set on that todo. Gray means no reminder.
|
|
823
|
+
|
|
824
|
+
## Delivery
|
|
825
|
+
- Push notifications are delivered via the browser's Web Push API (works on Chrome, Firefox, Edge)
|
|
826
|
+
- A Cloudflare R2 worker checks for due reminders every 5 minutes
|
|
827
|
+
- When a reminder fires, you get a browser notification \u2014 click it to open the note
|
|
828
|
+
- Notification title shows the task text
|
|
829
|
+
|
|
830
|
+
## Repeating Reminders
|
|
831
|
+
For daily/weekly/monthly/yearly reminders:
|
|
832
|
+
- After firing, the reminder auto-reschedules to the next interval
|
|
833
|
+
- The task is automatically unchecked so you get reminded again
|
|
834
|
+
- For example, a daily reminder at 9 AM will fire every day at 9 AM
|
|
835
|
+
|
|
836
|
+
## Notify Until Done
|
|
837
|
+
The reminder re-fires every N minutes (configurable delay) until you mark the task as done. Marking it done deletes the reminder and checks the task.
|
|
838
|
+
|
|
839
|
+
## Viewing Reminders
|
|
840
|
+
- Sidebar \u2192 Reminders section lists all active reminders
|
|
841
|
+
- Each entry shows: task text, note title, and relative due date
|
|
842
|
+
- You can mark a reminder done directly from the sidebar
|
|
843
|
+
|
|
844
|
+
## Stable Block IDs
|
|
845
|
+
Each task item has a persistent UUID (crypto.randomUUID()) that survives page reloads. This ensures reminders stay matched to their todo even after editing.`,
|
|
846
|
+
"ai-integration": `# AI Integration
|
|
847
|
+
|
|
848
|
+
## AI Chat Panel
|
|
849
|
+
Press Cmd+Shift+A or click the AI button in the status bar to toggle the AI Chat panel.
|
|
850
|
+
|
|
851
|
+
### Two Modes
|
|
852
|
+
1. Ask mode \u2014 Q&A and research. The AI can search your notes, list todos, find tagged blocks, read daily notes, and answer questions based on your data.
|
|
853
|
+
2. Edit mode \u2014 Structured inline content editing. The AI returns actions (insert, replace, delete, etc.) that appear as green/red suggestions in the editor. Accept or reject each change individually.
|
|
854
|
+
|
|
855
|
+
### How Ask Mode Works
|
|
856
|
+
The AI has 7 tools it can use to fetch information:
|
|
857
|
+
1. get_current_note \u2014 Read the currently open note's content
|
|
858
|
+
2. search_notes \u2014 Full-text search across all notes
|
|
859
|
+
3. get_note_by_title \u2014 Find a note by its title
|
|
860
|
+
4. get_note_content \u2014 Get full content of a specific note by its ID
|
|
861
|
+
5. get_recent_daily_notes \u2014 Get recent daily journal entries
|
|
862
|
+
6. list_all_notes \u2014 List all note titles and dates
|
|
863
|
+
7. list_todos \u2014 List tasks from notes, scoped to the current note + latest daily note by default
|
|
864
|
+
|
|
865
|
+
### How Edit Mode Works
|
|
866
|
+
The AI understands the block structure of your note. It can:
|
|
867
|
+
- Insert new blocks after/before a specific block
|
|
868
|
+
- Replace a block's content
|
|
869
|
+
- Delete a block
|
|
870
|
+
- Use special syntax: {summary}...{/summary}, {callout info}...{/callout}, {query value="..."}{/query}
|
|
871
|
+
|
|
872
|
+
Changes appear as inline suggestions (green for inserts, red strikethrough for deletions) with accept/reject buttons. The editor is locked while changes are pending.
|
|
873
|
+
|
|
874
|
+
### Providers & Settings
|
|
875
|
+
Supported AI providers: OpenRouter, Groq, OpenAI, Gemini, Cerebras
|
|
876
|
+
Configure API keys via the status bar gear icon \u2192 AI Settings. Keys are encrypted using AES-256-GCM and stored in the database.
|
|
877
|
+
|
|
878
|
+
## AI Privacy (Tag Locking)
|
|
879
|
+
Lock any tag from the sidebar to prevent AI from reading notes under that tag. The AI will receive "locked: true" with no content and will stop trying to access that note. Locking walks the full tag DAG \u2014 locking a parent locks all descendants.`,
|
|
880
|
+
"share-links": `# Share Links
|
|
881
|
+
|
|
882
|
+
Click the share button in the status bar to generate a public share link. Anyone with the link can view the note without signing in. The link uses an unguessable random token.
|
|
883
|
+
|
|
884
|
+
You can revoke share links at any time from the share dialog. Shared notes appear in a "Shared Notes" section in the sidebar.`,
|
|
885
|
+
"tag-locking": `# Tag Locking (AI Privacy)
|
|
886
|
+
|
|
887
|
+
## What It Does
|
|
888
|
+
Locking prevents AI from reading any note that has a locked tag. This is useful for sensitive notes that you don't want AI to access.
|
|
889
|
+
|
|
890
|
+
## How It Works
|
|
891
|
+
- Note-level: Toggle the lock in the status bar \u2014 locks the current note
|
|
892
|
+
- Tag-level: Lock a tag from the sidebar \u2014 ALL notes under that tag are locked
|
|
893
|
+
- The locking walks the entire tag DAG: if you lock #work, every note tagged with #work/projects, #work/meetings, etc. is also locked
|
|
894
|
+
- Locked notes show a lock icon in the tab bar, sidebar, and status bar
|
|
895
|
+
- The AI Chat panel shows a banner for locked notes
|
|
896
|
+
- When AI tries to access a locked note, it receives "NOTE LOCKED" and is instructed to stop immediately`,
|
|
897
|
+
"mobile": `# Mobile
|
|
898
|
+
|
|
899
|
+
On mobile devices, Vertex provides:
|
|
900
|
+
- A bottom toolbar with quick access to AI chat, undo, redo, quick capture, and search
|
|
901
|
+
- Touch-friendly interface with adjusted hit targets
|
|
902
|
+
- Sidebar slides in as a full-width overlay with backdrop
|
|
903
|
+
- Responsive design adapts to smaller screens`,
|
|
904
|
+
"trash": `# Trash
|
|
905
|
+
|
|
906
|
+
Deleted notes go to Trash (soft delete \u2014 they can be recovered).
|
|
907
|
+
|
|
908
|
+
- Open the sidebar \u2192 Trash section to see trashed notes
|
|
909
|
+
- Hover over a trashed note to reveal:
|
|
910
|
+
- \u21BA Restore \u2014 brings the note back
|
|
911
|
+
- \u2297 Permanently delete \u2014 deletes the note and all associated blocks, tags, links, and snapshots
|
|
912
|
+
- "Empty" button deletes all trashed notes permanently
|
|
913
|
+
- Notes in trash still count toward your total note count`,
|
|
914
|
+
"themes": `# Themes
|
|
915
|
+
|
|
916
|
+
Click the \u2600\uFE0F/\u{1F319} button in the top bar to toggle between dark and light mode. Your preference is saved in localStorage and persists across sessions.
|
|
917
|
+
|
|
918
|
+
The app uses CSS variables for theming (--foreground, --background, --accent, --border, etc.) giving a consistent look across all UI elements.`
|
|
919
|
+
};
|
|
920
|
+
var HELP_TOPICS = Object.keys(HELP_SECTIONS).sort();
|
|
484
921
|
function extractTextFromNode(node) {
|
|
485
922
|
if (typeof node.text === "string") return node.text;
|
|
486
923
|
const content = node.content;
|
|
@@ -927,7 +1364,7 @@ function parseInline(text) {
|
|
|
927
1364
|
}
|
|
928
1365
|
return nodes.length > 0 ? nodes : [{ type: "text", text: text || " " }];
|
|
929
1366
|
}
|
|
930
|
-
var VERTEX_VERSION = "0.
|
|
1367
|
+
var VERTEX_VERSION = "0.3.4";
|
|
931
1368
|
|
|
932
1369
|
export {
|
|
933
1370
|
createSupabaseClient,
|
package/dist/commands/capture.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
getBlocksForNote,
|
|
8
8
|
listNotes,
|
|
9
9
|
saveNoteContent
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-XJVEK2OB.js";
|
|
11
11
|
|
|
12
12
|
// src/commands/capture.ts
|
|
13
13
|
import { Command, Args } from "@oclif/core";
|
package/dist/commands/daily.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
exportNoteAsMarkdown,
|
|
8
8
|
getOrCreateDailyNote,
|
|
9
9
|
listNotes
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-XJVEK2OB.js";
|
|
11
11
|
|
|
12
12
|
// src/commands/daily.ts
|
|
13
13
|
import { Command, Flags } from "@oclif/core";
|
package/dist/commands/delete.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
listNotes,
|
|
8
8
|
softDeleteNote
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-XJVEK2OB.js";
|
|
10
10
|
|
|
11
11
|
// src/commands/delete.ts
|
|
12
12
|
import { Command, Args } from "@oclif/core";
|
package/dist/commands/edit.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
exportNoteAsMarkdown,
|
|
8
8
|
listNotes,
|
|
9
9
|
markdownToTiptap,
|
|
10
10
|
saveNoteContent
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-XJVEK2OB.js";
|
|
12
12
|
|
|
13
13
|
// src/commands/edit.ts
|
|
14
14
|
import { Command, Args } from "@oclif/core";
|
package/dist/commands/export.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
exportNoteAsJson,
|
|
8
8
|
exportNoteAsMarkdown,
|
|
9
9
|
listNotes
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-XJVEK2OB.js";
|
|
11
11
|
|
|
12
12
|
// src/commands/export.ts
|
|
13
13
|
import { Command, Args, Flags } from "@oclif/core";
|
package/dist/commands/hello.js
CHANGED
package/dist/commands/help.js
CHANGED
package/dist/commands/import.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
createNote,
|
|
8
8
|
markdownToTiptap,
|
|
9
9
|
saveNoteContent
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-XJVEK2OB.js";
|
|
11
11
|
|
|
12
12
|
// src/commands/import.ts
|
|
13
13
|
import { Command, Args, Flags } from "@oclif/core";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
exportNoteAsMarkdown,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
listNotes,
|
|
10
10
|
markdownToTiptap,
|
|
11
11
|
saveNoteContent
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-XJVEK2OB.js";
|
|
13
13
|
|
|
14
14
|
// src/commands/interactive.ts
|
|
15
15
|
import { Command, Args } from "@oclif/core";
|
package/dist/commands/login.js
CHANGED
package/dist/commands/new.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
createNote
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-XJVEK2OB.js";
|
|
9
9
|
|
|
10
10
|
// src/commands/new.ts
|
|
11
11
|
import { Command, Args } from "@oclif/core";
|
package/dist/commands/notes.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
listNotes
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-XJVEK2OB.js";
|
|
9
9
|
|
|
10
10
|
// src/commands/notes.ts
|
|
11
11
|
import { Command } from "@oclif/core";
|
package/dist/commands/restore.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
listNotes,
|
|
8
8
|
restoreNote
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-XJVEK2OB.js";
|
|
10
10
|
|
|
11
11
|
// src/commands/restore.ts
|
|
12
12
|
import { Command, Args } from "@oclif/core";
|
package/dist/commands/search.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
executeQuery
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-XJVEK2OB.js";
|
|
9
9
|
|
|
10
10
|
// src/commands/search.ts
|
|
11
11
|
import { Command, Args } from "@oclif/core";
|
package/dist/commands/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import {
|
|
6
6
|
loadConfig
|
|
7
7
|
} from "../chunk-DRIWYEQE.js";
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
formatFileSize,
|
|
11
11
|
getStorageInfo,
|
|
12
12
|
listNotes
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-XJVEK2OB.js";
|
|
14
14
|
|
|
15
15
|
// src/commands/status.ts
|
|
16
16
|
import { Command } from "@oclif/core";
|
package/dist/commands/tags.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
getUserTags
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-XJVEK2OB.js";
|
|
9
9
|
|
|
10
10
|
// src/commands/tags.ts
|
|
11
11
|
import { Command } from "@oclif/core";
|
package/dist/commands/today.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
listNotes
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-XJVEK2OB.js";
|
|
9
9
|
|
|
10
10
|
// src/commands/today.ts
|
|
11
11
|
import { Command } from "@oclif/core";
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
getClient,
|
|
6
6
|
getUserId
|
|
7
|
-
} from "../../chunk-
|
|
7
|
+
} from "../../chunk-QQO4JMNC.js";
|
|
8
8
|
import "../../chunk-DRIWYEQE.js";
|
|
9
9
|
import {
|
|
10
10
|
emptyTrash
|
|
11
|
-
} from "../../chunk-
|
|
11
|
+
} from "../../chunk-XJVEK2OB.js";
|
|
12
12
|
|
|
13
13
|
// src/commands/trash/empty.ts
|
|
14
14
|
import { Command } from "@oclif/core";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../../chunk-
|
|
4
|
+
} from "../../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
listNotes
|
|
8
|
-
} from "../../chunk-
|
|
8
|
+
} from "../../chunk-XJVEK2OB.js";
|
|
9
9
|
|
|
10
10
|
// src/commands/trash/index.ts
|
|
11
11
|
import { Command } from "@oclif/core";
|
package/dist/commands/view.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient,
|
|
3
3
|
getUserId
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-QQO4JMNC.js";
|
|
5
5
|
import "../chunk-DRIWYEQE.js";
|
|
6
6
|
import {
|
|
7
7
|
exportNoteAsMarkdown,
|
|
8
8
|
listNotes
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-XJVEK2OB.js";
|
|
10
10
|
|
|
11
11
|
// src/commands/view.ts
|
|
12
12
|
import { Command, Args } from "@oclif/core";
|
package/dist/lib/client.js
CHANGED
package/package.json
CHANGED
|
@@ -1,54 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "vertex-notes",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Vertex — keyboard-first knowledge workspace CLI",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"vertex": "bin/run.js"
|
|
8
|
-
},
|
|
9
|
-
"main": "dist/index.js",
|
|
10
|
-
"files": [
|
|
11
|
-
"bin",
|
|
12
|
-
"dist",
|
|
13
|
-
"README.md"
|
|
14
|
-
],
|
|
15
|
-
"keywords": [
|
|
16
|
-
"vertex",
|
|
17
|
-
"notes",
|
|
18
|
-
"cli",
|
|
19
|
-
"knowledge",
|
|
20
|
-
"todo",
|
|
21
|
-
"markdown",
|
|
22
|
-
"productivity"
|
|
23
|
-
],
|
|
24
|
-
"author": "Sahil Kumar Sinha",
|
|
25
|
-
"license": "MIT",
|
|
26
|
-
"repository": {
|
|
27
|
-
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/sahilcodes2002/vertex.git"
|
|
29
|
-
},
|
|
30
|
-
"engines": {
|
|
31
|
-
"node": ">=18"
|
|
32
|
-
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"build": "tsup",
|
|
35
|
-
"typecheck": "tsc --noEmit",
|
|
36
|
-
"clean": "rm -rf dist",
|
|
37
|
-
"prepublishOnly": "pnpm build"
|
|
38
|
-
},
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"@oclif/core": "^4.
|
|
41
|
-
"@supabase/supabase-js": "^2.49.0"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "vertex-notes",
|
|
3
|
+
"version": "0.3.4",
|
|
4
|
+
"description": "Vertex — keyboard-first knowledge workspace CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vertex": "bin/run.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"vertex",
|
|
17
|
+
"notes",
|
|
18
|
+
"cli",
|
|
19
|
+
"knowledge",
|
|
20
|
+
"todo",
|
|
21
|
+
"markdown",
|
|
22
|
+
"productivity"
|
|
23
|
+
],
|
|
24
|
+
"author": "Sahil Kumar Sinha",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/sahilcodes2002/vertex.git"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"clean": "rm -rf dist",
|
|
37
|
+
"prepublishOnly": "pnpm build"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@oclif/core": "^4.11.0",
|
|
41
|
+
"@supabase/supabase-js": "^2.49.0",
|
|
42
|
+
"ws": "^8.18.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/ws": "^8.18.0",
|
|
46
|
+
"@vertex/core": "workspace:*",
|
|
47
|
+
"tsup": "^8.4.0",
|
|
48
|
+
"typescript": "^5.7.0",
|
|
49
|
+
"oclif": "^4.17.0"
|
|
50
|
+
},
|
|
51
|
+
"oclif": {
|
|
52
|
+
"bin": "vertex",
|
|
53
|
+
"dirname": "vertex",
|
|
54
|
+
"commands": "./dist/commands"
|
|
55
|
+
}
|
|
56
|
+
}
|