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/README.md +100 -2
- package/bin/banyan.mjs +2 -0
- package/client-dist/assets/index-BglH_tnC.js +50 -0
- package/client-dist/assets/index-pfWyhoPY.css +1 -0
- package/client-dist/index.html +14 -0
- package/dist/index.mjs +14863 -0
- package/package.json +37 -3
- package/plugins/backlinks/client/index.js +44 -0
- package/plugins/backlinks/package.json +6 -0
- package/plugins/backlinks/plugin.json +17 -0
- package/plugins/conflict-demo-a/package.json +6 -0
- package/plugins/conflict-demo-a/plugin.json +17 -0
- package/plugins/conflict-demo-a/server/index.ts +6 -0
- package/plugins/conflict-demo-b/package.json +6 -0
- package/plugins/conflict-demo-b/plugin.json +17 -0
- package/plugins/conflict-demo-b/server/index.ts +6 -0
- package/plugins/daily-note/client/index.js +23 -0
- package/plugins/daily-note/package.json +6 -0
- package/plugins/daily-note/plugin.json +17 -0
- package/plugins/net-violation-demo/package.json +6 -0
- package/plugins/net-violation-demo/plugin.json +17 -0
- package/plugins/net-violation-demo/server/index.ts +17 -0
- package/plugins/tags/client/index.js +64 -0
- package/plugins/tags/package.json +6 -0
- package/plugins/tags/plugin.json +17 -0
- package/plugins/word-count/client/index.js +13 -0
- package/plugins/word-count/package.json +6 -0
- package/plugins/word-count/plugin.json +18 -0
- package/plugins/word-count/server/index.ts +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "banyan-notes",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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,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,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,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,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,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,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,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,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
|
+
}
|