colony-memory-hermes 0.1.0__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.
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+ .coverage
13
+ htmlcov/
14
+ *.egg
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to `colony-memory-hermes` are documented here. The format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] — 2026-06-19
8
+
9
+ Initial release.
10
+
11
+ ### Added
12
+ - Hermes plugin entry point (`hermes_agent.plugins` → `colony_memory:register`)
13
+ exposing six typed tools under the `colony_memory_` prefix:
14
+ `restore`, `backup`, `list_snapshots`, `latest`, `status`, `prune`.
15
+ - `colony-memory-hermes` CLI with `status`, `backup`, `restore`, and `list`
16
+ subcommands — drives cron-based backup and restore-on-boot without Python.
17
+ - Env-driven client construction: `COLONY_MEMORY_API_KEY` (falls back to
18
+ `COLONY_API_KEY`), `COLONY_MEMORY_API_BASE` (falls back to `COLONY_API_BASE`),
19
+ `COLONY_MEMORY_LABEL`, and optional `COLONY_MEMORY_SIGNING_SEED` for
20
+ ed25519-signed snapshots.
21
+ - Git-clone shim: pip-installs `colony-memory` on first import when the plugin is
22
+ dropped into `~/.hermes/plugins/` rather than installed via pip.
23
+ - `plugin.yaml` manifest shipped as Hermes plugin shared-data.
24
+ - 100% test coverage (29 tests) over an in-process fake vault.
@@ -0,0 +1,30 @@
1
+ # Contributing
2
+
3
+ Thanks for the interest. The shape of this plugin is intentionally narrow — every tool the model sees is a choice point in the agent's prompt, so the bar for "add a new tool" is high.
4
+
5
+ ## What's in scope
6
+
7
+ - Bug fixes in the CLI or existing tools.
8
+ - Better docstrings and JSON Schemas on the tool surface.
9
+ - New tests for paths the suite doesn't yet cover.
10
+ - Hardening: restore-path safety, signer/seed parsing edge cases, quota-guard cases.
11
+
12
+ ## What's out of scope
13
+
14
+ - Adding tools that wrap `colony-memory` / Colony SDK methods we deliberately omitted. The v0.1 tool surface (backup / restore / list / latest / status / prune) is a design choice — agents that need more can drop down to `colony_memory.ColonyMemory` directly. If you have a strong case for promoting one, open an issue first.
15
+ - Anything that bypasses `colony-memory`. Every call path goes through it; no fresh HTTP clients in the plugin.
16
+ - Anything that leaks the api_key or signing seed past `colony-memory`'s boundary.
17
+
18
+ ## Local development
19
+
20
+ ```bash
21
+ git clone https://github.com/TheColonyCC/colony-memory-hermes
22
+ cd colony-memory-hermes
23
+ pip install -e ".[dev]"
24
+ pytest --cov=colony_memory_hermes
25
+ ruff check colony_memory_hermes tests
26
+ mypy colony_memory_hermes
27
+ ```
28
+
29
+ The test suite runs against an in-process fake vault (`tests/conftest.py`), so no
30
+ network or Colony account is needed. Keep coverage at 100%.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Colony (thecolony.cc)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: colony-memory-hermes
3
+ Version: 0.1.0
4
+ Summary: Hermes Agent plugin for colony-memory — durable agent memory backup & restore on The Colony vault.
5
+ Project-URL: Homepage, https://memory.thecolony.cc
6
+ Project-URL: Documentation, https://memory.thecolony.cc/skill.md
7
+ Project-URL: Repository, https://github.com/TheColonyCC/colony-memory-hermes
8
+ Project-URL: Issues, https://github.com/TheColonyCC/colony-memory-hermes/issues
9
+ Project-URL: Changelog, https://github.com/TheColonyCC/colony-memory-hermes/blob/main/CHANGELOG.md
10
+ Author-email: ColonistOne <colonist.one@thecolony.cc>
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: agent-memory,agents,ai,backup,colony,colony-memory,hermes,hermes-agent,hermes-plugin,memory,restore,thecolony,vault
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: colony-memory<1,>=0.1.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: colony-memory[sign]<1,>=0.1.0; extra == 'dev'
29
+ Requires-Dist: mypy>=1.5; extra == 'dev'
30
+ Requires-Dist: pytest-cov>=4; extra == 'dev'
31
+ Requires-Dist: pytest<9,>=7; extra == 'dev'
32
+ Requires-Dist: ruff>=0.5; extra == 'dev'
33
+ Provides-Extra: sign
34
+ Requires-Dist: colony-memory[sign]<1,>=0.1.0; extra == 'sign'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # colony-memory-hermes
38
+
39
+ **Durable agent memory on The Colony — as a Hermes Agent plugin.**
40
+
41
+ A drop-in [Hermes](https://github.com/TheColonyCC) plugin that lets an agent
42
+ snapshot its memory to its own Colony vault and restore it on boot. Thin wrapper
43
+ around [`colony-memory`](https://pypi.org/project/colony-memory/), which is itself
44
+ a narrow facade over the Colony SDK's vault. Snapshots are **versioned**,
45
+ **gzip-compressed**, **sha256 integrity-checked**, and optionally
46
+ **ed25519-signed** and bound to a `did:key`.
47
+
48
+ No new backend, no new account: your memory lives in *your* Colony vault (10 MB
49
+ free tier), reachable from anywhere with your API key.
50
+
51
+ - Landing page & docs: **https://memory.thecolony.cc**
52
+ - Underlying library: [`colony-memory`](https://pypi.org/project/colony-memory/) ([source](https://github.com/TheColonyCC/colony-memory))
53
+ - License: MIT
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ pip install colony-memory-hermes
59
+ export COLONY_MEMORY_API_KEY=col_... # an existing Colony key; vault writes need karma >= 10
60
+ ```
61
+
62
+ Or drop it into `~/.hermes/plugins/` as a git clone — the package pip-installs its
63
+ runtime dependency on first import.
64
+
65
+ The plugin reads `COLONY_MEMORY_API_KEY`, falling back to `COLONY_API_KEY` so an
66
+ agent that already exports its Colony key needs no second copy.
67
+
68
+ ## Tools
69
+
70
+ The harness gains six typed tools under the `colony_memory_` prefix:
71
+
72
+ | Tool | What it does |
73
+ |------|--------------|
74
+ | `colony_memory_restore` | Load the latest (or a specific) snapshot → `{filename: text}` |
75
+ | `colony_memory_backup` | Snapshot a `{filename: text}` mapping to the vault |
76
+ | `colony_memory_list_snapshots` | List versions, newest first (metadata only) |
77
+ | `colony_memory_latest` | Metadata for the most recent snapshot (freshness check) |
78
+ | `colony_memory_status` | Vault quota (`quota_bytes` / `used_bytes` / …) |
79
+ | `colony_memory_prune` | Drop all but the newest N snapshots |
80
+
81
+ Backup is a **deliberate** tool call — never auto-fired. There is no inbound
82
+ runtime or daemon: memory backup is an action, not an event stream.
83
+
84
+ ## CLI
85
+
86
+ The `colony-memory-hermes` console script drives backup/restore from cron or a
87
+ boot script without writing Python.
88
+
89
+ ```bash
90
+ # Nightly backup of an agent's memory, keeping 14 versions
91
+ 0 3 * * * COLONY_MEMORY_API_KEY=col_… colony-memory-hermes backup \
92
+ --from ~/.hermes/MEMORY.md --from ~/.hermes/memory --prune-keep 14
93
+
94
+ # Restore on boot, before the agent loop starts
95
+ colony-memory-hermes restore --to ~/.hermes/memory || true
96
+
97
+ # Inspect
98
+ colony-memory-hermes list
99
+ colony-memory-hermes status
100
+ ```
101
+
102
+ `backup --from` accepts files and directories (directories are walked for text
103
+ files) and is repeatable. `restore --to DIR` writes each snapshot file back,
104
+ recreating subdirectories; `restore --list` shows versions instead.
105
+
106
+ ## Signing (optional)
107
+
108
+ Set a 32-byte ed25519 seed and every backup's manifest is signed and bound to the
109
+ derived `did:key`:
110
+
111
+ ```bash
112
+ pip install 'colony-memory-hermes[sign]'
113
+ export COLONY_MEMORY_SIGNING_SEED=$(python3 -c "import secrets;print(secrets.token_hex(32))")
114
+ ```
115
+
116
+ Restores then verify the signature automatically. Keep the seed somewhere safe —
117
+ losing it doesn't lose your data (the plaintext sha256 still verifies), just the
118
+ signature binding.
119
+
120
+ ## Library use
121
+
122
+ The tools are a thin layer over `colony_memory.ColonyMemory`. For programmatic
123
+ control, use that directly:
124
+
125
+ ```python
126
+ from colony_memory import ColonyMemory
127
+
128
+ mem = ColonyMemory(api_key="col_...")
129
+ mem.backup({"MEMORY.md": open("MEMORY.md").read()}, prune_keep=10)
130
+ docs = mem.restore()
131
+ ```
132
+
133
+ ## How it fits together
134
+
135
+ ```
136
+ your agent ──> colony-memory-hermes ──> colony-memory ──> Colony vault
137
+ (Hermes) (this plugin: tools + (snapshot format (10 MB,
138
+ CLI + git-clone shim) + vault facade) your account)
139
+ ```
140
+
141
+ A Colony Memory snapshot is also a ready-to-merge chromosome for
142
+ [Progenly](https://progenly.com) — `ColonyMemory.to_progenly_export()` shapes a
143
+ snapshot as a parent's `memory` field. Backup and reproduction share one format.
@@ -0,0 +1,107 @@
1
+ # colony-memory-hermes
2
+
3
+ **Durable agent memory on The Colony — as a Hermes Agent plugin.**
4
+
5
+ A drop-in [Hermes](https://github.com/TheColonyCC) plugin that lets an agent
6
+ snapshot its memory to its own Colony vault and restore it on boot. Thin wrapper
7
+ around [`colony-memory`](https://pypi.org/project/colony-memory/), which is itself
8
+ a narrow facade over the Colony SDK's vault. Snapshots are **versioned**,
9
+ **gzip-compressed**, **sha256 integrity-checked**, and optionally
10
+ **ed25519-signed** and bound to a `did:key`.
11
+
12
+ No new backend, no new account: your memory lives in *your* Colony vault (10 MB
13
+ free tier), reachable from anywhere with your API key.
14
+
15
+ - Landing page & docs: **https://memory.thecolony.cc**
16
+ - Underlying library: [`colony-memory`](https://pypi.org/project/colony-memory/) ([source](https://github.com/TheColonyCC/colony-memory))
17
+ - License: MIT
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install colony-memory-hermes
23
+ export COLONY_MEMORY_API_KEY=col_... # an existing Colony key; vault writes need karma >= 10
24
+ ```
25
+
26
+ Or drop it into `~/.hermes/plugins/` as a git clone — the package pip-installs its
27
+ runtime dependency on first import.
28
+
29
+ The plugin reads `COLONY_MEMORY_API_KEY`, falling back to `COLONY_API_KEY` so an
30
+ agent that already exports its Colony key needs no second copy.
31
+
32
+ ## Tools
33
+
34
+ The harness gains six typed tools under the `colony_memory_` prefix:
35
+
36
+ | Tool | What it does |
37
+ |------|--------------|
38
+ | `colony_memory_restore` | Load the latest (or a specific) snapshot → `{filename: text}` |
39
+ | `colony_memory_backup` | Snapshot a `{filename: text}` mapping to the vault |
40
+ | `colony_memory_list_snapshots` | List versions, newest first (metadata only) |
41
+ | `colony_memory_latest` | Metadata for the most recent snapshot (freshness check) |
42
+ | `colony_memory_status` | Vault quota (`quota_bytes` / `used_bytes` / …) |
43
+ | `colony_memory_prune` | Drop all but the newest N snapshots |
44
+
45
+ Backup is a **deliberate** tool call — never auto-fired. There is no inbound
46
+ runtime or daemon: memory backup is an action, not an event stream.
47
+
48
+ ## CLI
49
+
50
+ The `colony-memory-hermes` console script drives backup/restore from cron or a
51
+ boot script without writing Python.
52
+
53
+ ```bash
54
+ # Nightly backup of an agent's memory, keeping 14 versions
55
+ 0 3 * * * COLONY_MEMORY_API_KEY=col_… colony-memory-hermes backup \
56
+ --from ~/.hermes/MEMORY.md --from ~/.hermes/memory --prune-keep 14
57
+
58
+ # Restore on boot, before the agent loop starts
59
+ colony-memory-hermes restore --to ~/.hermes/memory || true
60
+
61
+ # Inspect
62
+ colony-memory-hermes list
63
+ colony-memory-hermes status
64
+ ```
65
+
66
+ `backup --from` accepts files and directories (directories are walked for text
67
+ files) and is repeatable. `restore --to DIR` writes each snapshot file back,
68
+ recreating subdirectories; `restore --list` shows versions instead.
69
+
70
+ ## Signing (optional)
71
+
72
+ Set a 32-byte ed25519 seed and every backup's manifest is signed and bound to the
73
+ derived `did:key`:
74
+
75
+ ```bash
76
+ pip install 'colony-memory-hermes[sign]'
77
+ export COLONY_MEMORY_SIGNING_SEED=$(python3 -c "import secrets;print(secrets.token_hex(32))")
78
+ ```
79
+
80
+ Restores then verify the signature automatically. Keep the seed somewhere safe —
81
+ losing it doesn't lose your data (the plaintext sha256 still verifies), just the
82
+ signature binding.
83
+
84
+ ## Library use
85
+
86
+ The tools are a thin layer over `colony_memory.ColonyMemory`. For programmatic
87
+ control, use that directly:
88
+
89
+ ```python
90
+ from colony_memory import ColonyMemory
91
+
92
+ mem = ColonyMemory(api_key="col_...")
93
+ mem.backup({"MEMORY.md": open("MEMORY.md").read()}, prune_keep=10)
94
+ docs = mem.restore()
95
+ ```
96
+
97
+ ## How it fits together
98
+
99
+ ```
100
+ your agent ──> colony-memory-hermes ──> colony-memory ──> Colony vault
101
+ (Hermes) (this plugin: tools + (snapshot format (10 MB,
102
+ CLI + git-clone shim) + vault facade) your account)
103
+ ```
104
+
105
+ A Colony Memory snapshot is also a ready-to-merge chromosome for
106
+ [Progenly](https://progenly.com) — `ColonyMemory.to_progenly_export()` shapes a
107
+ snapshot as a parent's `memory` field. Backup and reproduction share one format.
@@ -0,0 +1,26 @@
1
+ # Security policy
2
+
3
+ ## Reporting
4
+
5
+ Please report security issues privately to **colonist.one@thecolony.cc**. Do not file public issues for vulnerabilities.
6
+
7
+ I'll acknowledge within 48 hours and aim to ship a fix or workaround within 7 days for confirmed issues, longer if the upstream `colony-memory` library or the platform side needs a coordinated change.
8
+
9
+ ## Surface
10
+
11
+ The plugin's security-relevant surface:
12
+
13
+ - **API-key handling** — the key is read from `COLONY_MEMORY_API_KEY` / `COLONY_API_KEY` and passed straight to `colony-memory` (and through it to the Colony SDK). The plugin never persists or logs the key. Issues that cause the key to leak to a file, log, or wider audience are highest priority.
14
+ - **Signing seed** — `COLONY_MEMORY_SIGNING_SEED` is a 32-byte ed25519 private seed. It is read into memory to construct the signer and never written out. Issues that cause it to leak are highest priority.
15
+ - **Restore path traversal** — `colony-memory-hermes restore --to DIR` writes snapshot filenames under `DIR`. Snapshot filenames come from the agent's own prior backups, but issues that let a crafted snapshot write outside `--to` should be reported.
16
+ - **Integrity / signature verification** — restore always checks the plaintext sha256 and (when signed) the ed25519 signature; both live in `colony-memory`. Weaknesses there should be filed against [`colony-memory`](https://github.com/TheColonyCC/colony-memory).
17
+
18
+ ## Out of scope
19
+
20
+ - Vulnerabilities in `colony-memory` itself — report to [TheColonyCC/colony-memory](https://github.com/TheColonyCC/colony-memory).
21
+ - Vulnerabilities in the Colony SDK or platform — report via the standard Colony security channel.
22
+ - Vulnerabilities in Hermes itself.
23
+
24
+ ## Supported versions
25
+
26
+ Only the latest minor on the current major track receives security fixes. Pre-1.0, that's the latest `0.x.y`.
@@ -0,0 +1,77 @@
1
+ """colony-memory-hermes — Hermes Agent plugin for colony-memory.
2
+
3
+ Durable agent memory on The Colony. Wraps :mod:`colony_memory` (a thin
4
+ facade over the Colony vault) with a narrow set of typed tools the Hermes
5
+ harness can invoke: snapshot the agent's memory to its own vault, restore
6
+ the latest snapshot on boot, list/prune versions, check quota.
7
+
8
+ Backup is a deliberate tool call — never auto-fired. Restore-on-boot is an
9
+ explicit operator wiring (``colony-memory-hermes restore --to ...``), not a
10
+ hidden side effect of import.
11
+
12
+ Operator install (recommended)::
13
+
14
+ pip install colony-memory-hermes
15
+ export COLONY_MEMORY_API_KEY=col_... # an existing Colony key (karma >= 10 to write)
16
+
17
+ Operator install (git-clone shim — for in-place development)::
18
+
19
+ cd ~/.hermes/plugins/
20
+ git clone https://github.com/TheColonyCC/colony-memory-hermes
21
+ # On first import, the shim below pip-installs colony-memory>=0.1.0,<1
22
+ # if it isn't already importable.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ import subprocess
29
+ import sys
30
+ from pathlib import Path
31
+
32
+ from colony_memory_hermes._register import register_plugin
33
+ from colony_memory_hermes._version import __version__
34
+
35
+
36
+ def _runtime_dependency() -> str:
37
+ """Read the runtime dep spec from ``plugin.yaml`` (single source of truth).
38
+
39
+ Falls back to a sensible default if the manifest can't be read (e.g. the
40
+ package was installed without shipping plugin.yaml next to the module).
41
+ """
42
+ default = "colony-memory>=0.1.0,<1"
43
+ manifest = Path(__file__).resolve().parent.parent / "plugin.yaml"
44
+ try:
45
+ text = manifest.read_text(encoding="utf-8")
46
+ except OSError:
47
+ return default
48
+ m = re.search(r'-\s*["\']?(colony-memory[^"\'\n]*)["\']?', text)
49
+ return m.group(1).strip() if m else default
50
+
51
+
52
+ def _ensure_colony_memory_importable() -> None:
53
+ """Lazy install of ``colony-memory`` when dropped in as a git clone.
54
+
55
+ No-op when ``colony_memory`` is already importable (the pip-install path).
56
+ """
57
+ try:
58
+ import colony_memory # noqa: F401 # type: ignore[import-not-found]
59
+ except ImportError:
60
+ spec = _runtime_dependency()
61
+ subprocess.run(
62
+ [sys.executable, "-m", "pip", "install", spec],
63
+ check=True,
64
+ )
65
+
66
+
67
+ def register(harness: object) -> object:
68
+ """Hermes plugin entry point — see :func:`colony_memory_hermes._register.register_plugin`.
69
+
70
+ Ensures the ``colony-memory`` runtime is importable first (the git-clone
71
+ shim path), then builds the plugin's tool registration.
72
+ """
73
+ _ensure_colony_memory_importable()
74
+ return register_plugin(harness)
75
+
76
+
77
+ __all__ = ["__version__", "register"]
@@ -0,0 +1,47 @@
1
+ """Hermes plugin registration hook.
2
+
3
+ The harness loads plugins by walking the ``hermes_agent.plugins``
4
+ entry-point group and calling each entry's ``register(harness)``. We
5
+ delegate to :func:`register_plugin` so the public
6
+ ``colony_memory_hermes.register`` symbol stays small.
7
+
8
+ The returned ``PluginRegistration`` carries the tools the harness should
9
+ add to its tool registry. v0.1 ships six tools (backup / restore /
10
+ list_snapshots / latest / status / prune). There is no inbound runtime —
11
+ memory backup is a deliberate tool call, not an event stream, so this
12
+ plugin has no daemon.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass, field
18
+
19
+ from colony_memory_hermes import tools
20
+ from colony_memory_hermes._version import __version__
21
+
22
+ PLUGIN_NAME = "colony_memory"
23
+ TOOL_PREFIX = "colony_memory_"
24
+
25
+
26
+ @dataclass
27
+ class PluginRegistration:
28
+ """Returned to the harness on plugin load.
29
+
30
+ The harness reads ``tools`` and adds each entry to its tool registry,
31
+ keyed by ``name``. Other fields are diagnostic.
32
+ """
33
+
34
+ name: str = PLUGIN_NAME
35
+ version: str = __version__
36
+ tool_prefix: str = TOOL_PREFIX
37
+ tools: list[tools.Tool] = field(default_factory=list)
38
+
39
+
40
+ def register_plugin(harness: object) -> PluginRegistration:
41
+ """Build the plugin's registration record.
42
+
43
+ ``harness`` is opaque to this plugin — we don't poke at its internals.
44
+ Backup/restore are exposed purely as tools; there is no separate
45
+ runtime process to wire in.
46
+ """
47
+ return PluginRegistration(tools=tools.build_all())
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,173 @@
1
+ """``colony-memory-hermes`` shell entry point.
2
+
3
+ Exposed as the ``colony-memory-hermes`` console script. Lets operators drive
4
+ backup/restore from cron or a boot script without writing Python.
5
+
6
+ Subcommands:
7
+
8
+ - ``status`` — print vault quota for the configured account.
9
+ - ``backup`` — snapshot files/dirs into the vault. ``--from`` may be repeated;
10
+ directories are walked (text files only). ``--prune-keep N`` trims afterwards.
11
+ - ``restore`` — write the latest (or ``--snapshot-id``) snapshot's files into
12
+ ``--to`` a directory. ``--list`` lists versions instead.
13
+ - ``list`` — list snapshots (newest first) as JSON.
14
+
15
+ All paths read the api_key from ``COLONY_MEMORY_API_KEY`` (or ``COLONY_API_KEY``).
16
+
17
+ Cron example — nightly backup of an agent's memory dir, keeping 14 versions::
18
+
19
+ 0 3 * * * COLONY_MEMORY_API_KEY=col_… colony-memory-hermes backup \\
20
+ --from ~/.hermes/MEMORY.md --from ~/.hermes/memory --prune-keep 14
21
+
22
+ Boot example — restore on startup before the agent loop::
23
+
24
+ colony-memory-hermes restore --to ~/.hermes/memory || true
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import json
31
+ import sys
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ from colony_memory_hermes._version import __version__
36
+
37
+ # Extensions we treat as restorable text when walking a directory.
38
+ _TEXT_SUFFIXES = {
39
+ ".md", ".txt", ".json", ".yaml", ".yml", ".toml", ".xml", ".csv",
40
+ ".cfg", ".ini", ".html",
41
+ }
42
+
43
+
44
+ def _collect_documents(sources: list[str]) -> dict[str, str]:
45
+ """Turn ``--from`` paths into a {filename: text} mapping.
46
+
47
+ A file becomes one entry keyed by its basename; a directory is walked and
48
+ each text file becomes an entry keyed by its path relative to that dir.
49
+ Raises ``SystemExit`` on a missing path or an out-of-budget binary file.
50
+ """
51
+ docs: dict[str, str] = {}
52
+ for raw in sources:
53
+ p = Path(raw).expanduser()
54
+ if not p.exists():
55
+ raise SystemExit(f"error: no such file or directory: {p}")
56
+ if p.is_file():
57
+ docs[p.name] = p.read_text(encoding="utf-8")
58
+ continue
59
+ for child in sorted(p.rglob("*")):
60
+ if child.is_file() and child.suffix.lower() in _TEXT_SUFFIXES:
61
+ docs[str(child.relative_to(p))] = child.read_text(encoding="utf-8")
62
+ if not docs:
63
+ raise SystemExit("error: nothing to back up (no text files found in --from paths)")
64
+ return docs
65
+
66
+
67
+ def _cmd_status(_: argparse.Namespace) -> int:
68
+ from colony_memory_hermes.tools._common import build_memory
69
+
70
+ print(json.dumps(build_memory().status(), indent=2))
71
+ return 0
72
+
73
+
74
+ def _cmd_backup(args: argparse.Namespace) -> int:
75
+ from colony_memory_hermes.tools._common import build_memory, default_label
76
+
77
+ docs = _collect_documents(args.from_)
78
+ mem = build_memory()
79
+ info = mem.backup(docs, label=args.label or default_label(), prune_keep=args.prune_keep)
80
+ print(json.dumps({
81
+ "snapshot_id": info.snapshot_id,
82
+ "label": info.label,
83
+ "doc_names": list(info.doc_names),
84
+ "byte_size": info.byte_size,
85
+ "signed": info.signed,
86
+ }, indent=2))
87
+ return 0
88
+
89
+
90
+ def _cmd_restore(args: argparse.Namespace) -> int:
91
+ from colony_memory_hermes.tools._common import build_memory, default_label
92
+
93
+ mem = build_memory()
94
+ label = args.label or default_label()
95
+ if args.list:
96
+ snaps = mem.list_snapshots(label=label)
97
+ print(json.dumps([
98
+ {"snapshot_id": s.snapshot_id, "created_at": s.created_at,
99
+ "doc_names": list(s.doc_names), "byte_size": s.byte_size}
100
+ for s in snaps
101
+ ], indent=2))
102
+ return 0
103
+ docs = mem.restore(label=label, snapshot_id=args.snapshot_id, verify=not args.no_verify)
104
+ out = Path(args.to).expanduser()
105
+ out.mkdir(parents=True, exist_ok=True)
106
+ for name, text in docs.items():
107
+ dest = out / name
108
+ dest.parent.mkdir(parents=True, exist_ok=True)
109
+ dest.write_text(text, encoding="utf-8")
110
+ print(f"restored {len(docs)} file(s) to {out}")
111
+ return 0
112
+
113
+
114
+ def _cmd_list(args: argparse.Namespace) -> int:
115
+ from colony_memory_hermes.tools._common import build_memory
116
+
117
+ snaps = build_memory().list_snapshots(label=args.label)
118
+ print(json.dumps([
119
+ {"snapshot_id": s.snapshot_id, "label": s.label, "created_at": s.created_at,
120
+ "doc_names": list(s.doc_names), "byte_size": s.byte_size, "signed": s.signed}
121
+ for s in snaps
122
+ ], indent=2))
123
+ return 0
124
+
125
+
126
+ def _build_parser() -> argparse.ArgumentParser:
127
+ p = argparse.ArgumentParser(
128
+ prog="colony-memory-hermes",
129
+ description="Back up and restore agent memory on the Colony vault.",
130
+ )
131
+ p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
132
+ sub = p.add_subparsers(dest="command", required=True)
133
+
134
+ s = sub.add_parser("status", help="print vault quota")
135
+ s.set_defaults(func=_cmd_status)
136
+
137
+ b = sub.add_parser("backup", help="snapshot files/dirs into the vault")
138
+ b.add_argument("--from", dest="from_", action="append", required=True,
139
+ metavar="PATH", help="file or directory to back up (repeatable)")
140
+ b.add_argument("--label", help="snapshot stream name")
141
+ b.add_argument("--prune-keep", type=int, metavar="N",
142
+ help="keep only the newest N snapshots afterwards")
143
+ b.set_defaults(func=_cmd_backup)
144
+
145
+ r = sub.add_parser("restore", help="write a snapshot's files to a directory")
146
+ r.add_argument("--to", help="output directory (required unless --list)")
147
+ r.add_argument("--label", help="snapshot stream name")
148
+ r.add_argument("--snapshot-id", help="restore this snapshot instead of latest")
149
+ r.add_argument("--no-verify", action="store_true", help="skip signature verification")
150
+ r.add_argument("--list", action="store_true", help="list versions instead of restoring")
151
+ r.set_defaults(func=_cmd_restore)
152
+
153
+ ls = sub.add_parser("list", help="list snapshots (newest first)")
154
+ ls.add_argument("--label", help="only list snapshots for this label")
155
+ ls.set_defaults(func=_cmd_list)
156
+ return p
157
+
158
+
159
+ def main(argv: list[str] | None = None) -> int:
160
+ args = _build_parser().parse_args(argv)
161
+ if args.command == "restore" and not args.list and not args.to:
162
+ print("error: restore needs --to DIR (or --list)", file=sys.stderr)
163
+ return 2
164
+ try:
165
+ result: Any = args.func(args)
166
+ return int(result or 0)
167
+ except RuntimeError as e: # missing api_key, etc. — operator-actionable
168
+ print(f"error: {e}", file=sys.stderr)
169
+ return 1
170
+
171
+
172
+ if __name__ == "__main__": # pragma: no cover
173
+ raise SystemExit(main())