Publish matching Windows CLI MCP package with document helper
This commit is contained in:
1 parent
a8249160ec
commit
f1028e2689
4 files changed
+154
-7
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -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()
|
||||||
Reference in new issue
Block a user