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/models.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FileStatus(str, Enum):
|
|
8
|
+
PRESENT = "present"
|
|
9
|
+
SKIPPED = "skipped"
|
|
10
|
+
DELETED = "deleted"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BlobStatus(str, Enum):
|
|
14
|
+
LOCAL = "local"
|
|
15
|
+
UPLOADED = "uploaded"
|
|
16
|
+
READY = "ready"
|
|
17
|
+
FAILED = "failed"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class FileRecord:
|
|
22
|
+
path: str
|
|
23
|
+
blob_name: str | None
|
|
24
|
+
status: FileStatus
|
|
25
|
+
content: str | None = None
|
|
26
|
+
size: int = 0
|
|
27
|
+
mtime_ns: int | None = None
|
|
28
|
+
source: str = "filesystem"
|
|
29
|
+
generation: int = 0
|
|
30
|
+
committed_blob_name: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class BlobUpload:
|
|
35
|
+
path: str
|
|
36
|
+
content: str
|
|
37
|
+
blob_name: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class BlobDelta:
|
|
42
|
+
checkpoint_id: str | None
|
|
43
|
+
added_blobs: tuple[str, ...] = ()
|
|
44
|
+
deleted_blobs: tuple[str, ...] = ()
|
|
45
|
+
|
|
46
|
+
def to_api_dict(self) -> dict[str, object]:
|
|
47
|
+
return {
|
|
48
|
+
"checkpoint_id": self.checkpoint_id or "",
|
|
49
|
+
"added_blobs": list(self.added_blobs),
|
|
50
|
+
"deleted_blobs": list(self.deleted_blobs),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class UploadPlan:
|
|
56
|
+
uploads: tuple[BlobUpload, ...]
|
|
57
|
+
delta: BlobDelta
|
|
58
|
+
skipped_paths: tuple[str, ...] = ()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class UploadResult:
|
|
63
|
+
blob_names: tuple[str, ...]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class MissingResult:
|
|
68
|
+
unknown_blob_names: tuple[str, ...] = ()
|
|
69
|
+
nonindexed_blob_names: tuple[str, ...] = ()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class BlobStatusResult:
|
|
74
|
+
unknown_blob_names: tuple[str, ...] = ()
|
|
75
|
+
nonindexed_blob_names: tuple[str, ...] = ()
|
|
76
|
+
checkpoint_not_found: bool = False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class CheckpointResult:
|
|
81
|
+
new_checkpoint_id: str
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class RetrievalResult:
|
|
86
|
+
formatted_retrieval: str
|
|
87
|
+
elapsed_ms: int = 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class SyncResult:
|
|
92
|
+
uploaded_blob_names: tuple[str, ...]
|
|
93
|
+
checkpoint_id: str | None
|
|
94
|
+
added_blobs: tuple[str, ...]
|
|
95
|
+
deleted_blobs: tuple[str, ...]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class WorkspaceSnapshot:
|
|
100
|
+
files: dict[str, FileRecord] = field(default_factory=dict)
|
|
101
|
+
checkpoint_id: str | None = None
|
|
102
|
+
generation: int = 0
|
oce_client/ports.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Protocol, Sequence
|
|
6
|
+
|
|
7
|
+
from .models import (
|
|
8
|
+
BlobStatusResult,
|
|
9
|
+
BlobUpload,
|
|
10
|
+
CheckpointResult,
|
|
11
|
+
MissingResult,
|
|
12
|
+
RetrievalResult,
|
|
13
|
+
UploadResult,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BlobIdentity(Protocol):
|
|
18
|
+
def calculate(self, path: str, content: str) -> str: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BlobApi(Protocol):
|
|
22
|
+
def find_missing(self, blob_names: Sequence[str]) -> MissingResult: ...
|
|
23
|
+
|
|
24
|
+
def batch_upload(self, blobs: Sequence[BlobUpload]) -> UploadResult: ...
|
|
25
|
+
|
|
26
|
+
def blob_status(
|
|
27
|
+
self, blob_names: Sequence[str], checkpoint_id: str | None = None
|
|
28
|
+
) -> BlobStatusResult: ...
|
|
29
|
+
|
|
30
|
+
def checkpoint(
|
|
31
|
+
self,
|
|
32
|
+
checkpoint_id: str | None,
|
|
33
|
+
added_blobs: Sequence[str],
|
|
34
|
+
deleted_blobs: Sequence[str],
|
|
35
|
+
) -> CheckpointResult: ...
|
|
36
|
+
|
|
37
|
+
def retrieve(
|
|
38
|
+
self,
|
|
39
|
+
query: str,
|
|
40
|
+
checkpoint_id: str | None,
|
|
41
|
+
added_blobs: Sequence[str],
|
|
42
|
+
deleted_blobs: Sequence[str],
|
|
43
|
+
) -> RetrievalResult: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class StateStore(Protocol):
|
|
47
|
+
def load_snapshot(self): ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class WatchHandle(Protocol):
|
|
51
|
+
def stop(self) -> None: ...
|
|
52
|
+
|
|
53
|
+
def join(self, timeout: float | None = None) -> None: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FileSource(Protocol):
|
|
57
|
+
def scan(self, root: Path, matcher: IgnoreMatcher) -> dict[str, str]: ...
|
|
58
|
+
|
|
59
|
+
def read(self, path: Path) -> str: ...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class IgnoreMatcher(Protocol):
|
|
63
|
+
def ignores(self, path: str, *, is_dir: bool = False) -> bool: ...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Watcher(Protocol):
|
|
67
|
+
def start(self, callback: Callable[[], None]) -> WatchHandle: ...
|
oce_client/runtime.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from collections.abc import Iterable, Sequence
|
|
7
|
+
|
|
8
|
+
from .context import WorkspaceContext
|
|
9
|
+
from .defaults import DEFAULT_API_KEY, DEFAULT_API_URL
|
|
10
|
+
from .http import OceHttpClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClientConfigurationError(ValueError):
|
|
14
|
+
"""Raised when a CLI/MCP runtime cannot be configured safely."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ClientSettings:
|
|
19
|
+
root: Path
|
|
20
|
+
api_url: str
|
|
21
|
+
api_key: str
|
|
22
|
+
state_path: Path | None = None
|
|
23
|
+
runtime_patterns: tuple[str, ...] = ()
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_environment(
|
|
27
|
+
cls,
|
|
28
|
+
*,
|
|
29
|
+
root: str | os.PathLike[str] | None = None,
|
|
30
|
+
api_url: str | None = None,
|
|
31
|
+
api_key: str | None = None,
|
|
32
|
+
state_path: str | os.PathLike[str] | None = None,
|
|
33
|
+
runtime_patterns: Iterable[str] | None = None,
|
|
34
|
+
require_api_key: bool = True,
|
|
35
|
+
) -> "ClientSettings":
|
|
36
|
+
resolved_root = Path(
|
|
37
|
+
root if root is not None else os.environ.get("OCE_WORKSPACE", ".")
|
|
38
|
+
).expanduser().resolve()
|
|
39
|
+
resolved_url = (
|
|
40
|
+
api_url if api_url is not None else os.environ.get("OCE_API_URL", DEFAULT_API_URL)
|
|
41
|
+
).strip()
|
|
42
|
+
resolved_key = api_key if api_key is not None else os.environ.get(
|
|
43
|
+
"OCE_API_KEY", DEFAULT_API_KEY
|
|
44
|
+
)
|
|
45
|
+
resolved_state = _resolve_path_option(state_path, "OCE_STATE_PATH")
|
|
46
|
+
if not resolved_url:
|
|
47
|
+
raise ClientConfigurationError("OCE API URL must not be empty")
|
|
48
|
+
if require_api_key and not resolved_key:
|
|
49
|
+
raise ClientConfigurationError(
|
|
50
|
+
"OCE API key is required; set OCE_API_KEY"
|
|
51
|
+
)
|
|
52
|
+
return cls(
|
|
53
|
+
root=resolved_root,
|
|
54
|
+
api_url=resolved_url.rstrip("/"),
|
|
55
|
+
api_key=resolved_key,
|
|
56
|
+
state_path=resolved_state,
|
|
57
|
+
runtime_patterns=tuple(
|
|
58
|
+
runtime_patterns
|
|
59
|
+
if runtime_patterns is not None
|
|
60
|
+
else iter_runtime_patterns((os.environ.get("OCE_IGNORE", ""),))
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class McpConfiguration:
|
|
67
|
+
"""Fully resolved configuration for the long-running MCP process."""
|
|
68
|
+
|
|
69
|
+
client: ClientSettings
|
|
70
|
+
workspace_roots: tuple[Path, ...]
|
|
71
|
+
state_dir: Path | None
|
|
72
|
+
debounce_ms: int
|
|
73
|
+
initial_sync: str
|
|
74
|
+
ready_timeout: float
|
|
75
|
+
log_level: str
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_environment(
|
|
79
|
+
cls,
|
|
80
|
+
*,
|
|
81
|
+
workspace_roots: Sequence[str | os.PathLike[str]] | None = None,
|
|
82
|
+
api_url: str | None = None,
|
|
83
|
+
state_path: str | os.PathLike[str] | None = None,
|
|
84
|
+
state_dir: str | os.PathLike[str] | None = None,
|
|
85
|
+
runtime_patterns: Iterable[str] | None = None,
|
|
86
|
+
debounce_ms: int | None = None,
|
|
87
|
+
initial_sync: str | None = None,
|
|
88
|
+
ready_timeout: float | None = None,
|
|
89
|
+
log_level: str | None = None,
|
|
90
|
+
) -> "McpConfiguration":
|
|
91
|
+
roots = _resolve_mcp_roots(workspace_roots)
|
|
92
|
+
if not roots:
|
|
93
|
+
raise ClientConfigurationError(
|
|
94
|
+
"MCP requires at least one workspace; pass --workspace or set "
|
|
95
|
+
"OCE_WORKSPACE/OCE_WORKSPACES"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
resolved_state_dir = _resolve_path_option(state_dir, "OCE_STATE_DIR")
|
|
99
|
+
client = ClientSettings.from_environment(
|
|
100
|
+
root=roots[0],
|
|
101
|
+
api_url=api_url,
|
|
102
|
+
state_path=state_path,
|
|
103
|
+
runtime_patterns=runtime_patterns,
|
|
104
|
+
require_api_key=True,
|
|
105
|
+
)
|
|
106
|
+
if client.state_path is not None and resolved_state_dir is not None:
|
|
107
|
+
raise ClientConfigurationError(
|
|
108
|
+
"MCP state configuration is ambiguous; choose OCE_STATE_PATH/--state-path "
|
|
109
|
+
"or OCE_STATE_DIR/--state-dir"
|
|
110
|
+
)
|
|
111
|
+
if len(roots) > 1 and client.state_path is not None and resolved_state_dir is None:
|
|
112
|
+
raise ClientConfigurationError(
|
|
113
|
+
"multiple MCP workspaces cannot share OCE_STATE_PATH; use --state-dir "
|
|
114
|
+
"or OCE_STATE_DIR"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
resolved_debounce = _resolve_int_option(
|
|
118
|
+
debounce_ms, "OCE_DEBOUNCE_MS", 500
|
|
119
|
+
)
|
|
120
|
+
if resolved_debounce < 0:
|
|
121
|
+
raise ClientConfigurationError("debounce-ms must not be negative")
|
|
122
|
+
resolved_initial = (
|
|
123
|
+
initial_sync
|
|
124
|
+
if initial_sync is not None
|
|
125
|
+
else os.environ.get("OCE_INITIAL_SYNC", "background")
|
|
126
|
+
).strip().lower()
|
|
127
|
+
if resolved_initial not in {"background", "blocking", "off"}:
|
|
128
|
+
raise ClientConfigurationError(
|
|
129
|
+
"initial-sync must be one of: background, blocking, off"
|
|
130
|
+
)
|
|
131
|
+
resolved_timeout = _resolve_float_option(
|
|
132
|
+
ready_timeout, "OCE_READY_TIMEOUT", 3.0
|
|
133
|
+
)
|
|
134
|
+
if resolved_timeout < 0:
|
|
135
|
+
raise ClientConfigurationError("ready-timeout must not be negative")
|
|
136
|
+
resolved_log_level = (
|
|
137
|
+
log_level
|
|
138
|
+
if log_level is not None
|
|
139
|
+
else os.environ.get("OCE_LOG_LEVEL", "warning")
|
|
140
|
+
).strip().lower()
|
|
141
|
+
if resolved_log_level not in {"debug", "info", "warning", "error", "critical"}:
|
|
142
|
+
raise ClientConfigurationError(
|
|
143
|
+
"log-level must be one of: debug, info, warning, error, critical"
|
|
144
|
+
)
|
|
145
|
+
return cls(
|
|
146
|
+
client=client,
|
|
147
|
+
workspace_roots=roots,
|
|
148
|
+
state_dir=resolved_state_dir,
|
|
149
|
+
debounce_ms=resolved_debounce,
|
|
150
|
+
initial_sync=resolved_initial,
|
|
151
|
+
ready_timeout=resolved_timeout,
|
|
152
|
+
log_level=resolved_log_level,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class ClientRuntime:
|
|
157
|
+
"""Lazy, closeable context shared by one CLI command or MCP process."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, settings: ClientSettings) -> None:
|
|
160
|
+
self.settings = settings
|
|
161
|
+
self._context: WorkspaceContext | None = None
|
|
162
|
+
|
|
163
|
+
def context(self) -> WorkspaceContext:
|
|
164
|
+
if self._context is None:
|
|
165
|
+
if not self.settings.root.is_dir():
|
|
166
|
+
raise ClientConfigurationError(
|
|
167
|
+
f"workspace is not a directory: {self.settings.root}"
|
|
168
|
+
)
|
|
169
|
+
api = OceHttpClient(self.settings.api_url, self.settings.api_key)
|
|
170
|
+
try:
|
|
171
|
+
self._context = WorkspaceContext.open(
|
|
172
|
+
self.settings.root,
|
|
173
|
+
api,
|
|
174
|
+
state_path=self.settings.state_path,
|
|
175
|
+
runtime_patterns=self.settings.runtime_patterns,
|
|
176
|
+
)
|
|
177
|
+
except Exception:
|
|
178
|
+
api.close()
|
|
179
|
+
raise
|
|
180
|
+
return self._context
|
|
181
|
+
|
|
182
|
+
def close(self) -> None:
|
|
183
|
+
if self._context is not None:
|
|
184
|
+
self._context.close()
|
|
185
|
+
self._context = None
|
|
186
|
+
|
|
187
|
+
def __enter__(self) -> "ClientRuntime":
|
|
188
|
+
return self
|
|
189
|
+
|
|
190
|
+
def __exit__(self, *_: object) -> None:
|
|
191
|
+
self.close()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def iter_runtime_patterns(values: Iterable[str]) -> tuple[str, ...]:
|
|
195
|
+
patterns: list[str] = []
|
|
196
|
+
for value in values:
|
|
197
|
+
for comma_group in value.replace("\r", "\n").split(","):
|
|
198
|
+
patterns.extend(
|
|
199
|
+
line.strip() for line in comma_group.splitlines() if line.strip()
|
|
200
|
+
)
|
|
201
|
+
return tuple(patterns)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _resolve_path_option(
|
|
205
|
+
value: str | os.PathLike[str] | None,
|
|
206
|
+
environment_name: str,
|
|
207
|
+
) -> Path | None:
|
|
208
|
+
raw = value if value is not None else os.environ.get(environment_name)
|
|
209
|
+
return None if raw is None or not str(raw).strip() else Path(raw).expanduser().resolve()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _resolve_mcp_roots(
|
|
213
|
+
values: Sequence[str | os.PathLike[str]] | None,
|
|
214
|
+
) -> tuple[Path, ...]:
|
|
215
|
+
if values is None:
|
|
216
|
+
raw_values: Sequence[str | os.PathLike[str]]
|
|
217
|
+
configured = os.environ.get("OCE_WORKSPACES")
|
|
218
|
+
if configured:
|
|
219
|
+
raw_values = tuple(part for part in configured.split(os.pathsep) if part)
|
|
220
|
+
else:
|
|
221
|
+
single = os.environ.get("OCE_WORKSPACE")
|
|
222
|
+
raw_values = () if single is None or not single.strip() else (single,)
|
|
223
|
+
else:
|
|
224
|
+
raw_values = values
|
|
225
|
+
return tuple(dict.fromkeys(Path(value).expanduser().resolve() for value in raw_values))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _resolve_int_option(value: int | None, environment_name: str, default: int) -> int:
|
|
229
|
+
raw = value if value is not None else os.environ.get(environment_name)
|
|
230
|
+
if raw is None:
|
|
231
|
+
return default
|
|
232
|
+
try:
|
|
233
|
+
return int(raw)
|
|
234
|
+
except (TypeError, ValueError) as exc:
|
|
235
|
+
raise ClientConfigurationError(f"{environment_name} must be an integer") from exc
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _resolve_float_option(value: float | None, environment_name: str, default: float) -> float:
|
|
239
|
+
raw = value if value is not None else os.environ.get(environment_name)
|
|
240
|
+
if raw is None:
|
|
241
|
+
return default
|
|
242
|
+
try:
|
|
243
|
+
return float(raw)
|
|
244
|
+
except (TypeError, ValueError) as exc:
|
|
245
|
+
raise ClientConfigurationError(f"{environment_name} must be a number") from exc
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oce-client
|
|
3
|
+
description: "Use the oce-client CLI to synchronize one local workspace and retrieve current code context from OpenContextEngine."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# OpenContextEngine CLI
|
|
7
|
+
|
|
8
|
+
This skill documents the `oce-client` command-line interface for an AI agent.
|
|
9
|
+
It is a CLI workflow: invoke a command, read its result, and continue the task.
|
|
10
|
+
|
|
11
|
+
## 1. Command Reference
|
|
12
|
+
|
|
13
|
+
### Version and package identity
|
|
14
|
+
|
|
15
|
+
- Distribution package: `opencontextengine-client`
|
|
16
|
+
- Python package: `oce_client`
|
|
17
|
+
- CLI executable: `oce-client`
|
|
18
|
+
- Check the installed CLI version with `oce-client --version`.
|
|
19
|
+
|
|
20
|
+
### Global options
|
|
21
|
+
|
|
22
|
+
Global options must appear before the subcommand:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
oce-client --root <workspace> --api-url <url> --state-path <file> <command>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- `--root`: workspace directory; otherwise use `OCE_WORKSPACE`, then the
|
|
29
|
+
current directory.
|
|
30
|
+
- `--api-url`: service URL; otherwise use `OCE_API_URL`, then
|
|
31
|
+
`http://127.0.0.1:8986`.
|
|
32
|
+
- `--state-path`: SQLite state file; otherwise use `OCE_STATE_PATH`, then
|
|
33
|
+
`<workspace>/.oce-client/state.sqlite3`.
|
|
34
|
+
- `--ignore PATTERN`: add a runtime ignore pattern; repeat it when needed.
|
|
35
|
+
|
|
36
|
+
The API key has no CLI option. Load it through `OCE_API_KEY`; the local default
|
|
37
|
+
is `sk-opencontextengine`.
|
|
38
|
+
|
|
39
|
+
### Workspace commands
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
oce-client --root <workspace> sync [--json]
|
|
43
|
+
oce-client --root <workspace> status [--json]
|
|
44
|
+
oce-client --root <workspace> retrieve [--scope workspace|working_set] [--json] <query>
|
|
45
|
+
oce-client --root <workspace> observe <path> [--content <text> | --file <file>] [--json]
|
|
46
|
+
oce-client --root <workspace> remove <path> [--json]
|
|
47
|
+
oce-client --root <workspace> watch [--debounce-ms <milliseconds>]
|
|
48
|
+
oce-client skill path [--json]
|
|
49
|
+
oce-client skill install [--target <directory>] [--force] [--json]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- `sync` scans the workspace, uploads new or changed blobs, and commits a new
|
|
53
|
+
server checkpoint. This is the command that makes retrieval reflect the
|
|
54
|
+
current files.
|
|
55
|
+
- `status` reads only the local SQLite inventory and checkpoint. It does not
|
|
56
|
+
contact the service and does not require an API key.
|
|
57
|
+
- `retrieve` sends a natural-language code question to the service using the
|
|
58
|
+
last local checkpoint. Use `--scope` only when the task requires the
|
|
59
|
+
corresponding retrieval scope.
|
|
60
|
+
- `observe` stages explicit editor content in the local state. It does not
|
|
61
|
+
publish the content until `sync` runs.
|
|
62
|
+
- `remove` stages a workspace-relative deletion. It also requires `sync` to
|
|
63
|
+
publish the change.
|
|
64
|
+
- `watch` keeps a foreground process alive and incrementally syncs filesystem
|
|
65
|
+
changes. Run an initial successful `sync` before starting it.
|
|
66
|
+
|
|
67
|
+
`skill path` and `skill install` are installation maintenance commands; they
|
|
68
|
+
are not part of normal code retrieval.
|
|
69
|
+
|
|
70
|
+
## 2. Workflow
|
|
71
|
+
|
|
72
|
+
### One-shot retrieval
|
|
73
|
+
|
|
74
|
+
Use this workflow when no long-running watcher is already maintaining the
|
|
75
|
+
workspace:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
1. Set OCE_WORKSPACE, OCE_API_URL, and OCE_API_KEY in the process environment.
|
|
79
|
+
2. Run `oce-client --root <workspace> sync --json`.
|
|
80
|
+
3. Run `oce-client --root <workspace> retrieve --json "<natural-language question>"`.
|
|
81
|
+
4. Parse JSON from stdout and use `formatted_retrieval` as code context.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Run `sync` again before a later retrieval when files may have changed. The
|
|
85
|
+
client persists inventory and checkpoint state, so unchanged files are not
|
|
86
|
+
uploaded again.
|
|
87
|
+
|
|
88
|
+
### Session retrieval with a watcher
|
|
89
|
+
|
|
90
|
+
For several questions against the same workspace:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
1. Run one successful `sync`.
|
|
94
|
+
2. Start `oce-client --root <workspace> watch` and keep it running.
|
|
95
|
+
3. Invoke `retrieve --json` for each AI question while the watcher is alive.
|
|
96
|
+
4. Stop the watcher when the workspace session ends.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The watcher handles changed paths incrementally. If it is stopped, return to
|
|
100
|
+
the one-shot workflow and run `sync` before retrieving.
|
|
101
|
+
|
|
102
|
+
### Unsaved editor state
|
|
103
|
+
|
|
104
|
+
When the agent has content that is not yet written to disk:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
oce-client --root <workspace> observe src/example.py --content "<text>"
|
|
108
|
+
oce-client --root <workspace> sync --json
|
|
109
|
+
oce-client --root <workspace> retrieve --json "<question>"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Use `remove` followed by `sync` for an unsaved deletion.
|
|
113
|
+
|
|
114
|
+
## 3. Important Notes
|
|
115
|
+
|
|
116
|
+
- This CLI handles one workspace per invocation. Pass the workspace explicitly
|
|
117
|
+
when the agent knows it; do not accidentally index the agent's own process
|
|
118
|
+
directory.
|
|
119
|
+
- Keep `OCE_API_KEY` in the environment or a secret manager. Never put the key
|
|
120
|
+
in prompts, command arguments, logs, JSON output, or committed files.
|
|
121
|
+
- Use `--json` whenever another program or agent will consume the result.
|
|
122
|
+
Treat stdout as the data channel and stderr as diagnostics.
|
|
123
|
+
- A successful `status` only proves that local state exists; it does not prove
|
|
124
|
+
that the files on disk have been synchronized. Do not claim that retrieval is
|
|
125
|
+
current until `sync` has succeeded or an active `watch` has processed the
|
|
126
|
+
changes.
|
|
127
|
+
- `sync` and `retrieve` can take time on a first run or after a large change.
|
|
128
|
+
Do not retry them concurrently against the same state file.
|
|
129
|
+
- Use only the commands and options documented above. In particular, do not
|
|
130
|
+
invent a background or initial-sync option for this one-shot CLI.
|
|
131
|
+
- Retrieval describes the current code on disk and the selected checkpoint. It
|
|
132
|
+
has no version-control history or knowledge of previous commits.
|
|
133
|
+
- Quote workspace paths and natural-language queries. Use workspace-relative
|
|
134
|
+
paths with `observe` and `remove`.
|
|
135
|
+
- A zero exit code means the command completed successfully. A non-zero exit
|
|
136
|
+
code means the result should not be treated as valid context; inspect stderr.
|