agentacct 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {agentacct-0.2.0/src/agentacct.egg-info → agentacct-0.3.0}/PKG-INFO +1 -1
- {agentacct-0.2.0 → agentacct-0.3.0}/pyproject.toml +1 -1
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/agent_capabilities.py +25 -12
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/legacy_import.py +1 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/product_parity.py +1 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/cli.py +137 -12
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/client_usage.py +279 -19
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/install_guide.py +2 -1
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/mcp.py +100 -22
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/refreshable_usage.py +1 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/source_discovery.py +33 -7
- {agentacct-0.2.0 → agentacct-0.3.0/src/agentacct.egg-info}/PKG-INFO +1 -1
- {agentacct-0.2.0 → agentacct-0.3.0}/LICENSE +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/MANIFEST.in +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/README.md +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/setup.cfg +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/__init__.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/activation.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/agent_loop.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/agent_smoke.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/api.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/autostart.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/__init__.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/benchmark.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/cutover.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/lane_inventory.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/legacy_recovery.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/live_paths.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/migration_archive.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/migration_disposition_policy.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/read_canary.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/repository.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/safe_scratch.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/schema.sql +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/snapshot.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/sqlite.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/types.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/unsupported_legacy_routing.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical/v1_events.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical_day_cube.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical_live.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/canonical_read.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/capture/__init__.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/capture/adapters.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/capture/base.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/capture/manifests.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/capture/registry.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/capture/service.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/capture_runtime.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/confidence.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/connector_runtime.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/connectors/__init__.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/connectors/base.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/connectors/control.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/connectors/entire.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/connectors/openlit.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/connectors/paperclip.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/connectors/registry.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/context_bridge.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/control_plane.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/control_web.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/cost.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/deep_provider_validation.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/env_compat.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/evidence.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/evidence_html.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/evidence_product.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/evidence_rebuild_activation.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/evidence_rebuild_candidate.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/evidence_rebuild_snapshot.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/evidence_runtime.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/evidence_store.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/finding_disposition.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/hooks.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/ingestion_health.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/join_rules.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/localhost_guard.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/log_evidence.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/mcp_client_smoke.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/mechanical_checks.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/outcome.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/platform_support.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/policy.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/pricing_catalog.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/provider_smoke.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/proxy.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/reports.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/runner.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/service.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/session_observations.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/source_paths.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/source_policy.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/storage.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/store_merge.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/store_resolution.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/supervisor.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/task_continuations.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/task_identity.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/task_intelligence.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/task_outcome.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/task_projection.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/usage_cube.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/usage_truth.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/work_events.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/work_ledger.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agent_chronicle/wrappers.py +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agentacct.egg-info/SOURCES.txt +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agentacct.egg-info/dependency_links.txt +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agentacct.egg-info/entry_points.txt +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agentacct.egg-info/requires.txt +0 -0
- {agentacct-0.2.0 → agentacct-0.3.0}/src/agentacct.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentacct"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Local-first Agent Work Intelligence for coding agents: usage truth, recorded work, and honest joins"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -193,6 +193,15 @@ _OPENCODE_USAGE_FIXTURE = _verification(
|
|
|
193
193
|
"tests/test_client_usage.py::test_discover_opencode_usage_reads_json_event_stream_tokens_and_cost",
|
|
194
194
|
),
|
|
195
195
|
)
|
|
196
|
+
_OPENCODE_DB_FIXTURE = _verification(
|
|
197
|
+
"synthetic_fixture",
|
|
198
|
+
verified_at="2026-07-29",
|
|
199
|
+
evidence_refs=(
|
|
200
|
+
"tests/test_client_usage.py::test_discover_opencode_usage_reads_native_session_db",
|
|
201
|
+
"tests/test_client_usage.py::test_discover_opencode_usage_recomputes_cost_from_tokens_when_stored_zero",
|
|
202
|
+
"tests/test_client_usage.py::test_discover_opencode_usage_presence_flags_track_schema_columns",
|
|
203
|
+
),
|
|
204
|
+
)
|
|
196
205
|
_OPENCLAW_USAGE_FIXTURE = _verification(
|
|
197
206
|
"synthetic_fixture",
|
|
198
207
|
verified_at="2026-07-17",
|
|
@@ -439,8 +448,8 @@ _CLIENTS: tuple[dict[str, Any], ...] = (
|
|
|
439
448
|
"client": "opencode",
|
|
440
449
|
"display_name": "OpenCode",
|
|
441
450
|
"roadmap_phase": "phase_1",
|
|
442
|
-
"source_formats": ["captured/exported JSON event stream"],
|
|
443
|
-
"session_scope": "Usage-bearing step-finish exports
|
|
451
|
+
"source_formats": ["native opencode.db SQLite session rollup", "captured/exported JSON event stream"],
|
|
452
|
+
"session_scope": "Usage-bearing session rollups (SQLite) or step-finish exports.",
|
|
444
453
|
"zero_usage_observation": "unavailable",
|
|
445
454
|
"namespace_hardening": "not_hardened",
|
|
446
455
|
"verified_stability": _stability(
|
|
@@ -453,19 +462,22 @@ _CLIENTS: tuple[dict[str, Any], ...] = (
|
|
|
453
462
|
"capabilities": {
|
|
454
463
|
"session_discovery": _capability(
|
|
455
464
|
"experimental",
|
|
456
|
-
"Session IDs
|
|
465
|
+
"Session IDs from the native opencode.db session rollup, or captured/exported JSON step-finish streams as a fallback.",
|
|
457
466
|
activation="opt_in_project",
|
|
458
|
-
verification=
|
|
459
|
-
limitations=("
|
|
467
|
+
verification=_OPENCODE_DB_FIXTURE,
|
|
468
|
+
limitations=("Only usage-bearing sessions are emitted; zero-token sessions are absent.",),
|
|
460
469
|
),
|
|
461
470
|
"usage_import": _capability(
|
|
462
471
|
"experimental",
|
|
463
|
-
"
|
|
472
|
+
"Per-session input, output, reasoning, and cache token totals from the native SQLite session rollup (JSON export fallback); cost recomputed from tokens when the store records none.",
|
|
464
473
|
activation="opt_in_project",
|
|
465
|
-
verification=
|
|
466
|
-
limitations=(
|
|
474
|
+
verification=_OPENCODE_DB_FIXTURE,
|
|
475
|
+
limitations=(
|
|
476
|
+
"Synthetic-fixture only; per-message granularity is not imported (session totals only).",
|
|
477
|
+
"OpenCode usually stores cost 0, so cost is typically estimated_from_tokens; a nonzero stored cost is kept as client_reported.",
|
|
478
|
+
),
|
|
467
479
|
usage_basis="client_reported",
|
|
468
|
-
cost_basis="
|
|
480
|
+
cost_basis="estimated_from_tokens",
|
|
469
481
|
),
|
|
470
482
|
"mechanical_capture": _unavailable("No realtime OpenCode plugin adapter is implemented."),
|
|
471
483
|
"mcp_semantics": _capability(
|
|
@@ -477,9 +489,10 @@ _CLIENTS: tuple[dict[str, Any], ...] = (
|
|
|
477
489
|
),
|
|
478
490
|
"model_attribution": _capability(
|
|
479
491
|
"experimental",
|
|
480
|
-
"
|
|
492
|
+
"Model id and provider id parsed from the session rollup's model JSON (JSON-export model field as fallback).",
|
|
481
493
|
activation="opt_in_project",
|
|
482
|
-
|
|
494
|
+
verification=_OPENCODE_DB_FIXTURE,
|
|
495
|
+
limitations=("Per-message model binding is not imported; the session rollup carries one model per session.",),
|
|
483
496
|
),
|
|
484
497
|
"cache_read": _capability(
|
|
485
498
|
"experimental",
|
|
@@ -501,7 +514,7 @@ _CLIENTS: tuple[dict[str, Any], ...] = (
|
|
|
501
514
|
"agentacct renders the setup command but does not write OpenCode user config or install a plugin."
|
|
502
515
|
),
|
|
503
516
|
},
|
|
504
|
-
"limitations": ["Remain experimental until real fixtures, namespace hardening, and
|
|
517
|
+
"limitations": ["Remain experimental until real-client fixtures, namespace hardening, and per-message SQLite granularity exist."],
|
|
505
518
|
},
|
|
506
519
|
{
|
|
507
520
|
"client": "openclaw",
|
|
@@ -75,6 +75,7 @@ _LEGACY_CUMULATIVE_USAGE_SEMANTICS = frozenset(
|
|
|
75
75
|
CODEX_LINEAGE_DELTA_SEMANTICS,
|
|
76
76
|
"claude_assistant_message_usage_rows",
|
|
77
77
|
"opencode_step_finish_events",
|
|
78
|
+
"opencode_session_rollup",
|
|
78
79
|
"hermes_state_db_session_rows",
|
|
79
80
|
"openclaw_assistant_usage_rows",
|
|
80
81
|
}
|
|
@@ -670,6 +670,7 @@ _SOURCE_CUMULATIVE_USAGE_SEMANTICS = frozenset(
|
|
|
670
670
|
"codex_sqlite_tokens_used_fallback",
|
|
671
671
|
"claude_assistant_message_usage_rows",
|
|
672
672
|
"opencode_step_finish_events",
|
|
673
|
+
"opencode_session_rollup",
|
|
673
674
|
"hermes_state_db_session_rows",
|
|
674
675
|
"openclaw_assistant_usage_rows",
|
|
675
676
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import errno
|
|
3
4
|
import hashlib
|
|
4
5
|
import json
|
|
5
6
|
import math
|
|
@@ -7,6 +8,7 @@ import os
|
|
|
7
8
|
import shlex
|
|
8
9
|
import shutil
|
|
9
10
|
import signal
|
|
11
|
+
import socket
|
|
10
12
|
import stat
|
|
11
13
|
import sys
|
|
12
14
|
import tempfile
|
|
@@ -112,7 +114,7 @@ from .hooks import (
|
|
|
112
114
|
from .log_evidence import build_log_evidence_index, summarize_log_evidence_donor_rows
|
|
113
115
|
from . import install_guide
|
|
114
116
|
from .install_guide import full_prompt as install_guide_full_prompt, one_line_prompt as install_guide_one_line_prompt
|
|
115
|
-
from .mcp import TOOLS, SentinelMCPServer, run_mcp_event_workflow_smoke, serve_stdio
|
|
117
|
+
from .mcp import DEGRADED_NO_STORE_MESSAGE, TOOLS, SentinelMCPServer, run_mcp_event_workflow_smoke, serve_stdio
|
|
116
118
|
from .mcp_client_smoke import MCPClientSmokeError, assert_deepseek_mcp_client_smoke_passed, run_deepseek_mcp_client_smoke
|
|
117
119
|
from .outcome import (
|
|
118
120
|
apply_judge_result,
|
|
@@ -949,11 +951,34 @@ def _print_codex_mcp_setup(config_store_dir: Path | str, *, command: str = "agen
|
|
|
949
951
|
print(_codex_mcp_toml_block(config_store_dir, command=command).rstrip())
|
|
950
952
|
|
|
951
953
|
|
|
954
|
+
def _print_stale_registration_remediation(agent: str) -> None:
|
|
955
|
+
"""Tell the user to remove any leftover pre-rename server before adding agentacct.
|
|
956
|
+
|
|
957
|
+
A registration left over from the ``agent-sentinel`` / ``agent-chronicle``
|
|
958
|
+
era points at a command name that no longer ships (only ``agentacct`` does),
|
|
959
|
+
so the host tries to launch a missing binary — an ENOENT that reads to the
|
|
960
|
+
client as a crashed MCP server. Removing the dead entry first is the fix.
|
|
961
|
+
"""
|
|
962
|
+
console.print(
|
|
963
|
+
"First remove any stale pre-rename server — a leftover agent-sentinel/agent-chronicle "
|
|
964
|
+
"entry launches a command that no longer exists (ENOENT), which the client reports as a crash:"
|
|
965
|
+
)
|
|
966
|
+
if agent == "generic":
|
|
967
|
+
console.print(
|
|
968
|
+
"In the agent's MCP config, delete any server named 'agent-sentinel' or 'agent-chronicle' "
|
|
969
|
+
"(only 'agentacct' ships now), then add the definition below."
|
|
970
|
+
)
|
|
971
|
+
return
|
|
972
|
+
print(f"{agent} mcp remove agent-sentinel")
|
|
973
|
+
print(f"{agent} mcp remove agent-chronicle")
|
|
974
|
+
|
|
975
|
+
|
|
952
976
|
def _print_agent_mcp_preview(agent: str, config_store_dir: Path | str, *, command: str = "agentacct") -> None:
|
|
953
977
|
quoted_store_dir = shlex.quote(str(config_store_dir))
|
|
954
978
|
quoted_command = shlex.quote(command)
|
|
955
979
|
if agent == "generic":
|
|
956
980
|
console.print("Generic MCP-capable agent")
|
|
981
|
+
_print_stale_registration_remediation(agent)
|
|
957
982
|
console.print("Use this stdio MCP server definition in the agent's project-local MCP config:")
|
|
958
983
|
print(_claude_mcp_json(config_store_dir, command=command).rstrip())
|
|
959
984
|
console.print("Command form:")
|
|
@@ -962,16 +987,19 @@ def _print_agent_mcp_preview(agent: str, config_store_dir: Path | str, *, comman
|
|
|
962
987
|
if agent == "hermes":
|
|
963
988
|
console.print("Hermes")
|
|
964
989
|
console.print("Hermes manages MCP servers in the active Hermes profile, not in this repo.")
|
|
990
|
+
_print_stale_registration_remediation(agent)
|
|
965
991
|
console.print("Copy/paste command:")
|
|
966
992
|
print(f"hermes mcp add agentacct --command {quoted_command} --args mcp serve --store-dir {quoted_store_dir}")
|
|
967
993
|
return
|
|
968
994
|
if agent == "opencode":
|
|
969
995
|
console.print("OpenCode")
|
|
996
|
+
_print_stale_registration_remediation(agent)
|
|
970
997
|
console.print("Copy/paste command:")
|
|
971
998
|
print(f"opencode mcp add agentacct -- {quoted_command} mcp serve --store-dir {quoted_store_dir}")
|
|
972
999
|
return
|
|
973
1000
|
if agent == "openclaw":
|
|
974
1001
|
console.print("OpenClaw")
|
|
1002
|
+
_print_stale_registration_remediation(agent)
|
|
975
1003
|
console.print("Copy/paste command:")
|
|
976
1004
|
print(
|
|
977
1005
|
"openclaw mcp add agentacct "
|
|
@@ -1233,7 +1261,7 @@ def onboard(
|
|
|
1233
1261
|
)
|
|
1234
1262
|
elif opencode_row is not None:
|
|
1235
1263
|
console.print(
|
|
1236
|
-
"OpenCode
|
|
1264
|
+
"OpenCode was detected but no single importable store resolved; ensure one home holds an opencode.db session store (or exported JSON) and select it with `--opencode-home`."
|
|
1237
1265
|
)
|
|
1238
1266
|
else:
|
|
1239
1267
|
console.print("No readable known local usage source was found.")
|
|
@@ -3235,15 +3263,31 @@ def mcp_serve(
|
|
|
3235
3263
|
"""Serve agentacct MCP tools over stdio.
|
|
3236
3264
|
|
|
3237
3265
|
This exposes safe report/outcome/value primitives and does not call paid judge APIs.
|
|
3238
|
-
Store resolution
|
|
3239
|
-
|
|
3266
|
+
Store resolution stays strict — the MCP client controls this process's cwd,
|
|
3267
|
+
so a silently wrong ledger is never acceptable — but a server that EXITS reads
|
|
3268
|
+
to the host as a crash. So an unresolvable store starts a degraded-but-connected
|
|
3269
|
+
session instead: initialize and tools/list still answer, recording tools return
|
|
3270
|
+
a legible "no store configured" error, and no store is ever silently created.
|
|
3240
3271
|
"""
|
|
3272
|
+
# As an MCP server this process's exit is the ONLY liveness signal the host
|
|
3273
|
+
# has: an exit at startup reads as a crash. So an unresolvable store must
|
|
3274
|
+
# NOT exit — it starts a degraded-but-connected session (initialize +
|
|
3275
|
+
# tools/list still answer; recording tools return a legible error). We still
|
|
3276
|
+
# never silently create or pick a store (honesty rule): degraded_reason is
|
|
3277
|
+
# set and no store path is resolved.
|
|
3278
|
+
resolved: Path | None = None
|
|
3279
|
+
degraded_reason: str | None = None
|
|
3241
3280
|
if store_dir is None:
|
|
3242
3281
|
try:
|
|
3243
3282
|
resolved = resolve_store_dir(None).path
|
|
3244
3283
|
except StoreResolutionError as exc:
|
|
3245
|
-
print(
|
|
3246
|
-
|
|
3284
|
+
print(
|
|
3285
|
+
f"agentacct mcp serve: {exc}\n"
|
|
3286
|
+
"Starting a degraded MCP session (connected, but not recording): restart the server with "
|
|
3287
|
+
f"--store-dir <absolute path> or set {ENV_STORE_DIR}=<absolute path>.",
|
|
3288
|
+
file=sys.stderr,
|
|
3289
|
+
)
|
|
3290
|
+
degraded_reason = DEGRADED_NO_STORE_MESSAGE
|
|
3247
3291
|
else:
|
|
3248
3292
|
expanded = store_dir.expanduser()
|
|
3249
3293
|
if expanded.is_absolute():
|
|
@@ -3258,7 +3302,7 @@ def mcp_serve(
|
|
|
3258
3302
|
# named the store, which beats a dead server.
|
|
3259
3303
|
try:
|
|
3260
3304
|
resolution = resolve_store_dir(None, env={})
|
|
3261
|
-
except StoreResolutionError
|
|
3305
|
+
except StoreResolutionError:
|
|
3262
3306
|
try:
|
|
3263
3307
|
env_value = store_env_dir_value(os.environ) or ""
|
|
3264
3308
|
except StoreResolutionError:
|
|
@@ -3284,13 +3328,19 @@ def mcp_serve(
|
|
|
3284
3328
|
)
|
|
3285
3329
|
print(
|
|
3286
3330
|
"Fix: re-run `agentacct setup mcp --agent <claude-code|codex> --write` in the project root to "
|
|
3287
|
-
f"write an absolute store path into the MCP config, or set {ENV_STORE_DIR}=<absolute path>."
|
|
3331
|
+
f"write an absolute store path into the MCP config, or set {ENV_STORE_DIR}=<absolute path>. "
|
|
3332
|
+
"Starting a degraded MCP session (connected, but not recording) until then.",
|
|
3288
3333
|
file=sys.stderr,
|
|
3289
3334
|
)
|
|
3290
|
-
|
|
3335
|
+
degraded_reason = DEGRADED_NO_STORE_MESSAGE
|
|
3291
3336
|
else:
|
|
3292
3337
|
anchor = resolution.project_root if resolution.project_root is not None else resolution.path.parent.parent
|
|
3293
3338
|
resolved = anchor / expanded
|
|
3339
|
+
if degraded_reason is not None:
|
|
3340
|
+
# No resolvable store: stay connected but refuse to record. serve_stdio
|
|
3341
|
+
# gets store_dir=None so it never constructs (or creates) a store.
|
|
3342
|
+
serve_stdio(store_dir=None, degraded_reason=degraded_reason)
|
|
3343
|
+
return
|
|
3294
3344
|
print(f"agentacct mcp serve: store={resolved}", file=sys.stderr)
|
|
3295
3345
|
serve_stdio(store_dir=resolved)
|
|
3296
3346
|
|
|
@@ -8006,8 +8056,55 @@ def usage_truth_table_command(
|
|
|
8006
8056
|
console.print(f"- {row['integration']}: {row['setup_path']}")
|
|
8007
8057
|
|
|
8008
8058
|
|
|
8059
|
+
_SERVE_PORT_FALLBACK_SPAN = 20
|
|
8060
|
+
"""How many ports past the default the dashboard probes before giving up."""
|
|
8061
|
+
|
|
8062
|
+
|
|
8063
|
+
def _probe_port_free(host: str, port: int) -> bool:
|
|
8064
|
+
"""Return True if ``(host, port)`` can be bound right now.
|
|
8065
|
+
|
|
8066
|
+
Mirrors uvicorn's own socket setup (``SO_REUSEADDR``) so the probe reflects
|
|
8067
|
+
what the server will attempt a moment later. There is an unavoidable TOCTOU
|
|
8068
|
+
window between this check and uvicorn's bind, but for a localhost dashboard
|
|
8069
|
+
the practical risk is negligible.
|
|
8070
|
+
"""
|
|
8071
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
8072
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
8073
|
+
try:
|
|
8074
|
+
sock.bind((host, port))
|
|
8075
|
+
except OSError:
|
|
8076
|
+
return False
|
|
8077
|
+
return True
|
|
8078
|
+
|
|
8079
|
+
|
|
8080
|
+
def _select_serve_port(
|
|
8081
|
+
host: str,
|
|
8082
|
+
port: int,
|
|
8083
|
+
*,
|
|
8084
|
+
allow_fallback: bool,
|
|
8085
|
+
max_offset: int = _SERVE_PORT_FALLBACK_SPAN,
|
|
8086
|
+
) -> int:
|
|
8087
|
+
"""Pick a bindable port for the local dashboard.
|
|
8088
|
+
|
|
8089
|
+
When ``allow_fallback`` is False (the user passed an explicit ``--port``) the
|
|
8090
|
+
requested port must be free or an ``OSError`` is raised so the caller can
|
|
8091
|
+
fail with an actionable message. When True (the default-port path) a busy
|
|
8092
|
+
port advances through ``port+1 .. port+max_offset`` and the first free port is
|
|
8093
|
+
returned; ``OSError`` is raised only if the whole range is occupied.
|
|
8094
|
+
"""
|
|
8095
|
+
if _probe_port_free(host, port):
|
|
8096
|
+
return port
|
|
8097
|
+
if not allow_fallback:
|
|
8098
|
+
raise OSError(errno.EADDRINUSE, f"port {port} is already in use")
|
|
8099
|
+
for candidate in range(port + 1, port + max_offset + 1):
|
|
8100
|
+
if _probe_port_free(host, candidate):
|
|
8101
|
+
return candidate
|
|
8102
|
+
raise OSError(errno.EADDRINUSE, f"no free port in range {port}-{port + max_offset}")
|
|
8103
|
+
|
|
8104
|
+
|
|
8009
8105
|
@app.command("serve")
|
|
8010
8106
|
def serve(
|
|
8107
|
+
ctx: typer.Context,
|
|
8011
8108
|
host: Annotated[str, typer.Option(help="Bind host. Default is 127.0.0.1 for local dashboard safety.")] = "127.0.0.1",
|
|
8012
8109
|
port: Annotated[int, typer.Option(help="Bind port for the local dashboard and event API.")] = 8765,
|
|
8013
8110
|
store_dir: Annotated[
|
|
@@ -8016,7 +8113,13 @@ def serve(
|
|
|
8016
8113
|
] = None,
|
|
8017
8114
|
allow_host: Annotated[Optional[list[str]], typer.Option("--allow-host", help=_ALLOW_HOST_HELP)] = None,
|
|
8018
8115
|
) -> None:
|
|
8019
|
-
"""Serve the local dashboard on localhost with local usage discovery enabled.
|
|
8116
|
+
"""Serve the local dashboard on localhost with local usage discovery enabled.
|
|
8117
|
+
|
|
8118
|
+
The default port (8765) auto-advances to the next free port when it is busy,
|
|
8119
|
+
so the dashboard never fails to start just because a port is taken. An
|
|
8120
|
+
explicit ``--port`` is honored strictly: if that exact port is occupied the
|
|
8121
|
+
command fails rather than silently moving.
|
|
8122
|
+
"""
|
|
8020
8123
|
import uvicorn
|
|
8021
8124
|
|
|
8022
8125
|
if host not in {"127.0.0.1", "localhost"}:
|
|
@@ -8026,7 +8129,29 @@ def serve(
|
|
|
8026
8129
|
# not a server crash mid-startup.
|
|
8027
8130
|
resolution = _resolve_dashboard_cli_store_dir(store_dir)
|
|
8028
8131
|
effective_store_dir = resolution.path
|
|
8029
|
-
|
|
8132
|
+
# Only auto-advance when the port came from the default; an explicit --port
|
|
8133
|
+
# is a specific request and must fail loudly instead of moving. Compare the
|
|
8134
|
+
# click ParameterSource by name -- typer's runtime enum is not identical to
|
|
8135
|
+
# click.core's, so identity/value comparisons are unreliable across versions.
|
|
8136
|
+
port_source = ctx.get_parameter_source("port")
|
|
8137
|
+
port_explicitly_set = getattr(port_source, "name", "DEFAULT") != "DEFAULT"
|
|
8138
|
+
try:
|
|
8139
|
+
bound_port = _select_serve_port(host, port, allow_fallback=not port_explicitly_set)
|
|
8140
|
+
except OSError:
|
|
8141
|
+
if port_explicitly_set:
|
|
8142
|
+
console.print(
|
|
8143
|
+
f"Port {port} is already in use. Pass a free --port, or omit --port to let "
|
|
8144
|
+
"agentacct pick the next free port automatically."
|
|
8145
|
+
)
|
|
8146
|
+
else:
|
|
8147
|
+
console.print(
|
|
8148
|
+
f"Ports {port}-{port + _SERVE_PORT_FALLBACK_SPAN} are all in use. "
|
|
8149
|
+
"Free one up or pass an explicit --port."
|
|
8150
|
+
)
|
|
8151
|
+
raise typer.Exit(1)
|
|
8152
|
+
if bound_port != port:
|
|
8153
|
+
console.print(f"Port {port} was busy; dashboard on http://{host}:{bound_port}")
|
|
8154
|
+
console.print(f"Starting agentacct dashboard: http://{host}:{bound_port}")
|
|
8030
8155
|
if resolution.source == "global":
|
|
8031
8156
|
console.print("Dashboard scope: All projects (machine-wide store).")
|
|
8032
8157
|
_warn_dashboard_mcp_store_shadow(effective_store_dir)
|
|
@@ -8045,7 +8170,7 @@ def serve(
|
|
|
8045
8170
|
extra_allowed_hosts=tuple(allow_host or ()),
|
|
8046
8171
|
),
|
|
8047
8172
|
host=host,
|
|
8048
|
-
port=
|
|
8173
|
+
port=bound_port,
|
|
8049
8174
|
log_level="info",
|
|
8050
8175
|
)
|
|
8051
8176
|
|