okb 1.0.0__tar.gz → 1.1.0a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {okb-1.0.0 → okb-1.1.0a0}/PKG-INFO +39 -65
  2. {okb-1.0.0 → okb-1.1.0a0}/README.md +32 -64
  3. {okb-1.0.0 → okb-1.1.0a0}/okb/cli.py +126 -0
  4. {okb-1.0.0 → okb-1.1.0a0}/okb/http_server.py +45 -0
  5. {okb-1.0.0 → okb-1.1.0a0}/okb/mcp_server.py +243 -0
  6. {okb-1.0.0 → okb-1.1.0a0}/okb/plugins/sources/__init__.py +2 -1
  7. {okb-1.0.0 → okb-1.1.0a0}/okb/plugins/sources/dropbox_paper.py +44 -9
  8. okb-1.1.0a0/okb/plugins/sources/todoist.py +254 -0
  9. {okb-1.0.0 → okb-1.1.0a0}/pyproject.toml +12 -1
  10. {okb-1.0.0 → okb-1.1.0a0}/okb/__init__.py +0 -0
  11. {okb-1.0.0 → okb-1.1.0a0}/okb/config.py +0 -0
  12. {okb-1.0.0 → okb-1.1.0a0}/okb/data/init.sql +0 -0
  13. {okb-1.0.0 → okb-1.1.0a0}/okb/ingest.py +0 -0
  14. {okb-1.0.0 → okb-1.1.0a0}/okb/llm/__init__.py +0 -0
  15. {okb-1.0.0 → okb-1.1.0a0}/okb/llm/base.py +0 -0
  16. {okb-1.0.0 → okb-1.1.0a0}/okb/llm/cache.py +0 -0
  17. {okb-1.0.0 → okb-1.1.0a0}/okb/llm/filter.py +0 -0
  18. {okb-1.0.0 → okb-1.1.0a0}/okb/llm/providers.py +0 -0
  19. {okb-1.0.0 → okb-1.1.0a0}/okb/local_embedder.py +0 -0
  20. {okb-1.0.0 → okb-1.1.0a0}/okb/migrate.py +0 -0
  21. {okb-1.0.0 → okb-1.1.0a0}/okb/migrations/0001.initial-schema.sql +0 -0
  22. {okb-1.0.0 → okb-1.1.0a0}/okb/migrations/0002.sync-state.sql +0 -0
  23. {okb-1.0.0 → okb-1.1.0a0}/okb/migrations/0003.structured-fields.sql +0 -0
  24. {okb-1.0.0 → okb-1.1.0a0}/okb/migrations/0004.tokens.sql +0 -0
  25. {okb-1.0.0 → okb-1.1.0a0}/okb/migrations/0005.database-metadata.sql +0 -0
  26. {okb-1.0.0 → okb-1.1.0a0}/okb/migrations/0006.llm-cache.sql +0 -0
  27. {okb-1.0.0 → okb-1.1.0a0}/okb/modal_embedder.py +0 -0
  28. {okb-1.0.0 → okb-1.1.0a0}/okb/modal_llm.py +0 -0
  29. {okb-1.0.0 → okb-1.1.0a0}/okb/plugins/__init__.py +0 -0
  30. {okb-1.0.0 → okb-1.1.0a0}/okb/plugins/base.py +0 -0
  31. {okb-1.0.0 → okb-1.1.0a0}/okb/plugins/registry.py +0 -0
  32. {okb-1.0.0 → okb-1.1.0a0}/okb/plugins/sources/github.py +0 -0
  33. {okb-1.0.0 → okb-1.1.0a0}/okb/rescan.py +0 -0
  34. {okb-1.0.0 → okb-1.1.0a0}/okb/scripts/__init__.py +0 -0
  35. {okb-1.0.0 → okb-1.1.0a0}/okb/scripts/watch.py +0 -0
  36. {okb-1.0.0 → okb-1.1.0a0}/okb/tokens.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: okb
3
- Version: 1.0.0
3
+ Version: 1.1.0a0
4
4
  Summary: Personal knowledge base with semantic search for LLMs
5
5
  Requires-Python: >=3.11
6
6
  Classifier: Programming Language :: Python :: 3
@@ -13,6 +13,7 @@ Provides-Extra: docx
13
13
  Provides-Extra: llm
14
14
  Provides-Extra: llm-bedrock
15
15
  Provides-Extra: pdf
16
+ Provides-Extra: todoist
16
17
  Provides-Extra: web
17
18
  Requires-Dist: PyGithub (>=2.0.0)
18
19
  Requires-Dist: anthropic (>=0.40.0) ; extra == "all"
@@ -35,10 +36,15 @@ Requires-Dist: python-docx (>=1.1.0) ; extra == "docx"
35
36
  Requires-Dist: pyyaml (>=6.0)
36
37
  Requires-Dist: ruff (>=0.1.0) ; extra == "dev"
37
38
  Requires-Dist: sentence-transformers (>=2.2.0)
39
+ Requires-Dist: todoist-api-python (>=3.0.0) ; extra == "all"
40
+ Requires-Dist: todoist-api-python (>=3.0.0) ; extra == "todoist"
38
41
  Requires-Dist: trafilatura (>=1.6.0) ; extra == "all"
39
42
  Requires-Dist: trafilatura (>=1.6.0) ; extra == "web"
40
43
  Requires-Dist: watchdog (>=3.0.0)
41
44
  Requires-Dist: yoyo-migrations (>=8.0.0)
45
+ Project-URL: Homepage, https://github.com/username/okb
46
+ Project-URL: Issues, https://github.com/username/okb/issues
47
+ Project-URL: Repository, https://github.com/username/okb
42
48
  Description-Content-Type: text/markdown
43
49
 
44
50
  # Owned Knowledge Base (OKB)
@@ -47,15 +53,14 @@ A local-first semantic search system for personal documents with Claude Code int
47
53
 
48
54
  ## Installation
49
55
 
56
+ pipx - preferred!
50
57
  ```bash
51
- pip install okb
58
+ pipx install okb
52
59
  ```
53
60
 
54
- Or from source:
61
+ Or pip:
55
62
  ```bash
56
- git clone https://github.com/yourusername/okb
57
- cd okb
58
- pip install -e .
63
+ pip install okb
59
64
  ```
60
65
 
61
66
  ## Quick Start
@@ -82,7 +87,7 @@ okb ingest ~/notes ~/docs
82
87
  | `okb db status` | Show database status |
83
88
  | `okb db destroy` | Remove container and volume (destructive) |
84
89
  | `okb ingest <paths>` | Ingest documents into knowledge base |
85
- | `okb ingest <paths> --local` | Ingest using CPU embedding (no Modal) |
90
+ | `okb ingest <paths> --local` | Ingest using local GPU/CPU embedding (no Modal) |
86
91
  | `okb serve` | Start MCP server (stdio, for Claude Code) |
87
92
  | `okb serve --http` | Start HTTP MCP server with token auth |
88
93
  | `okb watch <paths>` | Watch directories for changes |
@@ -93,7 +98,9 @@ okb ingest ~/notes ~/docs
93
98
  | `okb token list` | List tokens for a database |
94
99
  | `okb token revoke` | Revoke an API token |
95
100
  | `okb sync list` | List available API sources (plugins) |
101
+ | `okb sync list-projects <source>` | List projects from source (for config) |
96
102
  | `okb sync run <sources>` | Sync data from external APIs |
103
+ | `okb sync auth <source>` | Interactive OAuth setup (e.g., dropbox-paper) |
97
104
  | `okb sync status` | Show last sync times |
98
105
  | `okb rescan` | Check indexed files for changes, re-ingest stale |
99
106
  | `okb rescan --dry-run` | Show what would change without executing |
@@ -102,36 +109,6 @@ okb ingest ~/notes ~/docs
102
109
  | `okb llm deploy` | Deploy Modal LLM for open model inference |
103
110
  | `okb llm clear-cache` | Clear LLM response cache |
104
111
 
105
- ## Architecture
106
-
107
- ```
108
- ┌─────────────────────────────────────────────────────────────────────┐
109
- │ INGESTION (Burst GPU) │
110
- │ │
111
- │ Local Files → Contextual Chunking → Modal (GPU T4) → pgvector │
112
- │ │
113
- │ ~/notes/project-x/api-design.md │
114
- │ ↓ │
115
- │ "Document: API Design Notes │
116
- │ Project: project-x │
117
- │ Section: Authentication │
118
- │ Content: Use JWT tokens with..." │
119
- │ ↓ │
120
- │ [0.23, -0.41, 0.87, ...] → pgvector │
121
- └─────────────────────────────────────────────────────────────────────┘
122
-
123
- ┌─────────────────────────────────────────────────────────────────────┐
124
- │ RETRIEVAL (Always-on, Local) │
125
- │ │
126
- │ Claude Code → MCP Server → CPU Embedding → pgvector → Results │
127
- │ │
128
- │ "How do I handle auth?" │
129
- │ ↓ │
130
- │ [0.19, -0.38, 0.91, ...] (local CPU, ~300ms) │
131
- │ ↓ │
132
- │ Cosine similarity search → Top 5 chunks with context │
133
- └─────────────────────────────────────────────────────────────────────┘
134
- ```
135
112
 
136
113
  ## Configuration
137
114
 
@@ -273,7 +250,7 @@ Then configure Claude Code to connect via SSE:
273
250
  }
274
251
  ```
275
252
 
276
- ## MCP Tools (Available in Claude Code)
253
+ ## MCP Tools available to LLM
277
254
 
278
255
  | Tool | Purpose |
279
256
  |------|---------|
@@ -287,6 +264,11 @@ Then configure Claude Code to connect via SSE:
287
264
  | `save_knowledge` | Save knowledge from Claude for future reference |
288
265
  | `delete_knowledge` | Delete a Claude-saved knowledge entry |
289
266
  | `get_actionable_items` | Query tasks/events with structured filters |
267
+ | `get_database_info` | Get database description, topics, and stats |
268
+ | `set_database_description` | Update database description/topics (LLM can self-document) |
269
+ | `add_todo` | Create a TODO item in the knowledge base |
270
+ | `trigger_sync` | Sync API sources (Todoist, GitHub, Dropbox Paper) |
271
+ | `trigger_rescan` | Check indexed files for changes and re-ingest |
290
272
 
291
273
  ## Contextual Chunking
292
274
 
@@ -309,32 +291,6 @@ project: student-app
309
291
  category: backend
310
292
  ---
311
293
 
312
- # Query Optimization
313
-
314
- Use `select_related()` for foreign keys...
315
- ```
316
-
317
- ## Cost Estimate
318
-
319
- | Component | Local | Cloud Alternative |
320
- |-----------|-------|-------------------|
321
- | pgvector | $0 | ~$15-30/mo (CloudSQL) |
322
- | MCP Server | $0 | ~$5/mo (small VM) |
323
- | Modal embedding | ~$0.50-2/mo | N/A |
324
- | **Total** | **~$1-2/mo** | **~$20-35/mo** |
325
-
326
- ## Development
327
-
328
- ```bash
329
- # Install dev dependencies
330
- pip install -e ".[dev]"
331
-
332
- # Run tests
333
- pytest
334
-
335
- # Lint and format
336
- ruff check . && ruff format .
337
- ```
338
294
 
339
295
  ## Plugin System
340
296
 
@@ -385,12 +341,30 @@ plugins:
385
341
  enabled: true
386
342
  token: ${GITHUB_TOKEN} # Resolved from environment
387
343
  repos: [owner/repo1, owner/repo2]
344
+ todoist:
345
+ enabled: true
346
+ token: ${TODOIST_TOKEN}
347
+ include_completed: false # Sync completed tasks
348
+ completed_days: 30 # Days of completed history
349
+ include_comments: false # Include task comments (1 API call per task)
350
+ project_filter: [] # List of project IDs (use sync list-projects to find)
388
351
  dropbox-paper:
389
352
  enabled: true
390
- token: ${DROPBOX_TOKEN}
353
+ # Option 1: Refresh token (recommended, auto-refreshes)
354
+ app_key: ${DROPBOX_APP_KEY}
355
+ app_secret: ${DROPBOX_APP_SECRET}
356
+ refresh_token: ${DROPBOX_REFRESH_TOKEN}
357
+ # Option 2: Access token (short-lived, expires after ~4 hours)
358
+ # token: ${DROPBOX_TOKEN}
391
359
  folders: [/] # Optional: filter to specific folders
392
360
  ```
393
361
 
362
+ **Dropbox Paper OAuth Setup:**
363
+ ```bash
364
+ okb sync auth dropbox-paper
365
+ ```
366
+ This interactive command will guide you through getting a refresh token from Dropbox.
367
+
394
368
  ## License
395
369
 
396
370
  MIT
@@ -4,15 +4,14 @@ A local-first semantic search system for personal documents with Claude Code int
4
4
 
5
5
  ## Installation
6
6
 
7
+ pipx - preferred!
7
8
  ```bash
8
- pip install okb
9
+ pipx install okb
9
10
  ```
10
11
 
11
- Or from source:
12
+ Or pip:
12
13
  ```bash
13
- git clone https://github.com/yourusername/okb
14
- cd okb
15
- pip install -e .
14
+ pip install okb
16
15
  ```
17
16
 
18
17
  ## Quick Start
@@ -39,7 +38,7 @@ okb ingest ~/notes ~/docs
39
38
  | `okb db status` | Show database status |
40
39
  | `okb db destroy` | Remove container and volume (destructive) |
41
40
  | `okb ingest <paths>` | Ingest documents into knowledge base |
42
- | `okb ingest <paths> --local` | Ingest using CPU embedding (no Modal) |
41
+ | `okb ingest <paths> --local` | Ingest using local GPU/CPU embedding (no Modal) |
43
42
  | `okb serve` | Start MCP server (stdio, for Claude Code) |
44
43
  | `okb serve --http` | Start HTTP MCP server with token auth |
45
44
  | `okb watch <paths>` | Watch directories for changes |
@@ -50,7 +49,9 @@ okb ingest ~/notes ~/docs
50
49
  | `okb token list` | List tokens for a database |
51
50
  | `okb token revoke` | Revoke an API token |
52
51
  | `okb sync list` | List available API sources (plugins) |
52
+ | `okb sync list-projects <source>` | List projects from source (for config) |
53
53
  | `okb sync run <sources>` | Sync data from external APIs |
54
+ | `okb sync auth <source>` | Interactive OAuth setup (e.g., dropbox-paper) |
54
55
  | `okb sync status` | Show last sync times |
55
56
  | `okb rescan` | Check indexed files for changes, re-ingest stale |
56
57
  | `okb rescan --dry-run` | Show what would change without executing |
@@ -59,36 +60,6 @@ okb ingest ~/notes ~/docs
59
60
  | `okb llm deploy` | Deploy Modal LLM for open model inference |
60
61
  | `okb llm clear-cache` | Clear LLM response cache |
61
62
 
62
- ## Architecture
63
-
64
- ```
65
- ┌─────────────────────────────────────────────────────────────────────┐
66
- │ INGESTION (Burst GPU) │
67
- │ │
68
- │ Local Files → Contextual Chunking → Modal (GPU T4) → pgvector │
69
- │ │
70
- │ ~/notes/project-x/api-design.md │
71
- │ ↓ │
72
- │ "Document: API Design Notes │
73
- │ Project: project-x │
74
- │ Section: Authentication │
75
- │ Content: Use JWT tokens with..." │
76
- │ ↓ │
77
- │ [0.23, -0.41, 0.87, ...] → pgvector │
78
- └─────────────────────────────────────────────────────────────────────┘
79
-
80
- ┌─────────────────────────────────────────────────────────────────────┐
81
- │ RETRIEVAL (Always-on, Local) │
82
- │ │
83
- │ Claude Code → MCP Server → CPU Embedding → pgvector → Results │
84
- │ │
85
- │ "How do I handle auth?" │
86
- │ ↓ │
87
- │ [0.19, -0.38, 0.91, ...] (local CPU, ~300ms) │
88
- │ ↓ │
89
- │ Cosine similarity search → Top 5 chunks with context │
90
- └─────────────────────────────────────────────────────────────────────┘
91
- ```
92
63
 
93
64
  ## Configuration
94
65
 
@@ -230,7 +201,7 @@ Then configure Claude Code to connect via SSE:
230
201
  }
231
202
  ```
232
203
 
233
- ## MCP Tools (Available in Claude Code)
204
+ ## MCP Tools available to LLM
234
205
 
235
206
  | Tool | Purpose |
236
207
  |------|---------|
@@ -244,6 +215,11 @@ Then configure Claude Code to connect via SSE:
244
215
  | `save_knowledge` | Save knowledge from Claude for future reference |
245
216
  | `delete_knowledge` | Delete a Claude-saved knowledge entry |
246
217
  | `get_actionable_items` | Query tasks/events with structured filters |
218
+ | `get_database_info` | Get database description, topics, and stats |
219
+ | `set_database_description` | Update database description/topics (LLM can self-document) |
220
+ | `add_todo` | Create a TODO item in the knowledge base |
221
+ | `trigger_sync` | Sync API sources (Todoist, GitHub, Dropbox Paper) |
222
+ | `trigger_rescan` | Check indexed files for changes and re-ingest |
247
223
 
248
224
  ## Contextual Chunking
249
225
 
@@ -266,32 +242,6 @@ project: student-app
266
242
  category: backend
267
243
  ---
268
244
 
269
- # Query Optimization
270
-
271
- Use `select_related()` for foreign keys...
272
- ```
273
-
274
- ## Cost Estimate
275
-
276
- | Component | Local | Cloud Alternative |
277
- |-----------|-------|-------------------|
278
- | pgvector | $0 | ~$15-30/mo (CloudSQL) |
279
- | MCP Server | $0 | ~$5/mo (small VM) |
280
- | Modal embedding | ~$0.50-2/mo | N/A |
281
- | **Total** | **~$1-2/mo** | **~$20-35/mo** |
282
-
283
- ## Development
284
-
285
- ```bash
286
- # Install dev dependencies
287
- pip install -e ".[dev]"
288
-
289
- # Run tests
290
- pytest
291
-
292
- # Lint and format
293
- ruff check . && ruff format .
294
- ```
295
245
 
296
246
  ## Plugin System
297
247
 
@@ -342,12 +292,30 @@ plugins:
342
292
  enabled: true
343
293
  token: ${GITHUB_TOKEN} # Resolved from environment
344
294
  repos: [owner/repo1, owner/repo2]
295
+ todoist:
296
+ enabled: true
297
+ token: ${TODOIST_TOKEN}
298
+ include_completed: false # Sync completed tasks
299
+ completed_days: 30 # Days of completed history
300
+ include_comments: false # Include task comments (1 API call per task)
301
+ project_filter: [] # List of project IDs (use sync list-projects to find)
345
302
  dropbox-paper:
346
303
  enabled: true
347
- token: ${DROPBOX_TOKEN}
304
+ # Option 1: Refresh token (recommended, auto-refreshes)
305
+ app_key: ${DROPBOX_APP_KEY}
306
+ app_secret: ${DROPBOX_APP_SECRET}
307
+ refresh_token: ${DROPBOX_REFRESH_TOKEN}
308
+ # Option 2: Access token (short-lived, expires after ~4 hours)
309
+ # token: ${DROPBOX_TOKEN}
348
310
  folders: [/] # Optional: filter to specific folders
349
311
  ```
350
312
 
313
+ **Dropbox Paper OAuth Setup:**
314
+ ```bash
315
+ okb sync auth dropbox-paper
316
+ ```
317
+ This interactive command will guide you through getting a refresh token from Dropbox.
318
+
351
319
  ## License
352
320
 
353
321
  MIT
@@ -968,6 +968,132 @@ def sync_list():
968
968
  click.echo(f" {name}")
969
969
 
970
970
 
971
+ @sync.command("list-projects")
972
+ @click.argument("source")
973
+ def sync_list_projects(source: str):
974
+ """List projects from an API source (for finding project IDs).
975
+
976
+ Example: okb sync list-projects todoist
977
+ """
978
+ from .plugins.registry import PluginRegistry
979
+
980
+ # Get the plugin
981
+ source_obj = PluginRegistry.get_source(source)
982
+ if source_obj is None:
983
+ click.echo(f"Error: Source '{source}' not found.", err=True)
984
+ click.echo(f"Installed sources: {', '.join(PluginRegistry.list_sources())}")
985
+ sys.exit(1)
986
+
987
+ # Check if source supports list_projects
988
+ if not hasattr(source_obj, "list_projects"):
989
+ click.echo(f"Error: Source '{source}' does not support listing projects.", err=True)
990
+ sys.exit(1)
991
+
992
+ # Get and resolve config
993
+ source_cfg = config.get_source_config(source)
994
+ if source_cfg is None:
995
+ click.echo(f"Error: Source '{source}' not configured.", err=True)
996
+ click.echo("Add it to your config file under plugins.sources")
997
+ sys.exit(1)
998
+
999
+ try:
1000
+ source_obj.configure(source_cfg)
1001
+ except Exception as e:
1002
+ click.echo(f"Error configuring '{source}': {e}", err=True)
1003
+ sys.exit(1)
1004
+
1005
+ try:
1006
+ projects = source_obj.list_projects()
1007
+ if projects:
1008
+ click.echo(f"Projects in {source}:")
1009
+ for project_id, name in projects:
1010
+ click.echo(f" {project_id}: {name}")
1011
+ else:
1012
+ click.echo("No projects found.")
1013
+ except Exception as e:
1014
+ click.echo(f"Error listing projects: {e}", err=True)
1015
+ sys.exit(1)
1016
+
1017
+
1018
+ @sync.command("auth")
1019
+ @click.argument("source")
1020
+ def sync_auth(source: str):
1021
+ """Authenticate with an API source (get tokens).
1022
+
1023
+ Currently supports: dropbox-paper
1024
+
1025
+ Example: okb sync auth dropbox-paper
1026
+ """
1027
+ if source == "dropbox-paper":
1028
+ _auth_dropbox()
1029
+ else:
1030
+ click.echo(f"Error: Authentication helper not available for '{source}'", err=True)
1031
+ click.echo("Supported: dropbox-paper")
1032
+ sys.exit(1)
1033
+
1034
+
1035
+ def _auth_dropbox():
1036
+ """Interactive OAuth flow for Dropbox."""
1037
+ try:
1038
+ import dropbox
1039
+ from dropbox import DropboxOAuth2FlowNoRedirect
1040
+ except ImportError:
1041
+ click.echo("Error: dropbox package not installed", err=True)
1042
+ click.echo("Install with: pip install dropbox", err=True)
1043
+ sys.exit(1)
1044
+
1045
+ click.echo("Dropbox OAuth Setup")
1046
+ click.echo("=" * 50)
1047
+ click.echo("")
1048
+ click.echo("You'll need your Dropbox app credentials.")
1049
+ click.echo("Get them at: https://www.dropbox.com/developers/apps")
1050
+ click.echo("")
1051
+
1052
+ app_key = click.prompt("App key")
1053
+ app_secret = click.prompt("App secret")
1054
+
1055
+ # Start OAuth flow
1056
+ auth_flow = DropboxOAuth2FlowNoRedirect(
1057
+ app_key,
1058
+ app_secret,
1059
+ token_access_type="offline", # This gives us a refresh token
1060
+ )
1061
+
1062
+ authorize_url = auth_flow.start()
1063
+ click.echo("")
1064
+ click.echo("1. Go to this URL in your browser:")
1065
+ click.echo(f" {authorize_url}")
1066
+ click.echo("")
1067
+ click.echo("2. Click 'Allow' to authorize the app")
1068
+ click.echo("3. Copy the authorization code")
1069
+ click.echo("")
1070
+
1071
+ auth_code = click.prompt("Enter the authorization code")
1072
+
1073
+ try:
1074
+ oauth_result = auth_flow.finish(auth_code.strip())
1075
+ except Exception as e:
1076
+ click.echo(f"Error: Failed to get tokens - {e}", err=True)
1077
+ sys.exit(1)
1078
+
1079
+ click.echo("")
1080
+ click.echo("Success! Add these to your environment or config:")
1081
+ click.echo("")
1082
+ click.echo(f"DROPBOX_APP_KEY={app_key}")
1083
+ click.echo(f"DROPBOX_APP_SECRET={app_secret}")
1084
+ click.echo(f"DROPBOX_REFRESH_TOKEN={oauth_result.refresh_token}")
1085
+ click.echo("")
1086
+ click.echo("Config example (~/.config/okb/config.yaml):")
1087
+ click.echo("")
1088
+ click.echo("plugins:")
1089
+ click.echo(" sources:")
1090
+ click.echo(" dropbox-paper:")
1091
+ click.echo(" enabled: true")
1092
+ click.echo(" app_key: ${DROPBOX_APP_KEY}")
1093
+ click.echo(" app_secret: ${DROPBOX_APP_SECRET}")
1094
+ click.echo(" refresh_token: ${DROPBOX_REFRESH_TOKEN}")
1095
+
1096
+
971
1097
  @sync.command("status")
972
1098
  @click.argument("source", required=False)
973
1099
  @click.option("--db", "database", default=None, help="Database to check")
@@ -49,6 +49,8 @@ WRITE_TOOLS = frozenset(
49
49
  "delete_knowledge",
50
50
  "set_database_description",
51
51
  "add_todo",
52
+ "trigger_sync",
53
+ "trigger_rescan",
52
54
  }
53
55
  )
54
56
 
@@ -349,6 +351,49 @@ class HTTPMCPServer:
349
351
  content=[TextContent(type="text", text="No fields provided to update.")]
350
352
  )
351
353
 
354
+ elif name == "add_todo":
355
+ result = kb.save_todo(
356
+ title=arguments["title"],
357
+ content=arguments.get("content"),
358
+ due_date=arguments.get("due_date"),
359
+ priority=arguments.get("priority"),
360
+ project=arguments.get("project"),
361
+ tags=arguments.get("tags"),
362
+ )
363
+ parts = [
364
+ "TODO created:",
365
+ f"- Title: {result['title']}",
366
+ f"- Path: `{result['source_path']}`",
367
+ ]
368
+ if result.get("priority"):
369
+ parts.append(f"- Priority: P{result['priority']}")
370
+ if result.get("due_date"):
371
+ parts.append(f"- Due: {result['due_date']}")
372
+ return CallToolResult(content=[TextContent(type="text", text="\n".join(parts))])
373
+
374
+ elif name == "trigger_sync":
375
+ from .mcp_server import _run_sync
376
+
377
+ # Get the db_url from the knowledge base
378
+ result = _run_sync(
379
+ kb.db_url,
380
+ sources=arguments.get("sources", []),
381
+ sync_all=arguments.get("all", False),
382
+ full=arguments.get("full", False),
383
+ doc_ids=arguments.get("doc_ids"),
384
+ )
385
+ return CallToolResult(content=[TextContent(type="text", text=result)])
386
+
387
+ elif name == "trigger_rescan":
388
+ from .mcp_server import _run_rescan
389
+
390
+ result = _run_rescan(
391
+ kb.db_url,
392
+ dry_run=arguments.get("dry_run", False),
393
+ delete_missing=arguments.get("delete_missing", False),
394
+ )
395
+ return CallToolResult(content=[TextContent(type="text", text=result)])
396
+
352
397
  else:
353
398
  return CallToolResult(
354
399
  content=[TextContent(type="text", text=f"Unknown tool: {name}")]
@@ -676,6 +676,170 @@ class KnowledgeBase:
676
676
  return [dict(r) for r in results]
677
677
 
678
678
 
679
+ def _get_sync_state(conn, source_name: str, db_name: str):
680
+ """Get sync state from database."""
681
+ from .plugins.base import SyncState
682
+
683
+ result = conn.execute(
684
+ """SELECT last_sync, cursor, extra FROM sync_state
685
+ WHERE source_name = %s AND database_name = %s""",
686
+ (source_name, db_name),
687
+ ).fetchone()
688
+
689
+ if result:
690
+ return SyncState(
691
+ last_sync=result["last_sync"],
692
+ cursor=result["cursor"],
693
+ extra=result["extra"] or {},
694
+ )
695
+ return None
696
+
697
+
698
+ def _save_sync_state(conn, source_name: str, db_name: str, state):
699
+ """Save sync state to database."""
700
+ import json
701
+
702
+ conn.execute(
703
+ """INSERT INTO sync_state (source_name, database_name, last_sync, cursor, extra, updated_at)
704
+ VALUES (%s, %s, %s, %s, %s, NOW())
705
+ ON CONFLICT (source_name, database_name)
706
+ DO UPDATE SET last_sync = EXCLUDED.last_sync,
707
+ cursor = EXCLUDED.cursor,
708
+ extra = EXCLUDED.extra,
709
+ updated_at = NOW()""",
710
+ (source_name, db_name, state.last_sync, state.cursor, json.dumps(state.extra)),
711
+ )
712
+ conn.commit()
713
+
714
+
715
+ def _run_sync(
716
+ db_url: str,
717
+ sources: list[str],
718
+ sync_all: bool = False,
719
+ full: bool = False,
720
+ doc_ids: list[str] | None = None,
721
+ ) -> str:
722
+ """Run sync for specified sources and return formatted result."""
723
+ from psycopg.rows import dict_row
724
+
725
+ from .ingest import Ingester
726
+ from .plugins.registry import PluginRegistry
727
+
728
+ # Determine which sources to sync
729
+ if sync_all:
730
+ source_names = config.list_enabled_sources()
731
+ elif sources:
732
+ source_names = list(sources)
733
+ else:
734
+ # Return list of available sources
735
+ installed = PluginRegistry.list_sources()
736
+ configured = config.list_enabled_sources()
737
+ lines = ["Available API sources:"]
738
+ for name in installed:
739
+ status = "enabled" if name in configured else "disabled"
740
+ lines.append(f" - {name} ({status})")
741
+ if not installed:
742
+ lines.append(" (none installed)")
743
+ return "\n".join(lines)
744
+
745
+ if not source_names:
746
+ return "No sources to sync."
747
+
748
+ # Get database name from URL for sync state
749
+ db_name = config.get_database().name
750
+
751
+ results = []
752
+ ingester = Ingester(db_url, use_modal=True)
753
+
754
+ with psycopg.connect(db_url, row_factory=dict_row) as conn:
755
+ for source_name in source_names:
756
+ # Get the plugin
757
+ source = PluginRegistry.get_source(source_name)
758
+ if source is None:
759
+ results.append(f"{source_name}: not found")
760
+ continue
761
+
762
+ # Get and resolve config
763
+ source_cfg = config.get_source_config(source_name)
764
+ if source_cfg is None:
765
+ results.append(f"{source_name}: not configured or disabled")
766
+ continue
767
+
768
+ # Inject doc_ids if provided (for sources that support it)
769
+ if doc_ids:
770
+ source_cfg = {**source_cfg, "doc_ids": doc_ids}
771
+
772
+ try:
773
+ source.configure(source_cfg)
774
+ except Exception as e:
775
+ results.append(f"{source_name}: config error - {e}")
776
+ continue
777
+
778
+ # Get sync state (unless full)
779
+ state = None if full else _get_sync_state(conn, source_name, db_name)
780
+
781
+ try:
782
+ documents, new_state = source.fetch(state)
783
+ except Exception as e:
784
+ results.append(f"{source_name}: fetch error - {e}")
785
+ continue
786
+
787
+ if documents:
788
+ ingester.ingest_documents(documents)
789
+ results.append(f"{source_name}: synced {len(documents)} documents")
790
+ else:
791
+ results.append(f"{source_name}: no new documents")
792
+
793
+ # Save state
794
+ _save_sync_state(conn, source_name, db_name, new_state)
795
+
796
+ return "\n".join(results)
797
+
798
+
799
+ def _run_rescan(
800
+ db_url: str,
801
+ dry_run: bool = False,
802
+ delete_missing: bool = False,
803
+ ) -> str:
804
+ """Run rescan and return formatted result."""
805
+ from .rescan import Rescanner
806
+
807
+ rescanner = Rescanner(db_url, use_modal=True)
808
+ result = rescanner.rescan(dry_run=dry_run, delete_missing=delete_missing, verbose=False)
809
+
810
+ lines = []
811
+ if dry_run:
812
+ lines.append("(dry run - no changes made)")
813
+
814
+ if result.updated:
815
+ lines.append(f"Updated: {len(result.updated)} files")
816
+ for path in result.updated[:5]: # Show first 5
817
+ lines.append(f" - {path}")
818
+ if len(result.updated) > 5:
819
+ lines.append(f" ... and {len(result.updated) - 5} more")
820
+
821
+ if result.deleted:
822
+ lines.append(f"Deleted: {len(result.deleted)} files")
823
+
824
+ if result.missing:
825
+ lines.append(f"Missing (not deleted): {len(result.missing)} files")
826
+ for path in result.missing[:5]:
827
+ lines.append(f" - {path}")
828
+ if len(result.missing) > 5:
829
+ lines.append(f" ... and {len(result.missing) - 5} more")
830
+
831
+ lines.append(f"Unchanged: {result.unchanged} files")
832
+
833
+ if result.errors:
834
+ lines.append(f"Errors: {len(result.errors)}")
835
+ for path, error in result.errors[:3]:
836
+ lines.append(f" - {path}: {error}")
837
+ if len(result.errors) > 3:
838
+ lines.append(f" ... and {len(result.errors) - 3} more")
839
+
840
+ return "\n".join(lines) if lines else "No indexed files found."
841
+
842
+
679
843
  def build_server_instructions(db_config) -> str | None:
680
844
  """Build server instructions from database config and LLM metadata."""
681
845
  parts = []
@@ -1025,6 +1189,67 @@ async def list_tools() -> list[Tool]:
1025
1189
  "required": ["title"],
1026
1190
  },
1027
1191
  ),
1192
+ Tool(
1193
+ name="trigger_sync",
1194
+ description=(
1195
+ "Trigger sync of API sources (Todoist, GitHub, Dropbox Paper, etc.). "
1196
+ "Fetches new/updated content from external services. Requires write permission."
1197
+ ),
1198
+ inputSchema={
1199
+ "type": "object",
1200
+ "properties": {
1201
+ "sources": {
1202
+ "type": "array",
1203
+ "items": {"type": "string"},
1204
+ "description": (
1205
+ "List of source names to sync (e.g., ['todoist', 'github']). "
1206
+ "If empty and 'all' is false, returns list of available sources."
1207
+ ),
1208
+ },
1209
+ "all": {
1210
+ "type": "boolean",
1211
+ "default": False,
1212
+ "description": "Sync all enabled sources",
1213
+ },
1214
+ "full": {
1215
+ "type": "boolean",
1216
+ "default": False,
1217
+ "description": "Ignore incremental state and do full resync",
1218
+ },
1219
+ "doc_ids": {
1220
+ "type": "array",
1221
+ "items": {"type": "string"},
1222
+ "description": (
1223
+ "Specific document IDs to sync (for dropbox-paper). "
1224
+ "If provided, only these documents are synced."
1225
+ ),
1226
+ },
1227
+ },
1228
+ },
1229
+ ),
1230
+ Tool(
1231
+ name="trigger_rescan",
1232
+ description=(
1233
+ "Check indexed files for changes and re-ingest stale ones. "
1234
+ "Compares stored modification times with current filesystem. "
1235
+ "Requires write permission."
1236
+ ),
1237
+ inputSchema={
1238
+ "type": "object",
1239
+ "properties": {
1240
+ "dry_run": {
1241
+ "type": "boolean",
1242
+ "default": False,
1243
+ "description": "Only report what would change, don't actually re-ingest",
1244
+ },
1245
+ "delete_missing": {
1246
+ "type": "boolean",
1247
+ "default": False,
1248
+ "description": "Remove documents for files that no longer exist",
1249
+ },
1250
+ },
1251
+ },
1252
+ ),
1028
1253
  ]
1029
1254
 
1030
1255
 
@@ -1358,6 +1583,24 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> CallToolResult:
1358
1583
  parts.append(f"- Due: {result['due_date']}")
1359
1584
  return CallToolResult(content=[TextContent(type="text", text="\n".join(parts))])
1360
1585
 
1586
+ elif name == "trigger_sync":
1587
+ result = _run_sync(
1588
+ kb.db_url,
1589
+ sources=arguments.get("sources", []),
1590
+ sync_all=arguments.get("all", False),
1591
+ full=arguments.get("full", False),
1592
+ doc_ids=arguments.get("doc_ids"),
1593
+ )
1594
+ return CallToolResult(content=[TextContent(type="text", text=result)])
1595
+
1596
+ elif name == "trigger_rescan":
1597
+ result = _run_rescan(
1598
+ kb.db_url,
1599
+ dry_run=arguments.get("dry_run", False),
1600
+ delete_missing=arguments.get("delete_missing", False),
1601
+ )
1602
+ return CallToolResult(content=[TextContent(type="text", text=result)])
1603
+
1361
1604
  else:
1362
1605
  return CallToolResult(content=[TextContent(type="text", text=f"Unknown tool: {name}")])
1363
1606
 
@@ -1,5 +1,6 @@
1
1
  """Built-in API source plugins for OKB."""
2
2
 
3
3
  from okb.plugins.sources.dropbox_paper import DropboxPaperSource
4
+ from okb.plugins.sources.todoist import TodoistSource
4
5
 
5
- __all__ = ["DropboxPaperSource"]
6
+ __all__ = ["DropboxPaperSource", "TodoistSource"]
@@ -16,17 +16,27 @@ class DropboxPaperSource:
16
16
 
17
17
  Syncs Paper documents as markdown for searchable knowledge base entries.
18
18
 
19
- Config example:
19
+ Config example (refresh token - recommended):
20
20
  plugins:
21
21
  sources:
22
22
  dropbox-paper:
23
23
  enabled: true
24
- token: ${DROPBOX_TOKEN}
24
+ app_key: ${DROPBOX_APP_KEY}
25
+ app_secret: ${DROPBOX_APP_SECRET}
26
+ refresh_token: ${DROPBOX_REFRESH_TOKEN}
25
27
  folders: [/] # Optional: filter to specific folder paths
26
28
 
29
+ Config example (access token - short-lived):
30
+ plugins:
31
+ sources:
32
+ dropbox-paper:
33
+ enabled: true
34
+ token: ${DROPBOX_TOKEN} # Expires after ~4 hours
35
+
27
36
  Usage:
28
- lkb sync run dropbox-paper
29
- lkb sync run dropbox-paper --full # Ignore incremental state
37
+ okb sync run dropbox-paper
38
+ okb sync run dropbox-paper --full # Ignore incremental state
39
+ okb sync run dropbox-paper --doc <doc_id> # Sync specific document
30
40
  """
31
41
 
32
42
  name = "dropbox-paper"
@@ -38,18 +48,43 @@ class DropboxPaperSource:
38
48
  self._doc_ids: list[str] | None = None
39
49
 
40
50
  def configure(self, config: dict) -> None:
41
- """Initialize Dropbox client with OAuth token.
51
+ """Initialize Dropbox client with OAuth token or refresh token.
52
+
53
+ Supports two authentication modes:
54
+ 1. Access token only (short-lived, will expire):
55
+ token: <access_token>
56
+
57
+ 2. Refresh token (recommended, auto-refreshes):
58
+ app_key: <app_key>
59
+ app_secret: <app_secret>
60
+ refresh_token: <refresh_token>
42
61
 
43
62
  Args:
44
- config: Source configuration containing 'token' and optional 'folders' or 'doc_ids'
63
+ config: Source configuration containing auth credentials and optional 'folders'/'doc_ids'
45
64
  """
46
65
  import dropbox
47
66
 
67
+ app_key = config.get("app_key")
68
+ app_secret = config.get("app_secret")
69
+ refresh_token = config.get("refresh_token")
48
70
  token = config.get("token")
49
- if not token:
50
- raise ValueError("dropbox-paper source requires 'token' in config")
51
71
 
52
- self._client = dropbox.Dropbox(token)
72
+ if app_key and app_secret and refresh_token:
73
+ # Use refresh token - will auto-refresh access tokens
74
+ self._client = dropbox.Dropbox(
75
+ app_key=app_key,
76
+ app_secret=app_secret,
77
+ oauth2_refresh_token=refresh_token,
78
+ )
79
+ elif token:
80
+ # Legacy: direct access token (will expire)
81
+ self._client = dropbox.Dropbox(token)
82
+ else:
83
+ raise ValueError(
84
+ "dropbox-paper source requires either 'token' or "
85
+ "'app_key'/'app_secret'/'refresh_token' in config"
86
+ )
87
+
53
88
  self._folders = config.get("folders")
54
89
  self._doc_ids = config.get("doc_ids") # Specific doc IDs from CLI
55
90
 
@@ -0,0 +1,254 @@
1
+ """Todoist API source for syncing tasks into OKB."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from datetime import UTC, datetime, timedelta
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from okb.ingest import Document
11
+ from okb.plugins.base import SyncState
12
+
13
+
14
+ class TodoistSource:
15
+ """API source for Todoist tasks.
16
+
17
+ Syncs active and optionally completed tasks for semantic search and actionable item queries.
18
+
19
+ Config example:
20
+ plugins:
21
+ sources:
22
+ todoist:
23
+ enabled: true
24
+ token: ${TODOIST_TOKEN}
25
+ include_completed: false # Include recently completed tasks
26
+ completed_days: 30 # Days of completed tasks to sync
27
+ include_comments: false # Include task comments (expensive)
28
+ project_filter: [] # Optional: list of project IDs to sync
29
+
30
+ Usage:
31
+ okb sync run todoist
32
+ okb sync run todoist --full # Full resync
33
+ """
34
+
35
+ name = "todoist"
36
+ source_type = "todoist-task"
37
+
38
+ def __init__(self) -> None:
39
+ self._client = None
40
+ self._include_completed = False
41
+ self._completed_days = 30
42
+ self._include_comments = False
43
+ self._project_filter: list[str] | None = None
44
+ self._projects: dict[str, str] = {} # id -> name
45
+
46
+ def configure(self, config: dict) -> None:
47
+ """Initialize Todoist client with API token.
48
+
49
+ Args:
50
+ config: Source configuration containing 'token' and optional settings
51
+ """
52
+ from todoist_api_python.api import TodoistAPI
53
+
54
+ token = config.get("token")
55
+ if not token:
56
+ raise ValueError("todoist source requires 'token' in config")
57
+
58
+ self._client = TodoistAPI(token)
59
+ self._include_completed = config.get("include_completed", False)
60
+ self._completed_days = config.get("completed_days", 30)
61
+ self._include_comments = config.get("include_comments", False)
62
+ self._project_filter = config.get("project_filter")
63
+
64
+ def fetch(self, state: SyncState | None = None) -> tuple[list[Document], SyncState]:
65
+ """Fetch tasks from Todoist.
66
+
67
+ Active tasks are always fully synced (API has no "modified since" filter).
68
+ Completed tasks use state.last_sync for incremental fetching.
69
+
70
+ Args:
71
+ state: Previous sync state for incremental updates, or None for full sync
72
+
73
+ Returns:
74
+ Tuple of (list of documents, new sync state)
75
+ """
76
+ from okb.plugins.base import SyncState as SyncStateClass
77
+
78
+ if self._client is None:
79
+ raise RuntimeError("Source not configured. Call configure() first.")
80
+
81
+ documents: list[Document] = []
82
+
83
+ print("Fetching Todoist tasks...", file=sys.stderr)
84
+
85
+ # Load projects for name lookup
86
+ self._load_projects()
87
+
88
+ # Fetch active tasks
89
+ active_docs = self._fetch_active_tasks()
90
+ documents.extend(active_docs)
91
+ print(f" Synced {len(active_docs)} active tasks", file=sys.stderr)
92
+
93
+ # Fetch completed tasks if enabled
94
+ if self._include_completed:
95
+ since = state.last_sync if state and state.last_sync else None
96
+ completed_docs = self._fetch_completed_tasks(since)
97
+ documents.extend(completed_docs)
98
+ print(f" Synced {len(completed_docs)} completed tasks", file=sys.stderr)
99
+
100
+ new_state = SyncStateClass(last_sync=datetime.now(UTC))
101
+ return documents, new_state
102
+
103
+ def _load_projects(self) -> None:
104
+ """Load projects for name lookup."""
105
+ try:
106
+ self._projects = {}
107
+ for project_batch in self._client.get_projects():
108
+ for p in project_batch:
109
+ self._projects[p.id] = p.name
110
+ except Exception as e:
111
+ print(f" Warning: Could not load projects: {e}", file=sys.stderr)
112
+ self._projects = {}
113
+
114
+ def _fetch_active_tasks(self) -> list[Document]:
115
+ """Fetch all active tasks."""
116
+ documents = []
117
+
118
+ for task_batch in self._client.get_tasks():
119
+ for task in task_batch:
120
+ # Apply project filter if configured
121
+ if self._project_filter and task.project_id not in self._project_filter:
122
+ continue
123
+
124
+ doc = self._task_to_document(task, is_completed=False)
125
+ if doc:
126
+ documents.append(doc)
127
+
128
+ return documents
129
+
130
+ def _fetch_completed_tasks(self, since: datetime | None) -> list[Document]:
131
+ """Fetch completed tasks within the configured window."""
132
+ documents = []
133
+
134
+ # Determine date range
135
+ until = datetime.now(UTC)
136
+ if since:
137
+ start = since
138
+ else:
139
+ start = until - timedelta(days=self._completed_days)
140
+
141
+ try:
142
+ for task_batch in self._client.get_completed_tasks_by_completion_date(
143
+ since=start,
144
+ until=until,
145
+ ):
146
+ for task in task_batch:
147
+ # Apply project filter if configured
148
+ if self._project_filter and task.project_id not in self._project_filter:
149
+ continue
150
+
151
+ doc = self._task_to_document(task, is_completed=True)
152
+ if doc:
153
+ documents.append(doc)
154
+ except Exception as e:
155
+ print(f" Warning: Could not fetch completed tasks: {e}", file=sys.stderr)
156
+
157
+ return documents
158
+
159
+ def _task_to_document(self, task, is_completed: bool) -> Document | None:
160
+ """Convert a Todoist task to a Document."""
161
+ from okb.ingest import Document, DocumentMetadata
162
+
163
+ # Build content from task content + description + optional comments
164
+ content_parts = [task.content]
165
+ if task.description:
166
+ content_parts.append(task.description)
167
+
168
+ if self._include_comments:
169
+ comments = self._fetch_task_comments(task.id)
170
+ if comments:
171
+ content_parts.append("\n## Comments\n" + "\n".join(comments))
172
+
173
+ content = "\n\n".join(content_parts)
174
+
175
+ # Parse due date
176
+ due_date = None
177
+ if task.due:
178
+ due_date = self._parse_due(task.due)
179
+
180
+ # Map priority: Todoist uses 1-4 (4=urgent), OKB uses 1-5 (1=highest)
181
+ priority = 5 - task.priority if task.priority else None
182
+
183
+ # Get project name
184
+ project_name = self._projects.get(task.project_id)
185
+
186
+ # Build metadata
187
+ metadata = DocumentMetadata(
188
+ tags=task.labels or [],
189
+ project=project_name,
190
+ extra={
191
+ "todoist_id": task.id,
192
+ "project_id": task.project_id,
193
+ },
194
+ )
195
+
196
+ # Determine status
197
+ status = "completed" if is_completed or task.is_completed else "pending"
198
+
199
+ return Document(
200
+ source_path=f"todoist://task/{task.id}",
201
+ source_type=self.source_type,
202
+ title=task.content,
203
+ content=content,
204
+ metadata=metadata,
205
+ due_date=due_date,
206
+ status=status,
207
+ priority=priority,
208
+ )
209
+
210
+ def _parse_due(self, due) -> datetime | None:
211
+ """Parse Todoist Due object to datetime."""
212
+ if due is None:
213
+ return None
214
+
215
+ try:
216
+ # Due has 'datetime' (full datetime) or 'date' (date only)
217
+ if hasattr(due, "datetime") and due.datetime:
218
+ return datetime.fromisoformat(due.datetime.replace("Z", "+00:00"))
219
+ elif hasattr(due, "date") and due.date:
220
+ # Date-only: treat as end of day in UTC
221
+ if isinstance(due.date, str):
222
+ d = datetime.strptime(due.date, "%Y-%m-%d").date()
223
+ else:
224
+ d = due.date
225
+ return datetime(d.year, d.month, d.day, 23, 59, 59, tzinfo=UTC)
226
+ except Exception:
227
+ pass
228
+ return None
229
+
230
+ def _fetch_task_comments(self, task_id: str) -> list[str]:
231
+ """Fetch comments for a task."""
232
+ comments = []
233
+ try:
234
+ for comment_batch in self._client.get_comments(task_id=task_id):
235
+ for comment in comment_batch:
236
+ comments.append(f"- {comment.content}")
237
+ except Exception:
238
+ pass
239
+ return comments
240
+
241
+ def list_projects(self) -> list[tuple[str, str]]:
242
+ """List all projects with their IDs.
243
+
244
+ Returns:
245
+ List of (project_id, project_name) tuples
246
+ """
247
+ if self._client is None:
248
+ raise RuntimeError("Source not configured. Call configure() first.")
249
+
250
+ projects = []
251
+ for project_batch in self._client.get_projects():
252
+ for p in project_batch:
253
+ projects.append((p.id, p.name))
254
+ return projects
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "okb"
3
- version = "1.0.0"
3
+ version = "1.1.0a0"
4
4
  description = "Personal knowledge base with semantic search for LLMs"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -19,6 +19,11 @@ dependencies = [
19
19
  "PyGithub>=2.0.0",
20
20
  ]
21
21
 
22
+ [project.urls]
23
+ Homepage = "https://github.com/username/okb"
24
+ Repository = "https://github.com/username/okb"
25
+ Issues = "https://github.com/username/okb/issues"
26
+
22
27
  [project.optional-dependencies]
23
28
  pdf = ["pymupdf>=1.23.0"]
24
29
  docx = ["python-docx>=1.1.0"]
@@ -29,11 +34,13 @@ llm-bedrock = [
29
34
  "boto3>=1.28.0",
30
35
  "botocore>=1.31.0",
31
36
  ]
37
+ todoist = ["todoist-api-python>=3.0.0"]
32
38
  all = [
33
39
  "pymupdf>=1.23.0",
34
40
  "python-docx>=1.1.0",
35
41
  "trafilatura>=1.6.0",
36
42
  "anthropic>=0.40.0",
43
+ "todoist-api-python>=3.0.0",
37
44
  ]
38
45
  dev = [
39
46
  "pytest>=7.0.0",
@@ -59,6 +66,7 @@ okb = "okb.cli:main"
59
66
  # Built-in sources
60
67
  dropbox-paper = "okb.plugins.sources.dropbox_paper:DropboxPaperSource"
61
68
  github = "okb.plugins.sources.github:GitHubSource"
69
+ todoist = "okb.plugins.sources.todoist:TodoistSource"
62
70
  # External plugins register additional API sources here
63
71
 
64
72
  [build-system]
@@ -68,6 +76,9 @@ build-backend = "poetry.core.masonry.api"
68
76
  [tool.poetry]
69
77
  packages = [{include = "okb"}]
70
78
 
79
+ [tool.poetry.dependencies]
80
+ python = ">=3.11,<4.0"
81
+
71
82
  [tool.ruff]
72
83
  line-length = 100
73
84
  target-version = "py311"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes