Support optional CLI and MCP in Windows source archives

This commit is contained in:
karl committed 2026-10-13 11:55:16 +02:00
1 parent 4e277a107f
commit 2be6b3d2b4
4 files changed
+72 -5

No files matched your search

+25 -1
View File
@@ -11,6 +11,30 @@ Build the CLI with `cargo build --release -p searchi-cli --locked`. Create and
index files through the normal CLI/desktop workflow first. MCP requires an
explicit existing database; it never creates a new empty database after a typo.
Windows source archives can include the compiled CLI using
`python tools/package_windows.py --cli target/release/searchi.exe`. In that
archive, `searchi.exe` opens the desktop and `searchi-cli.exe` runs CLI/MCP
commands. Keep both beside the bundled runtime helpers. The published v0.1.0
installer does not include the CLI; this packaging option is for a subsequent
archive and does not change that release.
Packaging evidence, Windows VM 2026-10-13: 16 package tests passed in normal
and optimized Python modes, including CLI checksum inventory and rejection of
a mismatched CLI architecture without replacing the previous archive. The
locked release CLI built successfully. An actual local archive containing the
existing desktop release and this CLI passed size/SHA verification for all 40
files. Its extracted `searchi-cli.exe mcp --help` executed successfully outside
the build directory. This checks packaging and startup, not a new semantic MCP
session, GPU behavior or a clean-machine install.
Ignored archive: `.searchi/packages/searchi-windows-agent-preview.zip`,
152,098,987 bytes, SHA-256
`9721a657458f140dc9271b508c810ca52b7e4b89ccff8f1cd3edb053dbe591a0`.
CLI SHA-256:
`7b40a3959e5bca811f79e0a0da1e0cf77505ede703cf92c8394f79b9a6167ab5`.
The archive is local only; the Gitea attachment size limit previously rejected
even the smaller desktop archive. Published installer contents are unchanged.
For names/content only:
```sh
@@ -105,4 +129,4 @@ cargo test -p searchi-cli --test mcp
Default tests use genuine SQLite retrieval without downloaded model assets.
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
recorded checks; Windows CPU checks do not establish those releases.
recorded checks; Windows CPU checks do not establish those releases.
+13 -1
View File
@@ -210,11 +210,23 @@ python tools/embeddinggemma2/build_runtime.py --backend auto --jobs 2
python tools/embeddinggemma2/stage_runtime.py --backend auto
python tools/media-runtime/provision.py --stage
Set-Location apps/desktop
npm run tauri -- build --no-bundle
npm run tauri -- build --features custom-protocol --no-bundle
Set-Location ../..
python tools/package_windows.py
```
To include the CLI and read-only MCP server in this source archive:
```powershell
cargo build --release -p searchi-cli --locked
python tools/package_windows.py --cli target/release/searchi.exe
```
The additional binary is named `searchi-cli.exe`; `searchi.exe` remains the
desktop launcher. Its model cache and database must be selected explicitly,
as described in [agent integration](AGENT-INTEGRATION.md). This option does not
add the CLI to the already published v0.1.0 installer.
The generated `.searchi/packages/searchi-windows.zip` contains the desktop,
matching-architecture Google worker, FFmpeg/FFprobe and notices. It excludes
model caches, local settings and indexes. `PACKAGE.json` records SHA-256 for
+16 -3
View File
@@ -101,7 +101,7 @@ def pe_machine(path):
return struct.unpack_from("<H", signature, 4)[0]
def package(root, executable, output):
def package(root, executable, output, cli=None):
resources = root / "apps/desktop/src-tauri/resources"
machine = pe_machine(executable)
architecture = {0x8664: "x86_64", 0xAA64: "aarch64"}.get(machine)
@@ -145,6 +145,10 @@ def package(root, executable, output):
media = media_provenance(root, resources, architecture)
expected.update(media)
selected = [(executable, "searchi.exe"), (root / "LICENSE", "LICENSE.txt")]
if cli is not None:
if cli.is_symlink() or not cli.is_file() or not 0 < cli.stat().st_size <= MAX_MEMBER:
raise ValueError("CLI executable is missing, linked or oversized")
selected.append((cli, "searchi-cli.exe"))
for name in ["searchi-gemma-worker.exe", "LLAMA-LICENSE.txt", "MSVC-REDISTRIBUTION.txt", "vulkan-1.dll", "VULKAN-LICENSE.txt", "media-runtime/manifest.json",
"media-runtime/bin/ffmpeg.exe", "media-runtime/bin/ffprobe.exe",
"licenses/Inter-OFL.txt", "licenses/Fluent-MIT.txt", "media-runtime/licenses/LICENSE.txt"]:
@@ -162,7 +166,7 @@ def package(root, executable, output):
for path, name in selected:
if not path.is_file():
raise ValueError(f"Required package file is missing: {name}")
if path != executable and path != root / "LICENSE" and not path.resolve().is_relative_to(resources.resolve()):
if path not in (executable, root / "LICENSE", cli) and not path.resolve().is_relative_to(resources.resolve()):
raise ValueError("Resource resolves outside the staging directory")
if name.endswith((".exe", ".dll")) and pe_machine(path) != machine:
raise ValueError(f"Executable architecture differs: {name}")
@@ -194,6 +198,14 @@ def package(root, executable, output):
"First launch downloads approximately 865 MB of verified Google model files.\n"
"Choose Add folder to index your own files; ordinary search stays local.\n"
"This is an unsigned development build, not a certified production release.\n")
if cli is not None:
archive.writestr("AGENT-SETUP.txt",
"searchi-cli.exe provides command-line search and a read-only MCP server.\n"
"Run searchi-cli.exe --help, or searchi-cli.exe mcp --help.\n"
"Select your existing desktop index explicitly with --db; do not create another index accidentally.\n"
"For semantic MCP search also supply --model-dir and --embedding-worker.\n"
"Use the verified model cache prepared by the desktop and the bundled searchi-gemma-worker.exe.\n"
"See https://tea.karlcom.de/karl/searchi/src/branch/main/docs/AGENT-INTEGRATION.md\n")
os.replace(temporary_path, output)
finally:
temporary_path.unlink(missing_ok=True)
@@ -204,8 +216,9 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--executable", type=Path, default=ROOT / "apps/desktop/src-tauri/target/release/searchi-desktop.exe")
parser.add_argument("--output", type=Path, default=ROOT / ".searchi/packages/searchi-windows.zip")
parser.add_argument("--cli", type=Path, help="Include the compiled CLI/MCP client as searchi-cli.exe")
args = parser.parse_args()
architecture = package(ROOT, args.executable, args.output)
architecture = package(ROOT, args.executable, args.output, args.cli)
print(f"Packaged Windows {architecture}: {args.output}")
+18
View File
@@ -30,6 +30,24 @@ def inventory(resources):
class PackageTests(unittest.TestCase):
def test_optional_cli_manifest_and_architecture(self):
with tempfile.TemporaryDirectory() as directory:
root, _, app, output = self.fixture(directory)
cli = root / "build/cli.exe"
executable(cli)
package(root, app, output, cli)
with zipfile.ZipFile(output) as archive:
self.assertEqual(archive.read("searchi-cli.exe"), cli.read_bytes())
manifest = json.loads(archive.read("PACKAGE.json"))
record = next(row for row in manifest["files"] if row["path"] == "searchi-cli.exe")
self.assertEqual(record["sha256"], hashlib.sha256(cli.read_bytes()).hexdigest())
self.assertIn("--db", archive.read("AGENT-SETUP.txt").decode())
previous = output.read_bytes()
executable(cli, 0xAA64)
with self.assertRaisesRegex(ValueError, "architecture differs"):
package(root, app, output, cli)
self.assertEqual(output.read_bytes(), previous)
def fixture(self, directory):
root = Path(directory)
resources = root / "apps/desktop/src-tauri/resources"