Skip to content

Commit 802258b

Browse files
authored
Merge pull request #17 from devwhodevs/feature/v1.5-chatgpt-actions
feat: v1.5 — ChatGPT Actions Integration
2 parents c7bcab3 + 7d5923b commit 802258b

8 files changed

Lines changed: 823 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## v1.5.0 — ChatGPT Actions (2026-03-26)
4+
5+
### Added
6+
- **OpenAPI 3.1.0 spec** (`openapi.rs`) — hand-written spec for all 23 endpoints, served at `GET /openapi.json`
7+
- **ChatGPT plugin manifest** — served at `GET /.well-known/ai-plugin.json`
8+
- **`--setup-chatgpt` CLI helper** — interactive setup: enables HTTP, creates API key, configures CORS, prompts for public URL
9+
- **Plugin config**`[http.plugin]` section for name, description, contact_email, public_url
10+
11+
### Changed
12+
- Module count: 25 → 26
13+
- Test count: 417 → 426
14+
- `/openapi.json` and `/.well-known/ai-plugin.json` routes require no authentication
15+
316
## v1.4.0 — PARA Migration (2026-03-26)
417

518
### Added

CLAUDE.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Local knowledge graph + intelligence layer for Obsidian vaults. Rust CLI + MCP s
44

55
## Architecture
66

7-
Single binary with 25 modules behind a lib crate:
7+
Single binary with 26 modules behind a lib crate:
88

99
- `config.rs` — loads `~/.engraph/config.toml` and `vault.toml`, merges CLI args, provides `data_dir()`. Includes `intelligence: Option<bool>`, `[models]` section for model overrides, `[obsidian]` section (CLI path, enabled flag), and `[agents]` section (registered AI agent names). `Config::save()` writes back to disk.
1010
- `chunker.rs` — smart chunking with break-point scoring algorithm. Finds optimal split points considering headings, code fences, blank lines, and thematic breaks. `split_oversized_chunks()` handles token-aware secondary splitting with overlap
@@ -23,16 +23,17 @@ Single binary with 25 modules behind a lib crate:
2323
- `placement.rs` — folder placement engine. Uses folder centroids (online mean of embeddings per folder) to suggest the best folder for new notes. Falls back to inbox when confidence is low. Includes placement correction detection (`detect_correction_from_frontmatter`) and frontmatter stripping for moved files
2424
- `writer.rs` — write pipeline orchestrator. 5-step pipeline: resolve tags (fuzzy match + register new), discover links (exact + fuzzy), place in folder, atomic file write (temp + rename), and index update. Supports create, append, update_metadata, move_note, archive, unarchive, edit (section-level replace/prepend/append), rewrite (full content with frontmatter preservation), edit_frontmatter (granular set/remove/add_tag/remove_tag/add_alias/remove_alias ops), and delete (soft archive or hard permanent) operations with mtime-based conflict detection and crash recovery via temp file cleanup
2525
- `watcher.rs` — file watcher for `engraph serve`. OS thread producer (notify-debouncer-full, 2s debounce) sends `Vec<WatchEvent>` over tokio::mpsc to async consumer task. Two-pass batch processing: mutations (index_file/remove_file/rename_file) then edge rebuild. Move detection via content hash matching. Placement correction on file moves. Centroid adjustment on file add/remove. Startup reconciliation via `run_index_shared`. `recent_writes` map coordination with MCP server to prevent double re-indexing of files written through the write pipeline
26-
- `serve.rs` — MCP stdio server via rmcp SDK. Exposes 22 tools: 8 read (search, read, read_section, list, vault_map, who, project, context) + 10 write (create, append, update_metadata, move_note, archive, unarchive, edit, rewrite, edit_frontmatter, delete) + 1 diagnostic (health) + 3 migrate (migrate_preview, migrate_apply, migrate_undo). `edit_frontmatter` replaces `update_metadata` for granular frontmatter mutations. EngraphServer struct with Arc+Mutex wrapping for async handlers. Loads intelligence models (orchestrator + reranker) when enabled, wires into `search_with_intelligence`. Spawns file watcher on startup. CLI events table provides audit log for write operations. `recent_writes` map prevents double re-indexing of MCP-written files
26+
- `serve.rs` — MCP stdio server via rmcp SDK. Exposes 22 tools: 8 read (search, read, read_section, list, vault_map, who, project, context) + 10 write (create, append, update_metadata, move_note, archive, unarchive, edit, rewrite, edit_frontmatter, delete) + 1 diagnostic (health) + 3 migrate (migrate_preview, migrate_apply, migrate_undo). `edit_frontmatter` replaces `update_metadata` for granular frontmatter mutations. EngraphServer struct with Arc+Mutex wrapping for async handlers. Loads intelligence models (orchestrator + reranker) when enabled, wires into `search_with_intelligence`. Spawns file watcher on startup. CLI events table provides audit log for write operations. `recent_writes` map prevents double re-indexing of MCP-written files. HTTP mode also serves `openapi.rs` routes (`/openapi.json`, `/.well-known/ai-plugin.json`) with no auth required
2727
- `http.rs` — axum-based HTTP REST API server, enabled via `engraph serve --http`. 23 REST endpoints mirroring all 22 MCP tools + update-metadata. API key authentication with `eg_` prefixed keys and read/write permission levels. Per-key token bucket rate limiting (configurable requests/minute). CORS with configurable allowed origins for web-based agents. `--no-auth` mode for local development (127.0.0.1 only). Graceful shutdown via `CancellationToken` coordinating MCP + HTTP + watcher exit
28+
- `openapi.rs` — OpenAPI 3.1.0 spec builder and ChatGPT plugin manifest. Hand-written spec for all 23 HTTP endpoints, served at `GET /openapi.json`. Plugin manifest served at `GET /.well-known/ai-plugin.json`. Both routes require no authentication. `[http.plugin]` config section for name, description, contact_email, and public_url. Used by `engraph configure --setup-chatgpt` for interactive ChatGPT Actions setup
2829
- `graph.rs` — vault graph agent. Extracts wikilink targets, expands search results by following graph connections 1-2 hops. Relevance filtering via FTS5 term check and shared tags
2930
- `profile.rs` — vault profile detection. Auto-detects PARA/Folders/Flat structure, vault type (Obsidian/Logseq/Plain), wikilinks, frontmatter, tags. Content-based role detection for people/daily/archive folders by content patterns (not just names). Writes/loads `vault.toml`
3031
- `store.rs` — SQLite persistence. Tables: `meta`, `files` (with docid, created_by), `chunks` (with vector BLOBs), `chunks_fts` (FTS5), `edges` (vault graph), `tombstones`, `tag_registry`, `folder_centroids`, `placement_corrections`, `link_skiplist` (reserved), `llm_cache` (orchestrator result cache), `cli_events` (audit log for CLI operations). `vec_chunks` virtual table (sqlite-vec) for KNN search. Dynamic embedding dimension stored in meta. `has_dimension_mismatch()` and `reset_for_reindex()` for migration. Enhanced `resolve_file()` with fuzzy Levenshtein matching as final fallback
3132
- `indexer.rs` — orchestrates vault walking (via `ignore` crate for `.gitignore` support), diffing, chunking, embedding, writes to store + sqlite-vec + FTS5, vault graph edge building (wikilinks + people detection), and folder centroid computation. Exposes `index_file`, `remove_file`, `rename_file` as public per-file functions. `run_index_shared` accepts external store/embedder for watcher FullRescan. Dimension migration on model change.
3233
- `temporal.rs` — temporal search lane. Extracts note dates from frontmatter `date:` field or `YYYY-MM-DD` filename patterns. Heuristic date parsing for natural language ("today", "yesterday", "last week", "this month", "recent", month names, ISO dates, date ranges). Smooth decay scoring for files near but outside target date range. Provides `extract_note_date()` for indexing and `score_temporal()` + `parse_date_range_heuristic()` for search
3334
- `search.rs` — hybrid search orchestrator. `search_with_intelligence()` runs the full pipeline: orchestrate (intent + expansions) → 5-lane RRF retrieval (semantic + FTS5 + graph + reranker + temporal) per expansion → two-pass RRF fusion. `search_internal()` is a thin wrapper without intelligence models. Adaptive lane weights per query intent including temporal (1.5 weight for time-aware queries). Results display normalized confidence percentages (0-100%) instead of raw RRF scores.
3435

35-
`main.rs` is a thin clap CLI (async via `#[tokio::main]`). Subcommands: `index` (with progress bar), `search` (with `--explain`, loads intelligence models when enabled), `status` (shows intelligence state + date coverage stats), `clear`, `init` (intelligence onboarding prompt, detects Obsidian CLI + AI agents), `configure` (`--enable-intelligence`, `--disable-intelligence`, `--model`, `--obsidian-cli`, `--no-obsidian-cli`, `--agent`, `--add-api-key`, `--list-api-keys`, `--revoke-api-key`), `models`, `graph` (show/stats), `context` (read/list/vault-map/who/project/topic), `write` (create/append/update-metadata/move/edit/rewrite/edit-frontmatter/delete), `migrate` (para with `--preview`/`--apply`/`--undo` for PARA vault restructuring), `serve` (MCP stdio server with file watcher + intelligence + optional `--http`/`--port`/`--host`/`--no-auth` for HTTP REST API).
36+
`main.rs` is a thin clap CLI (async via `#[tokio::main]`). Subcommands: `index` (with progress bar), `search` (with `--explain`, loads intelligence models when enabled), `status` (shows intelligence state + date coverage stats), `clear`, `init` (intelligence onboarding prompt, detects Obsidian CLI + AI agents), `configure` (`--enable-intelligence`, `--disable-intelligence`, `--model`, `--obsidian-cli`, `--no-obsidian-cli`, `--agent`, `--add-api-key`, `--list-api-keys`, `--revoke-api-key`, `--setup-chatgpt`), `models`, `graph` (show/stats), `context` (read/list/vault-map/who/project/topic), `write` (create/append/update-metadata/move/edit/rewrite/edit-frontmatter/delete), `migrate` (para with `--preview`/`--apply`/`--undo` for PARA vault restructuring), `serve` (MCP stdio server with file watcher + intelligence + optional `--http`/`--port`/`--host`/`--no-auth` for HTTP REST API).
3637

3738
## Key patterns
3839

@@ -81,7 +82,7 @@ Single vault only. Re-indexing a different vault path triggers a confirmation pr
8182

8283
## Testing
8384

84-
- Unit tests in each module (`cargo test --lib`) — 417 tests, no network required
85+
- Unit tests in each module (`cargo test --lib`) — 426 tests, no network required
8586
- Integration tests (`cargo test --test integration -- --ignored`) — require GGUF model download
8687
- Build requires CMake (for llama.cpp C++ compilation)
8788

README.md

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,55 @@ Notes that don't match any signal with sufficient confidence stay in place. Dail
374374

375375
**HTTP endpoints:** `POST /api/migrate/preview`, `/api/migrate/apply`, `/api/migrate/undo` — available via `engraph serve --http`.
376376

377+
## ChatGPT Actions
378+
379+
`engraph serve --http` can expose your vault to ChatGPT as a custom Action. engraph generates a standards-compliant OpenAPI 3.1.0 spec and a ChatGPT plugin manifest automatically.
380+
381+
**Set up:**
382+
383+
```bash
384+
engraph configure --setup-chatgpt
385+
```
386+
387+
Interactive helper that:
388+
1. Enables HTTP mode (if not already on)
389+
2. Creates a write-permission API key
390+
3. Configures CORS for `https://chat.openai.com`
391+
4. Prompts for your public URL (ngrok or similar)
392+
393+
**Endpoints served automatically (no auth required):**
394+
395+
| Endpoint | Description |
396+
|----------|-------------|
397+
| `GET /openapi.json` | OpenAPI 3.1.0 spec for all 23 endpoints |
398+
| `GET /.well-known/ai-plugin.json` | ChatGPT plugin manifest |
399+
400+
**Quick setup steps:**
401+
402+
```bash
403+
# 1. Run the setup helper
404+
engraph configure --setup-chatgpt
405+
406+
# 2. Start engraph with HTTP
407+
engraph serve --http
408+
409+
# 3. Expose via tunnel (example with ngrok)
410+
ngrok http 3030
411+
412+
# 4. In ChatGPT → Explore GPTs → Create → Configure → Add Action
413+
# Import from URL: https://<your-ngrok-url>/openapi.json
414+
```
415+
416+
**Plugin config in `~/.engraph/config.toml`:**
417+
418+
```toml
419+
[http.plugin]
420+
name = "My Vault"
421+
description = "Search and read my personal knowledge base"
422+
contact_email = "you@example.com"
423+
public_url = "https://abc123.ngrok.io"
424+
```
425+
377426
## Use cases
378427

379428
**AI-assisted knowledge work** — Give Claude or Cursor deep access to your personal knowledge base. Instead of copy-pasting context, the agent searches, reads, and cross-references your notes directly.
@@ -425,7 +474,7 @@ engraph is not a replacement for Obsidian — it's the intelligence layer that s
425474
- Content-based folder role detection (people, daily, archive) by content patterns
426475
- PARA migration: AI-assisted vault restructuring into Projects/Areas/Resources/Archive with preview, apply, and undo workflow
427476
- Configurable model overrides for multilingual support
428-
- 417 unit tests, CI on macOS + Ubuntu
477+
- 426 unit tests, CI on macOS + Ubuntu
429478

430479
## Roadmap
431480

@@ -437,7 +486,8 @@ engraph is not a replacement for Obsidian — it's the intelligence layer that s
437486
- [x] ~~Temporal search — find notes by time period, date-aware queries~~ (v1.2)
438487
- [x] ~~HTTP/REST API — complement MCP with a standard web API~~ (v1.3)
439488
- [x] ~~PARA migration — AI-assisted vault restructuring with preview/apply/undo~~ (v1.4)
440-
- [ ] Multi-vault — search across multiple vaults (v1.5)
489+
- [x] ~~ChatGPT Actions — OpenAPI 3.1.0 spec + plugin manifest + `--setup-chatgpt` helper~~ (v1.5)
490+
- [ ] Multi-vault — search across multiple vaults (v1.6)
441491

442492
## Configuration
443493

@@ -471,7 +521,7 @@ All data stored in `~/.engraph/` — single SQLite database (~10MB typical), GGU
471521
## Development
472522

473523
```bash
474-
cargo test --lib # 417 unit tests, no network (requires CMake for llama.cpp)
524+
cargo test --lib # 426 unit tests, no network (requires CMake for llama.cpp)
475525
cargo clippy -- -D warnings
476526
cargo fmt --check
477527

@@ -483,7 +533,7 @@ cargo test --test integration -- --ignored
483533

484534
Contributions welcome. Please open an issue first to discuss what you'd like to change.
485535

486-
The codebase is 25 Rust modules behind a lib crate. `CLAUDE.md` in the repo root has detailed architecture documentation for AI-assisted development.
536+
The codebase is 26 Rust modules behind a lib crate. `CLAUDE.md` in the repo root has detailed architecture documentation for AI-assisted development.
487537

488538
## License
489539

src/config.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ pub struct AgentsConfig {
3434
pub windsurf: bool,
3535
}
3636

37+
/// ChatGPT Actions plugin metadata.
38+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
39+
pub struct PluginConfig {
40+
pub name: Option<String>,
41+
pub description: Option<String>,
42+
pub contact_email: Option<String>,
43+
pub public_url: Option<String>,
44+
}
45+
3746
/// HTTP REST API configuration.
3847
#[derive(Debug, Clone, Serialize, Deserialize)]
3948
#[serde(default)]
@@ -44,6 +53,8 @@ pub struct HttpConfig {
4453
pub rate_limit: u32, // requests per minute per key, 0 = unlimited
4554
pub cors_origins: Vec<String>,
4655
pub api_keys: Vec<ApiKeyConfig>,
56+
#[serde(default)]
57+
pub plugin: PluginConfig,
4758
}
4859

4960
impl Default for HttpConfig {
@@ -55,6 +66,7 @@ impl Default for HttpConfig {
5566
rate_limit: 60,
5667
cors_origins: vec![],
5768
api_keys: vec![],
69+
plugin: PluginConfig::default(),
5870
}
5971
}
6072
}
@@ -356,4 +368,15 @@ permissions = "read"
356368
Some("hf:custom/model/embed.gguf".into())
357369
);
358370
}
371+
372+
#[test]
373+
fn test_config_with_plugin() {
374+
let toml = r#"
375+
[http.plugin]
376+
name = "my-vault"
377+
public_url = "https://vault.example.com"
378+
"#;
379+
let config: Config = toml::from_str(toml).unwrap();
380+
assert_eq!(config.http.plugin.name.as_deref(), Some("my-vault"));
381+
}
359382
}

src/http.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,9 @@ pub fn build_router(state: ApiState) -> Router {
380380
.route("/api/migrate/preview", post(handle_migrate_preview))
381381
.route("/api/migrate/apply", post(handle_migrate_apply))
382382
.route("/api/migrate/undo", post(handle_migrate_undo))
383+
// OpenAPI / ChatGPT plugin discovery (no auth required)
384+
.route("/openapi.json", get(handle_openapi))
385+
.route("/.well-known/ai-plugin.json", get(handle_plugin_manifest))
383386
.layer(cors)
384387
.with_state(state)
385388
}
@@ -388,6 +391,36 @@ async fn health_check() -> &'static str {
388391
"ok"
389392
}
390393

394+
async fn handle_openapi(State(state): State<ApiState>) -> impl IntoResponse {
395+
let default_url = format!(
396+
"http://{}:{}",
397+
state.http_config.host, state.http_config.port
398+
);
399+
let server_url = state
400+
.http_config
401+
.plugin
402+
.public_url
403+
.as_deref()
404+
.unwrap_or(&default_url);
405+
let spec = crate::openapi::build_openapi_spec(server_url);
406+
Json(spec)
407+
}
408+
409+
async fn handle_plugin_manifest(State(state): State<ApiState>) -> impl IntoResponse {
410+
let default_url = format!(
411+
"http://{}:{}",
412+
state.http_config.host, state.http_config.port
413+
);
414+
let server_url = state
415+
.http_config
416+
.plugin
417+
.public_url
418+
.as_deref()
419+
.unwrap_or(&default_url);
420+
let manifest = crate::openapi::build_plugin_manifest(&state.http_config, server_url);
421+
Json(manifest)
422+
}
423+
391424
// ---------------------------------------------------------------------------
392425
// Read endpoint handlers
393426
// ---------------------------------------------------------------------------
@@ -941,6 +974,7 @@ mod tests {
941974
permissions: "write".into(),
942975
},
943976
],
977+
plugin: crate::config::PluginConfig::default(),
944978
}
945979
}
946980

@@ -1272,4 +1306,40 @@ mod tests {
12721306
assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
12731307
assert!(response.headers().get("retry-after").is_some());
12741308
}
1309+
1310+
// -----------------------------------------------------------------------
1311+
// OpenAPI / Plugin manifest tests (no auth required)
1312+
// -----------------------------------------------------------------------
1313+
1314+
#[tokio::test]
1315+
async fn test_openapi_no_auth_required() {
1316+
let state = test_api_state();
1317+
let app = build_router(state);
1318+
let response = app
1319+
.oneshot(
1320+
axum::http::Request::builder()
1321+
.uri("/openapi.json")
1322+
.body(Body::empty())
1323+
.unwrap(),
1324+
)
1325+
.await
1326+
.unwrap();
1327+
assert_eq!(response.status(), StatusCode::OK);
1328+
}
1329+
1330+
#[tokio::test]
1331+
async fn test_plugin_manifest_no_auth_required() {
1332+
let state = test_api_state();
1333+
let app = build_router(state);
1334+
let response = app
1335+
.oneshot(
1336+
axum::http::Request::builder()
1337+
.uri("/.well-known/ai-plugin.json")
1338+
.body(Body::empty())
1339+
.unwrap(),
1340+
)
1341+
.await
1342+
.unwrap();
1343+
assert_eq!(response.status(), StatusCode::OK);
1344+
}
12751345
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod llm;
1313
pub mod markdown;
1414
pub mod migrate;
1515
pub mod obsidian;
16+
pub mod openapi;
1617
pub mod placement;
1718
pub mod profile;
1819
pub mod search;

0 commit comments

Comments
 (0)