agentgraph-server 0.5.4__tar.gz → 0.6.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.
Files changed (95) hide show
  1. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/PKG-INFO +1 -1
  2. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/backends/sqlite/backend.py +16 -8
  3. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/cli.py +58 -4
  4. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/connectors/base.py +1 -0
  5. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/PKG-INFO +1 -1
  6. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/pyproject.toml +1 -1
  7. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_cli.py +115 -0
  8. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_connectors.py +6 -0
  9. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_schema.py +97 -0
  10. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/.agents/skills/AgentGraph/SKILL.md +0 -0
  11. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/.agents/skills/AgentGraph/references/commands.md +0 -0
  12. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/.agents/skills/AgentGraph/references/data-model.md +0 -0
  13. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/.agents/skills/AgentGraph/references/operations.md +0 -0
  14. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/.agents/skills/slack-auth/SKILL.md +0 -0
  15. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/LICENSE +0 -0
  16. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/README.md +0 -0
  17. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/__init__.py +0 -0
  18. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/auth/__init__.py +0 -0
  19. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/auth/credentials.py +0 -0
  20. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/backends/__init__.py +0 -0
  21. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/backends/sqlite/__init__.py +0 -0
  22. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/backends/sqlite/schema.sql +0 -0
  23. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/backends/sqlite/vector.py +0 -0
  24. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/cli_query.py +0 -0
  25. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/cli_sync.py +0 -0
  26. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/config.py +0 -0
  27. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/connectors/__init__.py +0 -0
  28. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/connectors/command_effects.py +0 -0
  29. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/connectors/registry.py +0 -0
  30. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/connectors/status.py +0 -0
  31. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/core/__init__.py +0 -0
  32. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/core/context.py +0 -0
  33. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/core/runtime.py +0 -0
  34. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/core/storage.py +0 -0
  35. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/demo.py +0 -0
  36. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/demo_fixtures/reliable-webhooks.md +0 -0
  37. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/demo_fixtures/retry-guidance.md +0 -0
  38. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/graph/__init__.py +0 -0
  39. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/graph/bookmark.py +0 -0
  40. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/graph/delete.py +0 -0
  41. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/graph/download.py +0 -0
  42. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/graph/embeddings.py +0 -0
  43. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/graph/expiration.py +0 -0
  44. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/graph/fetch.py +0 -0
  45. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/graph/link.py +0 -0
  46. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/graph/operations.py +0 -0
  47. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/graph/person.py +0 -0
  48. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/graph/query.py +0 -0
  49. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/graph/upsert.py +0 -0
  50. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/logging.py +0 -0
  51. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/mcp/__init__.py +0 -0
  52. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/mcp/server.py +0 -0
  53. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/perf.py +0 -0
  54. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/server/__init__.py +0 -0
  55. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/server/app.py +0 -0
  56. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/server/cli_api.py +0 -0
  57. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/server/meta_api.py +0 -0
  58. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/server/observation.py +0 -0
  59. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/server/router.py +0 -0
  60. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/server/static/viewer.html +0 -0
  61. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/server/sync.py +0 -0
  62. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/server/sync_api.py +0 -0
  63. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph/skills.py +0 -0
  64. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/SOURCES.txt +0 -0
  65. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/dependency_links.txt +0 -0
  66. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/entry_points.txt +0 -0
  67. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/requires.txt +0 -0
  68. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/agentgraph_server.egg-info/top_level.txt +0 -0
  69. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/setup.cfg +0 -0
  70. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_auth.py +0 -0
  71. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_benchmarks.py +0 -0
  72. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_browse.py +0 -0
  73. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_command_effects.py +0 -0
  74. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_config.py +0 -0
  75. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_discord_attachments.py +0 -0
  76. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_docs_build.py +0 -0
  77. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_embeddings.py +0 -0
  78. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_expiration.py +0 -0
  79. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_fetch.py +0 -0
  80. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_graph_operations.py +0 -0
  81. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_launch_demo.py +0 -0
  82. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_logging.py +0 -0
  83. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_observation.py +0 -0
  84. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_query.py +0 -0
  85. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_registry.py +0 -0
  86. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_release_metadata.py +0 -0
  87. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_router.py +0 -0
  88. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_rss_connector.py +0 -0
  89. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_server_imports.py +0 -0
  90. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_slack_auth_artifacts.py +0 -0
  91. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_slack_oauth.py +0 -0
  92. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_sync.py +0 -0
  93. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_upsert.py +0 -0
  94. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_viewer_layout_contract.py +0 -0
  95. {agentgraph_server-0.5.4 → agentgraph_server-0.6.0}/tests/test_web_connector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentgraph-server
3
- Version: 0.5.4
3
+ Version: 0.6.0
4
4
  Summary: A local-first CLI and MCP server that turns selected digital sources into a searchable graph for the AI agent you already use.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -64,7 +64,7 @@ _LIST_PAGE_ORDER_BY = {
64
64
  _COLUMN_FILTERS = {"platform", "platform_entity_id", "entity_type"}
65
65
  _FTS_DELETE_CHUNK_SIZE = 500
66
66
  _BUSY_TIMEOUT_MS = 5_000
67
- _SCHEMA_VERSION = 2
67
+ _SCHEMA_VERSION = 3
68
68
 
69
69
 
70
70
  def _now() -> str:
@@ -208,7 +208,9 @@ class SQLiteBackend(StorageBackend):
208
208
  async def _run_migrations(self) -> None:
209
209
  conn = self._conn_or_raise()
210
210
  cursor = await conn.execute("PRAGMA table_info(entities)")
211
- entity_columns = {str(row["name"]): bool(row["notnull"]) for row in await cursor.fetchall()}
211
+ table_info = await cursor.fetchall()
212
+ entity_columns = {str(row["name"]): bool(row["notnull"]) for row in table_info}
213
+ entity_defaults = {str(row["name"]): row["dflt_value"] for row in table_info}
212
214
  columns = set(entity_columns)
213
215
  if "cumulative_dwell_ms" in columns:
214
216
  await conn.execute(
@@ -234,6 +236,8 @@ class SQLiteBackend(StorageBackend):
234
236
  "last_accessed" in columns
235
237
  or not entity_columns.get("created_at", False)
236
238
  or not entity_columns.get("updated_at", False)
239
+ or entity_defaults.get("created_at") is None
240
+ or entity_defaults.get("updated_at") is None
237
241
  or entity_columns.get("observed_at", False)
238
242
  or "source_created_at" not in columns
239
243
  or "source_updated_at" not in columns
@@ -370,8 +374,8 @@ class SQLiteBackend(StorageBackend):
370
374
  content TEXT,
371
375
  content_embedding BLOB,
372
376
  metadata TEXT NOT NULL DEFAULT '{}',
373
- created_at TEXT NOT NULL,
374
- updated_at TEXT NOT NULL,
377
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
378
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
375
379
  source_created_at TEXT,
376
380
  source_updated_at TEXT,
377
381
  synced_at TEXT,
@@ -660,8 +664,8 @@ class SQLiteBackend(StorageBackend):
660
664
  """
661
665
  INSERT INTO entities
662
666
  (id, entity_type, platform, platform_entity_id, title, metadata,
663
- retention_policy, retention_parent_id)
664
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
667
+ created_at, updated_at, retention_policy, retention_parent_id)
668
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
665
669
  ON CONFLICT (platform, platform_entity_id) DO UPDATE SET
666
670
  entity_type = entities.entity_type
667
671
  RETURNING id
@@ -673,6 +677,8 @@ class SQLiteBackend(StorageBackend):
673
677
  e.platform_entity_id,
674
678
  e.title,
675
679
  json.dumps(dict(e.metadata)),
680
+ now,
681
+ now,
676
682
  e.retention_policy,
677
683
  parent_id,
678
684
  ],
@@ -756,8 +762,8 @@ class SQLiteBackend(StorageBackend):
756
762
  INSERT INTO entities
757
763
  (id, entity_type, platform, platform_entity_id, title, content,
758
764
  content_embedding, metadata, source_created_at, source_updated_at,
759
- synced_at, retention_policy, retention_parent_id)
760
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
765
+ synced_at, created_at, updated_at, retention_policy, retention_parent_id)
766
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
761
767
  ON CONFLICT (platform, platform_entity_id) DO UPDATE SET
762
768
  entity_type = CASE WHEN entities.entity_type = 'Document' THEN EXCLUDED.entity_type ELSE entities.entity_type END,
763
769
  title = COALESCE(EXCLUDED.title, entities.title),
@@ -784,6 +790,8 @@ class SQLiteBackend(StorageBackend):
784
790
  source_created,
785
791
  source_updated,
786
792
  now,
793
+ now,
794
+ local_updated,
787
795
  e.retention_policy,
788
796
  parent_id,
789
797
  local_updated,
@@ -4,8 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import json as _json
7
+ import urllib.error
8
+ import urllib.request
7
9
  from collections.abc import Iterator
8
10
  from contextlib import contextmanager
11
+ from importlib.metadata import PackageNotFoundError
12
+ from importlib.metadata import version as package_version
9
13
  from typing import TYPE_CHECKING, cast
10
14
 
11
15
  import typer
@@ -18,6 +22,33 @@ app = typer.Typer(
18
22
  help="Local knowledge graph for AI agents.",
19
23
  no_args_is_help=True,
20
24
  )
25
+
26
+
27
+ def _version_callback(value: bool) -> None:
28
+ if not value:
29
+ return
30
+
31
+ try:
32
+ current = package_version("agentgraph-server")
33
+ except PackageNotFoundError:
34
+ current = "unknown"
35
+ typer.echo(f"agentgraph {current}")
36
+ raise typer.Exit()
37
+
38
+
39
+ @app.callback()
40
+ def main(
41
+ version: bool = typer.Option(
42
+ False,
43
+ "--version",
44
+ callback=_version_callback,
45
+ is_eager=True,
46
+ help="Show the installed version and exit.",
47
+ ),
48
+ ) -> None:
49
+ """Configure global CLI options."""
50
+
51
+
21
52
  demo_app = typer.Typer(
22
53
  help="Create reproducible local demo graphs.",
23
54
  no_args_is_help=True,
@@ -50,6 +81,20 @@ def _status_label(status: str) -> str:
50
81
  )
51
82
 
52
83
 
84
+ def _server_is_running() -> bool:
85
+ """Return whether the configured AgentGraph server responds to health checks."""
86
+ from agentgraph.config import get_settings
87
+
88
+ settings = get_settings()
89
+ host = "127.0.0.1" if settings.server_host in ("", "0.0.0.0", "::") else settings.server_host
90
+ url = f"http://{host}:{settings.server_port}/health"
91
+ try:
92
+ with urllib.request.urlopen(url, timeout=1) as response:
93
+ return response.status == 200
94
+ except (OSError, urllib.error.URLError):
95
+ return False
96
+
97
+
53
98
  @demo_app.command("add")
54
99
  def add_demo_command(
55
100
  json: bool = typer.Option(False, "--json", help="Output as JSON"),
@@ -430,6 +475,7 @@ def serve(
430
475
  settings = get_settings()
431
476
  configure_logging(settings.log_level, settings.log_file)
432
477
  typer.echo(f"AgentGraph config directory: {get_config_paths()[0]}")
478
+ typer.echo(f"AgentGraph log file: {settings.log_file.expanduser()}")
433
479
  uvicorn.run(
434
480
  "agentgraph.server.app:app",
435
481
  host=settings.server_host,
@@ -595,8 +641,15 @@ def onboard() -> None:
595
641
  if label not in seen:
596
642
  seen[label] = connector
597
643
 
598
- steps = list(seen.items())
599
- total = len(steps)
644
+ # Only connectors with an explicit onboarding prompt own an interactive setup flow.
645
+ # Generic connectors such as Web are configured through connector commands instead.
646
+ steps = [
647
+ (label, connector)
648
+ for label, connector in seen.items()
649
+ if getattr(connector, "onboard_prompt", None)
650
+ ]
651
+ steps.sort(key=lambda item: getattr(item[1], "onboard_last", False))
652
+ total = len(steps) + 1
600
653
 
601
654
  typer.echo("=== AgentGraph Setup ===\n")
602
655
 
@@ -611,11 +664,12 @@ def onboard() -> None:
611
664
  if i < total:
612
665
  typer.echo()
613
666
 
667
+ typer.echo(f"\nStep {total}/{total}: Install the AgentGraph Chrome Extension")
614
668
  typer.echo(
615
- "\nInstall the AgentGraph Chrome Extension: "
616
669
  "https://chromewebstore.google.com/detail/agentgraph-extension/iilkfclglabllelhjacijldknapbhidi"
617
670
  )
618
- typer.echo("Run `agentgraph serve` to start the server.")
671
+ if not _server_is_running():
672
+ typer.echo("Run `agentgraph serve` to start the server.")
619
673
 
620
674
 
621
675
  @app.command()
@@ -246,6 +246,7 @@ class BaseConnector(ABC):
246
246
  auth_label: ClassVar[str | None] = None
247
247
  auth_description: ClassVar[str | None] = None
248
248
  onboard_prompt: ClassVar[str | None] = None
249
+ onboard_last: ClassVar[bool] = False
249
250
  appears_in_auth_status: ClassVar[bool] = True
250
251
  """True for connectors backed by user/provider credentials.
251
252
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentgraph-server
3
- Version: 0.5.4
3
+ Version: 0.6.0
4
4
  Summary: A local-first CLI and MCP server that turns selected digital sources into a searchable graph for the AI agent you already use.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentgraph-server"
3
- version = "0.5.4"
3
+ version = "0.6.0"
4
4
  description = "A local-first CLI and MCP server that turns selected digital sources into a searchable graph for the AI agent you already use."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -58,6 +58,38 @@ def test_help() -> None:
58
58
  assert "ingest" not in result.output
59
59
 
60
60
 
61
+ def test_version() -> None:
62
+ result = runner.invoke(app, ["--version"])
63
+
64
+ assert result.exit_code == 0
65
+ assert result.output == "agentgraph 0.6.0\n"
66
+
67
+
68
+ def test_serve_outputs_log_file_path() -> None:
69
+ log_file = Path("/tmp/agentgraph-test/agentgraph.log")
70
+ settings = SimpleNamespace(
71
+ log_level="INFO",
72
+ log_file=log_file,
73
+ server_host="127.0.0.1",
74
+ server_port=8765,
75
+ )
76
+ with (
77
+ patch("agentgraph.config.get_settings", return_value=settings),
78
+ patch("agentgraph.logging.configure_logging"),
79
+ patch("uvicorn.run") as run,
80
+ ):
81
+ result = runner.invoke(app, ["serve"])
82
+
83
+ assert result.exit_code == 0
84
+ assert f"AgentGraph log file: {log_file}" in result.output
85
+ run.assert_called_once_with(
86
+ "agentgraph.server.app:app",
87
+ host="127.0.0.1",
88
+ port=8765,
89
+ reload=False,
90
+ )
91
+
92
+
61
93
  def test_demo_add_creates_fixture_and_outputs_json(
62
94
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
63
95
  ) -> None:
@@ -781,10 +813,12 @@ def test_onboard_directs_users_to_install_chrome_extension() -> None:
781
813
  with (
782
814
  patch("agentgraph.connectors.registry.bootstrap"),
783
815
  patch("agentgraph.connectors.registry.get_all_connectors", return_value=[_FakeConnector()]),
816
+ patch("agentgraph.cli._server_is_running", return_value=False),
784
817
  ):
785
818
  result = runner.invoke(app, ["onboard"], input="n\n")
786
819
 
787
820
  assert result.exit_code == 0
821
+ assert "Step 2/2: Install the AgentGraph Chrome Extension" in result.output
788
822
  assert "Install the AgentGraph Chrome Extension" in result.output
789
823
  assert (
790
824
  "https://chromewebstore.google.com/detail/agentgraph-extension/"
@@ -793,6 +827,87 @@ def test_onboard_directs_users_to_install_chrome_extension() -> None:
793
827
  assert "Run `agentgraph serve` to start the server." in result.output
794
828
 
795
829
 
830
+ def test_onboard_omits_server_hint_when_server_is_running() -> None:
831
+ with (
832
+ patch("agentgraph.connectors.registry.bootstrap"),
833
+ patch("agentgraph.connectors.registry.get_all_connectors", return_value=[_FakeConnector()]),
834
+ patch("agentgraph.cli._server_is_running", return_value=True),
835
+ ):
836
+ result = runner.invoke(app, ["onboard"], input="n\n")
837
+
838
+ assert result.exit_code == 0
839
+ assert "Step 2/2: Install the AgentGraph Chrome Extension" in result.output
840
+ assert "Run `agentgraph serve` to start the server." not in result.output
841
+
842
+
843
+ def test_onboard_skips_connectors_without_an_onboarding_flow() -> None:
844
+ class _FakeWebConnector:
845
+ source = "web"
846
+ auth_label = None
847
+ auth_description = "Web"
848
+ onboard_prompt = None
849
+ onboard_last = False
850
+ poll_interval = None
851
+ poll_delegates: list[str] = []
852
+ url_patterns: list[str] = []
853
+
854
+ with (
855
+ patch("agentgraph.connectors.registry.bootstrap"),
856
+ patch(
857
+ "agentgraph.connectors.registry.get_all_connectors",
858
+ return_value=[_FakeWebConnector()],
859
+ ),
860
+ ):
861
+ result = runner.invoke(app, ["onboard"])
862
+
863
+ assert result.exit_code == 0
864
+ assert "Step 1/1: Install the AgentGraph Chrome Extension" in result.output
865
+ assert "Set up web?" not in result.output
866
+
867
+
868
+ def test_onboard_places_last_step_connectors_at_the_end() -> None:
869
+ class _FakeRssOnboardConnector:
870
+ source = "rss"
871
+ auth_label = "rss"
872
+ auth_description = "RSS"
873
+ onboard_prompt = "Set up RSS feeds?"
874
+ onboard_last = True
875
+ poll_interval = None
876
+ poll_delegates: list[str] = []
877
+ url_patterns: list[str] = []
878
+ auth_calls = 0
879
+
880
+ @classmethod
881
+ def run_auth_flow(cls) -> None:
882
+ cls.auth_calls += 1
883
+
884
+ class _FakeSlackOnboardConnector:
885
+ source = "slack"
886
+ auth_label = "slack"
887
+ auth_description = "Slack"
888
+ onboard_prompt = "Set up Slack?"
889
+ onboard_last = False
890
+ poll_interval = None
891
+ poll_delegates: list[str] = []
892
+ url_patterns: list[str] = []
893
+
894
+ @classmethod
895
+ def run_auth_flow(cls) -> None:
896
+ pass
897
+
898
+ with (
899
+ patch("agentgraph.connectors.registry.bootstrap"),
900
+ patch(
901
+ "agentgraph.connectors.registry.get_all_connectors",
902
+ return_value=[_FakeRssOnboardConnector(), _FakeSlackOnboardConnector()],
903
+ ),
904
+ ):
905
+ result = runner.invoke(app, ["onboard"], input="n\nn\n")
906
+
907
+ assert result.exit_code == 0
908
+ assert result.output.index("Step 1/3: Slack") < result.output.index("Step 2/3: RSS")
909
+
910
+
796
911
  def test_auth_slack_accepts_noninteractive_options_after_provider() -> None:
797
912
  from agentgraph_connector_slack import SlackConnector
798
913
 
@@ -215,6 +215,12 @@ def gdocs_connector() -> GoogleDocsConnector:
215
215
  return GoogleDocsConnector()
216
216
 
217
217
 
218
+ def test_gdocs_onboarding_description_covers_google_connectors() -> None:
219
+ assert GoogleDocsConnector.auth_description == (
220
+ "Google: Gmail, Google Drive, Google Docs, and Google Sheets."
221
+ )
222
+
223
+
218
224
  @pytest.mark.asyncio
219
225
  async def test_gdocs_fetch_fresh_returns_empty_batch(gdocs_connector: GoogleDocsConnector) -> None:
220
226
  """When data is fresh, connector returns an empty batch without mutation."""
@@ -11,6 +11,7 @@ from unittest.mock import AsyncMock, patch
11
11
  import pytest
12
12
 
13
13
  from agentgraph.backends.sqlite.backend import SQLiteBackend
14
+ from agentgraph.connectors.base import EntityBatch, EntityRecord
14
15
  from agentgraph.core.context import set_backend
15
16
 
16
17
 
@@ -378,6 +379,8 @@ async def test_existing_database_gets_columns_and_renames_threads_to_email(tmp_p
378
379
  by_name = {row["name"]: row for row in columns}
379
380
  assert by_name["created_at"]["notnull"] == 1
380
381
  assert by_name["updated_at"]["notnull"] == 1
382
+ assert by_name["created_at"]["dflt_value"] is not None
383
+ assert by_name["updated_at"]["dflt_value"] is not None
381
384
  assert by_name["observed_at"]["notnull"] == 0
382
385
 
383
386
  entity = await migrated.get_entity_by_platform("gmail", "test-thread-001")
@@ -394,9 +397,103 @@ async def test_existing_database_gets_columns_and_renames_threads_to_email(tmp_p
394
397
  assert updated is not None
395
398
  assert updated["cumulative_observation_duration_ms"] == 2221
396
399
  assert updated["observed_at"] is not None
400
+ await migrated.upsert_batch(
401
+ EntityBatch(
402
+ entities=[
403
+ EntityRecord(
404
+ entity_type="Message",
405
+ platform="slack",
406
+ platform_entity_id="T/C/123.456",
407
+ content="A migrated database must accept new entities",
408
+ )
409
+ ]
410
+ ),
411
+ {},
412
+ {},
413
+ )
414
+ inserted = await migrated.get_entity_by_platform("slack", "T/C/123.456")
415
+ assert inserted is not None
416
+ assert inserted["created_at"] is not None
417
+ assert inserted["updated_at"] is not None
397
418
  event_type = await migrated._fetchval(
398
419
  "SELECT event_type FROM observations WHERE id = ?", ["legacy-observation"]
399
420
  )
400
421
  assert event_type == "observation_threshold"
401
422
  finally:
402
423
  await migrated.close()
424
+
425
+
426
+ async def test_version_two_database_gets_timestamp_default_repair(tmp_path: Path) -> None:
427
+ db_path = tmp_path / "missing-timestamp-defaults.db"
428
+ conn = sqlite3.connect(db_path)
429
+ conn.executescript(
430
+ """
431
+ CREATE TABLE entities (
432
+ id TEXT PRIMARY KEY,
433
+ entity_type TEXT NOT NULL,
434
+ platform TEXT NOT NULL,
435
+ platform_entity_id TEXT NOT NULL,
436
+ title TEXT,
437
+ content TEXT,
438
+ content_embedding BLOB,
439
+ metadata TEXT NOT NULL DEFAULT '{}',
440
+ created_at TEXT NOT NULL,
441
+ updated_at TEXT NOT NULL,
442
+ source_created_at TEXT,
443
+ source_updated_at TEXT,
444
+ synced_at TEXT,
445
+ observed_at TEXT,
446
+ retention_policy TEXT NOT NULL DEFAULT 'observed'
447
+ CHECK (retention_policy IN ('observed', 'owned', 'connected', 'persistent')),
448
+ retention_parent_id TEXT REFERENCES entities(id) ON DELETE CASCADE,
449
+ cumulative_observation_duration_ms INTEGER NOT NULL DEFAULT 0,
450
+ bookmarked INTEGER NOT NULL DEFAULT 0,
451
+ UNIQUE (platform, platform_entity_id)
452
+ );
453
+ CREATE VIRTUAL TABLE entities_fts USING fts5(id UNINDEXED, title, content);
454
+ CREATE TABLE observations (
455
+ id TEXT PRIMARY KEY,
456
+ event_type TEXT NOT NULL,
457
+ url TEXT NOT NULL,
458
+ title TEXT,
459
+ tab_id INTEGER,
460
+ timestamp TEXT NOT NULL,
461
+ evaluated INTEGER NOT NULL DEFAULT 0,
462
+ meta TEXT,
463
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
464
+ );
465
+ INSERT INTO entities (
466
+ id, entity_type, platform, platform_entity_id, created_at, updated_at
467
+ ) VALUES ('existing', 'Document', 'web', 'https://example.com', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z');
468
+ PRAGMA user_version=2;
469
+ """
470
+ )
471
+ conn.commit()
472
+ conn.close()
473
+
474
+ migrated = SQLiteBackend(str(db_path))
475
+ await migrated.initialize()
476
+ try:
477
+ columns = {
478
+ row["name"]: row for row in await migrated._fetchall("PRAGMA table_info(entities)")
479
+ }
480
+ assert columns["created_at"]["dflt_value"] is not None
481
+ assert columns["updated_at"]["dflt_value"] is not None
482
+ await migrated.upsert_batch(
483
+ EntityBatch(
484
+ entities=[
485
+ EntityRecord(
486
+ entity_type="Message",
487
+ platform="slack",
488
+ platform_entity_id="T/C/123.456",
489
+ )
490
+ ]
491
+ ),
492
+ {},
493
+ {},
494
+ )
495
+ inserted = await migrated.get_entity_by_platform("slack", "T/C/123.456")
496
+ assert inserted is not None
497
+ assert inserted["created_at"] is not None
498
+ finally:
499
+ await migrated.close()