feat: initial release of Claude Context plugin

Obsidian plugin to copy vault context files to clipboard for AI assistants.

Features:
- Copy vault context files to clipboard with one hotkey
- Context generator with configurable conventions
- Settings for folder, separator, preview, exclusions
- Auto-detect vault folder structure
- Include active note option
- Ribbon icon for quick access
- Full English UI and documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Luca G. Oelfke 2026-02-05 16:55:43 +01:00
commit 2d08546847
No known key found for this signature in database
GPG key ID: E22BABF67200F864
20 changed files with 2280 additions and 0 deletions

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

28
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Node.js build
on:
push:
branches: ["**"]
pull_request:
branches: ["**"]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x, 22.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm ci
- run: npm run build --if-present
- run: npm run lint

22
.gitignore vendored Normal file
View file

@ -0,0 +1,22 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store

1
.npmrc Normal file
View file

@ -0,0 +1 @@
tag-version-prefix=""

251
AGENTS.md Normal file
View file

@ -0,0 +1,251 @@
# Obsidian community plugin
## Project overview
- Target: Obsidian Community Plugin (TypeScript → bundled JavaScript).
- Entry point: `main.ts` compiled to `main.js` and loaded by Obsidian.
- Required release artifacts: `main.js`, `manifest.json`, and optional `styles.css`.
## Environment & tooling
- Node.js: use current LTS (Node 18+ recommended).
- **Package manager: npm** (required for this sample - `package.json` defines npm scripts and dependencies).
- **Bundler: esbuild** (required for this sample - `esbuild.config.mjs` and build scripts depend on it). Alternative bundlers like Rollup or webpack are acceptable for other projects if they bundle all external dependencies into `main.js`.
- Types: `obsidian` type definitions.
**Note**: This sample project has specific technical dependencies on npm and esbuild. If you're creating a plugin from scratch, you can choose different tools, but you'll need to replace the build configuration accordingly.
### Install
```bash
npm install
```
### Dev (watch)
```bash
npm run dev
```
### Production build
```bash
npm run build
```
## Linting
- To use eslint install eslint from terminal: `npm install -g eslint`
- To use eslint to analyze this project use this command: `eslint main.ts`
- eslint will then create a report with suggestions for code improvement by file and line number.
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder: `eslint ./src/`
## File & folder conventions
- **Organize code into multiple files**: Split functionality across separate modules rather than putting everything in `main.ts`.
- Source lives in `src/`. Keep `main.ts` small and focused on plugin lifecycle (loading, unloading, registering commands).
- **Example file structure**:
```
src/
main.ts # Plugin entry point, lifecycle management
settings.ts # Settings interface and defaults
commands/ # Command implementations
command1.ts
command2.ts
ui/ # UI components, modals, views
modal.ts
view.ts
utils/ # Utility functions, helpers
helpers.ts
constants.ts
types.ts # TypeScript interfaces and types
```
- **Do not commit build artifacts**: Never commit `node_modules/`, `main.js`, or other generated files to version control.
- Keep the plugin small. Avoid large dependencies. Prefer browser-compatible packages.
- Generated output should be placed at the plugin root or `dist/` depending on your build setup. Release artifacts must end up at the top level of the plugin folder in the vault (`main.js`, `manifest.json`, `styles.css`).
## Manifest rules (`manifest.json`)
- Must include (non-exhaustive):
- `id` (plugin ID; for local dev it should match the folder name)
- `name`
- `version` (Semantic Versioning `x.y.z`)
- `minAppVersion`
- `description`
- `isDesktopOnly` (boolean)
- Optional: `author`, `authorUrl`, `fundingUrl` (string or map)
- Never change `id` after release. Treat it as stable API.
- Keep `minAppVersion` accurate when using newer APIs.
- Canonical requirements are coded here: https://github.com/obsidianmd/obsidian-releases/blob/master/.github/workflows/validate-plugin-entry.yml
## Testing
- Manual install for testing: copy `main.js`, `manifest.json`, `styles.css` (if any) to:
```
<Vault>/.obsidian/plugins/<plugin-id>/
```
- Reload Obsidian and enable the plugin in **Settings → Community plugins**.
## Commands & settings
- Any user-facing commands should be added via `this.addCommand(...)`.
- If the plugin has configuration, provide a settings tab and sensible defaults.
- Persist settings using `this.loadData()` / `this.saveData()`.
- Use stable command IDs; avoid renaming once released.
## Versioning & releases
- Bump `version` in `manifest.json` (SemVer) and update `versions.json` to map plugin version → minimum app version.
- Create a GitHub release whose tag exactly matches `manifest.json`'s `version`. Do not use a leading `v`.
- Attach `manifest.json`, `main.js`, and `styles.css` (if present) to the release as individual assets.
- After the initial release, follow the process to add/update your plugin in the community catalog as required.
## Security, privacy, and compliance
Follow Obsidian's **Developer Policies** and **Plugin Guidelines**. In particular:
- Default to local/offline operation. Only make network requests when essential to the feature.
- No hidden telemetry. If you collect optional analytics or call third-party services, require explicit opt-in and document clearly in `README.md` and in settings.
- Never execute remote code, fetch and eval scripts, or auto-update plugin code outside of normal releases.
- Minimize scope: read/write only what's necessary inside the vault. Do not access files outside the vault.
- Clearly disclose any external services used, data sent, and risks.
- Respect user privacy. Do not collect vault contents, filenames, or personal information unless absolutely necessary and explicitly consented.
- Avoid deceptive patterns, ads, or spammy notifications.
- Register and clean up all DOM, app, and interval listeners using the provided `register*` helpers so the plugin unloads safely.
## UX & copy guidelines (for UI text, commands, settings)
- Prefer sentence case for headings, buttons, and titles.
- Use clear, action-oriented imperatives in step-by-step copy.
- Use **bold** to indicate literal UI labels. Prefer "select" for interactions.
- Use arrow notation for navigation: **Settings → Community plugins**.
- Keep in-app strings short, consistent, and free of jargon.
## Performance
- Keep startup light. Defer heavy work until needed.
- Avoid long-running tasks during `onload`; use lazy initialization.
- Batch disk access and avoid excessive vault scans.
- Debounce/throttle expensive operations in response to file system events.
## Coding conventions
- TypeScript with `"strict": true` preferred.
- **Keep `main.ts` minimal**: Focus only on plugin lifecycle (onload, onunload, addCommand calls). Delegate all feature logic to separate modules.
- **Split large files**: If any file exceeds ~200-300 lines, consider breaking it into smaller, focused modules.
- **Use clear module boundaries**: Each file should have a single, well-defined responsibility.
- Bundle everything into `main.js` (no unbundled runtime deps).
- Avoid Node/Electron APIs if you want mobile compatibility; set `isDesktopOnly` accordingly.
- Prefer `async/await` over promise chains; handle errors gracefully.
## Mobile
- Where feasible, test on iOS and Android.
- Don't assume desktop-only behavior unless `isDesktopOnly` is `true`.
- Avoid large in-memory structures; be mindful of memory and storage constraints.
## Agent do/don't
**Do**
- Add commands with stable IDs (don't rename once released).
- Provide defaults and validation in settings.
- Write idempotent code paths so reload/unload doesn't leak listeners or intervals.
- Use `this.register*` helpers for everything that needs cleanup.
**Don't**
- Introduce network calls without an obvious user-facing reason and documentation.
- Ship features that require cloud services without clear disclosure and explicit opt-in.
- Store or transmit vault contents unless essential and consented.
## Common tasks
### Organize code across multiple files
**main.ts** (minimal, lifecycle only):
```ts
import { Plugin } from "obsidian";
import { MySettings, DEFAULT_SETTINGS } from "./settings";
import { registerCommands } from "./commands";
export default class MyPlugin extends Plugin {
settings: MySettings;
async onload() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
registerCommands(this);
}
}
```
**settings.ts**:
```ts
export interface MySettings {
enabled: boolean;
apiKey: string;
}
export const DEFAULT_SETTINGS: MySettings = {
enabled: true,
apiKey: "",
};
```
**commands/index.ts**:
```ts
import { Plugin } from "obsidian";
import { doSomething } from "./my-command";
export function registerCommands(plugin: Plugin) {
plugin.addCommand({
id: "do-something",
name: "Do something",
callback: () => doSomething(plugin),
});
}
```
### Add a command
```ts
this.addCommand({
id: "your-command-id",
name: "Do the thing",
callback: () => this.doTheThing(),
});
```
### Persist settings
```ts
interface MySettings { enabled: boolean }
const DEFAULT_SETTINGS: MySettings = { enabled: true };
async onload() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
await this.saveData(this.settings);
}
```
### Register listeners safely
```ts
this.registerEvent(this.app.workspace.on("file-open", f => { /* ... */ }));
this.registerDomEvent(window, "resize", () => { /* ... */ });
this.registerInterval(window.setInterval(() => { /* ... */ }, 1000));
```
## Troubleshooting
- Plugin doesn't load after build: ensure `main.js` and `manifest.json` are at the top level of the plugin folder under `<Vault>/.obsidian/plugins/<plugin-id>/`.
- Build issues: if `main.js` is missing, run `npm run build` or `npm run dev` to compile your TypeScript source code.
- Commands not appearing: verify `addCommand` runs after `onload` and IDs are unique.
- Settings not persisting: ensure `loadData`/`saveData` are awaited and you re-render the UI after changes.
- Mobile-only issues: confirm you're not using desktop-only APIs; check `isDesktopOnly` and adjust.
## References
- Obsidian sample plugin: https://github.com/obsidianmd/obsidian-sample-plugin
- API documentation: https://docs.obsidian.md
- Developer policies: https://docs.obsidian.md/Developer+policies
- Plugin guidelines: https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines
- Style guide: https://help.obsidian.md/style-guide

5
LICENSE Normal file
View file

@ -0,0 +1,5 @@
Copyright (C) 2020-2025 by Dynalist Inc.
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

285
README.md Normal file
View file

@ -0,0 +1,285 @@
<div align="center">
# Claude Context
[![Obsidian](https://img.shields.io/badge/Obsidian-Plugin-7C3AED?logo=obsidian&logoColor=white)](https://obsidian.md)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.8-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
**Copy your vault context to clipboard with one hotkey.**
Give AI assistants (Claude, ChatGPT, etc.) instant understanding of your vault's conventions, structure, and workflows.
[Features](#features) • [Installation](#installation) • [Usage](#usage) • [Configuration](#configuration) • [Generator](#context-generator)
</div>
---
## The Problem
When working with AI assistants on your Obsidian vault, you constantly need to explain:
- How you name files
- Where things go
- What link style you use
- Your tag conventions
- Your workflows
This is repetitive and error-prone. The AI forgets, you forget to mention something, and inconsistencies creep in.
---
## The Solution
**Claude Context** stores your vault's conventions in a dedicated folder (`_claude/`) and copies everything to your clipboard with a single hotkey. Paste it into any AI chat, and the assistant immediately understands how to work with your vault.
---
<div align="center">
## Features
</div>
### 📋 One-Click Context Copy
Press a hotkey or click the ribbon icon to copy all context files to your clipboard. The output is formatted with clear separators between files.
### ⚙️ Context Generator
Don't want to write context files manually? The built-in generator walks you through your preferences:
- **Language** - English or German
- **File naming** - kebab-case, snake_case, camelCase, or free
- **Link style** - Wikilinks or Markdown
- **Tag style** - Hierarchical, flat, or none
- **Frontmatter fields** - Customize required fields
- **Custom rules** - Add your own conventions
- **Folder purposes** - Auto-detects your folders, you describe their purpose
- **Templates** - Define your note templates
Click "Generate" and your context files are created instantly.
### 📁 Auto-Detect Vault Structure
The generator automatically scans your vault's folder structure. Just fill in the purpose for each folder, and `structure.md` is generated with your actual directories.
### 🔧 Flexible Settings
- **Context folder** - Change from `_claude/` to any name
- **Separator** - Customize the separator between files
- **Include filenames** - Toggle `# === filename.md ===` headers
- **Show preview** - Preview before copying
- **Include active note** - Append the currently open note
- **Excluded files** - Skip specific files from being copied
### 📝 Include Active Note
Working on a specific note and want context for it? Use "Copy context with current note" or enable "Include active note" in settings. The current note is appended to your context with an `ACTIVE:` prefix.
---
<div align="center">
## Installation
</div>
### From Source
1. Clone this repository into your vault's plugins folder:
```bash
cd /path/to/vault/.obsidian/plugins
git clone https://github.com/yourusername/obsidian-claude-context.git claude-context
```
2. Install dependencies and build:
```bash
cd claude-context
npm install
npm run build
```
3. Enable the plugin in Obsidian:
- Settings → Community Plugins → Enable "Claude Context"
### Development Setup
For development with hot reload:
```bash
cd /path/to/vault/.obsidian/plugins/claude-context
npm run dev
```
---
<div align="center">
## Usage
</div>
### Quick Start
1. **Generate context files**: `Ctrl+P` → "Claude Context: Generate context files"
2. **Configure your preferences** in the generator modal
3. **Click "Generate"** to create your context files
4. **Copy context**: `Ctrl+P` → "Claude Context: Copy context to clipboard"
5. **Paste** into ChatGPT, Claude, or any AI assistant
### Commands
| Command | Description |
|---------|-------------|
| Copy context to clipboard | Copies all context files |
| Copy context with current note | Copies context + active note |
| Generate context files | Opens the context generator |
### Ribbon Icon
Click the clipboard icon in the left sidebar for quick access to "Copy context to clipboard".
---
<div align="center">
## Configuration
</div>
### Settings
Access via Settings → Claude Context:
| Setting | Description | Default |
|---------|-------------|---------|
| Context folder | Folder containing context files | `_claude` |
| Separator | Text between files | `---` |
| Include filenames | Add filename headers | On |
| Show preview | Preview before copying | Off |
| Include active note | Always include open note | Off |
| Excluded files | Files to skip (comma-separated) | Empty |
### Context Files
The plugin expects markdown files in your context folder:
```
_claude/
├── VAULT.md # Main entry point (always first)
├── conventions.md # Naming and formatting rules
├── structure.md # Folder organization
├── workflows.md # How to do things
├── templates.md # Note templates
└── examples.md # Concrete examples
```
`VAULT.md` is always copied first. Other files are sorted alphabetically.
---
<div align="center">
## Context Generator
</div>
The generator creates a complete context setup based on your preferences.
### Sections
**General**
- Vault description (free text explaining your vault's purpose)
- Language selection
**Formatting**
- File naming convention
- Link style (Wikilinks vs Markdown)
- Maximum heading depth
- Date format
**Tags**
- Tag style (hierarchical, flat, or none)
- Predefined tags for consistency
**Frontmatter**
- Required fields for all notes
**Rules**
- Custom rules for the AI to follow
- Forbidden actions (folders/files the AI should never touch)
**Folder Structure**
- Auto-detected folders with purpose fields
- Generates "where does what go" documentation
**Note Templates**
- Define template names, target folders, and tags
- Generates template documentation
**Files to Generate**
- Toggle which context files to create
- Skip files you don't need
---
<div align="center">
## Example Output
</div>
When you copy context, you get something like:
```markdown
# === VAULT.md ===
# Vault Context for AI Assistants
Personal Zettelkasten for development and knowledge management.
## Quick Reference
- Language: English
- File naming: kebab-case
- Links: [[wikilinks]]
- Frontmatter: `date`, `tags`
...
---
# === conventions.md ===
# Conventions
## Language
- Vault language: English
...
---
# === structure.md ===
...
```
Paste this into any AI chat, and it immediately understands your vault.
---
<div align="center">
## License
</div>
MIT License - see [LICENSE](LICENSE) for details.
---
<div align="center">
Made with ♥ for the Obsidian community
</div>

49
esbuild.config.mjs Normal file
View file

@ -0,0 +1,49 @@
import esbuild from "esbuild";
import process from "process";
import { builtinModules } from 'node:module';
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const context = await esbuild.context({
banner: {
js: banner,
},
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",
...builtinModules],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

34
eslint.config.mts Normal file
View file

@ -0,0 +1,34 @@
import tseslint from 'typescript-eslint';
import obsidianmd from "eslint-plugin-obsidianmd";
import globals from "globals";
import { globalIgnores } from "eslint/config";
export default tseslint.config(
{
languageOptions: {
globals: {
...globals.browser,
},
parserOptions: {
projectService: {
allowDefaultProject: [
'eslint.config.js',
'manifest.json'
]
},
tsconfigRootDir: import.meta.dirname,
extraFileExtensions: ['.json']
},
},
},
...obsidianmd.configs.recommended,
globalIgnores([
"node_modules",
"dist",
"esbuild.config.mjs",
"eslint.config.js",
"version-bump.mjs",
"versions.json",
"main.js",
]),
);

9
manifest.json Normal file
View file

@ -0,0 +1,9 @@
{
"id": "claude-context",
"name": "Claude Context",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Copy .claude/ context files to clipboard with one hotkey.",
"author": "Luca",
"isDesktopOnly": true
}

614
package-lock.json generated Normal file
View file

@ -0,0 +1,614 @@
{
"name": "obsidian-claude-context",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-claude-context",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"obsidian": "latest"
},
"devDependencies": {
"@types/node": "^16.11.6",
"esbuild": "0.25.5",
"tslib": "2.4.0",
"typescript": "^5.8.3"
}
},
"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==",
"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==",
"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.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"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==",
"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==",
"license": "MIT",
"dependencies": {
"@types/tern": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "16.18.126",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
"dev": true,
"license": "MIT"
},
"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==",
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT",
"peer": true
},
"node_modules/esbuild": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.5",
"@esbuild/android-arm": "0.25.5",
"@esbuild/android-arm64": "0.25.5",
"@esbuild/android-x64": "0.25.5",
"@esbuild/darwin-arm64": "0.25.5",
"@esbuild/darwin-x64": "0.25.5",
"@esbuild/freebsd-arm64": "0.25.5",
"@esbuild/freebsd-x64": "0.25.5",
"@esbuild/linux-arm": "0.25.5",
"@esbuild/linux-arm64": "0.25.5",
"@esbuild/linux-ia32": "0.25.5",
"@esbuild/linux-loong64": "0.25.5",
"@esbuild/linux-mips64el": "0.25.5",
"@esbuild/linux-ppc64": "0.25.5",
"@esbuild/linux-riscv64": "0.25.5",
"@esbuild/linux-s390x": "0.25.5",
"@esbuild/linux-x64": "0.25.5",
"@esbuild/netbsd-arm64": "0.25.5",
"@esbuild/netbsd-x64": "0.25.5",
"@esbuild/openbsd-arm64": "0.25.5",
"@esbuild/openbsd-x64": "0.25.5",
"@esbuild/sunos-x64": "0.25.5",
"@esbuild/win32-arm64": "0.25.5",
"@esbuild/win32-ia32": "0.25.5",
"@esbuild/win32-x64": "0.25.5"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/obsidian": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.10.3.tgz",
"integrity": "sha512-VP+ZSxNMG7y6Z+sU9WqLvJAskCfkFrTz2kFHWmmzis+C+4+ELjk/sazwcTHrHXNZlgCeo8YOlM6SOrAFCynNew==",
"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==",
"license": "MIT",
"peer": true
},
"node_modules/tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
"dev": true,
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"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==",
"license": "MIT",
"peer": true
}
}
}

21
package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "obsidian-claude-context",
"version": "1.0.0",
"description": "Copy .claude/ context files to clipboard",
"main": "main.js",
"type": "module",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
},
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"esbuild": "0.25.5",
"tslib": "2.4.0",
"typescript": "^5.8.3"
},
"dependencies": {
"obsidian": "latest"
}
}

629
src/generator.ts Normal file
View file

@ -0,0 +1,629 @@
import { App, Modal, Setting, TFolder } from 'obsidian';
import ClaudeContextPlugin from './main';
interface FolderConfig {
name: string;
purpose: string;
}
interface TemplateConfig {
name: string;
folder: string;
tag: string;
}
interface ContextConfig {
// Basic
vaultDescription: string;
language: string;
// Formatting
fileNaming: string;
linkStyle: string;
headingDepth: string;
dateFormat: string;
// Tags
tagsStyle: string;
customTags: string[];
// Frontmatter
frontmatterFields: string[];
// Rules
customRules: string[];
forbiddenActions: string[];
// Structure
folders: FolderConfig[];
// Templates
templates: TemplateConfig[];
// Files to generate
generateFiles: {
conventions: boolean;
structure: boolean;
workflows: boolean;
templates: boolean;
examples: boolean;
};
}
const DEFAULT_CONFIG: ContextConfig = {
vaultDescription: '',
language: 'english',
fileNaming: 'kebab-case',
linkStyle: 'wikilinks',
headingDepth: 'h3',
dateFormat: 'YYYY-MM-DD',
tagsStyle: 'hierarchical',
customTags: [],
frontmatterFields: ['date', 'tags'],
customRules: [],
forbiddenActions: ['.obsidian/'],
folders: [],
templates: [],
generateFiles: {
conventions: true,
structure: true,
workflows: true,
templates: true,
examples: true,
},
};
export class ContextGeneratorModal extends Modal {
plugin: ClaudeContextPlugin;
config: ContextConfig;
constructor(app: App, plugin: ClaudeContextPlugin) {
super(app);
this.plugin = plugin;
this.config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
this.config.folders = this.scanVaultStructure().map(name => ({ name, purpose: '' }));
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass('claude-context-generator');
contentEl.style.maxHeight = '80vh';
contentEl.style.overflow = 'auto';
contentEl.createEl('h2', { text: 'Context Generator' });
// === BASIC SECTION ===
contentEl.createEl('h3', { text: 'General' });
new Setting(contentEl)
.setName('Vault description')
.setDesc('What is this vault used for?')
.addTextArea(text => {
text.setPlaceholder('e.g. Personal Zettelkasten for development and knowledge management')
.setValue(this.config.vaultDescription)
.onChange(v => this.config.vaultDescription = v);
text.inputEl.rows = 2;
text.inputEl.style.width = '100%';
});
new Setting(contentEl)
.setName('Language')
.addDropdown(dropdown => dropdown
.addOption('english', 'English')
.addOption('german', 'Deutsch')
.setValue(this.config.language)
.onChange(v => this.config.language = v));
// === FORMATTING SECTION ===
contentEl.createEl('h3', { text: 'Formatting' });
new Setting(contentEl)
.setName('File naming')
.addDropdown(dropdown => dropdown
.addOption('kebab-case', 'kebab-case')
.addOption('snake_case', 'snake_case')
.addOption('camelCase', 'camelCase')
.addOption('free', 'Free / no convention')
.setValue(this.config.fileNaming)
.onChange(v => this.config.fileNaming = v));
new Setting(contentEl)
.setName('Link style')
.addDropdown(dropdown => dropdown
.addOption('wikilinks', '[[Wikilinks]]')
.addOption('markdown', '[Markdown](links)')
.setValue(this.config.linkStyle)
.onChange(v => this.config.linkStyle = v));
new Setting(contentEl)
.setName('Heading depth')
.addDropdown(dropdown => dropdown
.addOption('h2', 'H1 - H2')
.addOption('h3', 'H1 - H3')
.addOption('h4', 'H1 - H4')
.addOption('h6', 'Unlimited')
.setValue(this.config.headingDepth)
.onChange(v => this.config.headingDepth = v));
new Setting(contentEl)
.setName('Date format')
.addDropdown(dropdown => dropdown
.addOption('YYYY-MM-DD', 'YYYY-MM-DD (ISO)')
.addOption('DD.MM.YYYY', 'DD.MM.YYYY')
.addOption('MM/DD/YYYY', 'MM/DD/YYYY')
.setValue(this.config.dateFormat)
.onChange(v => this.config.dateFormat = v));
// === TAGS SECTION ===
contentEl.createEl('h3', { text: 'Tags' });
new Setting(contentEl)
.setName('Tag style')
.addDropdown(dropdown => dropdown
.addOption('hierarchical', 'Hierarchical (#area/tag)')
.addOption('flat', 'Flat (#tag)')
.addOption('none', 'No tags')
.setValue(this.config.tagsStyle)
.onChange(v => this.config.tagsStyle = v));
new Setting(contentEl)
.setName('Predefined tags')
.setDesc('Comma-separated (e.g. status/active, status/done, project)')
.addText(text => text
.setPlaceholder('tag1, tag2, area/tag3')
.setValue(this.config.customTags.join(', '))
.onChange(v => {
this.config.customTags = v.split(',').map(s => s.trim()).filter(s => s);
}));
// === FRONTMATTER SECTION ===
contentEl.createEl('h3', { text: 'Frontmatter' });
new Setting(contentEl)
.setName('Frontmatter fields')
.setDesc('Comma-separated (e.g. date, tags, aliases, status)')
.addText(text => text
.setValue(this.config.frontmatterFields.join(', '))
.onChange(v => {
this.config.frontmatterFields = v.split(',').map(s => s.trim()).filter(s => s);
}));
// === RULES SECTION ===
contentEl.createEl('h3', { text: 'Rules' });
new Setting(contentEl)
.setName('Custom rules')
.setDesc('One rule per line')
.addTextArea(text => {
text.setPlaceholder('Proactively link related notes\nAlways fill frontmatter')
.setValue(this.config.customRules.join('\n'))
.onChange(v => {
this.config.customRules = v.split('\n').map(s => s.trim()).filter(s => s);
});
text.inputEl.rows = 3;
text.inputEl.style.width = '100%';
});
new Setting(contentEl)
.setName('Forbidden actions')
.setDesc('Comma-separated (e.g. .obsidian/, certain folders)')
.addText(text => text
.setValue(this.config.forbiddenActions.join(', '))
.onChange(v => {
this.config.forbiddenActions = v.split(',').map(s => s.trim()).filter(s => s);
}));
// === STRUCTURE SECTION ===
contentEl.createEl('h3', { text: 'Folder structure' });
contentEl.createEl('p', {
text: 'Describe the purpose of your folders:',
cls: 'setting-item-description'
});
const foldersContainer = contentEl.createDiv({ cls: 'folders-container' });
this.renderFolders(foldersContainer);
// === TEMPLATES SECTION ===
contentEl.createEl('h3', { text: 'Note templates' });
const templatesContainer = contentEl.createDiv({ cls: 'templates-container' });
this.renderTemplates(templatesContainer);
new Setting(contentEl)
.addButton(btn => btn
.setButtonText('+ Add template')
.onClick(() => {
this.config.templates.push({ name: '', folder: '', tag: '' });
this.renderTemplates(templatesContainer);
}));
// === FILES TO GENERATE ===
contentEl.createEl('h3', { text: 'Files to generate' });
new Setting(contentEl)
.setName('conventions.md')
.addToggle(toggle => toggle
.setValue(this.config.generateFiles.conventions)
.onChange(v => this.config.generateFiles.conventions = v));
new Setting(contentEl)
.setName('structure.md')
.addToggle(toggle => toggle
.setValue(this.config.generateFiles.structure)
.onChange(v => this.config.generateFiles.structure = v));
new Setting(contentEl)
.setName('workflows.md')
.addToggle(toggle => toggle
.setValue(this.config.generateFiles.workflows)
.onChange(v => this.config.generateFiles.workflows = v));
new Setting(contentEl)
.setName('templates.md')
.addToggle(toggle => toggle
.setValue(this.config.generateFiles.templates)
.onChange(v => this.config.generateFiles.templates = v));
new Setting(contentEl)
.setName('examples.md')
.addToggle(toggle => toggle
.setValue(this.config.generateFiles.examples)
.onChange(v => this.config.generateFiles.examples = v));
// === GENERATE BUTTON ===
contentEl.createEl('hr');
new Setting(contentEl)
.addButton(btn => btn
.setButtonText('Generate')
.setCta()
.onClick(() => this.generate()));
}
renderFolders(container: HTMLElement) {
container.empty();
for (const folder of this.config.folders) {
const row = container.createDiv({ cls: 'folder-row' });
row.style.display = 'flex';
row.style.gap = '10px';
row.style.marginBottom = '8px';
row.style.alignItems = 'center';
const label = row.createEl('span', { text: `${folder.name}/` });
label.style.minWidth = '120px';
label.style.fontFamily = 'monospace';
const input = row.createEl('input', { type: 'text' });
input.placeholder = 'Purpose...';
input.value = folder.purpose;
input.style.flex = '1';
input.addEventListener('input', () => {
folder.purpose = input.value;
});
}
}
renderTemplates(container: HTMLElement) {
container.empty();
this.config.templates.forEach((template, index) => {
const row = container.createDiv({ cls: 'template-row' });
row.style.display = 'flex';
row.style.gap = '10px';
row.style.marginBottom = '8px';
row.style.alignItems = 'center';
const nameInput = row.createEl('input', { type: 'text' });
nameInput.placeholder = 'Name (e.g. daily)';
nameInput.value = template.name;
nameInput.style.flex = '1';
nameInput.addEventListener('input', () => {
template.name = nameInput.value;
});
const folderInput = row.createEl('input', { type: 'text' });
folderInput.placeholder = 'Target folder';
folderInput.value = template.folder;
folderInput.style.flex = '1';
folderInput.addEventListener('input', () => {
template.folder = folderInput.value;
});
const tagInput = row.createEl('input', { type: 'text' });
tagInput.placeholder = 'Tag';
tagInput.value = template.tag;
tagInput.style.flex = '1';
tagInput.addEventListener('input', () => {
template.tag = tagInput.value;
});
const removeBtn = row.createEl('button', { text: '✕' });
removeBtn.addEventListener('click', () => {
this.config.templates.splice(index, 1);
this.renderTemplates(container);
});
});
}
scanVaultStructure(): string[] {
const root = this.app.vault.getRoot();
const folders: string[] = [];
for (const child of root.children) {
if (child instanceof TFolder && !child.name.startsWith('.')) {
folders.push(child.name);
}
}
return folders.sort();
}
async generate() {
const folder = this.plugin.settings.contextFolder;
if (!this.app.vault.getAbstractFileByPath(folder)) {
await this.app.vault.createFolder(folder);
}
await this.createFile(folder, 'VAULT.md', this.generateVaultMd());
if (this.config.generateFiles.conventions) {
await this.createFile(folder, 'conventions.md', this.generateConventionsMd());
}
if (this.config.generateFiles.structure) {
await this.createFile(folder, 'structure.md', this.generateStructureMd());
}
if (this.config.generateFiles.workflows) {
await this.createFile(folder, 'workflows.md', this.generateWorkflowsMd());
}
if (this.config.generateFiles.templates) {
await this.createFile(folder, 'templates.md', this.generateTemplatesMd());
}
if (this.config.generateFiles.examples) {
await this.createFile(folder, 'examples.md', this.generateExamplesMd());
}
this.close();
new (await import('obsidian')).Notice(`Context files generated in ${folder}/`);
}
async createFile(folder: string, name: string, content: string) {
const path = `${folder}/${name}`;
const existing = this.app.vault.getAbstractFileByPath(path);
if (existing) {
await this.app.vault.modify(existing as any, content);
} else {
await this.app.vault.create(path, content);
}
}
generateVaultMd(): string {
const lang = this.config.language === 'german' ? 'German' : 'English';
const links = this.config.linkStyle === 'wikilinks' ? '[[wikilinks]]' : '[markdown](links)';
const tags = this.config.tagsStyle === 'hierarchical' ? 'hierarchical (#area/tag)' :
this.config.tagsStyle === 'flat' ? 'flat (#tag)' : 'none';
const depth = this.config.headingDepth.toUpperCase();
const description = this.config.vaultDescription
? `${this.config.vaultDescription}\n\n`
: '';
const docLinks: string[] = [];
if (this.config.generateFiles.conventions) docLinks.push('- [[conventions]] - Naming and formatting conventions');
if (this.config.generateFiles.structure) docLinks.push('- [[structure]] - Folder structure and organization');
if (this.config.generateFiles.workflows) docLinks.push('- [[workflows]] - Workflows and processes');
if (this.config.generateFiles.templates) docLinks.push('- [[templates]] - Note templates');
if (this.config.generateFiles.examples) docLinks.push('- [[examples]] - Concrete examples');
const defaultRules = [
'Proactively link related notes',
`Never delete \`${this.plugin.settings.contextFolder}/\` or \`.obsidian/\``,
'Always fill frontmatter',
];
const allRules = [...defaultRules, ...this.config.customRules];
const rulesText = allRules.map((r, i) => `${i + 1}. ${r}`).join('\n');
return `# Vault Context for AI Assistants
${description}## Quick Reference
- Language: ${lang}
- File naming: ${this.config.fileNaming}
- Links: ${links}
- Frontmatter: \`${this.config.frontmatterFields.join('`, `')}\`
- Date format: ${this.config.dateFormat}
- Tags: ${tags}
- Headings: H1 to ${depth}
## Documentation
${docLinks.join('\n')}
## Important Rules
${rulesText}
`;
}
generateConventionsMd(): string {
const lang = this.config.language === 'german' ? 'German' : 'English';
const depth = this.config.headingDepth.toUpperCase();
const linkDesc = this.config.linkStyle === 'wikilinks'
? '- Wikilinks: [[note-name]]\n- No Markdown links'
: '- Markdown links: [text](path)\n- No Wikilinks';
let tagsSection = '';
if (this.config.tagsStyle !== 'none') {
const tagsDesc = this.config.tagsStyle === 'hierarchical'
? '- Hierarchical: #area/subcategory'
: '- Flat: #tagname';
let customTagsText = '';
if (this.config.customTags.length > 0) {
customTagsText = `\n- Predefined: ${this.config.customTags.map(t => `#${t}`).join(', ')}`;
}
tagsSection = `\n## Tags\n\n${tagsDesc}${customTagsText}\n`;
}
const frontmatterText = this.config.frontmatterFields.map(f => ` - ${f}`).join('\n');
return `# Conventions
## Language
- Vault language: ${lang}
## File Naming
- Format: ${this.config.fileNaming}
- Purely semantic, no date prefixes
## Folder Naming
- Format: ${this.config.fileNaming}
## Formatting
- Headings: H1 to ${depth}
- Date format: ${this.config.dateFormat}
## Linking
${linkDesc}
${tagsSection}
## Frontmatter
- Required fields in every note:
${frontmatterText}
`;
}
generateStructureMd(): string {
const contextFolder = this.plugin.settings.contextFolder;
const folderRows = this.config.folders
.filter(f => f.name !== contextFolder)
.map(f => `| \`${f.name}/\` | ${f.purpose || ''} |`)
.join('\n');
const contextRow = `| \`${contextFolder}/\` | AI context (this documentation) |`;
const whereRows = this.config.folders
.filter(f => f.name !== contextFolder && f.purpose)
.map(f => `- **${f.purpose}?** → \`${f.name}/\``)
.join('\n');
return `# Vault Structure
## Directories
| Folder | Purpose |
|--------|---------|
${folderRows}
${contextRow}
## Where Does What Go?
${whereRows || '*Add examples here.*'}
`;
}
generateWorkflowsMd(): string {
const linkStyle = this.config.linkStyle === 'wikilinks' ? '[[note-name]]' : '[text](path)';
const forbidden = this.config.forbiddenActions.map(a => `- Never touch \`${a}\``).join('\n');
return `# Workflows
## General
- Unrestricted operation - all actions allowed
- Proactively link related notes
- Follow existing conventions
## Forbidden
${forbidden}
## Creating Notes
1. Place in the correct target folder
2. Fill frontmatter (${this.config.frontmatterFields.join(', ')})
3. Link relevant existing notes
## Linking
- When creating/editing: Search for related notes
- Use links: ${linkStyle}
- Think bidirectionally: Also backlink from target notes
`;
}
generateTemplatesMd(): string {
if (this.config.templates.length === 0) {
return `# Templates
*No templates configured. Add templates in the generator.*
`;
}
const rows = this.config.templates
.map(t => `| ${t.name} | ${t.folder || '-'} | ${t.tag ? `#${t.tag}` : '-'} |`)
.join('\n');
return `# Templates
## Available Templates
| Template | Target Folder | Tag |
|----------|---------------|-----|
${rows}
## Template Structure
Every template follows this structure:
1. Frontmatter with \`${this.config.frontmatterFields.join('`, `')}\`
2. H1 title
3. H2 sections depending on type
## Variables
- \`{{date}}\` - Current date (${this.config.dateFormat})
- \`{{title}}\` - Note title
`;
}
generateExamplesMd(): string {
const frontmatter = this.config.frontmatterFields.includes('tags')
? `---
date: ${this.config.dateFormat === 'YYYY-MM-DD' ? '2025-02-05' : this.config.dateFormat === 'DD.MM.YYYY' ? '05.02.2025' : '02/05/2025'}
tags:
- example
---`
: `---
date: ${this.config.dateFormat === 'YYYY-MM-DD' ? '2025-02-05' : this.config.dateFormat === 'DD.MM.YYYY' ? '05.02.2025' : '02/05/2025'}
---`;
const link = this.config.linkStyle === 'wikilinks' ? '[[other-note]]' : '[other note](other-note.md)';
return `# Examples
## Simple Note
\`\`\`markdown
${frontmatter}
# Example Title
## Content
Here is the content with a link to ${link}.
\`\`\`
`;
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

110
src/main.ts Normal file
View file

@ -0,0 +1,110 @@
import { MarkdownView, Notice, Plugin, TFile, TFolder } from 'obsidian';
import { ClaudeContextSettings, ClaudeContextSettingTab, DEFAULT_SETTINGS } from './settings';
import { ContextGeneratorModal } from './generator';
import { PreviewModal } from './preview';
export default class ClaudeContextPlugin extends Plugin {
settings: ClaudeContextSettings;
async onload() {
await this.loadSettings();
// Ribbon icon
this.addRibbonIcon('clipboard-copy', 'Copy Claude context', () => {
this.copyContextToClipboard();
});
this.addCommand({
id: 'copy-context',
name: 'Copy context to clipboard',
callback: () => this.copyContextToClipboard()
});
this.addCommand({
id: 'copy-context-with-note',
name: 'Copy context with current note',
callback: () => this.copyContextToClipboard(true)
});
this.addCommand({
id: 'generate-context',
name: 'Generate context files',
callback: () => new ContextGeneratorModal(this.app, this).open()
});
this.addSettingTab(new ClaudeContextSettingTab(this.app, this));
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
async copyContextToClipboard(forceIncludeNote = false) {
const folder = this.app.vault.getAbstractFileByPath(this.settings.contextFolder);
if (!folder || !(folder instanceof TFolder)) {
new Notice(`Folder "${this.settings.contextFolder}" not found`);
return;
}
const excludedFiles = this.settings.excludedFiles.map(f => f.toLowerCase());
const files = folder.children
.filter((f): f is TFile =>
f instanceof TFile &&
f.extension === 'md' &&
!excludedFiles.includes(f.name.toLowerCase())
)
.sort((a, b) => {
if (a.basename === 'VAULT') return -1;
if (b.basename === 'VAULT') return 1;
return a.basename.localeCompare(b.basename);
});
if (files.length === 0) {
new Notice(`No markdown files in "${this.settings.contextFolder}"`);
return;
}
const parts: string[] = [];
for (const file of files) {
const content = await this.app.vault.read(file);
if (this.settings.includeFilenames) {
parts.push(`# === ${file.name} ===\n\n${content}`);
} else {
parts.push(content);
}
}
// Include active note
if (forceIncludeNote || this.settings.includeActiveNote) {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView?.file) {
const content = await this.app.vault.read(activeView.file);
if (this.settings.includeFilenames) {
parts.push(`# === ACTIVE: ${activeView.file.name} ===\n\n${content}`);
} else {
parts.push(`--- ACTIVE NOTE ---\n\n${content}`);
}
}
}
const combined = parts.join(`\n\n${this.settings.separator}\n\n`);
const fileCount = files.length + (forceIncludeNote || this.settings.includeActiveNote ? 1 : 0);
if (this.settings.showPreview) {
new PreviewModal(this.app, combined, fileCount, async () => {
await navigator.clipboard.writeText(combined);
new Notice(`Copied ${fileCount} files to clipboard`);
}).open();
} else {
await navigator.clipboard.writeText(combined);
new Notice(`Copied ${fileCount} files to clipboard`);
}
}
}

54
src/preview.ts Normal file
View file

@ -0,0 +1,54 @@
import { App, Modal } from 'obsidian';
export class PreviewModal extends Modal {
content: string;
fileCount: number;
onConfirm: () => void;
constructor(app: App, content: string, fileCount: number, onConfirm: () => void) {
super(app);
this.content = content;
this.fileCount = fileCount;
this.onConfirm = onConfirm;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass('claude-context-preview');
contentEl.createEl('h2', { text: `Preview (${this.fileCount} files)` });
const previewContainer = contentEl.createDiv({ cls: 'preview-container' });
previewContainer.style.maxHeight = '400px';
previewContainer.style.overflow = 'auto';
previewContainer.style.border = '1px solid var(--background-modifier-border)';
previewContainer.style.borderRadius = '4px';
previewContainer.style.padding = '10px';
previewContainer.style.marginBottom = '15px';
previewContainer.style.fontFamily = 'monospace';
previewContainer.style.fontSize = '12px';
previewContainer.style.whiteSpace = 'pre-wrap';
previewContainer.setText(this.content);
const buttonContainer = contentEl.createDiv({ cls: 'button-container' });
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'flex-end';
buttonContainer.style.gap = '10px';
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel' });
cancelBtn.addEventListener('click', () => this.close());
const copyBtn = buttonContainer.createEl('button', { text: 'Copy to Clipboard', cls: 'mod-cta' });
copyBtn.addEventListener('click', () => {
this.onConfirm();
this.close();
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

100
src/settings.ts Normal file
View file

@ -0,0 +1,100 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import ClaudeContextPlugin from './main';
export interface ClaudeContextSettings {
contextFolder: string;
separator: string;
includeFilenames: boolean;
showPreview: boolean;
includeActiveNote: boolean;
excludedFiles: string[];
}
export const DEFAULT_SETTINGS: ClaudeContextSettings = {
contextFolder: '_claude',
separator: '---',
includeFilenames: true,
showPreview: false,
includeActiveNote: false,
excludedFiles: [],
};
export class ClaudeContextSettingTab extends PluginSettingTab {
plugin: ClaudeContextPlugin;
constructor(app: App, plugin: ClaudeContextPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName('Context folder')
.setDesc('Folder containing your context files')
.addText(text => text
.setPlaceholder('_claude')
.setValue(this.plugin.settings.contextFolder)
.onChange(async (value) => {
this.plugin.settings.contextFolder = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Separator')
.setDesc('Text between files (e.g. "---" or "***")')
.addText(text => text
.setPlaceholder('---')
.setValue(this.plugin.settings.separator)
.onChange(async (value) => {
this.plugin.settings.separator = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Include filenames')
.setDesc('Add "# === filename.md ===" headers before each file')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.includeFilenames)
.onChange(async (value) => {
this.plugin.settings.includeFilenames = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Show preview')
.setDesc('Show preview modal before copying')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showPreview)
.onChange(async (value) => {
this.plugin.settings.showPreview = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Include active note')
.setDesc('Append currently open note to context')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.includeActiveNote)
.onChange(async (value) => {
this.plugin.settings.includeActiveNote = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Excluded files')
.setDesc('Comma-separated filenames to exclude (e.g. "examples.md, drafts.md")')
.addText(text => text
.setPlaceholder('file1.md, file2.md')
.setValue(this.plugin.settings.excludedFiles.join(', '))
.onChange(async (value) => {
this.plugin.settings.excludedFiles = value
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0);
await this.plugin.saveSettings();
}));
}
}

8
styles.css Normal file
View file

@ -0,0 +1,8 @@
/*
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/

30
tsconfig.json Normal file
View file

@ -0,0 +1,30 @@
{
"compilerOptions": {
"baseUrl": "src",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"moduleResolution": "node",
"importHelpers": true,
"noUncheckedIndexedAccess": true,
"isolatedModules": true,
"strictNullChecks": true,
"strictBindCallApply": true,
"allowSyntheticDefaultImports": true,
"useUnknownInCatchVariables": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"src/**/*.ts"
]
}

17
version-bump.mjs Normal file
View file

@ -0,0 +1,17 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json
// but only if the target version is not already in versions.json
const versions = JSON.parse(readFileSync('versions.json', 'utf8'));
if (!Object.values(versions).includes(minAppVersion)) {
versions[targetVersion] = minAppVersion;
writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));
}

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}