Publish matching Windows CLI MCP package with document helper

This commit is contained in:
karl committed 2026-10-13 15:58:26 +02:00
1 parent a8249160ec
commit f1028e2689
4 files changed
+154 -7

No files matched your search

+4 -3
View File
@@ -221,9 +221,10 @@ Your agent receives the returned excerpts; cloud agents handle them according
to that client's configuration. to that client's configuration.
On Windows x64, your agent can use the optional On Windows x64, your agent can use the optional
[CLI/MCP download](https://tea.karlcom.de/karl/searchi/releases/download/v0.1.0/searchi-cli_0.1.0_x64.exe) [CLI/MCP download](https://tea.karlcom.de/karl/searchi/releases/download/v0.1.3/searchi-cli_0.1.0_x64-013.zip)
with its [own checksum](https://tea.karlcom.de/karl/searchi/releases/download/v0.1.0/CLI-SHA256SUMS.txt). with its [own checksum](https://tea.karlcom.de/karl/searchi/releases/download/v0.1.3/CLI-SHA256SUMS-0.1.3.txt).
No Rust build is needed. Install the desktop app first for the model setup and Extract the ZIP alongside the installed desktop runtime helpers; it includes
the document extraction helper. No Rust build is needed. Install the desktop app first for the model setup and
runtime helpers, then connect the CLI to that app's existing index. runtime helpers, then connect the CLI to that app's existing index.
## Optional answers, your choice ## Optional answers, your choice
+17 -4
View File
@@ -8,10 +8,10 @@ same actual Rust/Google search engine.
## Configure an existing index ## Configure an existing index
Windows x64 users can download the optional Windows x64 users can download the optional
[CLI/MCP executable](https://tea.karlcom.de/karl/searchi/releases/download/v0.1.0/searchi-cli_0.1.0_x64.exe) [CLI/MCP ZIP](https://tea.karlcom.de/karl/searchi/releases/download/v0.1.3/searchi-cli_0.1.0_x64-013.zip)
and verify its [separate checksum file](https://tea.karlcom.de/karl/searchi/releases/download/v0.1.0/CLI-SHA256SUMS.txt). and verify its [separate checksum file](https://tea.karlcom.de/karl/searchi/releases/download/v0.1.3/CLI-SHA256SUMS-0.1.3.txt).
Keep it alongside the installed desktop's runtime helpers; rename it to Extract it alongside the installed desktop's runtime helpers. Use
`searchi-cli.exe` if desired. The desktop installer itself is unchanged. `searchi-cli.exe`; keep `searchi-extract-worker.exe` beside it for PDF/DOCX indexing.
Use the desktop to finish model setup and semantic indexing before connecting. Use the desktop to finish model setup and semantic indexing before connecting.
Pass that existing `searchi.sqlite`, model cache and `searchi-gemma-worker.exe` Pass that existing `searchi.sqlite`, model cache and `searchi-gemma-worker.exe`
explicitly; avoid creating a second database for the same folders. explicitly; avoid creating a second database for the same folders.
@@ -193,3 +193,16 @@ Default tests use genuine SQLite retrieval without downloaded model assets.
Hybrid mode uses the same real Google service; it has no simulated embeddings. Hybrid mode uses the same real Google service; it has no simulated embeddings.
Native Linux/macOS MCP execution and cross-platform packaging require their own Native Linux/macOS MCP execution and cross-platform packaging require their own
recorded checks; Windows CPU checks do not establish those releases. recorded checks; Windows CPU checks do not establish those releases.
## Current Windows CLI package
The optional ZIP published alongside 0.1.3 contains CLI component 0.1.0 and
its verified document helper. Locked release build passed. Actual extracted
CLI indexed PDF/DOCX without helper overrides or extraction failures; MCP
initialization, three read-only tools, document search and cited source reads
passed on CPU. Two packaging tests passed normally and with Python `-O`.
Archive: 8784871 bytes, SHA-256
`4ae415d6cf7d3c9e566933e1de84a85c08c1a92b3f89b7f285384795bab30d64`.
Public full-byte archive and checksum downloads matched. This does not prove
clean-machine installation or GPU behavior. Earlier evidence describes older
assets. Rebuild with `python tools/package_windows_cli.py --executable target/release/searchi.exe`.
+69
View File
@@ -0,0 +1,69 @@
"""Package the optional Windows CLI/MCP client; reuse the installed desktop models."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import tempfile
import zipfile
from package_windows import pe_machine
from stage_extraction_worker import verify
ROOT = Path(__file__).resolve().parents[1]
MAX_FILE = 64 * 1024 * 1024
def package(root, executable, resources, output):
root, executable, resources, output = map(Path, (root, executable, resources, output))
def read(path):
if path.is_symlink() or not path.is_file() or path.absolute() != path.resolve():
raise ValueError('Package input missing or linked')
with path.open('rb') as stream:
data = stream.read(MAX_FILE + 1)
if not data or len(data) > MAX_FILE:
raise ValueError('Package input empty or oversized')
return data
cli = read(executable)
architecture = {0x8664: 'x86_64', 0xAA64: 'aarch64'}.get(pe_machine(executable))
if architecture is None:
raise ValueError('Unsupported Windows CLI architecture')
expected = verify(resources, root, 'windows', architecture)
selected = {'searchi-cli.exe': cli, 'LICENSE.txt': read(root / 'LICENSE')}
for name, record in expected.items():
data = read(resources / name)
if len(data) != record['bytes'] or hashlib.sha256(data).hexdigest() != record['sha256']:
raise ValueError('Extraction resource changed while packaging')
selected[name] = data
# The captured CLI must still match the header used for architecture validation.
if read(executable) != cli:
raise ValueError('CLI changed while packaging')
records = [{'path': name, 'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
for name, data in selected.items()]
output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(dir=output.parent, suffix='.partial', delete=False) as temporary:
temporary_path = Path(temporary.name)
try:
with zipfile.ZipFile(temporary_path, 'w', zipfile.ZIP_DEFLATED) as archive:
for name, data in selected.items():
archive.writestr(name, data)
archive.writestr('PACKAGE.json', json.dumps({'platform': 'windows', 'architecture': architecture,
'status': 'optional unsigned CLI/MCP client; install desktop for model setup', 'files': records}, indent=2))
os.replace(temporary_path, output)
finally:
temporary_path.unlink(missing_ok=True)
return architecture
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--executable', type=Path, required=True)
parser.add_argument('--resources', type=Path, default=ROOT / 'apps/desktop/src-tauri/resources')
parser.add_argument('--output', type=Path, default=ROOT / '.searchi/packages/searchi-cli_0.1.0_x64-013.zip')
args = parser.parse_args()
print(f"Packaged Windows {package(ROOT, args.executable, args.resources, args.output)} CLI: {args.output}")
if __name__ == '__main__':
main()
+64
View File
@@ -0,0 +1,64 @@
import hashlib
import json
from pathlib import Path
import tempfile
import unittest
import zipfile
from package_windows_cli import package
from test_package_windows import executable
import stage_extraction_worker as extraction
class CliPackageTests(unittest.TestCase):
def fixture(self, directory):
root = Path(directory)
(root / 'Cargo.toml').write_text('workspace')
(root / 'Cargo.lock').write_text('lock')
for crate in ('searchi-core', 'searchi-media', 'searchi-extract-worker'):
base = root / 'crates' / crate
(base / 'src').mkdir(parents=True)
(base / 'Cargo.toml').write_text(crate)
(base / 'src' / ('main.rs' if crate == 'searchi-extract-worker' else 'lib.rs')).write_text('// fixture')
(root / 'LICENSE').write_text('license')
cli = root / 'cli.exe'
helper = root / 'helper.exe'
executable(cli)
executable(helper)
resources = root / 'resources'
extraction.stage(helper, extraction.fingerprint(root), resources, root)
return root, cli, resources, root / 'client.zip'
def test_integrity_and_minimal_contents(self):
with tempfile.TemporaryDirectory() as directory:
root, cli, resources, output = self.fixture(directory)
self.assertEqual(package(root, cli, resources, output), 'x86_64')
with zipfile.ZipFile(output) as archive:
self.assertEqual(set(archive.namelist()), {'searchi-cli.exe', 'searchi-extract-worker.exe',
'extraction-worker.json', 'LICENSE.txt', 'PACKAGE.json'})
manifest = json.loads(archive.read('PACKAGE.json'))
for record in manifest['files']:
data = archive.read(record['path'])
self.assertEqual(len(data), record['bytes'])
self.assertEqual(hashlib.sha256(data).hexdigest(), record['sha256'])
def test_rejected_helper_preserves_output(self):
for mutation in ('missing', 'tampered', 'architecture'):
with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as directory:
root, cli, resources, output = self.fixture(directory)
output.write_bytes(b'previous')
helper = resources / 'searchi-extract-worker.exe'
if mutation == 'missing':
helper.unlink()
elif mutation == 'tampered':
helper.write_bytes(helper.read_bytes() + b'changed')
else:
executable(cli, 0xAA64)
with self.assertRaises(ValueError):
package(root, cli, resources, output)
self.assertEqual(output.read_bytes(), b'previous')
self.assertEqual(list(root.glob('*.partial')), [])
if __name__ == '__main__':
unittest.main()