Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions bin/agent-memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ function pythonPackageSpec() {
return PYTHON_PACKAGE_NAME;
}

function readStdinForChild() {
if (process.stdin.isTTY) {
return undefined;
}
return readFileSync(0, 'utf8');
}

function buildInvocation(args) {
const forcedPython = process.env.AGENT_MEMORY_PYTHON_EXECUTABLE;
if (forcedPython) {
Expand Down Expand Up @@ -86,6 +93,7 @@ function main() {
}

const child = spawnSync(invocation.command, invocation.args, {
input: readStdinForChild(),
stdio: 'pipe',
encoding: 'utf8',
env: process.env,
Expand Down
32 changes: 31 additions & 1 deletion scripts/smoke_published_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,12 @@ def _read_package_version(repo_root: Path) -> str:
return version


def _run(command: Sequence[str], *, cwd: Path, env: dict[str, str], timeout: int) -> PublishedSmokeStep:
def _run(command: Sequence[str], *, cwd: Path, env: dict[str, str], timeout: int, stdin: str | None = None) -> PublishedSmokeStep:
result = subprocess.run(
list(command),
cwd=cwd,
env=env,
input=stdin,
capture_output=True,
text=True,
timeout=timeout,
Expand Down Expand Up @@ -98,6 +99,12 @@ def _assert_doctor_ok(step: PublishedSmokeStep) -> None:
raise RuntimeError(f"doctor field {key} was not true for {step.name}: {step.stdout}")


def _assert_hook_ok(step: PublishedSmokeStep) -> None:
payload = _parse_json(step)
if not isinstance(payload.get("context"), str):
raise RuntimeError(f"hook did not emit a context string for {step.name}: {step.stdout}")


def _assert_registry_version(step: PublishedSmokeStep, version: str) -> None:
if step.returncode != 0:
raise RuntimeError(f"registry version lookup failed: {step.name}\n{step.stderr}")
Expand All @@ -115,16 +122,19 @@ def build_command_matrix(version: str, *, include_pipx: bool = True, python_exec
SmokeCommand("npm-exec-help", ["npm", "exec", "--yes", "--package", npm_spec, "agent-memory", "--", "--help"]),
SmokeCommand("npm-exec-bootstrap", ["npm", "exec", "--yes", "--package", npm_spec, "agent-memory", "--", "bootstrap"]),
SmokeCommand("npm-exec-doctor", ["npm", "exec", "--yes", "--package", npm_spec, "agent-memory", "--", "doctor"]),
SmokeCommand("npm-exec-hook", ["npm", "exec", "--yes", "--package", npm_spec, "agent-memory", "--", "hermes-pre-llm-hook"]),
SmokeCommand("uvx-help", ["uvx", "--from", python_spec, "agent-memory", "--help"]),
SmokeCommand("uvx-bootstrap", ["uvx", "--from", python_spec, "agent-memory", "bootstrap"]),
SmokeCommand("uvx-doctor", ["uvx", "--from", python_spec, "agent-memory", "doctor"]),
SmokeCommand("uvx-hook", ["uvx", "--from", python_spec, "agent-memory", "hermes-pre-llm-hook"]),
]
if include_pipx:
commands.extend(
[
SmokeCommand("pipx-help", ["pipx", "run", "--python", pipx_python, "--spec", python_spec, "agent-memory", "--help"]),
SmokeCommand("pipx-bootstrap", ["pipx", "run", "--python", pipx_python, "--spec", python_spec, "agent-memory", "bootstrap"]),
SmokeCommand("pipx-doctor", ["pipx", "run", "--python", pipx_python, "--spec", python_spec, "agent-memory", "doctor"]),
SmokeCommand("pipx-hook", ["pipx", "run", "--python", pipx_python, "--spec", python_spec, "agent-memory", "hermes-pre-llm-hook"]),
]
)
return commands
Expand All @@ -143,9 +153,26 @@ def _stateful_surface_name(command: SmokeCommand) -> str:
def _command_with_paths(command: SmokeCommand, *, db_path: Path, config_path: Path) -> list[str]:
if command.name.endswith("bootstrap") or command.name.endswith("doctor"):
return [*command.command, str(db_path), "--config-path", str(config_path)]
if command.name.endswith("hook"):
return [*command.command, str(db_path)]
return command.command


def _stdin_for_command(command: SmokeCommand) -> str | None:
if not command.name.endswith("hook"):
return None
return json.dumps(
{
"hook_event_name": "pre_llm_call",
"tool_name": None,
"tool_input": None,
"session_id": "published-install-smoke",
"cwd": "/tmp",
"extra": {"user_message": "published install smoke hook stdin forwarding check"},
}
)


def _tool_is_available(command: SmokeCommand) -> bool:
return shutil.which(command.command[0]) is not None

Expand Down Expand Up @@ -178,6 +205,7 @@ def _run_once(version: str, *, timeout: int, include_pipx: bool) -> dict[str, ob
cwd=root,
env=env,
timeout=timeout,
stdin=_stdin_for_command(command),
)
steps.append(step)

Expand All @@ -191,6 +219,8 @@ def _run_once(version: str, *, timeout: int, include_pipx: bool) -> dict[str, ob
raise RuntimeError(f"bootstrap did not initialize db for {command.name}: {step.stdout}")
elif command.name.endswith("doctor"):
_assert_doctor_ok(step)
elif command.name.endswith("hook"):
_assert_hook_ok(step)

command_results[command.name] = {
"returncode": step.returncode,
Expand Down
60 changes: 60 additions & 0 deletions tests/test_npm_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import sys
from pathlib import Path

from agent_memory.core.curation import approve_fact, create_candidate_fact
from agent_memory.core.ingestion import ingest_source_text
from agent_memory.storage.sqlite import initialize_database


REPO_ROOT = Path(__file__).resolve().parents[1]
WRAPPER_PATH = REPO_ROOT / "bin" / "agent-memory.js"
Expand Down Expand Up @@ -90,6 +94,62 @@ def test_npm_wrapper_help_passthrough(tmp_path: Path) -> None:
assert "hermes-doctor" in result.stdout


def test_npm_wrapper_forwards_stdin_to_hermes_hook(tmp_path: Path) -> None:
home = tmp_path / "home"
home.mkdir()
db_path = tmp_path / "wrapper-hermes-hook.db"
initialize_database(db_path)
source = ingest_source_text(
db_path=db_path,
source_type="transcript",
content="NPM launcher stdin forwarding uses target NPM_STDIN_OK.",
metadata={"project": "npm-wrapper"},
)
fact = create_candidate_fact(
db_path=db_path,
subject_ref="NPM launcher stdin forwarding",
predicate="target",
object_ref_or_value="NPM_STDIN_OK",
evidence_ids=[source.id],
scope="project:npm-wrapper",
confidence=0.96,
)
approve_fact(db_path=db_path, fact_id=fact.id)
env = _wrapper_env(home)
hook_payload = {
"hook_event_name": "pre_llm_call",
"tool_name": None,
"tool_input": None,
"session_id": "npm-wrapper-stdin-test",
"cwd": str(tmp_path),
"extra": {"user_message": "What target does NPM launcher stdin forwarding use?"},
}

result = subprocess.run(
[
"node",
str(WRAPPER_PATH),
"hermes-pre-llm-hook",
str(db_path),
"--preferred-scope",
"project:npm-wrapper",
"--top-k",
"1",
"--max-prompt-lines",
"8",
],
cwd=REPO_ROOT,
env=env,
input=json.dumps(hook_payload),
capture_output=True,
text=True,
)

assert result.returncode == 0, result.stderr
payload = json.loads(result.stdout)
assert "NPM_STDIN_OK" in payload["context"]


def test_npm_wrapper_pins_python_package_to_npm_version(tmp_path: Path) -> None:
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
Expand Down
26 changes: 26 additions & 0 deletions tests/test_published_install_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,18 @@ def test_published_install_command_matrix_pins_exact_package_versions() -> None:
"@cafitac/agent-memory@1.2.3",
"agent-memory",
]
assert by_name["npm-exec-hook"][:6] == [
"npm",
"exec",
"--yes",
"--package",
"@cafitac/agent-memory@1.2.3",
"agent-memory",
]
assert by_name["uvx-bootstrap"][:3] == ["uvx", "--from", "cafitac-agent-memory==1.2.3"]
assert by_name["uvx-hook"][:3] == ["uvx", "--from", "cafitac-agent-memory==1.2.3"]
assert by_name["pipx-bootstrap"][:6] == ["pipx", "run", "--python", "/usr/bin/python3.11", "--spec", "cafitac-agent-memory==1.2.3"]
assert by_name["pipx-hook"][:6] == ["pipx", "run", "--python", "/usr/bin/python3.11", "--spec", "cafitac-agent-memory==1.2.3"]


def test_published_install_command_matrix_can_skip_pipx() -> None:
Expand All @@ -46,9 +56,11 @@ def test_published_install_command_matrix_can_skip_pipx() -> None:
"npm-exec-help",
"npm-exec-bootstrap",
"npm-exec-doctor",
"npm-exec-hook",
"uvx-help",
"uvx-bootstrap",
"uvx-doctor",
"uvx-hook",
}


Expand Down Expand Up @@ -89,11 +101,25 @@ def test_published_install_script_appends_isolated_bootstrap_paths(tmp_path: Pat
def test_stateful_smoke_commands_share_surface_directories() -> None:
assert smoke_published_install._stateful_surface_name(SmokeCommand("npm-exec-bootstrap", [])) == "npm-exec"
assert smoke_published_install._stateful_surface_name(SmokeCommand("npm-exec-doctor", [])) == "npm-exec"
assert smoke_published_install._stateful_surface_name(SmokeCommand("npm-exec-hook", [])) == "npm-exec"
assert smoke_published_install._stateful_surface_name(SmokeCommand("uvx-bootstrap", [])) == "uvx"
assert smoke_published_install._stateful_surface_name(SmokeCommand("uvx-doctor", [])) == "uvx"
assert smoke_published_install._stateful_surface_name(SmokeCommand("uvx-hook", [])) == "uvx"
assert smoke_published_install._stateful_surface_name(SmokeCommand("npm-registry-version", [])) == "npm-registry-version"


def test_published_install_script_appends_isolated_hook_paths(tmp_path: Path) -> None:
command = SmokeCommand("npm-exec-hook", ["npm", "exec", "--yes", "--package", "@cafitac/agent-memory@1.2.3", "agent-memory", "--", "hermes-pre-llm-hook"])

argv = smoke_published_install._command_with_paths(
command,
db_path=tmp_path / "memory.db",
config_path=tmp_path / "hermes.yaml",
)

assert argv[-1:] == [str(tmp_path / "memory.db")]


def test_published_install_script_rejects_bad_doctor_payload() -> None:
step = PublishedSmokeStep(
name="doctor",
Expand Down