Skip to content

Commit 33d226e

Browse files
az9713claude
andcommitted
[resources] Add ob CLI tool for direct Open Brain access without MCP
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8dc199e commit 33d226e

4 files changed

Lines changed: 337 additions & 0 deletions

File tree

docs/CLI_DIRECT_APPROACH.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,10 @@ The complete script is in [Appendix A](#appendix-a-complete-ob-script).
818818
819819
### 7.4 Installation
820820
821+
> **Note:** The `ob` script lives at `resources/ob-cli/ob` in this repository.
822+
> See [`resources/ob-cli/README.md`](../resources/ob-cli/README.md) for quick
823+
> installation instructions.
824+
821825
**Step 1 — Download the script:**
822826
823827
```bash
@@ -1159,6 +1163,10 @@ consistently takes more than 5 seconds:
11591163
11601164
## Appendix A: Complete `ob` Script
11611165
1166+
> **Note:** The canonical, installable copy of this script lives at
1167+
> [`resources/ob-cli/ob`](../resources/ob-cli/ob). The version below is kept
1168+
> for reference — use the file in `resources/ob-cli/` for installation.
1169+
11621170
Copy this entire script to `~/.local/bin/ob` and run `chmod +x ~/.local/bin/ob`.
11631171
11641172
```bash

resources/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ Companion tools and files for Open Brain.
55
| Resource | What It Is |
66
| -------- | ---------- |
77
| [Open Brain Companion](open-brain-companion.skill) | Claude Skill file for AI-assisted Open Brain help |
8+
| [ob CLI](ob-cli/) | Lightweight bash CLI for direct Open Brain access without MCP (`curl` + `jq` only) |

resources/ob-cli/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# ob CLI
2+
3+
A lightweight command-line interface for Open Brain personal memory. Uses `curl` and `jq` to talk directly to Supabase and OpenRouter — no MCP, no Edge Function, no runtime dependencies.
4+
5+
## Prerequisites
6+
7+
- `curl` and `jq` installed
8+
- A Supabase project with the `thoughts` table, `match_thoughts()` function, and indexes (see `docs/01-getting-started.md` Steps 1–3)
9+
- An OpenRouter API key
10+
11+
## Installation
12+
13+
```bash
14+
# Copy to a directory in your PATH
15+
mkdir -p ~/.local/bin
16+
cp resources/ob-cli/ob ~/.local/bin/ob
17+
chmod +x ~/.local/bin/ob
18+
19+
# Add to PATH if needed (in ~/.bashrc or ~/.zshrc)
20+
export PATH="$HOME/.local/bin:$PATH"
21+
```
22+
23+
## Configuration
24+
25+
Set these environment variables (in `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
26+
27+
```bash
28+
export OB_SUPABASE_URL="https://your-project-ref.supabase.co"
29+
export OB_SUPABASE_KEY="your-service-role-key"
30+
export OB_OPENROUTER_KEY="your-openrouter-key"
31+
```
32+
33+
Optional:
34+
35+
| Variable | Default | Description |
36+
|---|---|---|
37+
| `OB_THRESHOLD` | `0.7` | Similarity threshold for search |
38+
| `OB_COUNT` | `10` | Default result count |
39+
40+
## Commands
41+
42+
| Command | Description |
43+
|---|---|
44+
| `ob capture "thought text"` | Save a thought with embedding + metadata |
45+
| `ob search "query" [--threshold N] [--count N] [--json]` | Semantic search |
46+
| `ob recent [count]` | List recent thoughts |
47+
| `ob stats` | Knowledge base statistics |
48+
| `ob version` | Version and config status |
49+
50+
## Verification
51+
52+
```bash
53+
ob version # Should show version and config status
54+
ob stats # Should show thought count (requires configured env vars)
55+
```
56+
57+
## More Information
58+
59+
See [docs/CLI_DIRECT_APPROACH.md](../../docs/CLI_DIRECT_APPROACH.md) for full architecture context, CLI AI tool configuration, troubleshooting, and the comparison between MCP and CLI-Direct approaches.

resources/ob-cli/ob

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
#!/usr/bin/env bash
2+
#
3+
# ob — Open Brain CLI
4+
# A lightweight command-line interface for Open Brain personal memory.
5+
# Dependencies: curl, jq
6+
# License: FSL-1.1-MIT (same as Open Brain)
7+
#
8+
set -euo pipefail
9+
10+
OB_VERSION="1.0.0"
11+
12+
# ─── Configuration ────────────────────────────────────────────────────────────
13+
14+
: "${OB_SUPABASE_URL:=}"
15+
: "${OB_SUPABASE_KEY:=}"
16+
: "${OB_OPENROUTER_KEY:=}"
17+
: "${OB_THRESHOLD:=0.7}"
18+
: "${OB_COUNT:=10}"
19+
20+
OPENROUTER_BASE="https://openrouter.ai/api/v1"
21+
22+
# ─── Helpers ──────────────────────────────────────────────────────────────────
23+
24+
die() { echo "ob: error: $*" >&2; exit 1; }
25+
26+
check_deps() {
27+
command -v curl >/dev/null 2>&1 || die "curl is required but not installed"
28+
command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
29+
}
30+
31+
check_config() {
32+
[[ -n "$OB_SUPABASE_URL" ]] || die "OB_SUPABASE_URL is not set"
33+
[[ -n "$OB_SUPABASE_KEY" ]] || die "OB_SUPABASE_KEY is not set"
34+
[[ -n "$OB_OPENROUTER_KEY" ]] || die "OB_OPENROUTER_KEY is not set"
35+
}
36+
37+
supabase_headers() {
38+
echo -H "apikey: $OB_SUPABASE_KEY" -H "Authorization: Bearer $OB_SUPABASE_KEY"
39+
}
40+
41+
# ─── API Functions ────────────────────────────────────────────────────────────
42+
43+
get_embedding() {
44+
local text="$1"
45+
curl -sf "$OPENROUTER_BASE/embeddings" \
46+
-H "Authorization: Bearer $OB_OPENROUTER_KEY" \
47+
-H "Content-Type: application/json" \
48+
-d "$(jq -n --arg t "$text" '{model: "openai/text-embedding-3-small", input: $t}')" \
49+
| jq -c '.data[0].embedding'
50+
}
51+
52+
extract_metadata() {
53+
local text="$1"
54+
local system_prompt='Extract metadata from the user'\''s captured thought. Return JSON with: "people" (array of people mentioned, empty if none), "action_items" (array of implied to-dos, empty if none), "topics" (array of 1-3 short topic tags, always at least one), "type" (one of "observation", "task", "idea", "reference", "person_note"). Only extract what'\''s explicitly there.'
55+
56+
curl -sf "$OPENROUTER_BASE/chat/completions" \
57+
-H "Authorization: Bearer $OB_OPENROUTER_KEY" \
58+
-H "Content-Type: application/json" \
59+
-d "$(jq -n --arg t "$text" --arg s "$system_prompt" '{
60+
model: "openai/gpt-4o-mini",
61+
response_format: {type: "json_object"},
62+
messages: [
63+
{role: "system", content: $s},
64+
{role: "user", content: $t}
65+
]
66+
}')" \
67+
| jq -c '.choices[0].message.content | fromjson'
68+
}
69+
70+
# ─── Commands ─────────────────────────────────────────────────────────────────
71+
72+
cmd_capture() {
73+
local content="${1:?Usage: ob capture \"thought text\"}"
74+
75+
echo "Generating embedding..." >&2
76+
local embedding
77+
embedding=$(get_embedding "$content") || die "Failed to generate embedding"
78+
79+
echo "Extracting metadata..." >&2
80+
local metadata
81+
metadata=$(extract_metadata "$content") || metadata='{"topics":["uncategorized"],"type":"observation"}'
82+
83+
echo "Saving thought..." >&2
84+
local result
85+
result=$(curl -sf -X POST "$OB_SUPABASE_URL/rest/v1/thoughts" \
86+
-H "apikey: $OB_SUPABASE_KEY" \
87+
-H "Authorization: Bearer $OB_SUPABASE_KEY" \
88+
-H "Content-Type: application/json" \
89+
-H "Prefer: return=representation" \
90+
-d "$(jq -n \
91+
--arg c "$content" \
92+
--argjson e "$embedding" \
93+
--argjson m "$metadata" \
94+
'{content: $c, embedding: $e, metadata: $m}')" \
95+
) || die "Failed to insert thought"
96+
97+
echo "$result" | jq '{
98+
success: true,
99+
id: .[0].id,
100+
content: .[0].content,
101+
metadata: .[0].metadata,
102+
created_at: .[0].created_at
103+
}'
104+
}
105+
106+
cmd_search() {
107+
local query="${1:?Usage: ob search \"query text\"}"
108+
local threshold="${OB_THRESHOLD}"
109+
local count="${OB_COUNT}"
110+
local json_output=false
111+
112+
shift
113+
while [[ $# -gt 0 ]]; do
114+
case "$1" in
115+
--threshold) threshold="$2"; shift 2 ;;
116+
--count) count="$2"; shift 2 ;;
117+
--json) json_output=true; shift ;;
118+
*) die "Unknown flag: $1" ;;
119+
esac
120+
done
121+
122+
local embedding
123+
embedding=$(get_embedding "$query") || die "Failed to generate query embedding"
124+
125+
local results
126+
results=$(curl -sf -X POST "$OB_SUPABASE_URL/rest/v1/rpc/match_thoughts" \
127+
-H "apikey: $OB_SUPABASE_KEY" \
128+
-H "Authorization: Bearer $OB_SUPABASE_KEY" \
129+
-H "Content-Type: application/json" \
130+
-d "$(jq -n \
131+
--argjson e "$embedding" \
132+
--argjson t "$threshold" \
133+
--argjson c "$count" \
134+
'{query_embedding: $e, match_threshold: $t, match_count: $c, filter: {}}')" \
135+
) || die "Failed to search thoughts"
136+
137+
if [[ "$json_output" == "true" ]]; then
138+
echo "$results" | jq .
139+
return
140+
fi
141+
142+
local result_count
143+
result_count=$(echo "$results" | jq 'length')
144+
145+
if [[ "$result_count" -eq 0 ]]; then
146+
echo "No results found for: $query"
147+
return
148+
fi
149+
150+
echo "Found $result_count results:"
151+
echo ""
152+
153+
echo "$results" | jq -r '.[] | "[" + (.similarity * 100 | round / 100 | tostring) + "] " + .content + "\n Topics: " + ((.metadata.topics // []) | join(", ")) + (if (.metadata.people // []) | length > 0 then " | People: " + ((.metadata.people // []) | join(", ")) else "" end) + (if .metadata.type then " | Type: " + .metadata.type else "" end) + "\n Captured: " + .created_at + "\n"'
154+
}
155+
156+
cmd_recent() {
157+
local count="${1:-$OB_COUNT}"
158+
159+
local results
160+
results=$(curl -sf \
161+
"$OB_SUPABASE_URL/rest/v1/thoughts?order=created_at.desc&limit=$count&select=id,content,metadata,created_at" \
162+
-H "apikey: $OB_SUPABASE_KEY" \
163+
-H "Authorization: Bearer $OB_SUPABASE_KEY" \
164+
) || die "Failed to list thoughts"
165+
166+
local result_count
167+
result_count=$(echo "$results" | jq 'length')
168+
169+
echo "$result_count most recent thoughts:"
170+
echo ""
171+
172+
echo "$results" | jq -r 'to_entries[] | "\(.key + 1). \(.value.content)\n Topics: \((.value.metadata.topics // []) | join(", "))" + (if (.value.metadata.people // []) | length > 0 then " | People: \((.value.metadata.people // []) | join(", "))" else "" end) + "\n Captured: \(.value.created_at)\n"'
173+
}
174+
175+
cmd_stats() {
176+
# Total count
177+
local total
178+
total=$(curl -sf \
179+
"$OB_SUPABASE_URL/rest/v1/thoughts?select=id" \
180+
-H "apikey: $OB_SUPABASE_KEY" \
181+
-H "Authorization: Bearer $OB_SUPABASE_KEY" \
182+
-H "Prefer: count=exact" \
183+
-H "Range: 0-0" \
184+
-I 2>/dev/null | grep -i content-range | sed 's/.*\///' | tr -d '\r\n') || total="unknown"
185+
186+
# Recent thoughts for date range and aggregation
187+
local all_thoughts
188+
all_thoughts=$(curl -sf \
189+
"$OB_SUPABASE_URL/rest/v1/thoughts?select=metadata,created_at&order=created_at.asc" \
190+
-H "apikey: $OB_SUPABASE_KEY" \
191+
-H "Authorization: Bearer $OB_SUPABASE_KEY" \
192+
) || die "Failed to fetch thoughts"
193+
194+
local oldest newest
195+
oldest=$(echo "$all_thoughts" | jq -r 'first.created_at // "N/A"' | cut -d'T' -f1)
196+
newest=$(echo "$all_thoughts" | jq -r 'last.created_at // "N/A"' | cut -d'T' -f1)
197+
198+
local top_topics
199+
top_topics=$(echo "$all_thoughts" | jq -r '[.[].metadata.topics // [] | .[]] | group_by(.) | map({topic: .[0], count: length}) | sort_by(-.count) | .[0:5] | .[].topic')
200+
201+
local top_people
202+
top_people=$(echo "$all_thoughts" | jq -r '[.[].metadata.people // [] | .[]] | group_by(.) | map({person: .[0], count: length}) | sort_by(-.count) | .[0:5] | .[].person')
203+
204+
local type_dist
205+
type_dist=$(echo "$all_thoughts" | jq -r '[.[].metadata.type // "unknown"] | group_by(.) | map(" " + .[0] + ": " + (length | tostring)) | .[]')
206+
207+
echo "Open Brain Statistics:"
208+
echo " Total thoughts: $total"
209+
echo " Oldest: $oldest"
210+
echo " Newest: $newest"
211+
echo " Top topics: $(echo "$top_topics" | tr '\n' ', ' | sed 's/,$//' | sed 's/,/, /g')"
212+
echo " Top people: $(echo "$top_people" | tr '\n' ', ' | sed 's/,$//' | sed 's/,/, /g')"
213+
echo " Type distribution:"
214+
echo "$type_dist"
215+
}
216+
217+
cmd_version() {
218+
echo "ob v$OB_VERSION"
219+
if [[ -n "$OB_SUPABASE_URL" ]]; then
220+
echo "Supabase URL: $(echo "$OB_SUPABASE_URL" | sed 's|https://\([^.]*\)\..*|\1|')... (configured)"
221+
else
222+
echo "Supabase URL: not configured"
223+
fi
224+
if [[ -n "$OB_OPENROUTER_KEY" ]]; then
225+
echo "OpenRouter: configured"
226+
else
227+
echo "OpenRouter: not configured"
228+
fi
229+
}
230+
231+
# ─── Main ─────────────────────────────────────────────────────────────────────
232+
233+
main() {
234+
check_deps
235+
236+
local cmd="${1:-help}"
237+
shift 2>/dev/null || true
238+
239+
case "$cmd" in
240+
capture) check_config; cmd_capture "$@" ;;
241+
search) check_config; cmd_search "$@" ;;
242+
recent) check_config; cmd_recent "$@" ;;
243+
stats) check_config; cmd_stats ;;
244+
version) cmd_version ;;
245+
help|--help|-h)
246+
echo "ob — Open Brain CLI v$OB_VERSION"
247+
echo ""
248+
echo "Usage:"
249+
echo " ob capture \"thought text\" Save a thought with embedding + metadata"
250+
echo " ob search \"query\" [--threshold N] [--count N] [--json]"
251+
echo " Semantic search across thoughts"
252+
echo " ob recent [count] List recent thoughts (default: 10)"
253+
echo " ob stats Show knowledge base statistics"
254+
echo " ob version Print version and config status"
255+
echo ""
256+
echo "Environment variables:"
257+
echo " OB_SUPABASE_URL Supabase project URL (required)"
258+
echo " OB_SUPABASE_KEY Supabase service role key (required)"
259+
echo " OB_OPENROUTER_KEY OpenRouter API key (required)"
260+
echo " OB_THRESHOLD Default similarity threshold (default: 0.7)"
261+
echo " OB_COUNT Default result count (default: 10)"
262+
;;
263+
*)
264+
die "Unknown command: $cmd (run 'ob help' for usage)"
265+
;;
266+
esac
267+
}
268+
269+
main "$@"

0 commit comments

Comments
 (0)