Skill: imakid/obsidian-headless (version 1.0.1)

--- name: obsidian-headless description: 在无显示器/无 GUI 环境下通过自然语言指令管理 Obsidian 笔记仓库。支持创建笔记、删除笔记（带确认）、搜索标题、搜索内容、模糊搜索、创建日记等功能。当用户需要在无头服务器、SSH 环境、或没有安装 Obsidian 桌面应用的情况下管理 Obsidian 笔记时使用此技能。触发词包括"obsidian", "笔记", "搜索笔记", "创建笔记", "删除笔记", "今天日记", "无头 obsidian", "命令行 obsidian"。 --- # Obsidian Headless - 无头 Obsidian 管理工具 在无显示器/无 GUI 环境下通过自然语言指令管理 Obsidian 笔记仓库。 ## 适用场景 - 在 SSH 远程服务器上管理 Obsidian 笔记 - 无显示器环境（如 VPS、Docker 容器） - 不想启动 Obsidian GUI 桌面应用时 - 需要通过脚本自动化管理笔记时 ## 文档 - **USAGE.md** - 完整使用手册（包含快速参考、详细说明、示例、故障排除） ## 目录结构 ``` obsidian-headless/ ├── obs # 便捷入口脚本 ├── install.sh # 安装脚本 ├── SKILL.md # 本文件（技能说明） ├── USAGE.md # 完整使用手册 ├── bin/ │ └── obsidian-headless.sh # 主程序 ├── examples/ │ └── examples.sh # 使用示例脚本 └── tests/ └── test.sh # 测试脚本 ``` ## 配置文件 首次使用时会提示输入仓库路径，配置保存在： ``` ~/.config/obsidian-headless/vault-path ``` ## 快速开始 ### 1. 安装 ```bash cd ~/.openclaw/skills/obsidian-headless ./install.sh ``` 或手动使用： ```bash ~/.openclaw/skills/obsidian-headless/obs "obs指令" ``` ### 指令格式 所有指令都支持 `obs` 前缀（**大小写不敏感**），`obs` 和指令之间可以有**空格、-、_、: 等连接符**： | 格式 | 示例 | |------|------| | `obs指令` | `obs创建笔记 新笔记` | | `obs 指令` | `obs 创建笔记 新笔记` | | `obs-指令` | `obs-创建笔记 新笔记` | | `OBS指令` | `OBS创建笔记 新笔记` | **推荐使用 `obs指令` 格式**，避免误触。 ### 2. 配置仓库路径 **首次使用：** 运行任意命令时会提示输入仓库路径，路径会保存到配置文件中供后续使用。 ``` 首次使用 Obsidian Headless ========================== 请输入 Obsidian 仓库路径: /home/user/my-vault ✓ 已保存配置 ``` **手动配置（可选）：** 方式 1 - 环境变量（优先级最高）： ```bash export OBSIDIAN_VAULT=/path/to/your/vault ``` 方式 2 - 删除配置文件重新输入： ```bash rm ~/.config/obsidian-headless/vault-path # 下次运行时会重新提示输入 ``` ### 3. 使用 ```bash # 使用安装后的快捷命令 obs "创建笔记 新想法" # 或直接调用 ~/.openclaw/skills/obsidian-headless/obs "搜索内容 home assistant" ``` ## 支持的指令 ### 创建笔记 ``` obs创建笔记 [文件名] [可选内容] ``` **说明：** - `文件名` 支持路径，如 `文件夹/笔记名` 或 `笔记名` - 如果**只输入文件名**（无内容），创建**空笔记文件** - 如果**输入了内容**，内容**直接写入文件**，不会自动添加标题 - 笔记的标题需要用户在 `内容` 中自行定义（如 `# 我的标题`） - **文件名和内容之间可以用空格或换行符分隔** **示例：** - `obs创建笔记 待办清单` → 创建空文件 `待办清单.md` - `obs创建笔记 项目想法 这个项目的核心目标是...` → 文件内容为 "这个项目的核心目标是..." - `obs创建笔记 AI/总结 # AI 总结\n\n今天学习了...` → 文件内容包含自定义标题 **多行内容示例：** ``` obs创建笔记 笔记名 # 标题 内容第一行 内容第二行 ``` ### 删除笔记（带确认） ``` obs删除笔记 [文件名] ``` **删除确认流程：** **单个匹配时：** ``` 即将删除笔记: 标题: xxx 路径: /path/to/xxx.md 内容预览 (共 15 行): # xxx ... 请回复确认: [Y] 确认删除 [N] 取消 ``` **多个匹配时：** ``` 找到多个匹配的笔记: [1] 笔记A 位置: folder1/ 大小: 20 行 [2] 笔记B 位置: folder2/ 大小: 35 行 请回复要删除的笔记编号 (或 0 取消) ``` ### 查看笔记内容 ``` obs查看笔记 [文件名] ``` ### 搜索笔记标题 ``` obs搜索标题 [关键词] ``` ### 搜索笔记内容 ``` obs搜索内容 [关键词] ``` ### 模糊搜索（标题+内容） ``` obs模糊搜索 [关键词] ``` ### 创建/打开今天日记 ``` obs今天日记 [可选内容] ``` 自动在 `日记/` 文件夹下创建 `YYYY-MM-DD.md` ### 修改库路径 ``` obs修改库路径 obs修改库目录 obs更改路径 obs切换仓库 ``` 交互式修改仓库路径，保存到配置文件。 **示例：** - `obs修改库路径` → 提示输入新路径 - `obs-修改库目录` → 同上 ### 列出所有笔记 ``` obs列出所有 ``` ### 列出所有文件夹 ``` obs列出文件夹 ``` ### 显示最近修改的笔记 ``` obs最近笔记 ``` ## 前置要求 1. **Obsidian 仓库：** 首次使用时会提示输入仓库路径 2. **依赖:** - `bash` (必须) - `find` (必须) - `grep` (必须) - `ripgrep (rg)` (推荐，搜索更快) 3. **安装 ripgrep：** ```bash # Ubuntu/Debian sudo apt install ripgrep # macOS brew install ripgrep ``` ## 测试 运行测试脚本验证功能： ```bash cd ~/.openclaw/skills/obsidian-headless ./tests/test.sh ``` ## 查看示例 运行示例脚本查看各种用法： ```bash cd ~/.openclaw/skills/obsidian-headless ./examples/examples.sh ``` ## 与 obsidian-cli 的对比 | 功能 | obsidian-cli | obsidian-headless | |------|-------------|-------------------| | 依赖 Obsidian GUI | 是（需要索引） | 否 | | search-content | 依赖索引，常失败 | 使用 ripgrep，可靠 | | 删除确认 | 无 | 有（显示预览） | | 多匹配处理 | 无 | 列出序号选择 | | 自然语言支持 | 有限 | 完整支持 | | 无头环境 | 不支持 | 专门设计 | ## 故障排除 ### 提示"未找到 Obsidian 仓库路径" 1. 首次使用时会自动提示输入路径 2. 或手动设置环境变量： ```bash export OBSIDIAN_VAULT=/path/to/your/vault ``` 3. 或删除配置重新输入： ```bash rm ~/.config/obsidian-headless/vault-path ``` ### 修改已保存的仓库路径 ```bash # 方法1: 使用环境变量覆盖 export OBSIDIAN_VAULT=/new/path # 方法2: 删除配置，下次重新输入 rm ~/.config/obsidian-headless/vault-path ``` ### 搜索速度慢 安装 ripgrep： ```bash sudo apt install ripgrep # Ubuntu/Debian brew install ripgrep # macOS ``` ### 删除时找不到笔记 使用更精确的标题，或使用"模糊搜索"先找到准确标题。 ## 脚本说明 | 脚本 | 用途 | |------|------| | `obs` | 便捷入口，推荐日常使用 | | `bin/obsidian-headless.sh` | 主程序 | | `install.sh` | 安装脚本，设置别名和快捷方式 | | `tests/test.sh` | 功能测试 | | `examples/examples.sh` | 使用示例 |

path: README.md
content: # Obsidian Headless

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![Shell](https://img.shields.io/badge/shell-bash-blue.svg)

> 在无显示器、无 GUI 的环境下，通过自然语言指令管理 Obsidian 笔记仓库

---

## 为什么需要这个工具？

- **SSH 远程服务器**上想管理 Obsidian 笔记？
- **无显示器环境**（VPS、Docker、NAS）需要使用 Obsidian？
- 不想每次都**启动 Obsidian GUI** 桌面应用？
- 希望通过**自然语言**而不是复杂命令来管理笔记？

**Obsidian Headless** 就是为解决这些场景而生的工具。

---

## 功能特性

### 核心功能
- ✍️ **创建笔记** - 支持子目录、多行内容
- 🗑️ **删除笔记** - 智能确认，防止误删
- 👁️ **查看笔记** - 快速预览内容
- 🔍 **搜索笔记** - 标题搜索、内容搜索、模糊搜索
- 📅 **日记功能** - 自动创建/追加今日日记
- 📂 **列出管理** - 列出所有笔记和文件夹

### 使用体验
- 🗣️ **自然语言** - `obs创建笔记 想法` 而不是 `touch note.md`
- 🔤 **大小写不敏感** - `OBS创建笔记` 也可以
- 🔗 **灵活分隔** - `obs-创建笔记`、`obs_创建笔记` 都支持
- 📄 **多行内容** - 文件名和内容可以用换行符分隔

### 安全保障
- 🛡️ **路径遍历防护** - 阻止 `../etc/passwd` 攻击
- ✅ **删除验证** - 确保只能删除仓库内文件
- 🔒 **输入验证** - 阻止非法字符和控制字符

---

## 快速开始

### 安装

```bash
# 克隆仓库
git clone https://github.com/imakid/obsidian-headless.git
cd obsidian-headless

# 安装（可选，添加 obs 别名到 shell）
./install.sh
```

或直接运行：
```bash
./obs "obs创建笔记 测试笔记"
```

### 首次使用

运行任意命令时会提示输入 Obsidian 仓库路径：

```
首次使用 Obsidian Headless
==========================

请输入 Obsidian 仓库路径: /path/to/your/vault
✓ 有效的 Obsidian 仓库
✓ 已保存配置
```

### 基本使用

```bash
# 创建笔记
obs创建笔记 待办清单
obs创建笔记 项目/想法 这个项目的核心目标是...

# 创建多行内容笔记
obs创建笔记 会议记录
# 会议主题：周会
## 参会人员
- Alice
- Bob

# 搜索笔记
obs搜索内容 home assistant
obs模糊搜索 openclaw

# 今天日记
obs今天日记 今天完成了技能发布...

# 查看笔记
obs查看笔记 待办清单

# 列出所有
obs列出所有
```

---

## 完整指令列表

| 指令 | 功能 | 示例 |
|------|------|------|
| `obs创建笔记 [文件名] [内容]` | 创建新笔记 | `obs创建笔记 想法` |
| `obs删除笔记 [文件名]` | 删除笔记（带确认） | `obs删除笔记 旧笔记` |
| `obs查看笔记 [文件名]` | 显示笔记内容 | `obs查看笔记 欢迎` |
| `obs搜索标题 [关键词]` | 搜索文件名 | `obs搜索标题 项目` |
| `obs搜索内容 [关键词]` | 搜索文件内容 | `obs搜索内容 docker` |
| `obs模糊搜索 [关键词]` | 标题+内容搜索 | `obs模糊搜索 ai` |
| `obs今天日记 [内容]` | 创建/追加日记 | `obs今天日记 今天...` |
| `obs列出所有` | 列出所有笔记 | `obs列出所有` |
| `obs列出文件夹` | 列出所有文件夹 | `obs列出文件夹` |
| `obs最近笔记` | 最近修改的笔记 | `obs最近笔记` |
| `obs修改库路径` | 修改仓库路径 | `obs修改库路径` |

**格式说明**：`obs` 前缀大小写不敏感，支持空格、`-`、`_`、`::` 等分隔符。

---

## 使用场景示例

### 场景 1：SSH 远程管理
```bash
ssh my-server
obs创建笔记 服务器配置/nginx配置
# 粘贴配置内容...
obs列出所有
```

### 场景 2：快速记录想法
```bash
obs创建笔记 灵感/新产品想法
# 产品名称：XXX
## 核心功能
1. ...
2. ...
```

### 场景 3：项目管理
```bash
obs创建笔记 项目A/需求文档 需求分析...
obs创建笔记 项目A/进度表 当前进度...
obs模糊搜索 项目A
```

---

## 依赖要求

### 必需（通常已预装）
- `bash` - 脚本解释器
- `find`, `grep` - 搜索工具
- `mkdir`, `touch`, `rm`, `cat` - 文件操作
- `date`, `chmod` - 系统命令
- `head`, `tail`, `cut`, `awk` - 文本处理

### macOS 用户注意
旧版 macOS 可能缺少 `realpath`：
```bash
brew install coreutils
```

### 可选（推荐）
- `ripgrep (rg)` - 更快的搜索速度

---

## 配置

### 环境变量
```bash
export OBSIDIAN_VAULT=/path/to/your/vault
```

### 配置文件位置
```
~/.config/obsidian-headless/vault-path
```

删除此文件可重置配置。

---

## 安全说明

- ✅ **路径遍历防护** - 阻止 `../../../etc/passwd` 等攻击
- ✅ **删除验证** - 确保只能删除仓库内的文件
- ✅ **输入验证** - 验证文件名，阻止非法字符
- ✅ **配置权限** - 配置文件权限设置为 644

---

## 与其他工具对比

| 工具 | 依赖 Obsidian GUI | 支持自然语言 | 无头环境 | 搜索速度 |
|------|------------------|-------------|---------|---------|
| Obsidian GUI | ✅ 必须 | ❌ 不支持 | ❌ 不支持 | 快 |
| obsidian-cli | ✅ 需要索引 | ⚠️ 有限 | ❌ 不支持 | 慢 |
| **Obsidian Headless** | ❌ 不需要 | ✅ 完整支持 | ✅ 专门设计 | 快 (rg) |

---

## 开发背景

这个工具最初是为了解决在 **OpenClaw 自动化环境** 中管理 Obsidian 笔记的需求。在无显示器的服务器上，无法启动 Obsidian GUI，但又需要：
- 创建和编辑笔记
- 搜索已有内容
- 管理日记

于是诞生了 **Obsidian Headless** - 一个真正为无头环境设计的 Obsidian 管理工具。

---

## 贡献

欢迎提交 Issue 和 Pull Request！

## 许可证

[MIT](LICENSE) © 2026 Obsidian Headless Contributors

---

## 相关链接

- 📦 [OpenClaw](https://github.com/openclaw/openclaw) - AI 自动化框架
- 📝 [Obsidian](https://obsidian.md/) - 强大的知识管理工具
- 🦞 [ClawHub](https://clawhub.com) - OpenClaw 技能市场


path: USAGE.md
content: # Obsidian Headless 使用手册

## 简介

Obsidian Headless 是一个在无显示器/无 GUI 环境下通过自然语言指令管理 Obsidian 笔记仓库的工具。

## 快速参考

### 指令速查表

| 指令 | 功能 | 示例 |
|------|------|------|
| `obs创建笔记 [文件名] [内容]` | 创建新笔记 | `obs创建笔记 待办清单 买东西` |
| `obs删除笔记 [文件名]` | 删除笔记（带确认） | `obs删除笔记 旧笔记` |
| `obs查看笔记 [文件名]` | 显示笔记内容 | `obs查看笔记 欢迎` |
| `obs搜索标题 [关键词]` | 搜索文件名 | `obs搜索标题 openclaw` |
| `obs搜索内容 [关键词]` | 搜索文件内容 | `obs搜索内容 tavily` |
| `obs模糊搜索 [关键词]` | 标题+内容搜索 | `obs模糊搜索 home` |
| `obs今天日记 [内容]` | 创建/追加日记 | `obs今天日记 今天...` |
| `obs列出所有` | 列出所有笔记 | `obs列出所有` |
| `obs列出文件夹` | 列出所有文件夹 | `obs列出文件夹` |
| `obs最近笔记` | 最近10篇笔记 | `obs最近笔记` |
| `obs修改库路径` | 修改仓库路径 | `obs修改库路径` |

### 格式说明

**obs 前缀格式（大小写不敏感）：**
- `obs指令` - 推荐
- `obs 指令` - 带空格
- `obs-指令` - 带连字符
- `OBS指令` - 大写

### 路径支持

```bash
# 根目录
obs创建笔记 笔记名

# 子目录
obs创建笔记 文件夹/笔记名
obs创建笔记 文件夹/子文件夹/笔记名
```

## 安装

```bash
cd ~/.openclaw/skills/obsidian-headless
./install.sh
```

或直接运行：
```bash
~/.openclaw/skills/obsidian-headless/obs "指令"
```

## 配置

### 首次使用

运行任意命令时会提示输入仓库路径：

```
首次使用 Obsidian Headless
==========================

请输入 Obsidian 仓库路径: /path/to/your/vault
✓ 有效的 Obsidian 仓库
✓ 已保存配置到: ~/.config/obsidian-headless/vault-path
```

### 修改配置

```bash
# 方法1: 使用环境变量（临时）
export OBSIDIAN_VAULT=/new/path

# 方法2: 删除配置重新输入
rm ~/.config/obsidian-headless/vault-path
./obs "obs创建笔记 测试"

# 方法3: 使用修改库路径命令
./obs "obs修改库路径"
```

## 指令格式

所有指令都支持 `obs` 前缀（**大小写不敏感**），`obs` 和指令之间可以有**空格、-、_、: 等连接符**：

| 格式 | 示例 |
|------|------|
| `obs指令` | `obs创建笔记 新笔记` |
| `obs 指令` | `obs 创建笔记 新笔记` |
| `obs-指令` | `obs-创建笔记 新笔记` |
| `obs_指令` | `obs_创建笔记 新笔记` |
| `OBS指令` | `OBS创建笔记 新笔记` |
| `Obs::指令` | `Obs::创建笔记 新笔记` |

## 完整指令列表

### 1. 创建笔记

```bash
obs创建笔记 [文件名] [内容]
```

**说明：**
- `文件名` 支持路径，如 `文件夹/笔记名` 或 `笔记名`
- 如果**只输入文件名**（无内容），创建**空笔记文件**
- 如果**输入了内容**，内容**直接写入文件**，不会自动添加标题
- 笔记的标题需要用户在 `内容` 中自行定义
- **文件名和内容之间可以用空格或换行符分隔**

**示例（空格分隔）：**
```bash
obs创建笔记 待办清单
obs创建笔记 项目想法 这个项目的核心目标是...
obs创建笔记 日记/2024-03-14 今天完成了...
```

**示例（换行符分隔）：**
```bash
obs创建笔记 AI/总结
# AI 总结

今天学习了...

obs创建笔记 想法
# 新想法 #idea
## 详细描述
具体的内容在这里...
```

### 2. 删除笔记

```bash
obs删除笔记 [文件名]
```

**说明：**
- 支持模糊匹配
- 找到唯一匹配时显示详情并要求确认
- 找到多个匹配时列出带序号的列表，用户选择

**示例：**
```bash
obs删除笔记 旧笔记
obs删除笔记 AI/测试
```

**删除确认流程：**

**单个匹配时：**
```
即将删除笔记:
  文件名: xxx
  路径: /path/to/xxx.md

  内容预览 (共 15 行):
    ...

请回复确认:
  [Y] 确认删除
  [N] 取消
```

**多个匹配时：**
```
找到多个匹配的笔记:

  [1] 笔记A
      位置: folder1/
      大小: 20 行

  [2] 笔记B
      位置: folder2/
      大小: 35 行

请回复要删除的笔记编号 (或 0 取消)
```

### 3. 查看笔记

```bash
obs查看笔记 [文件名]
```

**示例：**
```bash
obs查看笔记 欢迎
obs查看笔记 AI/总结
```

### 4. 搜索笔记标题

```bash
obs搜索标题 [关键词]
```

**说明：** 只搜索文件名（路径+文件名）

**示例：**
```bash
obs搜索标题 openclaw
obs搜索标题 2024
```

### 5. 搜索笔记内容

```bash
obs搜索内容 [关键词]
```

**说明：** 搜索所有 `.md` 文件的内容

**示例：**
```bash
obs搜索内容 home assistant
obs搜索内容 tavily
```

### 6. 模糊搜索

```bash
obs模糊搜索 [关键词]
```

**说明：** 同时搜索标题和内容

**示例：**
```bash
obs模糊搜索 obsidian
obs模糊搜索 项目
```

### 7. 今天日记

```bash
obs今天日记 [可选内容]
```

**说明：**
- 自动在 `日记/` 文件夹下创建 `YYYY-MM-DD.md`
- 如果今天日记已存在，追加内容

**示例：**
```bash
obs今天日记
obs今天日记 今天完成了技能开发...
```

### 8. 列出所有笔记

```bash
obs列出所有
```

**说明：** 列出仓库中所有 `.md` 文件

### 9. 列出所有文件夹

```bash
obs列出文件夹
```

**说明：** 列出仓库中所有文件夹（排除 `.obsidian`）

### 10. 最近笔记

```bash
obs最近笔记
```

**说明：** 显示最近修改的 10 篇笔记

### 11. 修改库路径

```bash
obs修改库路径
obs修改库目录
obs更改路径
obs切换仓库
```

**说明：** 交互式修改仓库路径

## 使用示例

### 日常工作流

```bash
# 1. 创建新笔记
obs创建笔记 会议纪要 今天讨论了Q3计划...

# 2. 查看笔记
obs查看笔记 会议纪要

# 3. 搜索相关内容
obs搜索内容 Q3计划

# 4. 记录今天的工作
obs今天日记 完成了会议纪要整理

# 5. 列出所有笔记检查
obs列出所有
```

### 项目管理

```bash
# 创建项目结构
obs创建笔记 项目/项目A/需求文档 需求分析...
obs创建笔记 项目/项目A/进度跟踪 当前进度...

# 搜索项目相关笔记
obs模糊搜索 项目A

# 查看具体笔记
obs查看笔记 项目/项目A/需求文档
```

### 知识整理

```bash
# 创建分类笔记
obs创建笔记 AI/ChatGPT技巧 使用技巧总结...
obs创建笔记 AI/Claude技巧 使用技巧总结...
obs创建笔记 前端/React技巧 使用技巧总结...

# 模糊搜索所有技巧
obs模糊搜索 技巧
```

## 故障排除

### 提示"未找到 Obsidian 仓库路径"

```bash
# 设置环境变量
export OBSIDIAN_VAULT=/path/to/your/vault

# 或使用修改库路径命令
obs修改库路径
```

### 搜索速度慢

安装 ripgrep：
```bash
# Ubuntu/Debian
sudo apt install ripgrep

# macOS
brew install ripgrep
```

### 删除时找不到笔记

使用更精确的标题，或使用模糊搜索先找到准确标题：
```bash
obs模糊搜索 关键词
obs删除笔记 准确文件名
```

### 配置文件位置

```
~/.config/obsidian-headless/vault-path
```

删除此文件可重置配置。

## 依赖要求

### 必需依赖

以下命令通常在 Linux/macOS 系统中已预装：

- `bash` - 脚本解释器
- `find` - 查找文件
- `grep` - 文本搜索
- `mkdir`, `touch`, `rm`, `cat` - 文件操作
- `date`, `chmod` - 系统命令
- `head`, `tail`, `cut`, `awk` - 文本处理

### macOS 用户注意

旧版 macOS 可能缺少 `realpath` 命令：

```bash
# 安装 coreutils（包含 realpath）
brew install coreutils
```

### 可选依赖（推荐）

- `ripgrep (rg)` - 更快的搜索

## 版本信息

- 版本: 1.0.0
- 作者: Obsidian Headless Team
- 许可证: MIT


path: bin/obsidian-headless.sh
content: #!/bin/bash
# obsidian-headless.sh - 无头 Obsidian 自然语言控制工具
# 在无显示器环境下通过自然语言指令管理 Obsidian 笔记仓库

# 严格模式（除了未绑定变量检查，因为 OBSIDIAN_VAULT 可能未设置）
set -eo pipefail

# 检查依赖
check_dependencies() {
    local missing=()
    
    for cmd in find grep mkdir touch rm cat date chmod head tail cut awk; do
        if ! command -v "$cmd" &>/dev/null; then
            missing+=("$cmd")
        fi
    done
    
    # realpath 检查（macOS 可能没有这个命令）
    if ! command -v realpath &>/dev/null; then
        printf '%b警告: realpath 命令未找到（macOS 用户可安装 coreutils）%b\n' "$YELLOW" "$NC" >&2
    fi
    
    if [[ ${#missing[@]} -gt 0 ]]; then
        printf '%b错误: 缺少必要命令: %s%b\n' "$RED" "${missing[*]}" "$NC" >&2
        printf '请安装缺失的依赖后重试\n' >&2
        exit 1
    fi
}

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# 配置文件路径
CONFIG_DIR="${HOME}/.config/obsidian-headless"
CONFIG_FILE="${CONFIG_DIR}/vault-path"

# 检查依赖
check_dependencies

# 获取仓库路径
get_vault_path() {
    # 1. 优先使用环境变量
    if [[ -n "${OBSIDIAN_VAULT}" ]]; then
        echo "${OBSIDIAN_VAULT}"
        return 0
    fi
    
    # 2. 使用保存的配置
    if [[ -f "${CONFIG_FILE}" ]]; then
        local saved_path=$(cat "${CONFIG_FILE}" 2>/dev/null | tr -d '\n')
        if [[ -d "${saved_path}" ]]; then
            echo "${saved_path}"
            return 0
        fi
    fi
    
    # 3. 首次使用，提示用户输入
    echo "首次使用 Obsidian Headless" >&2
    echo "==========================" >&2
    echo >&2
    
    while true; do
        read -p "请输入 Obsidian 仓库路径: " user_path
        
        # 展开 ~ 为 HOME
        user_path="${user_path/#\~/${HOME}}"
        
        if [[ -z "${user_path}" ]]; then
            echo "路径不能为空，请重新输入" >&2
            continue
        fi
        
        if [[ ! -d "${user_path}" ]]; then
            echo "目录不存在: ${user_path}" >&2
            read -p "是否创建该目录? (y/N): " create_dir
            if [[ "${create_dir}" == "y" || "${create_dir}" == "Y" ]]; then
                mkdir -p "${user_path}" && echo "已创建目录: ${user_path}" >&2
            else
                continue
            fi
        fi
        
        # 验证是否是有效的 Obsidian 仓库（检查是否有 .obsidian 文件夹或 .md 文件）
        if [[ -d "${user_path}/.obsidian" ]] || [[ $(find "${user_path}" -name "*.md" -type f 2>/dev/null | head -1) ]]; then
            echo "✓ 有效的 Obsidian 仓库" >&2
        else
            echo "警告: 该目录看起来不像 Obsidian 仓库（没有 .obsidian 文件夹或 .md 文件）" >&2
            read -p "仍要使用这个目录吗? (y/N): " confirm
            if [[ "${confirm}" != "y" && "${confirm}" != "Y" ]]; then
                continue
            fi
        fi
        
        # 保存配置
        mkdir -p "${CONFIG_DIR}"
        echo "${user_path}" > "${CONFIG_FILE}"
        # 设置配置文件权限（644: 所有者可读写，其他人只读）
        chmod 644 "${CONFIG_FILE}"
        echo >&2
        echo "✓ 已保存配置到: ${CONFIG_FILE}" >&2
        echo "下次使用会自动加载此路径" >&2
        echo "如需更改，可: 1) 设置环境变量 OBSIDIAN_VAULT，或 2) 删除 ${CONFIG_FILE}" >&2
        echo >&2
        
        echo "${user_path}"
        return 0
    done
}

VAULT_PATH=$(get_vault_path)

TODAY=$(date +%Y-%m-%d)
DAILY_NOTE="${VAULT_PATH}/日记/${TODAY}.md"

# 安全函数：验证文件名
validate_filename() {
    local name="$1"
    # 检查是否包含路径遍历
    if [[ "$name" == *".."* ]]; then
        printf '%b错误: 文件名不能包含 ".."%b\n' "$RED" "$NC" >&2
        return 1
    fi
    # 检查是否包含控制字符
    if [[ "$name" =~ [[:cntrl:]] ]]; then
        printf '%b错误: 文件名包含非法字符%b\n' "$RED" "$NC" >&2
        return 1
    fi
    return 0
}

# 安全函数：验证路径在仓库内
validate_path_in_vault() {
    local filepath="$1"
    local abs_filepath
    local abs_vault
    
    abs_filepath=$(realpath -m "$filepath" 2>/dev/null || echo "$filepath")
    abs_vault=$(realpath "$VAULT_PATH" 2>/dev/null || echo "$VAULT_PATH")
    
    if [[ "$abs_filepath" != "$abs_vault"* ]]; then
        printf '%b错误: 文件路径必须在仓库内%b\n' "$RED" "$NC" >&2
        return 1
    fi
    return 0
}

# 错误退出函数
error_exit() {
    printf '%b错误: %s%b\n' "$RED" "$1" "$NC" >&2
    exit 1
}

# 显示帮助
show_help() {
    cat << 'EOF'
Obsidian Headless - 无头 Obsidian 自然语言控制工具

在无显示器/无 GUI 环境下管理 Obsidian 笔记仓库

支持的指令格式（obs 前缀可选，大小写不敏感）:
  obs创建笔记 [文件名] [内容]     或  obs 创建笔记 / obs-创建笔记 / OBS创建笔记
  obs删除笔记 [文件名]            或  obs 删除笔记 / obs-删除笔记
  obs查看笔记 [文件名]            或  obs 查看笔记 / obs-查看笔记
  obs搜索标题 [关键词]            或  obs 搜索标题 / obs-搜索标题
  obs搜索内容 [关键词]            或  obs 搜索内容 / obs-搜索内容
  obs模糊搜索 [关键词]            或  obs 模糊搜索 / obs-模糊搜索
  obs今天日记 [可选内容]          或  obs 今天日记 / obs-今天日记
  obs列出所有                     或  obs 列出所有 / obs-列出所有
  obs列出文件夹                   或  obs 列出文件夹 / obs-列出文件夹
  obs最近笔记                     或  obs 最近笔记 / obs-最近笔记
  obs修改库路径                   或  obs 修改库路径 / obs-修改库路径

配置方式（优先级从高到低）:
  1. 环境变量 OBSIDIAN_VAULT
  2. 配置文件 ~/.config/obsidian-headless/vault-path
  3. 首次使用时交互式输入

示例:
  obs创建笔记 待办清单 今天要完成: 1. xxx 2. yyy
  obs 搜索内容 home assistant
  OBS-模糊搜索 openclaw
  obs今天日记
  obs 删除笔记 旧笔记
  obs-修改库路径
EOF
}

# 创建笔记
create_note() {
    local title="$1"
    local content="${2:-}"
    
    if [[ -z "$title" ]]; then
        printf '%b错误: 需要提供文件名%b\n' "$RED" "$NC" >&2
        return 1
    fi
    
    # 安全验证：检查文件名
    if ! validate_filename "$title"; then
        return 1
    fi
    
    # 添加 .md 后缀（如果没有）
    [[ "$title" != *.md ]] && title="${title}.md"
    
    local filepath="${VAULT_PATH}/${title}"
    
    # 安全验证：确保路径在仓库内
    if ! validate_path_in_vault "$filepath"; then
        return 1
    fi
    
    # 检查是否已存在
    if [[ -f "$filepath" ]]; then
        printf '%b警告: 笔记已存在: %s%b\n' "$YELLOW" "$title" "$NC" >&2
        printf '文件路径: %s\n' "$filepath" >&2
        return 1
    fi
    
    # 创建目录（如果不存在）
    mkdir -p "$(dirname "$filepath")"
    
    # 提取文件名（不含路径）用于显示
    local note_title=$(basename "$title" .md)
    
    # 创建笔记
    # 如果内容为空，创建空文件；如果有内容，直接使用用户内容
    if [[ -n "$content" ]]; then
        printf '%s\n' "$content" > "$filepath"
    else
        touch "$filepath"
    fi
    
    echo -e "${GREEN}✓ 创建笔记成功${NC}"
    echo "文件名: ${note_title}"
    echo "路径: $filepath"
}

# 查找笔记（返回文件路径）
find_note() {
    local title="$1"
    local exact="${2:-false}"
    
    if [[ -z "$title" ]]; then
        return 1
    fi
    
    # 尝试精确匹配
    [[ "$title" != *.md ]] && local title_md="${title}.md" || local title_md="$title"
    
    local exact_match=$(find "$VAULT_PATH" -name "$title_md" -type f 2>/dev/null | head -1)
    
    if [[ -n "$exact_match" ]]; then
        echo "$exact_match"
        return 0
    fi
    
    # 模糊匹配
    if [[ "$exact" != "true" ]]; then
        local fuzzy_matches=$(find "$VAULT_PATH" -iname "*${title}*" -name "*.md" -type f 2>/dev/null)
        
        if [[ -n "$fuzzy_matches" ]]; then
            local count=$(echo "$fuzzy_matches" | wc -l)
            
            if [[ $count -eq 1 ]]; then
                echo "$fuzzy_matches"
                return 0
            else
                # 返回多个匹配（用于删除确认）
                echo "$fuzzy_matches"
                return 2
            fi
        fi
    fi
    
    return 1
}

# 删除笔记（带确认）
delete_note() {
    local title="$1"
    
    if [[ -z "$title" ]]; then
        echo -e "${RED}错误: 需要提供笔记标题${NC}"
        return 1
    fi
    
    local result=$(find_note "$title" false)
    local exit_code=$?
    
    if [[ $exit_code -eq 1 ]]; then
        echo -e "${RED}未找到笔记: $title${NC}"
        return 1
    fi
    
    if [[ $exit_code -eq 0 ]]; then
        # 只有一个匹配
        local filepath="$result"
        local basename=$(basename "$filepath" .md)
        
        echo -e "${YELLOW}即将删除笔记:${NC}"
        echo "  标题: $basename"
        echo "  路径: $filepath"
        echo
        
        # 显示笔记内容预览
        local lines=$(wc -l < "$filepath")
        echo "  内容预览 (共 $lines 行):"
        head -3 "$filepath" | sed 's/^/    /'
        [[ $lines -gt 3 ]] && echo "    ..."
        echo
        
        echo "请回复确认:"
        echo "  [Y] 确认删除"
        echo "  [N] 取消"
        echo
        echo "DELETE_CONFIRM:$filepath"
        return 0
    fi
    
    if [[ $exit_code -eq 2 ]]; then
        # 多个匹配
        echo -e "${YELLOW}找到多个匹配的笔记:${NC}"
        echo
        
        local i=1
        while IFS= read -r filepath; do
            local basename=$(basename "$filepath" .md)
            local dir=$(dirname "$filepath" | sed "s|${VAULT_PATH}/||")
            local lines=$(wc -l < "$filepath")
            
            echo "  [$i] $basename"
            echo "      位置: $dir/"
            echo "      大小: $lines 行"
            echo
            
            ((i++))
        done <<< "$result"
        
        echo "请回复要删除的笔记编号 (或 0 取消):"
        echo "MULTI_DELETE_CONFIRM:$result"
        return 0
    fi
}

# 执行删除（由 agent 调用）
execute_delete() {
    local filepath="$1"
    
    # 安全验证：确保路径在仓库内
    if ! validate_path_in_vault "$filepath"; then
        return 1
    fi
    
    if [[ ! -f "$filepath" ]]; then
        printf '%b错误: 文件不存在: %s%b\n' "$RED" "$filepath" "$NC" >&2
        return 1
    fi
    
    # 使用 -- 防止文件名以 - 开头
    rm -- "$filepath"
    
    if [[ $? -eq 0 ]]; then
        printf '%b✓ 已删除笔记%b\n' "$GREEN" "$NC"
        printf '  路径: %s\n' "$filepath"
        return 0
    else
        printf '%b✗ 删除失败%b\n' "$RED" "$NC" >&2
        return 1
    fi
}

# 查看笔记内容
view_note() {
    local title="$1"
    
    if [[ -z "$title" ]]; then
        echo -e "${RED}错误: 需要提供笔记标题${NC}"
        return 1
    fi
    
    local result=$(find_note "$title" false)
    local exit_code=$?
    
    if [[ $exit_code -eq 1 ]]; then
        echo -e "${RED}未找到笔记: $title${NC}"
        return 1
    fi
    
    if [[ $exit_code -eq 2 ]]; then
        # 多个匹配
        echo -e "${YELLOW}找到多个匹配的笔记，请指定更精确的标题:${NC}"
        echo "$result" | sed "s|${VAULT_PATH}/||" | nl
        return 1
    fi
    
    local filepath="$result"
    local basename=$(basename "$filepath")
    
    echo -e "${GREEN}=== $basename ===${NC}"
    echo
    cat "$filepath"
}

# 搜索标题
search_titles() {
    local keyword="$1"
    
    if [[ -z "$keyword" ]]; then
        echo -e "${RED}错误: 需要提供搜索关键词${NC}"
        return 1
    fi
    
    echo -e "${GREEN}搜索标题包含 '$keyword' 的笔记:${NC}"
    echo
    
    local results=$(find "$VAULT_PATH" -iname "*${keyword}*" -name "*.md" -type f 2>/dev/null)
    
    if [[ -z "$results" ]]; then
        echo "  (未找到匹配)"
        return 0
    fi
    
    echo "$results" | sed "s|${VAULT_PATH}/||" | nl
}

# 搜索内容
search_content() {
    local keyword="$1"
    
    if [[ -z "$keyword" ]]; then
        echo -e "${RED}错误: 需要提供搜索关键词${NC}"
        return 1
    fi
    
    echo -e "${GREEN}搜索内容包含 '$keyword' 的笔记:${NC}"
    echo
    
    local results
    if command -v rg &>/dev/null; then
        results=$(rg -i "$keyword" "$VAULT_PATH" -t md -l 2>/dev/null)
    else
        results=$(grep -ril "$keyword" "$VAULT_PATH" --include="*.md" 2>/dev/null)
    fi
    
    if [[ -z "$results" ]]; then
        echo "  (未找到匹配)"
        return 0
    fi
    
    echo "$results" | sed "s|${VAULT_PATH}/||" | nl
}

# 模糊搜索（标题+内容）
fuzzy_search() {
    local keyword="$1"
    
    if [[ -z "$keyword" ]]; then
        echo -e "${RED}错误: 需要提供搜索关键词${NC}"
        return 1
    fi
    
    echo -e "${GREEN}模糊搜索 '$keyword':${NC}"
    echo
    
    # 标题匹配
    echo -e "${YELLOW}--- 标题匹配 ---${NC}"
    local title_results=$(find "$VAULT_PATH" -iname "*${keyword}*" -name "*.md" -type f 2>/dev/null | head -10)
    
    if [[ -z "$title_results" ]]; then
        echo "  (无)"
    else
        echo "$title_results" | sed "s|${VAULT_PATH}/||" | nl
    fi
    
    echo
    
    # 内容匹配
    echo -e "${YELLOW}--- 内容匹配 ---${NC}"
    local content_results
    if command -v rg &>/dev/null; then
        content_results=$(rg -i "$keyword" "$VAULT_PATH" -t md -l 2>/dev/null | head -10)
    else
        content_results=$(grep -ril "$keyword" "$VAULT_PATH" --include="*.md" 2>/dev/null | head -10)
    fi
    
    if [[ -z "$content_results" ]]; then
        echo "  (无)"
    else
        echo "$content_results" | sed "s|${VAULT_PATH}/||" | nl
    fi
}

# 创建今天的日记
daily_note() {
    local content="${1:-}"
    
    # 创建日记文件夹（如果不存在）
    mkdir -p "$(dirname "$DAILY_NOTE")"
    
    if [[ -f "$DAILY_NOTE" ]]; then
        echo -e "${YELLOW}今天的日记已存在${NC}"
        if [[ -n "$content" ]]; then
            echo -e "\n$content" >> "$DAILY_NOTE"
            echo -e "${GREEN}✓ 已追加内容${NC}"
        fi
    else
        cat > "$DAILY_NOTE" << EOF
# ${TODAY}

${content}

## 待办事项
- [ ] 

## 笔记

EOF
        echo -e "${GREEN}✓ 创建今天日记${NC}"
    fi
    
    echo "  路径: $DAILY_NOTE"
}

# 列出所有笔记
list_all() {
    echo -e "${GREEN}所有笔记 (${VAULT_PATH}):${NC}"
    echo
    
    local count=$(find "$VAULT_PATH" -name "*.md" -type f 2>/dev/null | wc -l)
    echo "共 $count 篇笔记"
    echo
    
    find "$VAULT_PATH" -name "*.md" -type f 2>/dev/null | sed "s|${VAULT_PATH}/||" | sort | nl
}

# 列出文件夹
list_folders() {
    echo -e "${GREEN}所有文件夹:${NC}"
    echo
    
    find "$VAULT_PATH" -type d 2>/dev/null | \
        sed "s|${VAULT_PATH}/||" | \
        grep -v '^$' | \
        grep -v '^\.obsidian' | \
        sort | nl
}

# 最近笔记
recent_notes() {
    echo -e "${GREEN}最近修改的笔记 (前10):${NC}"
    echo
    
    find "$VAULT_PATH" -name "*.md" -type f -printf '%T@ %p\n' 2>/dev/null | \
        sort -rn | \
        head -10 | \
        cut -d' ' -f2- | \
        sed "s|${VAULT_PATH}/||" | \
        nl
}

# 修改库路径
change_vault_path() {
    echo -e "${YELLOW}修改 Obsidian 仓库路径${NC}"
    echo "========================"
    echo
    
    # 显示当前路径
    local current_path=""
    if [[ -n "${OBSIDIAN_VAULT}" ]]; then
        current_path="${OBSIDIAN_VAULT}"
        echo "当前路径（环境变量）: ${current_path}"
    elif [[ -f "${CONFIG_FILE}" ]]; then
        current_path=$(cat "${CONFIG_FILE}" 2>/dev/null | tr -d '\n')
        echo "当前路径（配置文件）: ${current_path}"
    else
        echo "当前未配置路径"
    fi
    echo
    
    # 提示输入新路径
    while true; do
        read -p "请输入新的仓库路径 (留空取消): " new_path
        
        # 检查是否留空
        if [[ -z "${new_path}" ]]; then
            echo "已取消，保持当前路径"
            return 0
        fi
        
        # 展开 ~ 为 HOME
        new_path="${new_path/#\~/${HOME}}"
        
        if [[ ! -d "${new_path}" ]]; then
            echo "目录不存在: ${new_path}"
            read -p "是否创建该目录? (y/N): " create_dir
            if [[ "${create_dir}" == "y" || "${create_dir}" == "Y" ]]; then
                mkdir -p "${new_path}" && echo "已创建目录: ${new_path}"
            else
                continue
            fi
        fi
        
        # 验证是否是有效的 Obsidian 仓库
        if [[ -d "${new_path}/.obsidian" ]] || [[ $(find "${new_path}" -name "*.md" -type f 2>/dev/null | head -1) ]]; then
            echo -e "${GREEN}✓ 有效的 Obsidian 仓库${NC}"
        else
            echo -e "${YELLOW}警告: 该目录看起来不像 Obsidian 仓库${NC}"
            read -p "仍要使用这个目录吗? (y/N): " confirm
            if [[ "${confirm}" != "y" && "${confirm}" != "Y" ]]; then
                continue
            fi
        fi
        
        # 保存配置
        mkdir -p "${CONFIG_DIR}"
        echo "${new_path}" > "${CONFIG_FILE}"
        # 设置配置文件权限（644: 所有者可读写，其他人只读）
        chmod 644 "${CONFIG_FILE}"
        echo
        echo -e "${GREEN}✓ 已更新仓库路径${NC}"
        echo "新路径: ${new_path}"
        echo
        echo "注意: 本次会话仍使用旧路径，下次运行命令时生效"
        echo "或重新加载配置: export OBSIDIAN_VAULT=${new_path}"
        
        return 0
    done
}

# 解析自然语言指令
# 支持的格式: obs创建笔记, obs 创建笔记, obs-创建笔记, OBS创建笔记 等
parse_command() {
    local input="$1"
    
    # 检查是否以 obs 开头（大小写不敏感）
    # 支持格式: obs指令, obs 指令, obs-指令, OBS指令 等
    local lower_input_check=$(echo "$input" | tr '[:upper:]' '[:lower:]')
    
    if [[ "$lower_input_check" == obs* ]]; then
        # 去除 obs 前缀（3个字符）
        local clean_input="${input:3}"
        # 去除后续的各种连接符（空格、-、_、: 等）
        # 注意：- 必须放在字符类开头或结尾
        while [[ "$clean_input" =~ ^[-[:space:]_\:\/] ]]; do
            clean_input="${clean_input:1}"
        done
        input="$clean_input"
    fi
    
    local lower_input=$(echo "$input" | tr '[:upper:]' '[:lower:]')
    
    case "$lower_input" in
        # 帮助
        帮助|help|"?"|-h|--help)
            show_help
            ;;
            
        # 创建笔记
        创建笔记*|新建笔记*|添加笔记*)
            # 提取 "笔记" 之后的所有内容
            local rest="${input#*笔记}"
            # 去除开头的空格和换行
            rest=$(echo "$rest" | sed 's/^[[:space:]]*//')
            
            # 获取第一行
            local first_line=$(echo "$rest" | head -1)
            # 从第一行提取文件名（第一个空格或换行前的内容）
            local title=$(echo "$first_line" | awk '{print $1}')
            
            # 获取文件名之后的所有内容
            local content=""
            # 获取第一行中文件名之后的内容（如果有空格）
            local first_line_after_title=$(echo "$first_line" | cut -s -d' ' -f2-)
            # 获取后续所有行
            local other_lines=$(echo "$rest" | tail -n +2)
            
            # 合并内容
            if [[ -n "$first_line_after_title" && -n "$other_lines" ]]; then
                # 第一行有内容，且还有后续行
                content="${first_line_after_title}
${other_lines}"
            elif [[ -n "$first_line_after_title" ]]; then
                # 只有第一行有内容
                content="$first_line_after_title"
            elif [[ -n "$other_lines" ]]; then
                # 第一行只有文件名，内容在后续行
                content="$other_lines"
            fi
            
            create_note "$title" "$content"
            ;;
            
        # 删除笔记
        删除笔记*|移除笔记*)
            local title="${input#*笔记}"
            title=$(echo "$title" | sed 's/^[[:space:]]*//')
            delete_note "$title"
            ;;
            
        # 查看笔记
        查看笔记*|打开笔记*|读取笔记*|显示笔记*)
            local title="${input#*笔记}"
            title=$(echo "$title" | sed 's/^[[:space:]]*//')
            view_note "$title"
            ;;
            
        # 搜索标题
        搜索标题*|查找标题*|标题搜索*)
            local keyword="${input#*标题}"
            keyword=$(echo "$keyword" | sed 's/^[[:space:]]*//')
            search_titles "$keyword"
            ;;
            
        # 搜索内容
        搜索内容*|查找内容*|内容搜索*)
            local keyword="${input#*搜索}"
            keyword=$(echo "$keyword" | sed 's/^[[:space:]]*//')
            keyword=$(echo "$keyword" | sed 's/^内容//;s/^[[:space:]]*//')
            search_content "$keyword"
            ;;
            
        # 模糊搜索
        模糊搜索*|快速搜索*)
            local keyword="${input#*搜索}"
            keyword=$(echo "$keyword" | sed 's/^[[:space:]]*//;s/^模糊//;s/^[[:space:]]*//')
            fuzzy_search "$keyword"
            ;;
            
        # 今天日记
        今天日记*|今日日记*|daily*)
            local content="${input#*日记}"
            content=$(echo "$content" | sed 's/^[[:space:]]*//')
            daily_note "$content"
            ;;
            
        # 列出所有
        列出所有|所有笔记)
            list_all
            ;;
            
        # 列出文件夹
        列出文件夹|所有文件夹)
            list_folders
            ;;
            
        # 最近笔记
        最近笔记|最近修改)
            recent_notes
            ;;
            
        # 修改库路径
        修改库路径|修改库目录|更改路径|切换仓库)
            change_vault_path
            ;;
            
        # 执行删除（内部使用）
        EXECUTE_DELETE:*)
            local filepath="${input#EXECUTE_DELETE:}"
            execute_delete "$filepath"
            ;;
            
        # 未知命令
        *)
            echo -e "${RED}无法理解的指令: $input${NC}"
            echo "试试: 创建笔记, 搜索内容, 今天日记, 帮助"
            return 1
            ;;
    esac
}

# 主程序
main() {
    if [[ $# -eq 0 ]]; then
        echo -e "${YELLOW}Obsidian Headless - 无头 Obsidian 自然语言控制工具${NC}"
        echo "仓库: $VAULT_PATH"
        echo
        show_help
        exit 0
    fi
    
    local input="$*"
    
    if [[ ! -d "$VAULT_PATH" ]]; then
        echo -e "${RED}错误: 仓库路径不存在: $VAULT_PATH${NC}"
        echo "设置环境变量: export OBSIDIAN_VAULT=/path/to/vault"
        exit 1
    fi
    
    parse_command "$input"
}

main "$@"


path: config.json
content: {
  "name": "obsidian-headless",
  "version": "1.0.0",
  "description": "在无显示器/无 GUI 环境下通过自然语言指令管理 Obsidian 笔记仓库",
  "author": "mann",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/imakid/obsidian-headless"
  },
  "keywords": [
    "obsidian",
    "cli",
    "openclaw",
    "headless",
    "notes"
  ]
}


path: examples/examples.sh
content: #!/bin/bash
# examples.sh - Obsidian Headless 使用示例

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OBS="$SCRIPT_DIR/bin/obsidian-headless.sh"

echo "Obsidian Headless 使用示例"
echo "=========================="
echo

# 颜色
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'

run_example() {
    local description="$1"
    local cmd="$2"
    
    echo -e "${BLUE}$description${NC}"
    echo "命令: obs '$cmd'"
    echo "---"
    $OBS "$cmd"
    echo
}

echo "1. 创建笔记示例"
echo "---------------"
run_example "创建简单笔记" "创建笔记 想法收集"
run_example "创建带内容的笔记" "创建笔记 会议记录 今天讨论了项目进度..."

echo "2. 搜索笔记示例"
echo "---------------"
run_example "按标题搜索" "搜索标题 openclaw"
run_example "按内容搜索" "搜索内容 home assistant"
run_example "模糊搜索" "模糊搜索 tavily"

echo "3. 日记示例"
echo "-----------"
run_example "创建今天日记" "今天日记"
run_example "创建带内容的日记" "今天日记 今天完成了技能开发..."

echo "4. 查看和管理示例"
echo "-----------------"
run_example "列出所有笔记" "列出所有"
run_example "列出文件夹" "列出文件夹"
run_example "查看最近笔记" "最近笔记"

echo "5. 删除笔记示例（演示）"
echo "-----------------------"
echo "命令: obs '删除笔记 旧笔记'"
echo "说明: 会显示匹配笔记并要求确认"
echo


path: install.sh
content: #!/bin/bash
# install.sh - Obsidian Headless 安装脚本

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

echo "Obsidian Headless 安装程序"
echo "=========================="
echo

# 检查依赖
echo "检查依赖..."

# 检查 ripgrep
if command -v rg &>/dev/null; then
    echo -e "${GREEN}✓${NC} ripgrep (rg) 已安装"
else
    echo -e "${YELLOW}⚠${NC} ripgrep (rg) 未安装（推荐安装以获得更快的搜索速度）"
    echo "  安装命令:"
    echo "    Ubuntu/Debian: sudo apt install ripgrep"
    echo "    macOS: brew install ripgrep"
fi

# 检查 find
echo -e "${GREEN}✓${NC} find 已安装"

# 检查 grep
echo -e "${GREEN}✓${NC} grep 已安装"

echo

# 创建软链接到 PATH
echo "创建快捷命令..."

# 获取 bin 目录路径
BIN_DIR="$SCRIPT_DIR/bin"
OBS_SCRIPT="$SCRIPT_DIR/obs"

# 创建 obs 快捷命令
chmod +x "$OBS_SCRIPT"
chmod +x "$BIN_DIR/obsidian-headless.sh"

# 添加到 shell 配置文件
SHELL_RC=""
if [[ "$SHELL" == *"bash"* ]]; then
    SHELL_RC="$HOME/.bashrc"
elif [[ "$SHELL" == *"zsh"* ]]; then
    SHELL_RC="$HOME/.zshrc"
fi

if [[ -n "$SHELL_RC" && -f "$SHELL_RC" ]]; then
    # 检查是否已添加
    if ! grep -q "alias obs=" "$SHELL_RC" 2>/dev/null; then
        echo "alias obs='$OBS_SCRIPT'" >> "$SHELL_RC"
        echo -e "${GREEN}✓${NC} 已添加 'obs' 别名到 $SHELL_RC"
        echo "  请运行: source $SHELL_RC"
    else
        echo -e "${YELLOW}⚠${NC} 'obs' 别名已存在于 $SHELL_RC"
    fi
fi

# 创建系统级链接（可选）
if [[ -d "/usr/local/bin" && -w "/usr/local/bin" ]]; then
    if [[ ! -f "/usr/local/bin/obsidian-headless" ]]; then
        ln -s "$BIN_DIR/obsidian-headless.sh" /usr/local/bin/obsidian-headless 2>/dev/null && \
            echo -e "${GREEN}✓${NC} 已创建系统命令: obsidian-headless" || \
            echo -e "${YELLOW}⚠${NC} 无法创建系统命令（可能需要 sudo）"
    fi
fi

echo
echo -e "${GREEN}安装完成!${NC}"
echo
echo "使用方法:"
echo "  obs '创建笔记 新想法'"
echo "  obs '搜索内容 home assistant'"
echo "  obs '今天日记'"
echo
echo "查看帮助:"
echo "  obs --help"
echo


path: tests/test.sh
content: #!/bin/bash
# test.sh - Obsidian Headless 测试脚本

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BIN="$SCRIPT_DIR/bin/obsidian-headless.sh"

GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'

PASSED=0
FAILED=0

# 测试函数
run_test() {
    local name="$1"
    local cmd="$2"
    local expected="$3"
    
    echo -n "测试: $name ... "
    
    if result=$($cmd 2>&1); then
        if [[ -n "$expected" && "$result" == *"$expected"* ]]; then
            echo -e "${GREEN}通过${NC}"
            ((PASSED++))
        elif [[ -z "$expected" ]]; then
            echo -e "${GREEN}通过${NC}"
            ((PASSED++))
        else
            echo -e "${RED}失败${NC} (输出不匹配)"
            ((FAILED++))
        fi
    else
        if [[ "$expected" == "FAIL" ]]; then
            echo -e "${GREEN}通过${NC} (预期失败)"
            ((PASSED++))
        else
            echo -e "${RED}失败${NC}"
            ((FAILED++))
        fi
    fi
}

echo "Obsidian Headless 测试"
echo "====================="
echo

# 检查环境
if [[ -z "$OBSIDIAN_VAULT" ]]; then
    echo -e "${YELLOW}警告: OBSIDIAN_VAULT 未设置，使用自动检测${NC}"
fi

echo

# 基础功能测试
echo "基础功能测试:"
run_test "帮助命令" "$BIN '帮助'" "Obsidian Headless"
run_test "列出所有" "$BIN '列出所有'" "所有笔记"
run_test "列出文件夹" "$BIN '列出文件夹'" "所有文件夹"

# 创建笔记测试
echo
echo "创建笔记测试:"
TEST_NOTE="测试笔记_$(date +%s)"
run_test "创建笔记" "$BIN 创建笔记 ${TEST_NOTE} 测试内容" "创建笔记成功"

# 查看笔记测试
run_test "查看笔记" "$BIN 查看笔记 ${TEST_NOTE}" "测试内容"

# 搜索测试
echo
echo "搜索测试:"
run_test "搜索标题" "$BIN '搜索标题 测试'" "测试"
run_test "搜索内容" "$BIN '搜索内容 测试内容'" "测试内容"
run_test "模糊搜索" "$BIN '模糊搜索 测试'" "标题匹配"

# 日记测试
echo
echo "日记测试:"
run_test "今天日记" "$BIN '今天日记 测试日记内容'" "创建今天日记"

# 清理测试笔记
echo
echo "清理测试数据..."
rm -f "$OBSIDIAN_VAULT/$TEST_NOTE.md" 2>/dev/null || true
rm -f "$OBSIDIAN_VAULT/${TEST_NOTE}.md" 2>/dev/null || true

echo
echo "====================="
echo -e "通过: ${GREEN}$PASSED${NC}"
echo -e "失败: ${RED}$FAILED${NC}"

if [[ $FAILED -eq 0 ]]; then
    echo -e "${GREEN}所有测试通过!${NC}"
    exit 0
else
    echo -e "${RED}部分测试失败${NC}"
    exit 1
fi


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)