banyan-notes 0.0.0-stage → 0.1.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/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "banyan-notes",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Banyan (榕) — a local-first, plugin-based note-taking app for the web. Markdown folder is the only source of truth.",
5
+ "keywords": [
6
+ "notes",
7
+ "markdown",
8
+ "local-first",
9
+ "plugins",
10
+ "cordis",
11
+ "editor",
12
+ "knowledge-base"
13
+ ],
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "bin": {
17
+ "banyan": "bin/banyan.mjs"
18
+ },
19
+ "files": [
20
+ "dist/",
21
+ "client-dist/",
22
+ "plugins/",
23
+ "bin/"
24
+ ],
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+ssh://git@github.com/redjoker2025/Banyan.git"
31
+ },
32
+ "homepage": "https://github.com/redjoker2025/Banyan#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/redjoker2025/Banyan/issues"
35
+ },
36
+ "dependencies": {
37
+ "better-sqlite3": "^11.8.1",
38
+ "ws": "^8.18.0"
39
+ }
6
40
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * backlinks client entry — panel.right listing notes that link to the
3
+ * current note (Markdown links and [[wiki links]] resolved server-side).
4
+ */
5
+ export function activate(host) {
6
+ host.registerPanel({
7
+ side: 'right',
8
+ title: '反向链接',
9
+ render(el, api) {
10
+ let disposed = false
11
+ const refresh = async () => {
12
+ if (disposed) return
13
+ if (!api.currentPath) {
14
+ el.innerHTML = '<div class="plugin-empty">未打开笔记</div>'
15
+ return
16
+ }
17
+ try {
18
+ const res = await api.apiFetch(`/api/graph/backlinks?path=${encodeURIComponent(api.currentPath)}`)
19
+ if (disposed) return
20
+ const paths = res.paths ?? []
21
+ el.innerHTML = ''
22
+ if (!paths.length) {
23
+ el.innerHTML = '<div class="plugin-empty">暂无反向链接<br /><span>用 [[笔记名]] 或 Markdown 链接引用本笔记</span></div>'
24
+ return
25
+ }
26
+ const list = document.createElement('div')
27
+ list.className = 'plugin-list'
28
+ for (const p of paths) {
29
+ const item = document.createElement('a')
30
+ item.className = 'plugin-item'
31
+ item.textContent = p
32
+ item.onclick = () => api.openNote(p)
33
+ list.appendChild(item)
34
+ }
35
+ el.appendChild(list)
36
+ } catch (e) {
37
+ el.innerHTML = `<div class="plugin-empty">加载失败:${e.message}</div>`
38
+ }
39
+ }
40
+ refresh()
41
+ return api.onNoteChange(refresh)
42
+ },
43
+ })
44
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@banyan/plugin-backlinks",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module"
6
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "banyan.backlinks",
3
+ "name": "反向链接",
4
+ "version": "0.1.0",
5
+ "banyanApi": "^0.1",
6
+ "description": "在右侧面板显示当前笔记的反向链接(基于 ctx.graph 的链接图)",
7
+ "inject": [],
8
+ "provides": [],
9
+ "permissions": {
10
+ "fs": { "globs": [] },
11
+ "net": { "domains": [] },
12
+ "exec": false
13
+ },
14
+ "entry": {
15
+ "client": "./client/index.js"
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@banyan/plugin-conflict-demo-a",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module"
6
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "banyan.demo.conflict-a",
3
+ "name": "冲突演示 A",
4
+ "version": "0.1.0",
5
+ "banyanApi": "^0.1",
6
+ "description": "演示插件:提供 demo.greeter。与冲突演示 B 同时启用会报 E_CONFLICT(默认禁用)",
7
+ "inject": ["logger"],
8
+ "provides": ["demo.greeter"],
9
+ "permissions": {
10
+ "fs": { "globs": [] },
11
+ "net": { "domains": [] },
12
+ "exec": false
13
+ },
14
+ "entry": {
15
+ "server": "./server/index.ts"
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ /** Conflict demo A — provides `demo.greeter`; must never be enabled together with conflict demo B (§4.5). */
2
+ export default {
3
+ apply(ctx: any) {
4
+ ctx.logger.info('[conflict-a] demo plugin A is running, providing demo.greeter')
5
+ },
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@banyan/plugin-conflict-demo-b",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module"
6
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "banyan.demo.conflict-b",
3
+ "name": "冲突演示 B",
4
+ "version": "0.1.0",
5
+ "banyanApi": "^0.1",
6
+ "description": "演示插件:同样提供 demo.greeter。与冲突演示 A 同时启用会报 E_CONFLICT(默认禁用)",
7
+ "inject": ["logger"],
8
+ "provides": ["demo.greeter"],
9
+ "permissions": {
10
+ "fs": { "globs": [] },
11
+ "net": { "domains": [] },
12
+ "exec": false
13
+ },
14
+ "entry": {
15
+ "server": "./server/index.ts"
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ /** Conflict demo B — also provides `demo.greeter`; clashes with demo A (§4.5). */
2
+ export default {
3
+ apply(ctx: any) {
4
+ ctx.logger.info('[conflict-b] demo plugin B is running, providing demo.greeter')
5
+ },
6
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * daily-note client entry — registers the command「打开/创建今日笔记」
3
+ * which opens (creating if needed) the `YYYY-MM-DD.md` note for today.
4
+ */
5
+ export function activate(host) {
6
+ host.registerCommand({
7
+ id: 'banyan.daily-note.open',
8
+ title: '打开/创建今日笔记',
9
+ async run(api) {
10
+ const d = new Date()
11
+ const name = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}.md`
12
+ try {
13
+ await api.apiFetch('/api/note', {
14
+ method: 'POST',
15
+ json: { path: name, content: `# ${name.replace(/\.md$/, '')}\n\n` },
16
+ })
17
+ } catch (e) {
18
+ if (e.code !== 'E_CONFLICT') throw e // already exists → just open it
19
+ }
20
+ await api.openNote(name)
21
+ },
22
+ })
23
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@banyan/plugin-daily-note",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module"
6
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "banyan.daily-note",
3
+ "name": "今日笔记",
4
+ "version": "0.1.0",
5
+ "banyanApi": "^0.1",
6
+ "description": "注册命令「打开/创建今日笔记」,对应 YYYY-MM-DD.md",
7
+ "inject": [],
8
+ "provides": [],
9
+ "permissions": {
10
+ "fs": { "globs": [] },
11
+ "net": { "domains": [] },
12
+ "exec": false
13
+ },
14
+ "entry": {
15
+ "client": "./client/index.js"
16
+ }
17
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@banyan/plugin-net-violation-demo",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module"
6
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "banyan.demo.net-violation",
3
+ "name": "越权演示",
4
+ "version": "0.1.0",
5
+ "banyanApi": "^0.1",
6
+ "description": "演示插件:声明 net.domains=[example.com] 但启动时尝试访问 api.github.com → 被拦截并写入 audit.jsonl(默认禁用)",
7
+ "inject": ["net", "logger"],
8
+ "provides": [],
9
+ "permissions": {
10
+ "fs": { "globs": [] },
11
+ "net": { "domains": ["example.com"] },
12
+ "exec": false
13
+ },
14
+ "entry": {
15
+ "server": "./server/index.ts"
16
+ }
17
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Net violation demo — declares `net.domains: [example.com]` but attempts to
3
+ * reach api.github.com at startup. The permission proxy denies the call
4
+ * BEFORE any network I/O and appends an entry to `audit.jsonl` (§4.5).
5
+ */
6
+ export default {
7
+ inject: ['net', 'logger'],
8
+
9
+ async apply(ctx: any) {
10
+ try {
11
+ await ctx.net.fetch('https://api.github.com/repos/deepseek-ai/deepseek-harness')
12
+ ctx.logger.warn('[net-violation] call unexpectedly allowed — permission proxy is missing!')
13
+ } catch (e) {
14
+ ctx.logger.warn(`[net-violation] blocked as expected: ${e.message}`)
15
+ }
16
+ },
17
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * tags client entry — panel.right aggregating the frontmatter `tags` field
3
+ * across all notes; clicking a tag filters and lists the matching notes.
4
+ */
5
+ export function activate(host) {
6
+ host.registerPanel({
7
+ side: 'right',
8
+ title: '标签',
9
+ render(el, api) {
10
+ let disposed = false
11
+ let selected = null
12
+ const refresh = async () => {
13
+ if (disposed) return
14
+ try {
15
+ const res = await api.apiFetch('/api/tags')
16
+ if (disposed) return
17
+ const notes = res.notes ?? []
18
+ const counts = new Map()
19
+ for (const n of notes) {
20
+ for (const t of n.tags) counts.set(t, (counts.get(t) ?? 0) + 1)
21
+ }
22
+ el.innerHTML = ''
23
+ const tagList = document.createElement('div')
24
+ tagList.className = 'plugin-list'
25
+ if (counts.size === 0) {
26
+ tagList.innerHTML = '<div class="plugin-empty">暂无标签<br /><span>在笔记 frontmatter 中添加 tags 字段</span></div>'
27
+ }
28
+ for (const [tag, count] of [...counts.entries()].sort((a, b) => b[1] - a[1])) {
29
+ const item = document.createElement('a')
30
+ item.className = 'plugin-item' + (selected === tag ? ' active' : '')
31
+ item.textContent = `#${tag}(${count})`
32
+ item.onclick = () => {
33
+ selected = selected === tag ? null : tag
34
+ refresh()
35
+ }
36
+ tagList.appendChild(item)
37
+ }
38
+ el.appendChild(tagList)
39
+
40
+ if (selected) {
41
+ const list = document.createElement('div')
42
+ list.className = 'plugin-list'
43
+ const head = document.createElement('div')
44
+ head.className = 'plugin-heading'
45
+ head.textContent = `包含 #${selected} 的笔记`
46
+ list.appendChild(head)
47
+ for (const n of notes.filter((n) => n.tags.includes(selected))) {
48
+ const item = document.createElement('a')
49
+ item.className = 'plugin-item'
50
+ item.textContent = n.path
51
+ item.onclick = () => api.openNote(n.path)
52
+ list.appendChild(item)
53
+ }
54
+ el.appendChild(list)
55
+ }
56
+ } catch (e) {
57
+ el.innerHTML = `<div class="plugin-empty">加载失败:${e.message}</div>`
58
+ }
59
+ }
60
+ refresh()
61
+ return api.onNoteChange(refresh)
62
+ },
63
+ })
64
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@banyan/plugin-tags",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module"
6
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "banyan.tags",
3
+ "name": "标签聚合",
4
+ "version": "0.1.0",
5
+ "banyanApi": "^0.1",
6
+ "description": "聚合全部笔记 frontmatter 的 tags 字段,点击标签可过滤笔记",
7
+ "inject": [],
8
+ "provides": [],
9
+ "permissions": {
10
+ "fs": { "globs": [] },
11
+ "net": { "domains": [] },
12
+ "exec": false
13
+ },
14
+ "entry": {
15
+ "client": "./client/index.js"
16
+ }
17
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * word-count client entry — statusbar item showing the word count of the
3
+ * current note, published by the server half via `plugin.state`.
4
+ */
5
+ export function activate(host) {
6
+ host.registerStatusbar((api) => {
7
+ if (!api.currentPath) return ''
8
+ const counts = api.pluginState['banyan.word-count'] ?? {}
9
+ const count = counts[api.currentPath]
10
+ if (count === undefined) return ''
11
+ return `${count} 词`
12
+ })
13
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@banyan/plugin-word-count",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module"
6
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "banyan.word-count",
3
+ "name": "字数统计",
4
+ "version": "0.1.0",
5
+ "banyanApi": "^0.1",
6
+ "description": "保存时经 pipeline/save 瀑布链统计字数,在状态栏显示当前笔记字数",
7
+ "inject": ["pipeline", "broadcast", "logger"],
8
+ "provides": [],
9
+ "permissions": {
10
+ "fs": { "globs": [] },
11
+ "net": { "domains": [] },
12
+ "exec": false
13
+ },
14
+ "entry": {
15
+ "server": "./server/index.ts",
16
+ "client": "./client/index.js"
17
+ }
18
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * word-count server entry — demonstrates the `pipeline/save` waterfall:
3
+ * the listener delegates downstream with `next()` first, then counts the
4
+ * words of the final content and publishes it as `plugin.state`.
5
+ */
6
+ interface SaveMeta {
7
+ path: string
8
+ }
9
+
10
+ export default {
11
+ inject: ['pipeline', 'broadcast', 'logger'],
12
+
13
+ apply(ctx: any) {
14
+ ctx.on('pipeline/save', async (content: string, meta: SaveMeta, next: () => Promise<string>) => {
15
+ const result = await next() // delegate to downstream listeners first (onion order)
16
+ const count = countWords(result)
17
+ ctx.broadcast({
18
+ type: 'plugin.state',
19
+ payload: { plugin: 'banyan.word-count', counts: { [meta.path]: count } },
20
+ })
21
+ ctx.logger.info(`[word-count] ${meta.path}: ${count} words`)
22
+ return result
23
+ })
24
+ },
25
+ }
26
+
27
+ function countWords(content: string): number {
28
+ const cjk = (content.match(/[\u4e00-\u9fff]/g) ?? []).length
29
+ const latin = (content.match(/[A-Za-z0-9]+(?:['’-][A-Za-z0-9]+)*/g) ?? []).length
30
+ return cjk + latin
31
+ }