feat: init

This commit is contained in:
h
2026-05-21 10:23:01 +02:00
commit 2b00fa44d5
13 changed files with 1189 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
# deps
node_modules/
# build output (regenerated by `make install` / `make zip`)
main.js
*.zip
# plugin runtime — Obsidian writes settings here when the symlinked dir
# is used as the live plugin; never want it in the repo
data.json
# misc
*.log
.DS_Store
.vscode/
.idea/
+25
View File
@@ -0,0 +1,25 @@
PLUGIN_ID := beaver
ASSETS := manifest.json main.js versions.json
PM := $(shell command -v pnpm 2>/dev/null || command -v npm 2>/dev/null)
.PHONY: install zip _build
_build:
@test -n "$(PM)" || (echo "no pnpm or npm in PATH" >&2; exit 1)
$(PM) install --silent
$(PM) run build
install: _build
@test -n "$(VAULT)" || (echo "set VAULT=<path-to-vault>" >&2; exit 1)
@test -d "$(VAULT)/.obsidian" || (echo "no .obsidian dir at $(VAULT)" >&2; exit 1)
@target="$(VAULT)/.obsidian/plugins/$(PLUGIN_ID)"; \
mkdir -p "$$target"; \
cp $(ASSETS) "$$target/"; \
echo "installed → $$target"
zip: _build
@version=$$(node -p "require('./manifest.json').version"); \
out="beaver-plugin-$$version.zip"; \
rm -f "$$out"; \
zip -q -j "$$out" $(ASSETS); \
echo "wrote $$out"
+56
View File
@@ -0,0 +1,56 @@
# Beaver — Obsidian plugin
[![AI Slop Inside](https://sladge.net/badge.svg)](https://sladge.net)
Sends the current note to Beaver's `markdown frontend` (`POST /chat` on `beaver-gateway`) and writes the agent's reply back into the file. Skips the Obsidian Sync round-trip by POSTing the buffer contents directly.
## Commands
Both commands appear in the command palette only when the active note's YAML frontmatter has an `agent:` field:
- **Beaver: Send using selected agent** — uses `frontmatter.agent`.
- **Beaver: Send using different agent** — fetches the agent list from the gateway and opens a fuzzy picker.
## Settings
- **Base URL** — markdown-frontend root, e.g. `http://localhost:62993`.
- **Bearer token** — a token with the `messages` scope (mint via the admin frontend or `BOOTSTRAP_TOKENS`).
- **Test connection** — calls `GET /agents`.
## Note format
A minimal note that the plugin can send:
```markdown
---
agent: beaver-opus-medium
---
hi there
```
The gateway parses `### User:` / `### Assistant:` markers; a note with no markers is treated as a single user turn. After a successful send, the gateway returns the new file content (including an `### Assistant:` block and a fresh `### User:` scaffold), and the plugin writes it back to the buffer.
## Install
```
make install VAULT=~/Obsidian/my-vault # build + copy manifest.json + main.js into <vault>/.obsidian/plugins/beaver
make zip # build → beaver-plugin-<version>.zip you can carry to any vault
```
Then enable **Beaver** in Settings → Community plugins.
### Mobile
The plugin is desktop-and-mobile (`isDesktopOnly: false`). Two things to get right on a phone:
- The base URL in plugin settings has to be reachable from the phone — `http://localhost:62993` won't work; use the gateway's LAN IP, a tunnel, or a public hostname.
- Get the plugin files onto the phone vault via Obsidian Sync (toggle "Installed community plugins" in your sync settings) or any file-sync tool you already use (Working Copy, Syncthing, etc.). `make install` only knows how to write to a local path.
### Hand-rolled
Three files are all Obsidian needs — `manifest.json`, `main.js`, `versions.json`. Drop them in `<vault>/.obsidian/plugins/beaver/` however you like (unzip, `scp`, `rsync`, your own git, …).
## Bumping the version
Edit the version field in `manifest.json`, `package.json`, and `versions.json` by hand. Three files, one change each.
+40
View File
@@ -0,0 +1,40 @@
import esbuild from "esbuild";
import process from "node:process";
import builtins from "builtin-modules";
const prod = process.argv[2] === "production";
const ctx = await esbuild.context({
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
format: "cjs",
target: "es2022",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
});
if (prod) {
await ctx.rebuild();
await ctx.dispose();
} else {
await ctx.watch();
}
+9
View File
@@ -0,0 +1,9 @@
{
"id": "beaver",
"name": "Beaver",
"version": "0.1.0",
"minAppVersion": "1.5.0",
"description": "Send the current note to a Beaver agent via the markdown frontend.",
"author": "Beaver",
"isDesktopOnly": false
}
+602
View File
@@ -0,0 +1,602 @@
{
"name": "beaver-plugin-obsidian",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "beaver-plugin-obsidian",
"version": "0.1.0",
"devDependencies": {
"@types/node": "^20.11.0",
"builtin-modules": "^4.0.0",
"esbuild": "^0.21.5",
"obsidian": "^1.5.7",
"typescript": "^5.4.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.38.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@types/codemirror": {
"version": "5.60.8",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/tern": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/tern": {
"version": "0.23.9",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/builtin-modules": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz",
"integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/obsidian": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/codemirror": "5.60.8",
"moment": "2.29.4"
},
"peerDependencies": {
"@codemirror/state": "6.5.0",
"@codemirror/view": "6.38.6"
}
},
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"dev": true,
"license": "MIT",
"peer": true
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "beaver-plugin-obsidian",
"version": "0.1.0",
"description": "Obsidian plugin for Beaver's markdown frontend.",
"main": "main.js",
"private": true,
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
},
"devDependencies": {
"@types/node": "^20.11.0",
"builtin-modules": "^4.0.0",
"esbuild": "^0.21.5",
"obsidian": "^1.5.7",
"typescript": "^5.4.5"
}
}
+42
View File
@@ -0,0 +1,42 @@
import { App, FuzzySuggestModal } from "obsidian";
export class AgentPickerModal extends FuzzySuggestModal<string> {
private agents: string[];
private resolve: (agent: string | null) => void;
private resolved = false;
constructor(
app: App,
agents: string[],
resolve: (agent: string | null) => void,
) {
super(app);
this.agents = agents;
this.resolve = resolve;
this.setPlaceholder("Pick a Beaver agent…");
}
getItems(): string[] {
return this.agents;
}
getItemText(item: string): string {
return item;
}
onChooseItem(item: string): void {
this.resolved = true;
this.resolve(item);
}
onClose(): void {
super.onClose();
if (!this.resolved) this.resolve(null);
}
}
export function pickAgent(app: App, agents: string[]): Promise<string | null> {
return new Promise((resolve) => {
new AgentPickerModal(app, agents, resolve).open();
});
}
+91
View File
@@ -0,0 +1,91 @@
import { requestUrl, RequestUrlParam } from "obsidian";
import type { BeaverSettings } from "./settings";
export interface ChatRequest {
filename: string;
content: string;
agent?: string;
}
export interface ChatResponse {
status: "ok" | "nothing_to_do" | "in_progress";
reason?: string;
agent?: string;
turns_appended?: number;
new_content?: string;
}
export class BeaverApiError extends Error {
status: number;
body: unknown;
constructor(status: number, message: string, body: unknown) {
super(message);
this.status = status;
this.body = body;
}
}
function baseUrl(settings: BeaverSettings): string {
const url = settings.baseUrl.trim().replace(/\/+$/, "");
if (!url) throw new Error("Beaver: base URL is not configured");
return url;
}
function authHeader(settings: BeaverSettings): Record<string, string> {
const token = settings.token.trim();
if (!token) throw new Error("Beaver: bearer token is not configured");
return { Authorization: `Bearer ${token}` };
}
async function call(
settings: BeaverSettings,
init: Omit<RequestUrlParam, "url"> & { path: string },
): Promise<unknown> {
const { path, ...rest } = init;
const url = `${baseUrl(settings)}${path}`;
const headers = {
...authHeader(settings),
...(rest.headers ?? {}),
};
// throw=false → we handle non-2xx ourselves so we can extract FastAPI's
// {detail: "..."} body and surface a useful message.
const res = await requestUrl({ url, throw: false, ...rest, headers });
if (res.status < 200 || res.status >= 300) {
let detail: unknown;
try {
detail = res.json;
} catch {
detail = res.text;
}
const detailMsg =
detail && typeof detail === "object" && "detail" in detail
? String((detail as { detail: unknown }).detail)
: typeof detail === "string"
? detail
: `HTTP ${res.status}`;
throw new BeaverApiError(res.status, detailMsg, detail);
}
return res.json;
}
export async function listAgents(settings: BeaverSettings): Promise<string[]> {
const body = (await call(settings, { path: "/agents", method: "GET" })) as {
agents?: Array<{ name?: unknown }>;
};
const list = body.agents ?? [];
return list
.map((a) => (typeof a?.name === "string" ? a.name : null))
.filter((n): n is string => !!n);
}
export async function sendChat(
settings: BeaverSettings,
req: ChatRequest,
): Promise<ChatResponse> {
return (await call(settings, {
path: "/chat",
method: "POST",
contentType: "application/json",
body: JSON.stringify(req),
})) as ChatResponse;
}
+194
View File
@@ -0,0 +1,194 @@
import {
Editor,
MarkdownView,
Notice,
Plugin,
TFile,
} from "obsidian";
import {
BeaverApiError,
ChatResponse,
listAgents,
sendChat,
} from "./api";
import { pickAgent } from "./agentPicker";
import {
BeaverSettings,
BeaverSettingsTab,
DEFAULT_SETTINGS,
} from "./settings";
const AGENT_CACHE_TTL_MS = 5 * 60 * 1000;
export default class BeaverPlugin extends Plugin {
settings: BeaverSettings = { ...DEFAULT_SETTINGS };
private cachedAgents: string[] | null = null;
private cachedAt = 0;
private lastListFailed = false;
async onload(): Promise<void> {
await this.loadSettings();
this.addSettingTab(new BeaverSettingsTab(this.app, this));
this.addCommand({
id: "send-selected",
name: "Send using selected agent",
checkCallback: (checking) => {
const ctx = this.getFrontmatterAgentContext();
if (!ctx) return false;
if (!checking) void this.dispatch(ctx.file, ctx.agent);
return true;
},
});
this.addCommand({
id: "send-different",
name: "Send using different agent",
checkCallback: (checking) => {
const ctx = this.getFrontmatterAgentContext();
if (!ctx) return false;
if (!checking) void this.runDifferent(ctx.file);
return true;
},
});
}
async loadSettings(): Promise<void> {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData(),
);
}
async saveSettings(): Promise<void> {
await this.saveData(this.settings);
}
cacheAgents(agents: string[]): void {
this.cachedAgents = agents;
this.cachedAt = Date.now();
this.lastListFailed = false;
}
private getFrontmatterAgentContext():
| { file: TFile; agent: string }
| null {
const file = this.app.workspace.getActiveFile();
if (!file || file.extension !== "md") return null;
const cache = this.app.metadataCache.getFileCache(file);
const raw = cache?.frontmatter?.agent;
if (typeof raw !== "string" || !raw.trim()) return null;
return { file, agent: raw.trim() };
}
private async runDifferent(file: TFile): Promise<void> {
const agents = await this.getAgents();
if (!agents) return;
if (agents.length === 0) {
new Notice("Beaver: no agents available");
return;
}
const picked = await pickAgent(this.app, agents);
if (!picked) return;
await this.dispatch(file, picked);
}
private async getAgents(): Promise<string[] | null> {
const fresh =
this.cachedAgents &&
!this.lastListFailed &&
Date.now() - this.cachedAt < AGENT_CACHE_TTL_MS;
if (fresh) return this.cachedAgents;
try {
const agents = await listAgents(this.settings);
this.cacheAgents(agents);
return agents;
} catch (err) {
this.lastListFailed = true;
this.notifyError("listing agents", err);
return null;
}
}
private async dispatch(file: TFile, agent: string): Promise<void> {
const { editor, view } = this.findEditorFor(file);
const content = editor
? editor.getValue()
: await this.app.vault.read(file);
const notice = new Notice(`Beaver: sending to ${agent}`, 0);
let response: ChatResponse;
try {
response = await sendChat(this.settings, {
filename: file.path,
content,
agent,
});
} catch (err) {
notice.hide();
if (err instanceof BeaverApiError && err.status === 409) {
new Notice("Beaver: already running for this file", 6000);
return;
}
this.notifyError(`sending to ${agent}`, err);
return;
} finally {
notice.hide();
}
if (response.status === "nothing_to_do") {
new Notice(`Beaver: nothing to do (${response.reason ?? "no reason"})`);
return;
}
if (typeof response.new_content === "string") {
await this.writeBack(file, editor, view, response.new_content);
}
new Notice(`Beaver: ${agent} replied`);
}
private findEditorFor(file: TFile): {
editor: Editor | null;
view: MarkdownView | null;
} {
// Walk open markdown views — we want the editor instance that owns
// ``file`` so we can use setValue (keeps the buffer's edit history
// intact) instead of falling back to vault.modify.
const leaves = this.app.workspace.getLeavesOfType("markdown");
for (const leaf of leaves) {
const view = leaf.view as MarkdownView;
if (view?.file?.path === file.path) {
return { editor: view.editor, view };
}
}
return { editor: null, view: null };
}
private async writeBack(
file: TFile,
editor: Editor | null,
_view: MarkdownView | null,
newContent: string,
): Promise<void> {
if (editor && editor.getValue() !== newContent) {
editor.setValue(newContent);
return;
}
if (!editor) {
await this.app.vault.modify(file, newContent);
}
}
private notifyError(action: string, err: unknown): void {
const msg =
err instanceof BeaverApiError
? `${err.status}: ${err.message}`
: err instanceof Error
? err.message
: String(err);
new Notice(`Beaver (${action}): ${msg}`, 8000);
console.error("Beaver:", action, err);
}
}
+75
View File
@@ -0,0 +1,75 @@
import { App, Notice, PluginSettingTab, Setting } from "obsidian";
import { listAgents, BeaverApiError } from "./api";
import type BeaverPlugin from "./main";
export interface BeaverSettings {
baseUrl: string;
token: string;
}
export const DEFAULT_SETTINGS: BeaverSettings = {
baseUrl: "http://localhost:62993",
token: "",
};
export class BeaverSettingsTab extends PluginSettingTab {
plugin: BeaverPlugin;
constructor(app: App, plugin: BeaverPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName("Base URL")
.setDesc("Markdown frontend root, e.g. http://localhost:62993")
.addText((text) =>
text
.setPlaceholder("http://localhost:62993")
.setValue(this.plugin.settings.baseUrl)
.onChange(async (value) => {
this.plugin.settings.baseUrl = value.trim().replace(/\/+$/, "");
await this.plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName("Bearer token")
.setDesc("Token with the `messages` scope.")
.addText((text) => {
text.inputEl.type = "password";
text
.setPlaceholder("paste token")
.setValue(this.plugin.settings.token)
.onChange(async (value) => {
this.plugin.settings.token = value;
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("Test connection")
.setDesc("Calls GET /agents and reports the count.")
.addButton((btn) =>
btn.setButtonText("Test").onClick(async () => {
try {
const agents = await listAgents(this.plugin.settings);
this.plugin.cacheAgents(agents);
new Notice(`Beaver: found ${agents.length} agents`);
} catch (err) {
const msg =
err instanceof BeaverApiError
? `${err.status}: ${err.message}`
: err instanceof Error
? err.message
: String(err);
new Notice(`Beaver: ${msg}`, 8000);
}
}),
);
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES2022",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "Bundler",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"esModuleInterop": true,
"lib": ["DOM", "ES2022"]
},
"include": ["src/**/*.ts"]
}
+3
View File
@@ -0,0 +1,3 @@
{
"0.1.0": "1.5.0"
}