optio-opencode 0.1.4__tar.gz → 0.1.6__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.
Files changed (35) hide show
  1. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/PKG-INFO +1 -1
  2. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/pyproject.toml +1 -1
  3. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/src/optio_opencode/__init__.py +12 -0
  4. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/src/optio_opencode/host_actions.py +78 -51
  5. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/src/optio_opencode/prompt.py +9 -5
  6. optio_opencode-0.1.6/src/optio_opencode/seed_manifest.py +61 -0
  7. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/src/optio_opencode/session.py +244 -1
  8. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/src/optio_opencode/types.py +21 -7
  9. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/src/optio_opencode.egg-info/PKG-INFO +1 -1
  10. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/src/optio_opencode.egg-info/SOURCES.txt +5 -0
  11. optio_opencode-0.1.6/tests/test_host_actions.py +196 -0
  12. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_prompt.py +11 -0
  13. optio_opencode-0.1.6/tests/test_purge_seed.py +56 -0
  14. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_sanity.py +20 -0
  15. optio_opencode-0.1.6/tests/test_seed_config.py +76 -0
  16. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_session_hooks.py +1 -1
  17. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_session_local.py +2 -1
  18. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_session_resume.py +2 -1
  19. optio_opencode-0.1.6/tests/test_session_seed.py +491 -0
  20. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_smart_install.py +24 -12
  21. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/README.md +0 -0
  22. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/setup.cfg +0 -0
  23. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/src/optio_opencode/snapshots.py +0 -0
  24. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/src/optio_opencode.egg-info/dependency_links.txt +0 -0
  25. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/src/optio_opencode.egg-info/requires.txt +0 -0
  26. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/src/optio_opencode.egg-info/top_level.txt +0 -0
  27. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_host_local.py +0 -0
  28. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_host_primitives_local.py +0 -0
  29. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_host_primitives_remote.py +0 -0
  30. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_host_remote_resume.py +0 -0
  31. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_host_resume.py +0 -0
  32. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_session_blob_hooks.py +0 -0
  33. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_session_remote.py +0 -0
  34. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_snapshots.py +0 -0
  35. {optio_opencode-0.1.4 → optio_opencode-0.1.6}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optio-opencode
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Run opencode web as an optio task; local subprocess or remote via SSH.
5
5
  Author-email: Kristof Csillag <kristof.csillag@deai-labs.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "optio-opencode"
7
- version = "0.1.4"
7
+ version = "0.1.6"
8
8
  description = "Run opencode web as an optio task; local subprocess or remote via SSH."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -14,6 +14,13 @@ from optio_opencode.types import (
14
14
  HookCallback,
15
15
  OpencodeTaskConfig,
16
16
  )
17
+ from optio_opencode.seed_manifest import (
18
+ OPENCODE_SEED_MANIFEST,
19
+ OPENCODE_SEED_SUFFIX,
20
+ delete_seed,
21
+ list_seeds,
22
+ purge_seed,
23
+ )
17
24
 
18
25
  # asyncssh emits per-connection / per-channel INFO lines ("Opening SSH
19
26
  # connection...", "Received channel close", etc.) that flood the worker's
@@ -34,4 +41,9 @@ __all__ = [
34
41
  "HostCommandError",
35
42
  "RunResult",
36
43
  "HookCallback",
44
+ "OPENCODE_SEED_MANIFEST",
45
+ "OPENCODE_SEED_SUFFIX",
46
+ "delete_seed",
47
+ "list_seeds",
48
+ "purge_seed",
37
49
  ]
@@ -9,8 +9,10 @@ Install path is uniform: ``ensure_opencode_installed`` drives
9
9
  csillag/opencode's ``smart-install.sh --check`` and, when needed, downloads
10
10
  the release zip as an optio child task (`HookContext.download_file`),
11
11
  unpacks it on the host, and places the binary at
12
- ``<install_dir>/opencode``. ``install_dir`` defaults to
13
- ``~/.local/bin`` (resolved per host) and is overridable via the
12
+ ``<install_dir>/opencode``. ``install_dir`` defaults to the optio-owned
13
+ binary cache on the worker
14
+ (``OPENCODE_CACHE_DIR`` / ``${XDG_CACHE_HOME:-$HOME/.cache}/optio-opencode/bin``,
15
+ resolved per host — never the host home bin dir) and is overridable via the
14
16
  ``install_dir`` keyword argument on the public entry points; consumers
15
17
  expose this as ``OpencodeTaskConfig.opencode_install_dir``.
16
18
  No isinstance branches.
@@ -36,23 +38,53 @@ _SMART_INSTALL_URL = (
36
38
  "https://raw.githubusercontent.com/csillag/opencode/main/smart-install.sh"
37
39
  )
38
40
 
39
- # Sub-path of the host's $HOME used as the default opencode install
40
- # directory when no explicit ``install_dir`` is supplied. Kept as a
41
- # constant so the three places that care about it (smart-install PATH
42
- # augmentation, post-ok ``command -v`` lookup, ``_install_opencode_from_zip``
43
- # install target) stay in agreement.
44
- DEFAULT_INSTALL_SUBDIR = ".local/bin"
41
+ # The optio-owned opencode binary cache lives on the WORKER, never in the host
42
+ # user's home bin dir. Default cache:
43
+ # ``${XDG_CACHE_HOME:-$HOME/.cache}/optio-opencode/bin``, overridable via the
44
+ # ``OPENCODE_CACHE_DIR`` env var on the worker. Kept as a constant so the places
45
+ # that care about it (smart-install PATH augmentation, post-ok ``command -v``
46
+ # lookup, ``_install_opencode_from_zip`` install target) stay in agreement.
47
+ _OPENCODE_CACHE_SHELL_DEFAULT = (
48
+ '${OPENCODE_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/optio-opencode/bin}'
49
+ )
45
50
 
46
51
 
47
52
  async def _resolve_install_dir(host: "Host", install_dir: str | None) -> str:
48
- """Return ``install_dir`` if given, else the host's default install dir.
53
+ """Resolve the opencode binary-cache dir as an absolute path on the worker.
49
54
 
50
- Default: ``<host_home>/<DEFAULT_INSTALL_SUBDIR>``.
51
- """
55
+ ``install_dir`` (config.opencode_install_dir) overrides. Else the worker's
56
+ OPENCODE_CACHE_DIR / XDG_CACHE_HOME / $HOME decide it — resolved via a shell
57
+ echo so RemoteHost gets the remote cache. Resolved from the worker's REAL env
58
+ (this runs before per-task XDG isolation), so the cache stays shared and
59
+ outside any workdir → never snapshotted; evictable → smart-install re-downloads."""
52
60
  if install_dir is not None:
53
- return install_dir
54
- host_home = await host.resolve_host_home()
55
- return f"{host_home}/{DEFAULT_INSTALL_SUBDIR}"
61
+ return install_dir.rstrip("/")
62
+ r = await host.run_command(f'printf %s "{_OPENCODE_CACHE_SHELL_DEFAULT}"')
63
+ path = r.stdout.strip()
64
+ if r.exit_code != 0 or not path:
65
+ raise RuntimeError(
66
+ f"failed to resolve opencode cache dir on host (exit {r.exit_code}): "
67
+ f"{r.stderr.strip()[:200]}"
68
+ )
69
+ return path.rstrip("/")
70
+
71
+
72
+ def _isolation_env(host: "Host") -> dict[str, str]:
73
+ """Per-task HOME/XDG isolation env, derived from ``host.workdir``.
74
+
75
+ Merged into the launch env AND the export/import env so opencode's
76
+ auth/config/data go per-task under ``<workdir>/home`` (where the seed
77
+ manifest's ``home_subdir`` and merge/capture target). Distinct from the
78
+ binary cache (``_resolve_install_dir``), which is shared and resolved from
79
+ the worker's REAL env before this isolation applies.
80
+ """
81
+ home = f"{host.workdir.rstrip('/')}/home"
82
+ return {
83
+ "HOME": home,
84
+ "XDG_CONFIG_HOME": f"{home}/.config",
85
+ "XDG_DATA_HOME": f"{home}/.local/share",
86
+ "XDG_CACHE_HOME": f"{home}/.cache",
87
+ }
56
88
 
57
89
 
58
90
  def _path_augmented(cmd: str, install_dir: str) -> str:
@@ -61,9 +93,9 @@ def _path_augmented(cmd: str, install_dir: str) -> str:
61
93
  Used so smart-install's internal ``command -v opencode`` and the
62
94
  post-"ok" lookup find the binary at the install location even when
63
95
  the calling shell's PATH doesn't already include it (common: the
64
- python process inherits a slimmed-down PATH that doesn't add
65
- ``~/.local/bin``, so smart-install would falsely report "download"
66
- and we'd reinstall on every task run).
96
+ python process inherits a slimmed-down PATH that doesn't add the
97
+ optio-owned opencode binary cache dir, so smart-install would falsely
98
+ report "download" and we'd reinstall on every task run).
67
99
  """
68
100
  return f'export PATH={shlex.quote(install_dir)}:"$PATH"; {cmd}'
69
101
 
@@ -80,8 +112,9 @@ async def _smart_install_check(
80
112
 
81
113
  ``install_dir`` is prepended to PATH inside the remote shell so
82
114
  smart-install's internal ``command -v opencode`` can see binaries
83
- installed by a prior ``_install_opencode_from_zip``. Defaults to
84
- ``~/.local/bin`` on the host.
115
+ installed by a prior ``_install_opencode_from_zip``. Defaults to the
116
+ optio-owned opencode binary cache on the worker (see
117
+ ``_resolve_install_dir``).
85
118
 
86
119
  Raises RuntimeError on non-zero exit or unparseable output.
87
120
  """
@@ -124,7 +157,8 @@ async def _install_opencode_from_zip(
124
157
  4. mkdir -p ``install_dir``; move binary there; chmod +x.
125
158
  5. Remove the tempdir.
126
159
 
127
- ``install_dir`` defaults to ``~/.local/bin`` on the host when None.
160
+ ``install_dir`` defaults to the optio-owned opencode binary cache on
161
+ the worker when None (see ``_resolve_install_dir``).
128
162
 
129
163
  Returns the absolute install path on the host.
130
164
  """
@@ -194,7 +228,9 @@ async def ensure_opencode_installed(
194
228
 
195
229
  ``install_dir`` is the absolute path of the directory that holds (or
196
230
  will hold) the ``opencode`` binary on the host. When None (default),
197
- resolves to ``<host_home>/.local/bin``. Pass an explicit absolute path
231
+ resolves to the optio-owned binary cache on the worker
232
+ (``OPENCODE_CACHE_DIR`` / ``${XDG_CACHE_HOME:-$HOME/.cache}/optio-opencode/bin``;
233
+ see ``_resolve_install_dir``). Pass an explicit absolute path
198
234
  to opt out of the default — the same dir is used for installation, for
199
235
  smart-install's PATH lookup, and for the post-"ok" ``command -v``
200
236
  resolution, so all three stay in agreement.
@@ -278,7 +314,7 @@ async def opencode_import(
278
314
  try:
279
315
  result = await host.run_command(
280
316
  f"bash -lc {shlex.quote(opencode_executable + ' import ' + shlex.quote(scratch))}",
281
- env={"OPENCODE_DB": opencode_db_path},
317
+ env={**_isolation_env(host), "OPENCODE_DB": opencode_db_path},
282
318
  )
283
319
  if result.exit_code != 0:
284
320
  raise RuntimeError(
@@ -310,7 +346,7 @@ async def opencode_export(
310
346
  result = await host.run_command(
311
347
  f"bash -lc "
312
348
  f"{shlex.quote(opencode_executable + ' export ' + shlex.quote(session_id) + ' > ' + shlex.quote(scratch))}",
313
- env={"OPENCODE_DB": opencode_db_path},
349
+ env={**_isolation_env(host), "OPENCODE_DB": opencode_db_path},
314
350
  )
315
351
  if result.exit_code != 0:
316
352
  raise RuntimeError(
@@ -329,6 +365,7 @@ async def launch_opencode(
329
365
  ready_timeout_s: float = 30.0,
330
366
  opencode_executable: str = "opencode",
331
367
  hostname: str = "127.0.0.1",
368
+ extra_env: dict[str, str] | None = None,
332
369
  ) -> tuple[ProcessHandle, int]:
333
370
  """Launch ``opencode web`` on ``host``; wait for the listening URL.
334
371
 
@@ -336,10 +373,10 @@ async def launch_opencode(
336
373
  and references it via ``$(cat ...)`` in the launch command so the
337
374
  literal value never appears on the remote process's argv.
338
375
 
339
- Lays down no-op browser-opener stubs (xdg-open, gio, open,
340
- sensible-browser) under ``<workdir>/bin`` and prepends that
341
- directory to PATH so opencode's automatic browser-launch is
342
- suppressed.
376
+ Browser-open suppression is handled by the optio-agents protocol
377
+ driver (``get_protocol(browser="suppress")``), which installs no-op
378
+ opener stubs under ``<workdir>/bin`` and returns the ``BROWSER`` /
379
+ ``PATH`` env additions; the caller passes those in via ``extra_env``.
343
380
 
344
381
  ``hostname`` is passed to ``opencode web --hostname=`` so callers
345
382
  can bind to a non-loopback interface when consumers reach the server
@@ -354,48 +391,38 @@ async def launch_opencode(
354
391
  await host.write_text(pw_file, password)
355
392
  await host.run_command(f"chmod 600 {host.workdir}/{pw_file}")
356
393
 
357
- # Browser-suppression bin shadow.
358
- for noop in ("xdg-open", "gio", "open", "sensible-browser"):
359
- await host.write_text(f"bin/{noop}", "#!/bin/sh\nexit 0\n")
360
- chmod_result = await host.run_command(f"chmod +x {host.workdir}/bin/*")
361
- if chmod_result.exit_code != 0:
362
- # Non-fatal: the noop scripts may fail to be executable, but worst
363
- # case opencode tries to open a browser and we just live with it.
364
- pass
365
-
366
- # Build cmd: read password from file via $(cat), set BROWSER=true,
367
- # cd to workdir so opencode picks up opencode.json.
394
+ # Build cmd: read password from file via $(cat), cd to workdir so
395
+ # opencode picks up opencode.json. Browser suppression (the BROWSER
396
+ # env + the <workdir>/bin PATH prepend that shadows the openers) comes
397
+ # from the protocol driver's suppress shims, passed in via extra_env.
368
398
  #
369
399
  # NOTE: do NOT wrap in `bash -lc` / `bash -l`. A login shell sources
370
400
  # the user's profile (~/.profile, ~/.bash_profile, /etc/profile),
371
401
  # which on most Linux installs rewrites PATH from scratch and
372
- # therefore wipes the workdir/bin prefix we set in `env` below. With
373
- # the prefix gone, the noop xdg-open / sensible-browser / gio / open
374
- # shadows below stop hiding the real ones and opencode succeeds at
375
- # opening a real browser window. opencode_executable is an absolute
376
- # path (resolved by ensure_opencode_installed), so login-shell PATH
377
- # lookup is not needed to find the binary. Let LocalHost / RemoteHost
378
- # launch_subprocess do the shell wrapping; we just need the env-var
379
- # prefix and $(cat ...) substitution, which any POSIX sh handles.
402
+ # therefore wipes the workdir/bin prefix carried in `env` below. With
403
+ # the prefix gone, the suppress stubs stop hiding the real openers and
404
+ # opencode succeeds at opening a real browser window. opencode_executable
405
+ # is an absolute path (resolved by ensure_opencode_installed), so
406
+ # login-shell PATH lookup is not needed to find the binary.
380
407
  cmd = (
381
408
  f"exec env "
382
409
  f"OPENCODE_SERVER_PASSWORD=\"$(cat {shlex.quote(host.workdir + '/' + pw_file)})\" "
383
- f"BROWSER=true "
384
410
  f"{opencode_executable} web --port=0 --hostname={shlex.quote(hostname)}"
385
411
  )
386
412
 
387
- # Prepend the noop-browsers bin dir to PATH via env on launch_subprocess.
388
- workdir_bin = f"{host.workdir}/bin"
389
- extra_path = workdir_bin + ":" + os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin")
390
413
  # OPENCODE_DB must point at the same per-task db file used by the
391
414
  # subsequent export/import CLI calls. Without this, the server falls
392
415
  # back to opencode's global default db while export/import target the
393
416
  # taskdir-local file — causing snapshot capture to "Session not found"
394
417
  # against an empty file. Convention: opencode.db is a sibling of the
395
418
  # workdir under taskdir (session.py: opencode_db = f"{taskdir}/opencode.db").
419
+ # The browser-suppression env (PATH prepend + BROWSER) comes from extra_env.
420
+ # The HOME/XDG isolation env (from _isolation_env) points opencode's
421
+ # auth/config/data at <workdir>/home so per-task seeding works.
396
422
  env = {
397
- "PATH": extra_path,
423
+ **_isolation_env(host),
398
424
  "OPENCODE_DB": f"{host.taskdir}/opencode.db",
425
+ **(extra_env or {}),
399
426
  }
400
427
 
401
428
  handle = await host.launch_subprocess(cmd, env=env, cwd=host.workdir)
@@ -7,7 +7,7 @@ addressed. The consumer's own task description is then appended verbatim.
7
7
  """
8
8
 
9
9
 
10
- from optio_agents.protocol.prompt import LOG_CHANNEL_PROMPT
10
+ from optio_agents.protocol import build_log_channel_prompt
11
11
 
12
12
 
13
13
  _OPENCODE_INTRO = """# Coordination protocol with the host (optio-opencode)
@@ -18,9 +18,6 @@ throughout the session.
18
18
  """
19
19
 
20
20
 
21
- BASE_PROMPT_PRE = _OPENCODE_INTRO + LOG_CHANNEL_PROMPT
22
-
23
-
24
21
  BASE_PROMPT_POST = """## Task
25
22
 
26
23
  Here comes the description of your actual task to complete. Throughout
@@ -120,6 +117,7 @@ def compose_agents_md(
120
117
  consumer_instructions: str,
121
118
  *,
122
119
  workdir_exclude: list[str] | None,
120
+ documentation: str | None = None,
123
121
  supports_resume: bool = True,
124
122
  ) -> str:
125
123
  """Build the full AGENTS.md body.
@@ -130,12 +128,18 @@ def compose_agents_md(
130
128
  callers must pass it explicitly to prevent silent desync between
131
129
  archive.py defaults and the prompt's claims about what's preserved.
132
130
  Pass None to render with the framework defaults.
131
+ documentation: the keyword-protocol block; the session passes
132
+ ``get_protocol(browser="suppress").documentation``. Defaults (for
133
+ unit tests / standalone callers) to opencode's ``suppress`` docs.
133
134
  supports_resume: when False, the resume-detection section is omitted
134
135
  from the prompt. Default True.
135
136
  """
137
+ if documentation is None:
138
+ documentation = build_log_channel_prompt("suppress")
139
+ base_prompt_pre = _OPENCODE_INTRO + documentation
136
140
  body = consumer_instructions.rstrip()
137
141
  if supports_resume:
138
142
  resume_block = _render_resume_section(workdir_exclude) + "\n"
139
143
  else:
140
144
  resume_block = ""
141
- return f"{BASE_PROMPT_PRE}\n{resume_block}{BASE_PROMPT_POST}\n{body}\n"
145
+ return f"{base_prompt_pre}\n{resume_block}{BASE_PROMPT_POST}\n{body}\n"
@@ -0,0 +1,61 @@
1
+ """opencode adopter of the generic optio-agents seed engine.
2
+
3
+ Defines the opencode seed manifest (HOME layout + capture-time include
4
+ triage), the Mongo collection suffix, and ergonomic `delete_seed` /
5
+ `list_seeds` / `purge_seed` wrappers that bind the suffix for consuming
6
+ apps.
7
+
8
+ Unlike claudecode, opencode needs no consume-time rekey: its auth/config
9
+ are cwd-independent, so `consume_transform` is None.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from optio_agents import seeds
15
+
16
+ OPENCODE_SEED_SUFFIX = "_opencode_seeds"
17
+ OPENCODE_SEED_MANIFEST_VERSION = 1
18
+
19
+
20
+ OPENCODE_SEED_MANIFEST = seeds.SeedManifest(
21
+ home_subdir="home",
22
+ include=[
23
+ ".local/share/opencode/auth.json",
24
+ ".config/opencode/opencode.json",
25
+ ".config/opencode/plugins",
26
+ ],
27
+ version=OPENCODE_SEED_MANIFEST_VERSION,
28
+ consume_transform=None, # no cwd-rekey for opencode
29
+ )
30
+
31
+
32
+ async def delete_seed(store, seed_id: str):
33
+ """Delete an opencode seed doc; returns its GridFS blobId (or None).
34
+
35
+ Takes an optio store binding (``optio.store`` — exposes ``db`` and
36
+ ``prefix``) as-is, so consuming apps hand over the whole namespace handle
37
+ instead of threading db+prefix (or knowing the collection suffix). The
38
+ caller still removes the returned blob from GridFS.
39
+ """
40
+ return await seeds.delete_seed(
41
+ store.db, prefix=store.prefix, suffix=OPENCODE_SEED_SUFFIX, seed_id=seed_id,
42
+ )
43
+
44
+
45
+ async def list_seeds(store) -> list[dict]:
46
+ """List opencode seeds as [{seedId, createdAt}, ...]. Takes an optio store
47
+ binding (``optio.store``) as-is."""
48
+ return await seeds.list_seeds(store.db, prefix=store.prefix, suffix=OPENCODE_SEED_SUFFIX)
49
+
50
+
51
+ async def purge_seed(store, seed_id: str):
52
+ """Purge an opencode seed (doc + its GridFS blob) in one call.
53
+
54
+ Takes an optio store binding (``optio.store``) as-is, per the Shared-
55
+ contracts surface. Mirrors `optio_claudecode.purge_seed`; both are thin
56
+ re-exports of the `optio_agents.seeds.purge_seed` engine, which expunges
57
+ the seed doc and its GridFS blob and raises KeyError if absent.
58
+ """
59
+ return await seeds.purge_seed(
60
+ store.db, prefix=store.prefix, suffix=OPENCODE_SEED_SUFFIX, seed_id=seed_id,
61
+ )