opencontextengine-client 0.1.0__py3-none-any.whl
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.
- oce_client/__init__.py +33 -0
- oce_client/cli.py +250 -0
- oce_client/context.py +478 -0
- oce_client/defaults.py +2 -0
- oce_client/filesystem.py +62 -0
- oce_client/http.py +134 -0
- oce_client/identity.py +17 -0
- oce_client/ignore.py +84 -0
- oce_client/indexer.py +265 -0
- oce_client/mcp_server.py +309 -0
- oce_client/models.py +102 -0
- oce_client/ports.py +67 -0
- oce_client/runtime.py +245 -0
- oce_client/skill/SKILL.md +136 -0
- oce_client/skill/agents/openai.yaml +4 -0
- oce_client/state.py +288 -0
- oce_client/watcher.py +57 -0
- opencontextengine_client-0.1.0.dist-info/METADATA +162 -0
- opencontextengine_client-0.1.0.dist-info/RECORD +22 -0
- opencontextengine_client-0.1.0.dist-info/WHEEL +4 -0
- opencontextengine_client-0.1.0.dist-info/entry_points.txt +3 -0
- opencontextengine_client-0.1.0.dist-info/licenses/LICENSE +202 -0
oce_client/state.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Iterator
|
|
10
|
+
|
|
11
|
+
from .models import FileRecord, FileStatus, WorkspaceSnapshot
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SQLiteStateStore:
|
|
15
|
+
"""Durable workspace state with an outbox-friendly transactional boundary."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, path: Path) -> None:
|
|
18
|
+
self.path = Path(path)
|
|
19
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
self._lock = threading.RLock()
|
|
21
|
+
self._conn = sqlite3.connect(
|
|
22
|
+
str(self.path), timeout=30.0, check_same_thread=False
|
|
23
|
+
)
|
|
24
|
+
self._conn.row_factory = sqlite3.Row
|
|
25
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
26
|
+
self._conn.execute("PRAGMA busy_timeout=30000")
|
|
27
|
+
self._init_schema()
|
|
28
|
+
|
|
29
|
+
def close(self) -> None:
|
|
30
|
+
with self._lock:
|
|
31
|
+
self._conn.close()
|
|
32
|
+
|
|
33
|
+
def _init_schema(self) -> None:
|
|
34
|
+
with self._lock, self._conn:
|
|
35
|
+
self._conn.executescript(
|
|
36
|
+
"""
|
|
37
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
38
|
+
key TEXT PRIMARY KEY,
|
|
39
|
+
value TEXT NOT NULL
|
|
40
|
+
);
|
|
41
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
42
|
+
path TEXT PRIMARY KEY,
|
|
43
|
+
blob_name TEXT,
|
|
44
|
+
committed_blob_name TEXT,
|
|
45
|
+
status TEXT NOT NULL,
|
|
46
|
+
content TEXT,
|
|
47
|
+
size INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
mtime_ns INTEGER,
|
|
49
|
+
source TEXT NOT NULL DEFAULT 'filesystem',
|
|
50
|
+
generation INTEGER NOT NULL DEFAULT 0,
|
|
51
|
+
skip_reason TEXT
|
|
52
|
+
);
|
|
53
|
+
CREATE TABLE IF NOT EXISTS outbox_operations (
|
|
54
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
55
|
+
kind TEXT NOT NULL,
|
|
56
|
+
payload TEXT NOT NULL,
|
|
57
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
58
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
59
|
+
last_error TEXT,
|
|
60
|
+
created_at REAL NOT NULL
|
|
61
|
+
);
|
|
62
|
+
"""
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@contextmanager
|
|
66
|
+
def transaction(self) -> Iterator[sqlite3.Connection]:
|
|
67
|
+
with self._lock:
|
|
68
|
+
self._conn.execute("BEGIN IMMEDIATE")
|
|
69
|
+
try:
|
|
70
|
+
yield self._conn
|
|
71
|
+
except Exception:
|
|
72
|
+
self._conn.rollback()
|
|
73
|
+
raise
|
|
74
|
+
else:
|
|
75
|
+
self._conn.commit()
|
|
76
|
+
|
|
77
|
+
def get_meta(self, key: str) -> str | None:
|
|
78
|
+
row = self._conn.execute("SELECT value FROM meta WHERE key = ?", (key,)).fetchone()
|
|
79
|
+
return None if row is None else str(row["value"])
|
|
80
|
+
|
|
81
|
+
def set_meta(self, key: str, value: str | None) -> None:
|
|
82
|
+
with self.transaction() as conn:
|
|
83
|
+
if value is None:
|
|
84
|
+
conn.execute("DELETE FROM meta WHERE key = ?", (key,))
|
|
85
|
+
else:
|
|
86
|
+
conn.execute(
|
|
87
|
+
"INSERT INTO meta(key, value) VALUES (?, ?) "
|
|
88
|
+
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
89
|
+
(key, value),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def load_snapshot(self) -> WorkspaceSnapshot:
|
|
93
|
+
rows = self._conn.execute(
|
|
94
|
+
"SELECT path, blob_name, committed_blob_name, status, "
|
|
95
|
+
"CASE WHEN source = 'explicit' THEN content ELSE NULL END AS content, "
|
|
96
|
+
"size, mtime_ns, source, generation, skip_reason "
|
|
97
|
+
"FROM files ORDER BY path"
|
|
98
|
+
).fetchall()
|
|
99
|
+
files = {
|
|
100
|
+
row["path"]: FileRecord(
|
|
101
|
+
path=row["path"],
|
|
102
|
+
blob_name=row["blob_name"],
|
|
103
|
+
status=FileStatus(row["status"]),
|
|
104
|
+
committed_blob_name=row["committed_blob_name"],
|
|
105
|
+
content=row["content"],
|
|
106
|
+
size=row["size"],
|
|
107
|
+
mtime_ns=row["mtime_ns"],
|
|
108
|
+
source=row["source"],
|
|
109
|
+
generation=row["generation"],
|
|
110
|
+
)
|
|
111
|
+
for row in rows
|
|
112
|
+
}
|
|
113
|
+
return WorkspaceSnapshot(
|
|
114
|
+
files=files,
|
|
115
|
+
checkpoint_id=self.get_meta("checkpoint_id"),
|
|
116
|
+
generation=int(self.get_meta("generation") or "0"),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def load_file_rows(self) -> list[sqlite3.Row]:
|
|
120
|
+
return self._conn.execute(
|
|
121
|
+
"SELECT path, blob_name, committed_blob_name, status, size, mtime_ns, "
|
|
122
|
+
"source, generation, skip_reason FROM files ORDER BY path"
|
|
123
|
+
).fetchall()
|
|
124
|
+
|
|
125
|
+
def load_file_content(self, path: str) -> str | None:
|
|
126
|
+
row = self._conn.execute(
|
|
127
|
+
"SELECT content FROM files WHERE path = ?",
|
|
128
|
+
(path,),
|
|
129
|
+
).fetchone()
|
|
130
|
+
if row is None or row["content"] is None:
|
|
131
|
+
return None
|
|
132
|
+
return str(row["content"])
|
|
133
|
+
|
|
134
|
+
def upsert_file(self, record: FileRecord, *, committed_blob_name: str | None = None) -> None:
|
|
135
|
+
with self.transaction() as conn:
|
|
136
|
+
existing = conn.execute(
|
|
137
|
+
"SELECT committed_blob_name FROM files WHERE path = ?", (record.path,)
|
|
138
|
+
).fetchone()
|
|
139
|
+
committed = (
|
|
140
|
+
committed_blob_name
|
|
141
|
+
if committed_blob_name is not None
|
|
142
|
+
else (existing["committed_blob_name"] if existing else None)
|
|
143
|
+
)
|
|
144
|
+
conn.execute(
|
|
145
|
+
"""INSERT INTO files
|
|
146
|
+
(path, blob_name, committed_blob_name, status, content, size,
|
|
147
|
+
mtime_ns, source, generation, skip_reason)
|
|
148
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
149
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
150
|
+
blob_name=excluded.blob_name,
|
|
151
|
+
committed_blob_name=COALESCE(excluded.committed_blob_name, files.committed_blob_name),
|
|
152
|
+
status=excluded.status, content=excluded.content,
|
|
153
|
+
size=excluded.size, mtime_ns=excluded.mtime_ns,
|
|
154
|
+
source=excluded.source, generation=excluded.generation""",
|
|
155
|
+
(
|
|
156
|
+
record.path,
|
|
157
|
+
record.blob_name,
|
|
158
|
+
committed,
|
|
159
|
+
record.status.value,
|
|
160
|
+
record.content,
|
|
161
|
+
record.size,
|
|
162
|
+
record.mtime_ns,
|
|
163
|
+
record.source,
|
|
164
|
+
record.generation,
|
|
165
|
+
None,
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def apply_file_changes(
|
|
170
|
+
self,
|
|
171
|
+
records: list[FileRecord],
|
|
172
|
+
deleted_paths: list[str],
|
|
173
|
+
generation: int,
|
|
174
|
+
) -> None:
|
|
175
|
+
with self.transaction() as conn:
|
|
176
|
+
conn.executemany(
|
|
177
|
+
"""INSERT INTO files(path, blob_name, committed_blob_name, status,
|
|
178
|
+
content, size, mtime_ns, source, generation, skip_reason)
|
|
179
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
180
|
+
ON CONFLICT(path) DO UPDATE SET blob_name=excluded.blob_name,
|
|
181
|
+
status=excluded.status, content=excluded.content,
|
|
182
|
+
size=excluded.size, mtime_ns=excluded.mtime_ns,
|
|
183
|
+
source=excluded.source, generation=excluded.generation""",
|
|
184
|
+
[
|
|
185
|
+
(
|
|
186
|
+
record.path,
|
|
187
|
+
record.blob_name,
|
|
188
|
+
None,
|
|
189
|
+
record.status.value,
|
|
190
|
+
record.content,
|
|
191
|
+
record.size,
|
|
192
|
+
record.mtime_ns,
|
|
193
|
+
record.source,
|
|
194
|
+
generation,
|
|
195
|
+
None,
|
|
196
|
+
)
|
|
197
|
+
for record in records
|
|
198
|
+
],
|
|
199
|
+
)
|
|
200
|
+
if deleted_paths:
|
|
201
|
+
conn.executemany(
|
|
202
|
+
"UPDATE files SET status = ?, blob_name = NULL, content = NULL, "
|
|
203
|
+
"source = 'filesystem', generation = ? WHERE path = ?",
|
|
204
|
+
[
|
|
205
|
+
(FileStatus.DELETED.value, generation, path)
|
|
206
|
+
for path in deleted_paths
|
|
207
|
+
],
|
|
208
|
+
)
|
|
209
|
+
conn.execute(
|
|
210
|
+
"INSERT INTO meta(key,value) VALUES('generation',?) "
|
|
211
|
+
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
212
|
+
(str(generation),),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def mark_missing_paths(self, paths: list[str], generation: int) -> None:
|
|
216
|
+
if not paths:
|
|
217
|
+
return
|
|
218
|
+
with self.transaction() as conn:
|
|
219
|
+
conn.executemany(
|
|
220
|
+
"UPDATE files SET status = ?, blob_name = NULL, content = NULL, generation = ? "
|
|
221
|
+
"WHERE path = ? AND source != 'explicit'",
|
|
222
|
+
[(FileStatus.DELETED.value, generation, path) for path in paths],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def mark_deleted_paths(self, paths: list[str], generation: int) -> None:
|
|
226
|
+
if not paths:
|
|
227
|
+
return
|
|
228
|
+
with self.transaction() as conn:
|
|
229
|
+
conn.executemany(
|
|
230
|
+
"UPDATE files SET status = ?, blob_name = NULL, content = NULL, "
|
|
231
|
+
"source = 'filesystem', generation = ? WHERE path = ?",
|
|
232
|
+
[(FileStatus.DELETED.value, generation, path) for path in paths],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def commit_sync(
|
|
236
|
+
self,
|
|
237
|
+
checkpoint_id: str,
|
|
238
|
+
deleted_paths: list[str],
|
|
239
|
+
generation: int,
|
|
240
|
+
) -> None:
|
|
241
|
+
with self.transaction() as conn:
|
|
242
|
+
conn.execute("UPDATE files SET committed_blob_name = blob_name WHERE status = 'present'")
|
|
243
|
+
conn.execute("UPDATE files SET content = NULL WHERE source = 'filesystem'")
|
|
244
|
+
if deleted_paths:
|
|
245
|
+
conn.executemany("DELETE FROM files WHERE path = ?", [(p,) for p in deleted_paths])
|
|
246
|
+
conn.execute(
|
|
247
|
+
"INSERT INTO meta(key,value) VALUES('checkpoint_id',?) "
|
|
248
|
+
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
249
|
+
(checkpoint_id,),
|
|
250
|
+
)
|
|
251
|
+
conn.execute(
|
|
252
|
+
"INSERT INTO meta(key,value) VALUES('generation',?) "
|
|
253
|
+
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
254
|
+
(str(generation),),
|
|
255
|
+
)
|
|
256
|
+
conn.execute(
|
|
257
|
+
"INSERT INTO meta(key,value) VALUES('synced_generation',?) "
|
|
258
|
+
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
259
|
+
(str(generation),),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def add_outbox(self, kind: str, payload: dict[str, object]) -> int:
|
|
263
|
+
with self.transaction() as conn:
|
|
264
|
+
cursor = conn.execute(
|
|
265
|
+
"INSERT INTO outbox_operations(kind,payload,created_at) VALUES(?,?,?)",
|
|
266
|
+
(kind, json.dumps(payload, separators=(",", ":")), time.time()),
|
|
267
|
+
)
|
|
268
|
+
return int(cursor.lastrowid)
|
|
269
|
+
|
|
270
|
+
def update_outbox(self, operation_id: int, status: str, error: str | None = None) -> None:
|
|
271
|
+
with self.transaction() as conn:
|
|
272
|
+
conn.execute(
|
|
273
|
+
"UPDATE outbox_operations SET status=?, attempts=attempts+1, last_error=? WHERE id=?",
|
|
274
|
+
(status, error, operation_id),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def pending_outbox(self) -> list[sqlite3.Row]:
|
|
278
|
+
return self._conn.execute(
|
|
279
|
+
"SELECT * FROM outbox_operations WHERE status != 'complete' ORDER BY id"
|
|
280
|
+
).fetchall()
|
|
281
|
+
|
|
282
|
+
def supersede_pending(self) -> None:
|
|
283
|
+
"""Mark older attempts as superseded before a fresh sync is planned."""
|
|
284
|
+
with self.transaction() as conn:
|
|
285
|
+
conn.execute(
|
|
286
|
+
"UPDATE outbox_operations SET status = 'superseded' "
|
|
287
|
+
"WHERE status IN ('pending', 'failed')"
|
|
288
|
+
)
|
oce_client/watcher.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from watchfiles import Change, watch
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_INTERNAL_DIRECTORIES = {".git", ".oce-client"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _is_relevant(path: str) -> bool:
|
|
14
|
+
return _INTERNAL_DIRECTORIES.isdisjoint(Path(path).parts)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WatchHandle:
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
root: Path,
|
|
21
|
+
callback: Callable[[set[Path]], None],
|
|
22
|
+
debounce_ms: int = 300,
|
|
23
|
+
) -> None:
|
|
24
|
+
self._root = root
|
|
25
|
+
self._callback = callback
|
|
26
|
+
self._debounce_ms = debounce_ms
|
|
27
|
+
self._stop = threading.Event()
|
|
28
|
+
self._thread = threading.Thread(target=self._run, name="oce-client-watcher", daemon=True)
|
|
29
|
+
self._thread.start()
|
|
30
|
+
|
|
31
|
+
def _run(self) -> None:
|
|
32
|
+
try:
|
|
33
|
+
changes = watch(
|
|
34
|
+
self._root,
|
|
35
|
+
stop_event=self._stop,
|
|
36
|
+
debounce=self._debounce_ms,
|
|
37
|
+
yield_on_timeout=False,
|
|
38
|
+
)
|
|
39
|
+
for batch in changes:
|
|
40
|
+
if self._stop.is_set():
|
|
41
|
+
return
|
|
42
|
+
relevant = {
|
|
43
|
+
Path(path)
|
|
44
|
+
for _change, path in batch
|
|
45
|
+
if _is_relevant(path)
|
|
46
|
+
}
|
|
47
|
+
if relevant:
|
|
48
|
+
self._callback(relevant)
|
|
49
|
+
except (OSError, RuntimeError):
|
|
50
|
+
# The owning context remains usable; a later explicit reconcile can recover.
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
def stop(self) -> None:
|
|
54
|
+
self._stop.set()
|
|
55
|
+
|
|
56
|
+
def join(self, timeout: float | None = None) -> None:
|
|
57
|
+
self._thread.join(timeout)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: opencontextengine-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Standalone workspace and blob synchronization client for OpenContextEngine
|
|
5
|
+
Project-URL: Homepage, https://github.com/oce-ai/oce-client
|
|
6
|
+
Project-URL: Repository, https://github.com/oce-ai/oce-client
|
|
7
|
+
Project-URL: Documentation, https://github.com/oce-ai/oce-client/blob/master/README.md
|
|
8
|
+
Project-URL: Issues, https://github.com/oce-ai/oce-client/issues
|
|
9
|
+
Author: OpenContextEngine Contributors
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai,code,context,mcp,retrieval
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: httpx>=0.25.0
|
|
22
|
+
Requires-Dist: pathspec>=0.12.1
|
|
23
|
+
Requires-Dist: watchfiles>=1.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Provides-Extra: mcp
|
|
27
|
+
Requires-Dist: mcp<2,>=1.0.0; extra == 'mcp'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# opencontextengine-client
|
|
31
|
+
|
|
32
|
+
Standalone synchronous Python client for OpenContextEngine workspace and blob
|
|
33
|
+
management. The package owns local inventory, ignore rules, upload planning,
|
|
34
|
+
checkpoint state, and retrieval adapters. It does not depend on Auggie SDK.
|
|
35
|
+
|
|
36
|
+
Install the distribution package with `uv add opencontextengine-client` (or
|
|
37
|
+
`pip install opencontextengine-client`). The installed command remains
|
|
38
|
+
`oce-client`.
|
|
39
|
+
|
|
40
|
+
## Version Information
|
|
41
|
+
|
|
42
|
+
Run `oce-client --version` to print the installed client version.
|
|
43
|
+
|
|
44
|
+
| Item | Value |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| PyPI distribution | `opencontextengine-client` |
|
|
47
|
+
| Python package | `oce_client` |
|
|
48
|
+
| CLI | `oce-client` |
|
|
49
|
+
| MCP server | `oce-client-mcp` (separate interface) |
|
|
50
|
+
| Version command | `oce-client --version` |
|
|
51
|
+
|
|
52
|
+
The authoritative version is declared in `pyproject.toml` and mirrored by
|
|
53
|
+
`oce_client.__version__`.
|
|
54
|
+
|
|
55
|
+
## Release Preparation
|
|
56
|
+
|
|
57
|
+
The first release uses the current version because the repository has no
|
|
58
|
+
release tag yet:
|
|
59
|
+
|
|
60
|
+
```powershell
|
|
61
|
+
uv run python scripts/release.py 0.1.0 --dry-run
|
|
62
|
+
uv run python scripts/release.py 0.1.0
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For later releases, use `major`, `minor`, `patch`, or an exact higher version.
|
|
66
|
+
The script updates version metadata, generates the changelog, builds the
|
|
67
|
+
package, creates a release commit, and creates an annotated tag locally. It
|
|
68
|
+
never pushes or publishes automatically; review the result before pushing the
|
|
69
|
+
branch and tag.
|
|
70
|
+
|
|
71
|
+
## CLI
|
|
72
|
+
|
|
73
|
+
Install the package with `uv` and configure the service endpoint and key through
|
|
74
|
+
the environment:
|
|
75
|
+
|
|
76
|
+
```powershell
|
|
77
|
+
# These are the built-in defaults; override them only when needed.
|
|
78
|
+
$env:OCE_API_URL = "http://127.0.0.1:8986"
|
|
79
|
+
$env:OCE_API_KEY = "sk-opencontextengine"
|
|
80
|
+
$env:OCE_WORKSPACE = (Get-Location).Path
|
|
81
|
+
uv run oce-client sync
|
|
82
|
+
uv run oce-client retrieve "where is request authentication implemented?"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If unset, `OCE_API_URL` defaults to `http://127.0.0.1:8986` and `OCE_API_KEY`
|
|
86
|
+
defaults to `sk-opencontextengine`. `status` is local-only and does not require
|
|
87
|
+
an API key. `observe` and `remove`
|
|
88
|
+
stage explicit editor changes in SQLite; run `sync` to publish them. Add
|
|
89
|
+
`--json` to `sync`, `status`, `retrieve`, `observe`, or `remove` for
|
|
90
|
+
machine-readable output. CLI options are placed before the subcommand, for
|
|
91
|
+
example `oce-client --root C:\src\project sync`; `--root` falls back to
|
|
92
|
+
`OCE_WORKSPACE`, and `--api-url`, `--state-path`, and repeated `--ignore`
|
|
93
|
+
override `OCE_API_URL`, `OCE_STATE_PATH`, and `OCE_IGNORE`.
|
|
94
|
+
|
|
95
|
+
The two interfaces have different lifecycles:
|
|
96
|
+
|
|
97
|
+
| Interface | Workspace selection | State selection | Index lifecycle |
|
|
98
|
+
| --- | --- | --- | --- |
|
|
99
|
+
| CLI | one `--root` or `OCE_WORKSPACE` | `--state-path` or `OCE_STATE_PATH` | explicit `sync`, optional `watch` |
|
|
100
|
+
| MCP | repeated `--workspace`, `OCE_WORKSPACE`, or `OCE_WORKSPACES` | one `--state-path`, or per-workspace `--state-dir` | process-owned background and incremental sync |
|
|
101
|
+
|
|
102
|
+
## MCP
|
|
103
|
+
|
|
104
|
+
Install the optional MCP extra and expose the stdio server to an MCP host:
|
|
105
|
+
|
|
106
|
+
```powershell
|
|
107
|
+
uv sync --extra mcp
|
|
108
|
+
uv run oce-client-mcp --workspace C:\path\to\workspace
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The server exposes one tool, `codebase-retrieval`. Workspace indexing belongs
|
|
112
|
+
to the MCP process rather than the coding agent: the server starts the initial
|
|
113
|
+
index in the background, watches the filesystem, and synchronizes only changed
|
|
114
|
+
paths. Unchanged files are identified by stored filesystem metadata and are not
|
|
115
|
+
read or rehashed on restart.
|
|
116
|
+
|
|
117
|
+
Declare each allowed workspace with a repeated `--workspace` argument. With one
|
|
118
|
+
workspace, the tool's `workspace_folder` input is optional. With multiple
|
|
119
|
+
workspaces it is required and must exactly match an allowed path. Other paths
|
|
120
|
+
are rejected. For an environment-only setup, use `OCE_WORKSPACE` for one path
|
|
121
|
+
or `OCE_WORKSPACES` with paths separated by the platform path separator. MCP
|
|
122
|
+
does not fall back to the process current directory.
|
|
123
|
+
|
|
124
|
+
```powershell
|
|
125
|
+
oce-client-mcp `
|
|
126
|
+
--workspace C:\src\project-a `
|
|
127
|
+
--workspace C:\src\project-b `
|
|
128
|
+
--state-dir $env:LOCALAPPDATA\oce-client `
|
|
129
|
+
--initial-sync background `
|
|
130
|
+
--debounce-ms 500 `
|
|
131
|
+
--ready-timeout 3
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`--initial-sync` accepts `background` (default), `blocking`, or `off`; `off`
|
|
135
|
+
defers initialization until the first retrieval call. A tool call waits up to
|
|
136
|
+
`--ready-timeout` seconds for the latest observed filesystem generation. Its
|
|
137
|
+
result status is `ready`, `indexing`, or `error`; only a `ready` result contains
|
|
138
|
+
retrieval context. `OCE_API_URL`, `OCE_API_KEY`, `OCE_STATE_PATH`, `OCE_STATE_DIR`, `OCE_IGNORE`,
|
|
139
|
+
`OCE_DEBOUNCE_MS`, `OCE_INITIAL_SYNC`, `OCE_READY_TIMEOUT`, and
|
|
140
|
+
`OCE_LOG_LEVEL` provide environment equivalents. `--state-path` and
|
|
141
|
+
`OCE_STATE_PATH` are for one workspace; use `--state-dir` or `OCE_STATE_DIR`
|
|
142
|
+
for multiple workspaces. Keep the API key in the environment rather than
|
|
143
|
+
command arguments.
|
|
144
|
+
|
|
145
|
+
The service endpoint, API key, and ignore patterns are shared through the same
|
|
146
|
+
environment variables. State selection follows the interface table above. A
|
|
147
|
+
Codex-ready skill with the host configuration and command guidance is included
|
|
148
|
+
at `skills/oce-client/SKILL.md`.
|
|
149
|
+
|
|
150
|
+
After installing a wheel, locate or install that skill with:
|
|
151
|
+
|
|
152
|
+
```powershell
|
|
153
|
+
uv run oce-client skill path
|
|
154
|
+
uv run oce-client skill install
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The default installation target is `$CODEX_HOME/skills/oce-client` or
|
|
158
|
+
`$HOME/.codex/skills/oce-client`. Existing skill directories are preserved;
|
|
159
|
+
pass `--force` only when intentionally updating one.
|
|
160
|
+
|
|
161
|
+
Keep `OCE_API_KEY` in the host's environment or secret manager; do not commit it
|
|
162
|
+
to an MCP configuration file.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
oce_client/__init__.py,sha256=askZ-UkmsiCUu7UJgPSUpxmPU5gqk-MaRyTU33XlG7I,897
|
|
2
|
+
oce_client/cli.py,sha256=BLwezugvBw481yn3kF82oNcHZUIBmWjVrmpCVuKxf1I,9763
|
|
3
|
+
oce_client/context.py,sha256=-p5oEUEuGKdUt36Mie7ts225ZOT8wF0VEmwoKJ7L6fo,19337
|
|
4
|
+
oce_client/defaults.py,sha256=eUFMJjvIBtYpaaJCgU9Fhs7Wq-NTK-arMAB5BHeq_oI,83
|
|
5
|
+
oce_client/filesystem.py,sha256=9ztsNiNRM8GzqyyLUsUiaFDcJTzYboR8_pwEQMLObo8,2245
|
|
6
|
+
oce_client/http.py,sha256=YclxR1litwM7ILwe0XvLZbozJ-0kfBJnRjv-XRflp0o,4505
|
|
7
|
+
oce_client/identity.py,sha256=3LXllaDhn-_SwVKOi7xR7yKB-lvIeMNWkKSwm-OxTto,586
|
|
8
|
+
oce_client/ignore.py,sha256=U-w23UF68fqXcvxUFCuNtOg1aUVxm_y28gs_WkhYH80,2695
|
|
9
|
+
oce_client/indexer.py,sha256=uKWAkrVYEKOCycCw-fSTyzxo0gZ1xLcoA0G03eWXqKA,9742
|
|
10
|
+
oce_client/mcp_server.py,sha256=by8NkBA5Klyl2COetr1G8SiZAHsc-Vc_u9BPQZKGwJU,10743
|
|
11
|
+
oce_client/models.py,sha256=hqR1i96rMWp93MMfRcHgrtTXnz-Q5Bs5K_aVI9Tx83E,2154
|
|
12
|
+
oce_client/ports.py,sha256=y0v4l9c3nL7f0xCTef1wzFyaD6duR8bMaT5uxbYQBWA,1589
|
|
13
|
+
oce_client/runtime.py,sha256=LsUMJG2Aw16bXHxr3xzwFkfPzYVegUadVl5Eb_6cHIY,8836
|
|
14
|
+
oce_client/state.py,sha256=RR2Burbr8eJhHkA7dOEEga8a02Nd7Dx1mUaBsE9hUfs,11484
|
|
15
|
+
oce_client/watcher.py,sha256=O56ScabFJ4vZTmd2QJrNzEloBNWV-xR45XR_5FzWQTs,1611
|
|
16
|
+
oce_client/skill/SKILL.md,sha256=aI_bPGkBimvvOWUc81X6SC_E6L6hysuU15D-xeo6jAA,5516
|
|
17
|
+
oce_client/skill/agents/openai.yaml,sha256=YymbN6Lq_e2BmxNM_DdOkNH4AC1w-V1cUO7R6xygv6Y,185
|
|
18
|
+
opencontextengine_client-0.1.0.dist-info/METADATA,sha256=XOp3yP-1McARp7d7HyQw2NSom8LVmTSAz6dqeNK_6RU,6690
|
|
19
|
+
opencontextengine_client-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
20
|
+
opencontextengine_client-0.1.0.dist-info/entry_points.txt,sha256=CoPgl3vSqrnBjEXs9iCS2JzUgdemWOPqtkxTPvQyuXU,95
|
|
21
|
+
opencontextengine_client-0.1.0.dist-info/licenses/LICENSE,sha256=15ZzNvM0vOxXaFSjkXd36R7eJXymssYNJH3UvjTfe_o,11341
|
|
22
|
+
opencontextengine_client-0.1.0.dist-info/RECORD,,
|