bookmark-research 0.5.0-beta.1
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.
- package/.agents/plugins/marketplace.json +20 -0
- package/.codex-plugin/plugin.json +46 -0
- package/LICENSE +674 -0
- package/README.md +128 -0
- package/README.zh.md +126 -0
- package/bin/bookmark-research.js +30 -0
- package/config/providers.json +98 -0
- package/docs/bookmark-research-architecture.md +124 -0
- package/docs/cli-launcher-plan.md +58 -0
- package/docs/details.en.md +272 -0
- package/docs/details.md +93 -0
- package/docs/harness-compatibility.md +304 -0
- package/docs/harness-sources.json +342 -0
- package/docs/host-validation.json +298 -0
- package/docs/install-design-0.2.0.md +57 -0
- package/docs/installation.en.md +197 -0
- package/docs/installation.md +316 -0
- package/docs/instructions.en.md +42 -0
- package/docs/instructions.md +42 -0
- package/docs/mcp-aggregation-0.2.0.md +92 -0
- package/docs/multi-host-installation-plan.md +163 -0
- package/docs/prompt-reference.en.md +104 -0
- package/docs/prompt-reference.md +104 -0
- package/docs/provider-smoke-0.2.0.json +235 -0
- package/docs/release-notes-0.5.0-beta.1.md +33 -0
- package/docs/research-0.2.0.md +140 -0
- package/docs/research-sources-0.2.0.json +9360 -0
- package/docs/user-guide.en.md +74 -0
- package/docs/user-guide.md +74 -0
- package/docs/validation-0.2.0.md +126 -0
- package/docs/validation-0.4.0.md +154 -0
- package/docs/wiki-quality.md +105 -0
- package/docs/wiki-quality.zh.md +105 -0
- package/hosts/claude/runtime.js +13 -0
- package/hosts/codex/delegate.md +14 -0
- package/hosts/codex/prepare.py +61 -0
- package/hosts/dsh/plugin.js +36 -0
- package/hosts/dsh/runtime.js +14 -0
- package/hosts/dsh/workflow-call.py +45 -0
- package/hosts/pi/register-workflow.py +72 -0
- package/hosts/pi/runtime.js +18 -0
- package/hosts/shared/research-call.py +76 -0
- package/hosts/shared/research-flow.js +208 -0
- package/hosts/shared/workflow.json +15 -0
- package/install.sh +310 -0
- package/package.json +12 -0
- package/scripts/build_zip.py +143 -0
- package/scripts/export_bundle.py +496 -0
- package/scripts/host_assets.py +70 -0
- package/scripts/host_clients.py +196 -0
- package/scripts/host_install.py +277 -0
- package/scripts/install.py +593 -0
- package/scripts/launcher.py +186 -0
- package/scripts/verify_fixture.py +220 -0
- package/scripts/verify_live.py +62 -0
- package/scripts/verify_quality.py +87 -0
- package/skills/bookmark-research/SKILL.md +80 -0
- package/skills/bookmark-research/agents/openai.yaml +3 -0
- package/skills/bookmark-research/references/cli.md +150 -0
- package/skills/bookmark-research/references/deep-research.md +164 -0
- package/skills/bookmark-research/references/github-and-sync.md +30 -0
- package/skills/bookmark-research/references/host-workflows.md +101 -0
- package/skills/bookmark-research/references/package-semantics.md +78 -0
- package/skills/bookmark-research/references/research-methods.md +51 -0
- package/skills/bookmark-research/references/research-services.md +72 -0
- package/skills/bookmark-research/references/research-workflow.md +57 -0
- package/skills/bookmark-research/references/settings-and-archive.md +104 -0
- package/skills/bookmark-research/references/source-lifecycle.md +75 -0
- package/skills/bookmark-research/references/wiki-and-evaluation.md +63 -0
- package/skills/bookmark-research/references/zh/cli.md +150 -0
- package/skills/bookmark-research/references/zh/deep-research.md +164 -0
- package/skills/bookmark-research/references/zh/github-and-sync.md +30 -0
- package/skills/bookmark-research/references/zh/host-workflows.md +101 -0
- package/skills/bookmark-research/references/zh/package-semantics.md +78 -0
- package/skills/bookmark-research/references/zh/research-methods.md +51 -0
- package/skills/bookmark-research/references/zh/research-services.md +70 -0
- package/skills/bookmark-research/references/zh/research-workflow.md +57 -0
- package/skills/bookmark-research/references/zh/settings-and-archive.md +104 -0
- package/skills/bookmark-research/references/zh/skill-guide.md +75 -0
- package/skills/bookmark-research/references/zh/source-lifecycle.md +75 -0
- package/skills/bookmark-research/references/zh/wiki-and-evaluation.md +63 -0
- package/src/archive.py +302 -0
- package/src/bookmark_index.py +913 -0
- package/src/cli.py +435 -0
- package/src/credentials.py +82 -0
- package/src/evaluation.py +337 -0
- package/src/jina.py +96 -0
- package/src/mcp_server.py +641 -0
- package/src/onboarding.py +352 -0
- package/src/provider_adapters.py +152 -0
- package/src/readiness.py +283 -0
- package/src/remote_mcp.py +295 -0
- package/src/research.py +1334 -0
- package/src/research_coverage.py +160 -0
- package/src/research_services.py +684 -0
- package/src/routing.py +202 -0
- package/src/search_results.py +244 -0
- package/src/service_http.py +139 -0
- package/src/settings.py +247 -0
- package/src/source_inputs.py +201 -0
- package/src/source_manager.py +348 -0
- package/src/source_watcher.py +68 -0
- package/src/textio.py +9 -0
- package/src/tui.py +586 -0
- package/src/web_search.py +603 -0
- package/src/wiki.py +383 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build a source distribution ZIP with the native plugin and host exporter."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import zipfile
|
|
12
|
+
|
|
13
|
+
from export_bundle import EXCLUDED_DIRECTORIES, SOURCE_ROOT, _copy_plan, read_plugin_manifest
|
|
14
|
+
from host_assets import HOST_SOURCE_FILES
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
EXTRA_FILES = (
|
|
18
|
+
".codex-plugin/plugin.json",
|
|
19
|
+
".agents/plugins/marketplace.json",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"docs/installation.md",
|
|
22
|
+
"docs/installation.en.md",
|
|
23
|
+
"docs/bookmark-research-architecture.md",
|
|
24
|
+
"docs/install-design-0.2.0.md",
|
|
25
|
+
"docs/mcp-aggregation-0.2.0.md",
|
|
26
|
+
"docs/provider-smoke-0.2.0.json",
|
|
27
|
+
"docs/research-0.2.0.md",
|
|
28
|
+
"docs/validation-0.2.0.md",
|
|
29
|
+
"docs/validation-0.4.0.md",
|
|
30
|
+
"README.md",
|
|
31
|
+
"README.zh.md",
|
|
32
|
+
"install.sh",
|
|
33
|
+
"docs/harness-compatibility.md",
|
|
34
|
+
"docs/harness-sources.json",
|
|
35
|
+
"docs/host-validation.json",
|
|
36
|
+
"docs/wiki-quality.md",
|
|
37
|
+
"scripts/export_bundle.py",
|
|
38
|
+
"scripts/build_zip.py",
|
|
39
|
+
"scripts/install.py",
|
|
40
|
+
"scripts/host_install.py",
|
|
41
|
+
"scripts/host_clients.py",
|
|
42
|
+
"scripts/host_assets.py",
|
|
43
|
+
"scripts/launcher.py",
|
|
44
|
+
"bin/bookmark-research.js",
|
|
45
|
+
"package.json",
|
|
46
|
+
"docs/details.md",
|
|
47
|
+
"docs/details.en.md",
|
|
48
|
+
"skills/bookmark-research/agents/openai.yaml",
|
|
49
|
+
) + HOST_SOURCE_FILES
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _read_file(path, root):
|
|
53
|
+
"""Read a selected source file without following links outside the package."""
|
|
54
|
+
relative = path.relative_to(root)
|
|
55
|
+
current = root
|
|
56
|
+
for part in relative.parts:
|
|
57
|
+
current = current / part
|
|
58
|
+
if current.is_symlink():
|
|
59
|
+
raise ValueError("Source symlinks are not packaged: " + str(relative))
|
|
60
|
+
if not path.is_file():
|
|
61
|
+
raise ValueError("Required distribution file is missing: " + str(relative))
|
|
62
|
+
return path.read_bytes()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _test_files(source):
|
|
66
|
+
"""Include the curated test tree only for an explicit verification pack."""
|
|
67
|
+
root = source / "tests"
|
|
68
|
+
if root.is_symlink() or not root.is_dir():
|
|
69
|
+
raise ValueError("The verification pack requires a local tests directory")
|
|
70
|
+
selected = {Path("scripts/verify_fixture.py"), Path("scripts/verify_quality.py")}
|
|
71
|
+
for path in sorted(root.rglob("*")):
|
|
72
|
+
relative = path.relative_to(source)
|
|
73
|
+
if any(part.startswith(".") or part.casefold() in EXCLUDED_DIRECTORIES - {"tests", "test"}
|
|
74
|
+
for part in relative.parts):
|
|
75
|
+
continue
|
|
76
|
+
if path.is_symlink():
|
|
77
|
+
raise ValueError("Source symlinks are not packaged: " + relative.as_posix())
|
|
78
|
+
if path.is_file() and (path.suffix == ".py" or path.name.endswith(".test.js") or
|
|
79
|
+
relative.parts[:2] == ("tests", "fixtures")
|
|
80
|
+
and path.suffix in (".json", ".canvas", ".md", ".txt")):
|
|
81
|
+
selected.add(relative)
|
|
82
|
+
if not any(path.name.startswith("test_") and path.suffix == ".py" for path in selected):
|
|
83
|
+
raise ValueError("The verification pack requires Python tests")
|
|
84
|
+
return selected
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_zip(output, source_root=SOURCE_ROOT, include_tests=False):
|
|
88
|
+
source = Path(source_root).resolve()
|
|
89
|
+
manifest = read_plugin_manifest(source)
|
|
90
|
+
selected = set(_copy_plan(source, include_codex_metadata=True)) | {Path(name) for name in EXTRA_FILES}
|
|
91
|
+
if include_tests:
|
|
92
|
+
selected.update(_test_files(source))
|
|
93
|
+
payload = {path.as_posix(): _read_file(source / path, source)
|
|
94
|
+
for path in sorted(selected)}
|
|
95
|
+
checksums = "".join(hashlib.sha256(data).hexdigest() + " " + name + "\n"
|
|
96
|
+
for name, data in sorted(payload.items()))
|
|
97
|
+
payload["MANIFEST.sha256"] = checksums.encode("utf-8")
|
|
98
|
+
|
|
99
|
+
output = Path(output).expanduser()
|
|
100
|
+
if output.exists() or output.is_symlink():
|
|
101
|
+
raise ValueError("Refusing existing output: " + str(output))
|
|
102
|
+
if output.suffix.lower() != ".zip":
|
|
103
|
+
raise ValueError("Output must have a .zip extension")
|
|
104
|
+
output = output.resolve()
|
|
105
|
+
if output == source or (source in output.parents and source / "dist" not in output.parents):
|
|
106
|
+
raise ValueError("ZIP output inside the repository must be under dist/")
|
|
107
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
archive_root = ("bookmark-research-test-pack-" if include_tests else "bookmark-research-") + manifest["version"]
|
|
109
|
+
with tempfile.TemporaryDirectory(prefix=".bookmark-research-zip-", dir=output.parent) as temporary:
|
|
110
|
+
staged = Path(temporary) / "distribution.zip"
|
|
111
|
+
with zipfile.ZipFile(staged, "x", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
|
|
112
|
+
for relative, data in sorted(payload.items()):
|
|
113
|
+
# Fixed timestamps, permissions, order and compression make a
|
|
114
|
+
# source snapshot reproducible with the same Python/zlib.
|
|
115
|
+
entry = zipfile.ZipInfo(archive_root + "/" + relative, date_time=(1980, 1, 1, 0, 0, 0))
|
|
116
|
+
entry.create_system = 3
|
|
117
|
+
entry.external_attr = 0o100644 << 16
|
|
118
|
+
entry.compress_type = zipfile.ZIP_DEFLATED
|
|
119
|
+
archive.writestr(entry, data, compress_type=zipfile.ZIP_DEFLATED, compresslevel=9)
|
|
120
|
+
# A same-filesystem hard link publishes only a completed ZIP and cannot
|
|
121
|
+
# replace an existing path, even if another writer wins the race.
|
|
122
|
+
os.link(staged, output)
|
|
123
|
+
return {"output": str(output), "version": manifest["version"], "archive_root": archive_root, "files": len(payload),
|
|
124
|
+
"bytes": output.stat().st_size,
|
|
125
|
+
"sha256": hashlib.sha256(output.read_bytes()).hexdigest(), "includes_tests": include_tests, "installed": False}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def main(argv=None):
|
|
129
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
130
|
+
parser.add_argument("--output", required=True, help="New ZIP path under dist/ or outside the repository")
|
|
131
|
+
parser.add_argument("--include-tests", action="store_true", help="Include tests, synthetic fixtures, and the offline verifier")
|
|
132
|
+
args = parser.parse_args(argv)
|
|
133
|
+
try:
|
|
134
|
+
result = build_zip(args.output, include_tests=args.include_tests)
|
|
135
|
+
except (OSError, ValueError, zipfile.BadZipFile) as error:
|
|
136
|
+
print(json.dumps({"error": str(error)}, ensure_ascii=False), file=sys.stderr)
|
|
137
|
+
return 1
|
|
138
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Export shared runtime files into a selected host format, without installing it."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from host_assets import export_host_assets
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SOURCE_ROOT = Path(__file__).resolve().parents[1]
|
|
17
|
+
FORMATS = ("codex", "agent-plugin", "claude", "pi", "dsh")
|
|
18
|
+
NAME = "bookmark-research"
|
|
19
|
+
SCHEMA_ROOT = "https://agent-plugins.org/schemas/1.0.0/"
|
|
20
|
+
SHARED_ROOTS = ("src", "config", "skills")
|
|
21
|
+
SHARED_DOCUMENTS = (
|
|
22
|
+
"docs/wiki-quality.md", "docs/wiki-quality.zh.md",
|
|
23
|
+
"docs/user-guide.md", "docs/user-guide.en.md",
|
|
24
|
+
"docs/instructions.md", "docs/instructions.en.md",
|
|
25
|
+
"docs/prompt-reference.md", "docs/prompt-reference.en.md",
|
|
26
|
+
)
|
|
27
|
+
REQUIRED_FILES = (
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"src/archive.py",
|
|
30
|
+
"src/bookmark_index.py",
|
|
31
|
+
"src/cli.py",
|
|
32
|
+
"src/credentials.py",
|
|
33
|
+
"src/onboarding.py",
|
|
34
|
+
"src/readiness.py",
|
|
35
|
+
"src/mcp_server.py",
|
|
36
|
+
"src/remote_mcp.py",
|
|
37
|
+
"src/provider_adapters.py",
|
|
38
|
+
"src/research.py",
|
|
39
|
+
"src/research_coverage.py",
|
|
40
|
+
"src/research_services.py",
|
|
41
|
+
"src/service_http.py",
|
|
42
|
+
"src/routing.py",
|
|
43
|
+
"src/wiki.py",
|
|
44
|
+
"src/evaluation.py",
|
|
45
|
+
"src/search_results.py",
|
|
46
|
+
"src/settings.py",
|
|
47
|
+
"src/source_inputs.py",
|
|
48
|
+
"src/source_manager.py",
|
|
49
|
+
"src/source_watcher.py",
|
|
50
|
+
"src/web_search.py",
|
|
51
|
+
"config/providers.json",
|
|
52
|
+
"skills/bookmark-research/SKILL.md",
|
|
53
|
+
) + SHARED_DOCUMENTS
|
|
54
|
+
EXCLUDED_DIRECTORIES = {
|
|
55
|
+
"__pycache__", "node_modules", "tests", "test", "build", "dist", "target",
|
|
56
|
+
"cache", "caches", "coverage", "htmlcov", "venv",
|
|
57
|
+
"data", "knowledge", "reports", "archives",
|
|
58
|
+
}
|
|
59
|
+
EXCLUDED_SUFFIXES = {".pyc", ".pyo", ".log", ".tmp", ".swp", ".swo", ".canvas"}
|
|
60
|
+
DATABASE_NAME = re.compile(r"\.(?:db|db3|sqlite|sqlite3|s3db)(?:$|[.-])", re.IGNORECASE)
|
|
61
|
+
VERSION_PATTERN = re.compile(
|
|
62
|
+
r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)"
|
|
63
|
+
r"(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)"
|
|
64
|
+
r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?"
|
|
65
|
+
r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def read_plugin_manifest(source_root=SOURCE_ROOT):
|
|
69
|
+
"""Use the native manifest as the version source for every distribution."""
|
|
70
|
+
source_root = Path(source_root)
|
|
71
|
+
path = source_root / ".codex-plugin/plugin.json"
|
|
72
|
+
if path.is_symlink() or path.parent.is_symlink():
|
|
73
|
+
raise ValueError("Source symlinks are not exported: .codex-plugin/plugin.json")
|
|
74
|
+
manifest = json.loads(path.read_text(encoding="utf-8"))
|
|
75
|
+
if not isinstance(manifest, dict) or manifest.get("name") != NAME:
|
|
76
|
+
raise ValueError("Expected the bookmark-research native plugin manifest")
|
|
77
|
+
if not isinstance(manifest.get("description"), str) or not manifest["description"].strip():
|
|
78
|
+
raise ValueError("The native plugin description must be nonempty")
|
|
79
|
+
version = manifest.get("version")
|
|
80
|
+
if not isinstance(version, str) or not VERSION_PATTERN.fullmatch(version):
|
|
81
|
+
raise ValueError("The native plugin version must be a semantic version")
|
|
82
|
+
return manifest
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _copy_plan(source_root, include_codex_metadata=False):
|
|
86
|
+
"""Enumerate only shared roots; reject links rather than following external files."""
|
|
87
|
+
files = []
|
|
88
|
+
|
|
89
|
+
def visit(path):
|
|
90
|
+
relative = path.relative_to(source_root)
|
|
91
|
+
if path.name.startswith(".") or path.name.casefold() in EXCLUDED_DIRECTORIES:
|
|
92
|
+
return
|
|
93
|
+
# Codex presentation metadata is not needed by any of these exports.
|
|
94
|
+
codex_metadata = relative.parts[0] == "skills" and relative.parts[-2:] == ("agents", "openai.yaml")
|
|
95
|
+
if codex_metadata and not include_codex_metadata:
|
|
96
|
+
return
|
|
97
|
+
if DATABASE_NAME.search(path.name) or path.suffix.casefold() in EXCLUDED_SUFFIXES:
|
|
98
|
+
return
|
|
99
|
+
if path.is_symlink():
|
|
100
|
+
raise ValueError("Source symlinks are not exported: " + str(relative))
|
|
101
|
+
if path.is_dir():
|
|
102
|
+
# Runtime configuration has one public registry. Do not copy local
|
|
103
|
+
# preferences or credential JSON merely because it is under config/.
|
|
104
|
+
children = [path / "providers.json"] if relative.as_posix() == "config" else sorted(path.iterdir())
|
|
105
|
+
for child in children:
|
|
106
|
+
visit(child)
|
|
107
|
+
elif path.is_file():
|
|
108
|
+
if relative.parts[0] == "src" and path.suffix != ".py":
|
|
109
|
+
return
|
|
110
|
+
# Skills contain only Markdown references and optional native UI
|
|
111
|
+
# metadata. In particular, a user-provided JSON canvas is not code.
|
|
112
|
+
if relative.parts[0] == "skills" and path.suffix != ".md" and not codex_metadata:
|
|
113
|
+
return
|
|
114
|
+
with path.open("rb") as stream:
|
|
115
|
+
if stream.read(16) == b"SQLite format 3\x00":
|
|
116
|
+
return
|
|
117
|
+
files.append(relative)
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError("Unsupported or missing source entry: " + str(relative))
|
|
120
|
+
|
|
121
|
+
for root in SHARED_ROOTS:
|
|
122
|
+
visit(source_root / root)
|
|
123
|
+
for relative in SHARED_DOCUMENTS:
|
|
124
|
+
visit(source_root / relative)
|
|
125
|
+
visit(source_root / "LICENSE")
|
|
126
|
+
present = {path.as_posix() for path in files}
|
|
127
|
+
missing = set(REQUIRED_FILES) - present
|
|
128
|
+
if missing:
|
|
129
|
+
raise ValueError("Required shared files are missing: " + ", ".join(sorted(missing)))
|
|
130
|
+
return files
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _check_destination(output):
|
|
134
|
+
if output.is_symlink():
|
|
135
|
+
raise ValueError("Output must not be a symlink: " + str(output))
|
|
136
|
+
if output.exists():
|
|
137
|
+
if not output.is_dir():
|
|
138
|
+
raise ValueError("Output must be a directory: " + str(output))
|
|
139
|
+
if next(output.iterdir(), None) is not None:
|
|
140
|
+
raise ValueError("Refusing nonempty output directory: " + str(output))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _write_json(root, relative, value):
|
|
144
|
+
path = root / relative
|
|
145
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _readme(format_name, output):
|
|
150
|
+
introduction = """# Bookmark Research export
|
|
151
|
+
|
|
152
|
+
[English user guide](docs/user-guide.en.md) · [中文使用指南](docs/user-guide.md)
|
|
153
|
+
|
|
154
|
+
The execution Skill and method references are in English, with complete Chinese
|
|
155
|
+
reading copies. See the [instruction index](docs/instructions.en.md) or
|
|
156
|
+
[中文指令索引](docs/instructions.md) for both versions and the host prompts.
|
|
157
|
+
|
|
158
|
+
This directory includes the shared Skill, Python CLI, stdio MCP server, and public
|
|
159
|
+
provider configuration. It requires Python 3.9+ and the standard library;
|
|
160
|
+
`doctor` checks whether the local SQLite build supports FTS5.
|
|
161
|
+
The repository's [license](LICENSE) is included with the source files.
|
|
162
|
+
|
|
163
|
+
Supply your own Bookmark Canvas package through `sync`. A fresh data directory
|
|
164
|
+
has no registered sources; existing indexes remain part of your local data.
|
|
165
|
+
|
|
166
|
+
Run these commands from this directory to check the runtime and provider registry:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
python3 src/cli.py doctor
|
|
170
|
+
python3 src/cli.py providers
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
These checks run offline and do not create a persistent database. See the
|
|
174
|
+
[shared Skill](skills/bookmark-research/SKILL.md) for query and research workflows.
|
|
175
|
+
Exporting generates files; complete the client setup below to load them.
|
|
176
|
+
|
|
177
|
+
The database location follows the CLI rules: explicit `--db` takes precedence,
|
|
178
|
+
then `BOOKMARK_RESEARCH_DATA_DIR/index.sqlite3`, then
|
|
179
|
+
`${XDG_DATA_HOME:-~/.local/share}/bookmark-research/index.sqlite3`.
|
|
180
|
+
To share an index across clients, pass the same `BOOKMARK_RESEARCH_DATA_DIR` to
|
|
181
|
+
each MCP / CLI process, or use the same `--db` path. Clients may filter inherited
|
|
182
|
+
environment variables, so check what each client forwards to its child process.
|
|
183
|
+
|
|
184
|
+
Use `config show` and `config set` to inspect or change settings. The configuration
|
|
185
|
+
path is selected by `--config`, `BOOKMARK_RESEARCH_CONFIG`, or
|
|
186
|
+
`${XDG_CONFIG_HOME:-~/.config}/bookmark-research/settings.json`, in that order.
|
|
187
|
+
Page reads archive actual responses, recognized text, and source records under
|
|
188
|
+
`knowledge/` in the data directory by default. You can change this location or
|
|
189
|
+
disable archiving. To share settings, forward the same configuration path through
|
|
190
|
+
each client, including `BOOKMARK_RESEARCH_CONFIG` or `XDG_CONFIG_HOME` as needed.
|
|
191
|
+
See the [user guide and settings](docs/user-guide.en.md), or the detailed
|
|
192
|
+
[settings reference](skills/bookmark-research/references/settings-and-archive.md).
|
|
193
|
+
|
|
194
|
+
Keep databases, user settings, and page archives outside this package. Exports
|
|
195
|
+
exclude existing indexes, caches, tests, and build artifacts. They do not read
|
|
196
|
+
API key values from the environment; supply provider credentials at runtime.
|
|
197
|
+
|
|
198
|
+
Run `python3 src/cli.py setup` to guide preferences, hidden credential entry and
|
|
199
|
+
service checks. Use `--host codex|claude_code|pi|dsh` for host-specific MCP/login
|
|
200
|
+
guidance. Agents can use `setup --non-interactive --input FILE --skip-checks`.
|
|
201
|
+
Keys are saved separately in a private local `credentials.json`, never in this
|
|
202
|
+
package. Environment variables override saved keys. Before each new web research
|
|
203
|
+
question, use `readiness` (MCP `research_readiness`); it respects cached/always/manual
|
|
204
|
+
preferences. `--test-retrieval` opts into sample search/read quota. No professional
|
|
205
|
+
research job is created by checks. Local queries require neither keys nor checks.
|
|
206
|
+
|
|
207
|
+
"""
|
|
208
|
+
if format_name == "codex":
|
|
209
|
+
instructions = """## Codex
|
|
210
|
+
|
|
211
|
+
This clean bundle contains the native `.codex-plugin/plugin.json` and a local
|
|
212
|
+
`.agents/plugins/marketplace.json`. Keep this directory at a stable path.
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
codex plugin marketplace add '/absolute/path/to/this-bundle'
|
|
216
|
+
codex plugin add bookmark-research@bookmark-research
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
If that marketplace name already points elsewhere, choose the source you intend
|
|
220
|
+
to keep before installing. Start a new Codex thread after installation or update.
|
|
221
|
+
Do not put user data in the installed cache.
|
|
222
|
+
|
|
223
|
+
For a multi-group study, follow `hosts/codex/delegate.md` with the native
|
|
224
|
+
subagent tools. `python3 hosts/codex/prepare.py --research-id RID` reads every
|
|
225
|
+
inventory page and emits complete ID groups; it does not start agents.
|
|
226
|
+
|
|
227
|
+
Reference: https://developers.openai.com/codex/cli/reference#codex-plugin
|
|
228
|
+
"""
|
|
229
|
+
elif format_name == "agent-plugin":
|
|
230
|
+
instructions = """## Agent Plugins 1.0.0
|
|
231
|
+
|
|
232
|
+
The entry files are root `plugin.json` and `mcp.json`, with the Skill under
|
|
233
|
+
`skills/bookmark-research/`. The client must implement this specification and
|
|
234
|
+
expand `${PLUGIN_ROOT}` in MCP arguments to this package's absolute path.
|
|
235
|
+
The command is `python3`; database placement follows the CLI rules above.
|
|
236
|
+
This export does not force the use of `${PLUGIN_DATA}`.
|
|
237
|
+
|
|
238
|
+
This is a separate standard format. Export checks do not establish compatibility
|
|
239
|
+
with an untested client or make it equivalent to Codex's native plugin format.
|
|
240
|
+
|
|
241
|
+
Specification: https://agent-plugins.org/specification
|
|
242
|
+
"""
|
|
243
|
+
elif format_name == "claude":
|
|
244
|
+
instructions = """## Claude Code
|
|
245
|
+
|
|
246
|
+
The entry files are `.claude-plugin/plugin.json` and root `.mcp.json`. MCP
|
|
247
|
+
arguments use `${CLAUDE_PLUGIN_ROOT}`. A local marketplace manifest is included.
|
|
248
|
+
With Claude Code installed, register this stable export persistently:
|
|
249
|
+
|
|
250
|
+
```sh
|
|
251
|
+
claude plugin marketplace add /absolute/path/to/this-bundle
|
|
252
|
+
claude plugin install bookmark-research@bookmark-research --scope user
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Reuse existing registrations instead of replacing a different source. For session
|
|
256
|
+
development, load the export using its actual absolute path:
|
|
257
|
+
|
|
258
|
+
```sh
|
|
259
|
+
claude --plugin-dir /absolute/path/to/this-bundle
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The plugin includes `workflows/bookmark-research.js`, a native Dynamic Workflow.
|
|
263
|
+
After creating a research session, explicitly invoke
|
|
264
|
+
`/bookmark-research:bookmark-research` with structured `research_id` and a unique
|
|
265
|
+
`run_key` (for example, `review-1`). It reads groups, independently verifies them,
|
|
266
|
+
reads all coverage differences, and saves a report and full analysis attachment.
|
|
267
|
+
|
|
268
|
+
Dynamic Workflows require Claude Code 2.1.154+ and enabled workflow access. Runs
|
|
269
|
+
are managed through `/workflows`; pause/resume is limited to the same session
|
|
270
|
+
and some completed children can replay. `/deep-research` is a separate built-in
|
|
271
|
+
command, explicit-only since 2.1.218, and does not guarantee bookmark inventory
|
|
272
|
+
coverage. Injecting `ultracode` into a Skill or `-p` prompt does not invoke it.
|
|
273
|
+
Client version/manifest checks do not establish a paid research run succeeded.
|
|
274
|
+
|
|
275
|
+
Reference: https://code.claude.com/docs/en/plugins-reference
|
|
276
|
+
"""
|
|
277
|
+
elif format_name == "pi":
|
|
278
|
+
instructions = """## Pi Skill and saved workflow package
|
|
279
|
+
|
|
280
|
+
`package.json` declares `pi.skills`. The shared Skill calls the bundled Python
|
|
281
|
+
CLI. The saved workflow requires already-installed `pi-subagents` 0.43.0+ and
|
|
282
|
+
`pi-subagents-workflows` in the same parent session, Pi 0.83+, and Node 22.19+.
|
|
283
|
+
These are extension capabilities; exporting does not install them.
|
|
284
|
+
|
|
285
|
+
Load the Skill for one session, replacing the path with your export location:
|
|
286
|
+
|
|
287
|
+
```sh
|
|
288
|
+
pi --skill /absolute/path/to/this-bundle/skills/bookmark-research/SKILL.md
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Use `/skill:bookmark-research` in the session to load its workflow. For persistent
|
|
292
|
+
registration of the Skill, follow Pi's package instructions and run
|
|
293
|
+
`pi install /absolute/path/to/this-bundle`. That command changes Pi settings;
|
|
294
|
+
the exporter itself does not run it.
|
|
295
|
+
|
|
296
|
+
Register the saved workflow in a trusted project without modifying Pi settings:
|
|
297
|
+
|
|
298
|
+
```sh
|
|
299
|
+
python3 hosts/pi/register-workflow.py --project /absolute/path/to/project
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
This copies `workflow.json` and `script.js` into that project's
|
|
303
|
+
`.pi/subagent-workflows/bookmark-research/` and binds the stable bundle's shared
|
|
304
|
+
tool bridge. Existing different definitions are preserved and reported as a
|
|
305
|
+
conflict. Start Pi in that project. Use
|
|
306
|
+
`pi_subagent_workflow({action:"run",name:"bookmark-research",args:{research_id:"RID",run_key:"review-1"}})`.
|
|
307
|
+
Runs are detached; wait through pi-subagents status/artifacts, inspecting every
|
|
308
|
+
failed child. The registry adds no cross-session journal replay. The `delegate`
|
|
309
|
+
agent must have Bash access to the bundled Python bridge, or native research MCP
|
|
310
|
+
tools. The bridge calls the same stdio server; it is not a Pi core MCP feature.
|
|
311
|
+
Actual loading in Pi still needs client verification.
|
|
312
|
+
|
|
313
|
+
Reference: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md
|
|
314
|
+
"""
|
|
315
|
+
else:
|
|
316
|
+
patch_path = shlex.quote(str(output / "cordis.patch.yml"))
|
|
317
|
+
skill_root = str(output / "skills")
|
|
318
|
+
instructions = """## DeepSeek Harness bundle
|
|
319
|
+
|
|
320
|
+
`package.json` declares `dsh.bundle`, using `bundle.patch.yml` and a module that
|
|
321
|
+
resolves its Python runtime and Skill from its installed location. Select an
|
|
322
|
+
existing profile with the Skill registry and official MCP client available:
|
|
323
|
+
|
|
324
|
+
```sh
|
|
325
|
+
dsh plugin --profile web add /absolute/path/to/this-bundle
|
|
326
|
+
dsh --profile web --dump-config
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
The bundle connects both Skill discovery and stdio MCP. It explicitly forwards
|
|
330
|
+
the plugin's data/config/credential paths and provider-key environment variables
|
|
331
|
+
through the MCP client's filtered child environment. No values are baked into
|
|
332
|
+
the export. After moving the source export, reinstall it from the new location.
|
|
333
|
+
|
|
334
|
+
The legacy `cordis.patch.yml` connects MCP only and contains absolute local paths.
|
|
335
|
+
Regenerate it after moving the export. For this manual adapter, inspect the merged
|
|
336
|
+
configuration with:
|
|
337
|
+
|
|
338
|
+
```sh
|
|
339
|
+
dsh --profile web --patch %s --dump-config
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Start the client with `dsh web --patch /absolute/path/to/this-bundle/cordis.patch.yml`,
|
|
343
|
+
using your actual patch path. These integration commands still need client verification.
|
|
344
|
+
|
|
345
|
+
The MCP patch does not configure Skill discovery. To use the shared Skill, point
|
|
346
|
+
the existing Skill provider's `customSkillDirs` at `%s`, or use the documented
|
|
347
|
+
DSH Skill directories. DSH is in developer preview, so setup options may change.
|
|
348
|
+
|
|
349
|
+
The selected profile must separately expose `workflow` through its configured
|
|
350
|
+
workflow service, worker-thread engine and tool. Generate the complete call:
|
|
351
|
+
|
|
352
|
+
```sh
|
|
353
|
+
python3 hosts/dsh/workflow-call.py --research-id RID --run-key review-1
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Pass that JSON object directly to the visible workflow tool. Its fields are
|
|
357
|
+
separate `meta`, plain-JavaScript `script`, and object-valued `args`; there is no
|
|
358
|
+
`export const meta` inside the script. DSH waits for the entire workflow, returns
|
|
359
|
+
`{runId,agentsStarted,result}`, and cancellation is an error. Rendered JSON may
|
|
360
|
+
be truncated; the reporting child saves the complete analysis object through
|
|
361
|
+
`research_record` and returns its real `result_path`. There is no background
|
|
362
|
+
start/poll API or automatically generated full-result retrieval handle.
|
|
363
|
+
|
|
364
|
+
Reference: https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/mcp/mcp-client/README.md
|
|
365
|
+
""" % (patch_path, skill_root)
|
|
366
|
+
return introduction + instructions
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _write_adapter(stage, format_name, output, manifest, python="python3"):
|
|
370
|
+
metadata = {"name": NAME, "version": manifest["version"], "description": manifest["description"]}
|
|
371
|
+
if format_name == "codex":
|
|
372
|
+
_write_json(stage, ".codex-plugin/plugin.json", manifest)
|
|
373
|
+
_write_json(stage, ".agents/plugins/marketplace.json", {
|
|
374
|
+
"name": NAME, "interface": {"displayName": "Bookmark Research"},
|
|
375
|
+
"plugins": [{"name": NAME, "source": {"source": "local", "path": "./"},
|
|
376
|
+
"policy": {"installation": "AVAILABLE", "authentication": "ON_INSTALL"},
|
|
377
|
+
"category": "Productivity"}],
|
|
378
|
+
})
|
|
379
|
+
elif format_name == "agent-plugin":
|
|
380
|
+
_write_json(stage, "plugin.json", {"$schema": SCHEMA_ROOT + "plugin.schema.json", **metadata})
|
|
381
|
+
_write_json(stage, "mcp.json", {
|
|
382
|
+
"$schema": SCHEMA_ROOT + "mcp.schema.json",
|
|
383
|
+
"mcpServers": {NAME: {"type": "stdio", "command": python,
|
|
384
|
+
"args": ["${PLUGIN_ROOT}/src/cli.py", "serve"]}},
|
|
385
|
+
})
|
|
386
|
+
elif format_name == "claude":
|
|
387
|
+
claude_metadata = {**metadata, "workflows": "./workflows"}
|
|
388
|
+
if manifest.get("author"):
|
|
389
|
+
claude_metadata["author"] = manifest["author"]
|
|
390
|
+
_write_json(stage, ".claude-plugin/plugin.json", claude_metadata)
|
|
391
|
+
_write_json(stage, ".claude-plugin/marketplace.json", {
|
|
392
|
+
"name": NAME, "description": manifest["description"],
|
|
393
|
+
"owner": {"name": "Bookmark Research"},
|
|
394
|
+
"plugins": [{"name": NAME, "source": "./", "description": manifest["description"]}],
|
|
395
|
+
})
|
|
396
|
+
_write_json(stage, ".mcp.json", {"mcpServers": {NAME: {
|
|
397
|
+
"type": "stdio", "command": python,
|
|
398
|
+
"args": ["${CLAUDE_PLUGIN_ROOT}/src/cli.py", "serve"],
|
|
399
|
+
}}})
|
|
400
|
+
elif format_name == "pi":
|
|
401
|
+
_write_json(stage, "package.json", {**metadata, "keywords": ["pi-package"],
|
|
402
|
+
"pi": {"skills": ["./skills"]}})
|
|
403
|
+
else:
|
|
404
|
+
_write_json(stage, "package.json", {
|
|
405
|
+
**metadata, "type": "module", "main": "./hosts/dsh/plugin.js",
|
|
406
|
+
"exports": "./hosts/dsh/plugin.js",
|
|
407
|
+
"files": ["src", "config", "skills", "hosts", "workflows", "docs", "bundle.patch.yml", "LICENSE"],
|
|
408
|
+
"dsh": {"bundle": {"patch": "./bundle.patch.yml"}},
|
|
409
|
+
})
|
|
410
|
+
forwarded = manifest["mcpServers"][NAME]["env_vars"]
|
|
411
|
+
environment = json.dumps("Object.fromEntries(" + json.dumps(forwarded) +
|
|
412
|
+
".filter(key => process.env[key] !== undefined).map(key => [key, process.env[key]]))")
|
|
413
|
+
(stage / "bundle.patch.yml").write_text("""- insert:
|
|
414
|
+
- id: bookmark-research-skill
|
|
415
|
+
name: bookmark-research
|
|
416
|
+
- id: bookmark-research-mcp
|
|
417
|
+
name: '@deepseek-ai/dsh-mcp-client'
|
|
418
|
+
inject: [bookmarkResearchPaths]
|
|
419
|
+
config:
|
|
420
|
+
serverName: bookmark-research
|
|
421
|
+
transport: stdio
|
|
422
|
+
command: %s
|
|
423
|
+
args: !!js "[ctx.bookmarkResearchPaths.cli, 'serve']"
|
|
424
|
+
cwd: !!js ctx.bookmarkResearchPaths.root
|
|
425
|
+
env: !!js %s
|
|
426
|
+
failOnStartupError: true
|
|
427
|
+
""" % (json.dumps(python), environment), encoding="utf-8")
|
|
428
|
+
# JSON strings are valid YAML scalars, including paths with spaces/quotes.
|
|
429
|
+
cli_path = json.dumps(str(output / "src/cli.py"), ensure_ascii=False)
|
|
430
|
+
patch = """- insert:
|
|
431
|
+
- id: bookmark-research-mcp
|
|
432
|
+
name: '@deepseek-ai/dsh-mcp-client'
|
|
433
|
+
config:
|
|
434
|
+
serverName: bookmark-research
|
|
435
|
+
transport: stdio
|
|
436
|
+
command: %s
|
|
437
|
+
args:
|
|
438
|
+
- %s
|
|
439
|
+
- serve
|
|
440
|
+
env: !!js %s
|
|
441
|
+
failOnStartupError: true
|
|
442
|
+
""" % (json.dumps(python), cli_path, environment)
|
|
443
|
+
(stage / "cordis.patch.yml").write_text(patch, encoding="utf-8")
|
|
444
|
+
(stage / "README.md").write_text(_readme(format_name, output), encoding="utf-8")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def export_bundle(format_name, output, source_root=SOURCE_ROOT, python="python3"):
|
|
448
|
+
"""Publish a staged export to a missing or empty directory; never merge files."""
|
|
449
|
+
if format_name not in FORMATS:
|
|
450
|
+
raise ValueError("Unknown export format: " + str(format_name))
|
|
451
|
+
source_root = Path(source_root).expanduser().resolve()
|
|
452
|
+
output = Path(output).expanduser()
|
|
453
|
+
_check_destination(output)
|
|
454
|
+
output = output.resolve()
|
|
455
|
+
if output == source_root or output in source_root.parents:
|
|
456
|
+
raise ValueError("Output must be separate from the source directory")
|
|
457
|
+
for root in SHARED_ROOTS:
|
|
458
|
+
shared = source_root / root
|
|
459
|
+
if output == shared or shared in output.parents:
|
|
460
|
+
raise ValueError("Output must not be inside a shared source root: " + root)
|
|
461
|
+
manifest = read_plugin_manifest(source_root)
|
|
462
|
+
files = _copy_plan(source_root, include_codex_metadata=format_name == "codex")
|
|
463
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
464
|
+
with tempfile.TemporaryDirectory(prefix=".bookmark-research-export-", dir=output.parent) as temporary:
|
|
465
|
+
stage = Path(temporary) / "bundle"
|
|
466
|
+
stage.mkdir()
|
|
467
|
+
for relative in files:
|
|
468
|
+
destination = stage / relative
|
|
469
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
470
|
+
shutil.copy2(source_root / relative, destination)
|
|
471
|
+
host_files = export_host_assets(source_root, stage, format_name)
|
|
472
|
+
_write_adapter(stage, format_name, output, manifest, python)
|
|
473
|
+
_check_destination(output)
|
|
474
|
+
if output.exists():
|
|
475
|
+
output.rmdir() # Only an empty directory can be removed here.
|
|
476
|
+
stage.rename(output) # Renaming a directory cannot replace nonempty data.
|
|
477
|
+
return {"format": format_name, "version": manifest["version"], "output": str(output), "shared_files": len(files),
|
|
478
|
+
"host_files": host_files, "installed": False}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def main(argv=None):
|
|
482
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
483
|
+
parser.add_argument("--format", choices=FORMATS, required=True)
|
|
484
|
+
parser.add_argument("--output", required=True, help="New or empty output directory")
|
|
485
|
+
args = parser.parse_args(argv)
|
|
486
|
+
try:
|
|
487
|
+
result = export_bundle(args.format, args.output)
|
|
488
|
+
except (OSError, ValueError, RuntimeError) as error:
|
|
489
|
+
print(json.dumps({"error": str(error)}, ensure_ascii=False), file=sys.stderr)
|
|
490
|
+
return 1
|
|
491
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
492
|
+
return 0
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
if __name__ == "__main__":
|
|
496
|
+
raise SystemExit(main())
|