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/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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ oce-client = oce_client.cli:main
3
+ oce-client-mcp = oce_client.mcp_server:main