graph-mind-memory 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.
- associative_memory.py +507 -0
- automatic_capture.py +288 -0
- automatic_capture_cli.py +72 -0
- brain_log.py +501 -0
- coding_memory.py +438 -0
- concept_plan_benchmark_v071.py +248 -0
- conversation_memory.py +476 -0
- development_memory.py +450 -0
- development_paths.py +40 -0
- embedding_warmup.py +171 -0
- entity_timeline.py +130 -0
- executable_memory.py +312 -0
- executable_plan_v2_diagnostic.py +272 -0
- graph_mind_mcp_server.py +864 -0
- graph_mind_memory-0.1.0.dist-info/METADATA +252 -0
- graph_mind_memory-0.1.0.dist-info/RECORD +43 -0
- graph_mind_memory-0.1.0.dist-info/WHEEL +5 -0
- graph_mind_memory-0.1.0.dist-info/entry_points.txt +4 -0
- graph_mind_memory-0.1.0.dist-info/licenses/LICENSE +661 -0
- graph_mind_memory-0.1.0.dist-info/top_level.txt +37 -0
- grounded_concept_bridge_v072.py +511 -0
- ingestion_gate.py +113 -0
- ingestion_synonyms.py +165 -0
- install.py +198 -0
- llm_concept_planner.py +170 -0
- local_brain.py +354 -0
- longmemeval_adapter.py +120 -0
- longmemeval_ingestion_calibration.py +366 -0
- longmemeval_ingestion_preflight.py +188 -0
- longmemeval_ingestion_v7.py +356 -0
- memory_algebra.py +155 -0
- memory_contract.py +154 -0
- memory_normalization.py +13 -0
- memory_vocabulary.py +34 -0
- semantic_grounding_v073.py +501 -0
- semantic_llm_extractor.py +215 -0
- semantic_memory.py +155 -0
- semantic_recall.py +100 -0
- state_relation_contract.py +158 -0
- temporal_resolver.py +272 -0
- temporal_state.py +210 -0
- universal_personal_memory.py +234 -0
- vector_cache.py +194 -0
automatic_capture.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Persistent background capture for Graph-MIND code and conversation memory."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
from associative_memory import AssociativeMemoryIndex
|
|
12
|
+
from coding_memory import CodingMemoryCapture, CodingMemoryStore
|
|
13
|
+
from conversation_memory import (ClaudeCodeConversationCapture, ClaudeCoworkConversationCapture,
|
|
14
|
+
cowork_root, CodexConversationCapture,
|
|
15
|
+
ConversationMemoryStore)
|
|
16
|
+
from development_paths import default_coding_state_dir, default_development_db, graph_mind_home
|
|
17
|
+
from semantic_memory import SemanticMemoryIngestion
|
|
18
|
+
from semantic_llm_extractor import (OpenAIResponsesSemanticExtractor,
|
|
19
|
+
SemanticLLMIngestion)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
SERVICE_VERSION = "automatic-capture-v0.2"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def default_config_path():
|
|
26
|
+
return graph_mind_home() / "automatic-capture.json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def default_service_state_dir():
|
|
30
|
+
return graph_mind_home() / "automatic-capture"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _now():
|
|
34
|
+
return datetime.now().isoformat(timespec="microseconds")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _atomic_json(path, value):
|
|
38
|
+
path = Path(path)
|
|
39
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
|
41
|
+
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n",
|
|
42
|
+
encoding="utf-8")
|
|
43
|
+
os.replace(temporary, path)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _workspace_key(path):
|
|
47
|
+
return str(Path(path).expanduser().resolve())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def make_config(*, workspace, project_id, session_id, sessions_root,
|
|
51
|
+
poll_seconds=5, code_poll_seconds=120, db_path=None):
|
|
52
|
+
workspace = _workspace_key(workspace)
|
|
53
|
+
sessions_root = _workspace_key(sessions_root)
|
|
54
|
+
return {
|
|
55
|
+
"service_version": SERVICE_VERSION,
|
|
56
|
+
"db_path": str(Path(db_path or default_development_db()).expanduser().resolve()),
|
|
57
|
+
"poll_seconds": max(1, int(poll_seconds)),
|
|
58
|
+
"code_poll_seconds": max(int(poll_seconds), int(code_poll_seconds)),
|
|
59
|
+
"semantic_llm": {"enabled": False, "provider": "openai",
|
|
60
|
+
"model": None, "poll_seconds": 60, "limit": 25},
|
|
61
|
+
"codex_sessions_root": sessions_root,
|
|
62
|
+
"workspace_scopes": {workspace: project_id},
|
|
63
|
+
"code_workspaces": [{"workspace": workspace, "project_id": project_id,
|
|
64
|
+
"session_id": session_id,
|
|
65
|
+
"state_dir": str(default_coding_state_dir(project_id))}],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def load_config(path=None):
|
|
70
|
+
path = Path(path or default_config_path()).expanduser().resolve()
|
|
71
|
+
if not path.is_file():
|
|
72
|
+
raise ValueError("AUTOMATIC_CAPTURE_CONFIG_REQUIRED")
|
|
73
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
74
|
+
if value.get("service_version") != SERVICE_VERSION:
|
|
75
|
+
raise ValueError("AUTOMATIC_CAPTURE_CONFIG_VERSION_MISMATCH")
|
|
76
|
+
if not isinstance(value.get("code_workspaces"), list):
|
|
77
|
+
raise ValueError("AUTOMATIC_CAPTURE_WORKSPACES_REQUIRED")
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AutomaticCaptureService:
|
|
82
|
+
def __init__(self, config, *, state_dir=None, now=None):
|
|
83
|
+
self.config = config
|
|
84
|
+
self.state_dir = Path(state_dir or default_service_state_dir()).resolve()
|
|
85
|
+
self.state_dir.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
self.status_path = self.state_dir / "status.json"
|
|
87
|
+
self.pid_path = self.state_dir / "service.pid"
|
|
88
|
+
self.conversation_cursor = self.state_dir / "codex-session-cursor.json"
|
|
89
|
+
self.claude_cursor = self.state_dir / "claude-code-session-cursor.json"
|
|
90
|
+
self.cowork_cursor = self.state_dir / "claude-cowork-session-cursor.json"
|
|
91
|
+
self.now = now or _now
|
|
92
|
+
self.db_path = Path(config["db_path"]).expanduser().resolve()
|
|
93
|
+
|
|
94
|
+
def _coding_capture(self, entry, store):
|
|
95
|
+
return CodingMemoryCapture(entry["workspace"], store,
|
|
96
|
+
project_id=entry["project_id"], session_id=entry["session_id"],
|
|
97
|
+
state_dir=entry.get("state_dir") or default_coding_state_dir(
|
|
98
|
+
entry["project_id"]), now=self.now)
|
|
99
|
+
|
|
100
|
+
def _conversation_captures(self, store):
|
|
101
|
+
"""Every conversation source on this machine, each with its own read cursor.
|
|
102
|
+
|
|
103
|
+
Codex is configured explicitly. Claude Code is picked up from ~/.claude/projects when
|
|
104
|
+
that folder exists, unless claude_projects_root names another one, so that whatever the
|
|
105
|
+
user talks to is stored in full and not only what a model chose to remember.
|
|
106
|
+
"""
|
|
107
|
+
scopes = self.config.get("workspace_scopes", {})
|
|
108
|
+
# With a shared folder, every captured turn also goes into the shared log, so a
|
|
109
|
+
# conversation held on this PC can be recalled on any other (brain_log.py).
|
|
110
|
+
from brain_log import folder
|
|
111
|
+
shared = folder() # config, GRAPH_MIND_FOLDER, or the chosen one
|
|
112
|
+
if self.config.get("memory_folder"): # the service's own config wins
|
|
113
|
+
from brain_log import is_database
|
|
114
|
+
chosen = self.config["memory_folder"]
|
|
115
|
+
shared = chosen if is_database(chosen) else Path(chosen).expanduser() # folder() also reads the
|
|
116
|
+
# one chosen in conversation
|
|
117
|
+
publish = None
|
|
118
|
+
if shared:
|
|
119
|
+
from brain_log import append
|
|
120
|
+
publish = lambda turn: append(shared, "turn", turn) # a folder or a Postgres URL
|
|
121
|
+
sources = []
|
|
122
|
+
# A source whose folder does not exist yet (Codex never run on this PC) is skipped, not
|
|
123
|
+
# fatal: it used to stop every source, so a Claude-Code-only user captured nothing.
|
|
124
|
+
if Path(self.config["codex_sessions_root"]).is_dir():
|
|
125
|
+
sources.append(("codex", CodexConversationCapture(
|
|
126
|
+
self.config["codex_sessions_root"], store, state_path=self.conversation_cursor,
|
|
127
|
+
workspace_scopes=scopes, now=self.now, publish=publish), self.conversation_cursor))
|
|
128
|
+
claude_root = Path(self.config.get("claude_projects_root")
|
|
129
|
+
or Path.home() / ".claude" / "projects")
|
|
130
|
+
if claude_root.is_dir():
|
|
131
|
+
sources.append(("claude-code", ClaudeCodeConversationCapture(
|
|
132
|
+
claude_root, store, state_path=self.claude_cursor,
|
|
133
|
+
workspace_scopes=scopes, now=self.now, publish=publish), self.claude_cursor))
|
|
134
|
+
cowork = self.config.get("claude_cowork_root") or cowork_root()
|
|
135
|
+
if cowork and Path(cowork).is_dir():
|
|
136
|
+
sources.append(("claude-cowork", ClaudeCoworkConversationCapture(
|
|
137
|
+
cowork, store, state_path=self.cowork_cursor,
|
|
138
|
+
workspace_scopes=scopes, now=self.now, publish=publish), self.cowork_cursor))
|
|
139
|
+
return sources
|
|
140
|
+
|
|
141
|
+
def baseline(self):
|
|
142
|
+
coding = []
|
|
143
|
+
with CodingMemoryStore(self.db_path) as store:
|
|
144
|
+
for entry in self.config["code_workspaces"]:
|
|
145
|
+
capture = self._coding_capture(entry, store)
|
|
146
|
+
if capture.manifest_path.exists():
|
|
147
|
+
coding.append({"project_id": entry["project_id"],
|
|
148
|
+
"status": "BASELINE_ALREADY_EXISTS",
|
|
149
|
+
"manifest": str(capture.manifest_path)})
|
|
150
|
+
else:
|
|
151
|
+
coding.append({"project_id": entry["project_id"],
|
|
152
|
+
**capture.initialize_baseline()})
|
|
153
|
+
with ConversationMemoryStore(self.db_path) as store:
|
|
154
|
+
conversation = {
|
|
155
|
+
name: ({"status": "BASELINE_ALREADY_EXISTS", "state_path": str(cursor)}
|
|
156
|
+
if cursor.exists() else capture.initialize_baseline())
|
|
157
|
+
for name, capture, cursor in self._conversation_captures(store)}
|
|
158
|
+
result = {"status": "BASELINE_READY", "service_version": SERVICE_VERSION,
|
|
159
|
+
"at": self.now(), "coding": coding,
|
|
160
|
+
"conversation": conversation.get("codex"), "conversation_sources": conversation}
|
|
161
|
+
self._write_status(result)
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
def run_once(self, *, capture_code=True, capture_llm=True):
|
|
165
|
+
started = self.now()
|
|
166
|
+
coding, errors = [], []
|
|
167
|
+
if capture_code:
|
|
168
|
+
with CodingMemoryStore(self.db_path) as store:
|
|
169
|
+
for entry in self.config["code_workspaces"]:
|
|
170
|
+
try:
|
|
171
|
+
capture = self._coding_capture(entry, store)
|
|
172
|
+
if not capture.manifest_path.exists():
|
|
173
|
+
outcome = capture.initialize_baseline()
|
|
174
|
+
else:
|
|
175
|
+
outcome = capture.scan(
|
|
176
|
+
reason="Automatically observed workspace change; intent pending",
|
|
177
|
+
related_tests=[])
|
|
178
|
+
coding.append({"project_id": entry["project_id"], **outcome})
|
|
179
|
+
except Exception as error: # keep other capture sources alive
|
|
180
|
+
errors.append({"source": "code",
|
|
181
|
+
"project_id": entry.get("project_id"),
|
|
182
|
+
"error": type(error).__name__, "detail": str(error)})
|
|
183
|
+
else:
|
|
184
|
+
coding.append({"status": "SKIPPED_NOT_DUE"})
|
|
185
|
+
semantic = None
|
|
186
|
+
conversation = {}
|
|
187
|
+
try:
|
|
188
|
+
with ConversationMemoryStore(self.db_path) as store:
|
|
189
|
+
for name, capture, cursor in self._conversation_captures(store):
|
|
190
|
+
try: # one broken source must not stop the others
|
|
191
|
+
conversation[name] = (capture.scan() if cursor.exists()
|
|
192
|
+
else capture.initialize_baseline())
|
|
193
|
+
except Exception as error:
|
|
194
|
+
errors.append({"source": f"conversation:{name}",
|
|
195
|
+
"error": type(error).__name__, "detail": str(error)})
|
|
196
|
+
except Exception as error:
|
|
197
|
+
errors.append({"source": "conversation", "error": type(error).__name__,
|
|
198
|
+
"detail": str(error)})
|
|
199
|
+
try:
|
|
200
|
+
with SemanticMemoryIngestion(self.db_path, now=self.now) as ingestion:
|
|
201
|
+
semantic = ingestion.process_new(limit=200)
|
|
202
|
+
except Exception as error:
|
|
203
|
+
errors.append({"source": "semantic", "error": type(error).__name__,
|
|
204
|
+
"detail": str(error)})
|
|
205
|
+
semantic_llm = {"status": "DISABLED"}
|
|
206
|
+
llm_config = self.config.get("semantic_llm", {})
|
|
207
|
+
if llm_config.get("enabled") and not capture_llm:
|
|
208
|
+
semantic_llm = {"status": "SKIPPED_NOT_DUE"}
|
|
209
|
+
elif llm_config.get("enabled"):
|
|
210
|
+
try:
|
|
211
|
+
if llm_config.get("provider") != "openai":
|
|
212
|
+
raise ValueError("UNSUPPORTED_SEMANTIC_LLM_PROVIDER")
|
|
213
|
+
extractor = OpenAIResponsesSemanticExtractor(llm_config.get("model"))
|
|
214
|
+
with SemanticLLMIngestion(self.db_path, now=self.now) as ingestion:
|
|
215
|
+
semantic_llm = ingestion.process_new(
|
|
216
|
+
extractor, limit=int(llm_config.get("limit", 25)))
|
|
217
|
+
except Exception as error:
|
|
218
|
+
errors.append({"source": "semantic_llm",
|
|
219
|
+
"error": type(error).__name__, "detail": str(error)})
|
|
220
|
+
semantic_llm = {"status": "ERROR"}
|
|
221
|
+
associative = None
|
|
222
|
+
try:
|
|
223
|
+
with AssociativeMemoryIndex(self.db_path) as index:
|
|
224
|
+
associative = index.sync_sources(limit=1000)
|
|
225
|
+
except Exception as error:
|
|
226
|
+
errors.append({"source": "associative", "error": type(error).__name__,
|
|
227
|
+
"detail": str(error)})
|
|
228
|
+
result = {"status": "CAPTURE_ERRORS" if errors else "CAPTURE_OK",
|
|
229
|
+
"service_version": SERVICE_VERSION, "started_at": started,
|
|
230
|
+
"completed_at": self.now(), "coding": coding,
|
|
231
|
+
"conversation": conversation.get("codex"),
|
|
232
|
+
"conversation_sources": conversation, "semantic": semantic,
|
|
233
|
+
"semantic_llm": semantic_llm,
|
|
234
|
+
"associative": associative,
|
|
235
|
+
"errors": errors}
|
|
236
|
+
self._write_status(result)
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
def _write_status(self, result):
|
|
240
|
+
_atomic_json(self.status_path, {**result, "pid": os.getpid(),
|
|
241
|
+
"config_db": str(self.db_path)})
|
|
242
|
+
|
|
243
|
+
def embed_new_turns(self, limit=512):
|
|
244
|
+
"""Make what was just captured searchable by meaning before anyone asks about it (the
|
|
245
|
+
memory servers pick the vectors up from the shared cache). Only the long-running service
|
|
246
|
+
does this: a one-off `once` or a test should not load the embedding model."""
|
|
247
|
+
try:
|
|
248
|
+
from embedding_warmup import warm_turns
|
|
249
|
+
from semantic_recall import default_embedder
|
|
250
|
+
if getattr(self, "_embedder", None) is None:
|
|
251
|
+
self._embedder = default_embedder(self.db_path)
|
|
252
|
+
return warm_turns(self.db_path, limit=limit, embedder=self._embedder)
|
|
253
|
+
except Exception: # capture must go on if embedding cannot
|
|
254
|
+
return 0
|
|
255
|
+
|
|
256
|
+
def run_forever(self, *, max_iterations=None):
|
|
257
|
+
self.pid_path.write_text(str(os.getpid()) + "\n", encoding="ascii")
|
|
258
|
+
iteration = 0
|
|
259
|
+
last_code_scan = None
|
|
260
|
+
last_llm_scan = None
|
|
261
|
+
try:
|
|
262
|
+
while max_iterations is None or iteration < max_iterations:
|
|
263
|
+
monotonic_now = time.monotonic()
|
|
264
|
+
code_due = (last_code_scan is None or monotonic_now - last_code_scan
|
|
265
|
+
>= self.config.get("code_poll_seconds", 120))
|
|
266
|
+
llm_config = self.config.get("semantic_llm", {})
|
|
267
|
+
llm_due = (last_llm_scan is None or monotonic_now - last_llm_scan
|
|
268
|
+
>= llm_config.get("poll_seconds", 60))
|
|
269
|
+
self.run_once(capture_code=code_due, capture_llm=llm_due)
|
|
270
|
+
self.embed_new_turns()
|
|
271
|
+
if code_due:
|
|
272
|
+
last_code_scan = time.monotonic()
|
|
273
|
+
if llm_config.get("enabled") and llm_due:
|
|
274
|
+
last_llm_scan = time.monotonic()
|
|
275
|
+
iteration += 1
|
|
276
|
+
if max_iterations is None or iteration < max_iterations:
|
|
277
|
+
time.sleep(self.config["poll_seconds"])
|
|
278
|
+
finally:
|
|
279
|
+
try:
|
|
280
|
+
if self.pid_path.read_text(encoding="ascii").strip() == str(os.getpid()):
|
|
281
|
+
self.pid_path.unlink()
|
|
282
|
+
except (FileNotFoundError, OSError):
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def config_identity(config):
|
|
287
|
+
raw = json.dumps(config, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
288
|
+
return hashlib.sha256(raw).hexdigest()[:16]
|
automatic_capture_cli.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Command line control for the Graph-MIND automatic capture service."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from automatic_capture import (AutomaticCaptureService, default_config_path,
|
|
9
|
+
default_service_state_dir, load_config, make_config,
|
|
10
|
+
_atomic_json)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _emit(value):
|
|
14
|
+
print(json.dumps(value, ensure_ascii=False, indent=2))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(argv=None):
|
|
18
|
+
parser = argparse.ArgumentParser(description="Graph-MIND automatic capture v0.2")
|
|
19
|
+
parser.add_argument("--config", default=str(default_config_path()))
|
|
20
|
+
parser.add_argument("--state-dir", default=str(default_service_state_dir()))
|
|
21
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
22
|
+
configure = commands.add_parser("configure")
|
|
23
|
+
configure.add_argument("--workspace", required=True)
|
|
24
|
+
configure.add_argument("--project", required=True)
|
|
25
|
+
configure.add_argument("--session", required=True)
|
|
26
|
+
configure.add_argument("--sessions-root", required=True)
|
|
27
|
+
configure.add_argument("--db")
|
|
28
|
+
configure.add_argument("--poll-seconds", type=int, default=5)
|
|
29
|
+
configure.add_argument("--code-poll-seconds", type=int, default=120)
|
|
30
|
+
configure.add_argument("--enable-semantic-llm", action="store_true")
|
|
31
|
+
configure.add_argument("--semantic-model")
|
|
32
|
+
commands.add_parser("baseline")
|
|
33
|
+
commands.add_parser("once")
|
|
34
|
+
commands.add_parser("run")
|
|
35
|
+
commands.add_parser("status")
|
|
36
|
+
args = parser.parse_args(argv)
|
|
37
|
+
if args.command == "configure":
|
|
38
|
+
config = make_config(workspace=args.workspace, project_id=args.project,
|
|
39
|
+
session_id=args.session, sessions_root=args.sessions_root,
|
|
40
|
+
poll_seconds=args.poll_seconds, code_poll_seconds=args.code_poll_seconds,
|
|
41
|
+
db_path=args.db)
|
|
42
|
+
if args.enable_semantic_llm:
|
|
43
|
+
if not args.semantic_model:
|
|
44
|
+
parser.error("--semantic-model is required with --enable-semantic-llm")
|
|
45
|
+
config["semantic_llm"].update({"enabled": True,
|
|
46
|
+
"model": args.semantic_model})
|
|
47
|
+
_atomic_json(args.config, config)
|
|
48
|
+
_emit({"status": "CONFIGURED", "config": os.path.abspath(args.config),
|
|
49
|
+
"value": config})
|
|
50
|
+
return 0
|
|
51
|
+
if args.command == "status":
|
|
52
|
+
path = os.path.join(args.state_dir, "status.json")
|
|
53
|
+
if not os.path.isfile(path):
|
|
54
|
+
_emit({"status": "NOT_STARTED", "state": path})
|
|
55
|
+
else:
|
|
56
|
+
with open(path, encoding="utf-8") as handle:
|
|
57
|
+
_emit(json.load(handle))
|
|
58
|
+
return 0
|
|
59
|
+
service = AutomaticCaptureService(load_config(args.config), state_dir=args.state_dir)
|
|
60
|
+
if args.command == "baseline":
|
|
61
|
+
result = service.baseline()
|
|
62
|
+
elif args.command == "once":
|
|
63
|
+
result = service.run_once()
|
|
64
|
+
else:
|
|
65
|
+
service.run_forever()
|
|
66
|
+
return 0
|
|
67
|
+
_emit(result)
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
raise SystemExit(main())
|