agentgraph-server 0.7.2__tar.gz → 0.9.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 (118) hide show
  1. agentgraph_server-0.9.0/.agents/skills/agentgraph/SKILL.md +54 -0
  2. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/.agents/skills/agentgraph/references/commands.md +12 -2
  3. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/.agents/skills/agentgraph/references/data-model.md +6 -1
  4. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/.agents/skills/agentgraph/references/operations.md +32 -16
  5. {agentgraph_server-0.7.2/agentgraph_server.egg-info → agentgraph_server-0.9.0}/PKG-INFO +14 -14
  6. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/README.md +1 -1
  7. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/backends/sqlite/backend.py +105 -31
  8. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/cli.py +32 -7
  9. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/cli_query.py +18 -1
  10. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/connectors/base.py +75 -31
  11. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/connectors/command_effects.py +10 -0
  12. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/connectors/feed.py +22 -7
  13. agentgraph_server-0.9.0/agentgraph/connectors/registry.py +175 -0
  14. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/connectors/status.py +7 -2
  15. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/core/storage.py +27 -0
  16. agentgraph_server-0.9.0/agentgraph/graph/delete.py +102 -0
  17. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/graph/fetch.py +3 -1
  18. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/graph/link.py +3 -2
  19. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/graph/query.py +3 -0
  20. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/graph/upsert.py +7 -2
  21. agentgraph_server-0.9.0/agentgraph/mcp/models.py +131 -0
  22. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/mcp/server.py +299 -202
  23. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/query_client.py +19 -1
  24. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/server/graph_api.py +12 -1
  25. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/server/meta_api.py +4 -4
  26. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/server/static/viewer.html +187 -26
  27. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/skills.py +22 -7
  28. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0/agentgraph_server.egg-info}/PKG-INFO +14 -14
  29. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph_server.egg-info/SOURCES.txt +3 -0
  30. agentgraph_server-0.9.0/agentgraph_server.egg-info/requires.txt +38 -0
  31. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/pyproject.toml +13 -13
  32. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_auth.py +8 -9
  33. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_browse.py +24 -1
  34. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_cli.py +162 -11
  35. agentgraph_server-0.9.0/tests/test_connector_discovery.py +96 -0
  36. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_docs_build.py +15 -6
  37. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_entity_types.py +66 -3
  38. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_expiration.py +36 -0
  39. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_fetch.py +17 -2
  40. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_mcp_transport.py +17 -4
  41. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_observation.py +13 -0
  42. agentgraph_server-0.9.0/tests/test_observed_since.py +209 -0
  43. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_query.py +269 -89
  44. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_query_client.py +2 -2
  45. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_registry.py +78 -1
  46. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_release_metadata.py +2 -2
  47. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_route_resolution.py +1 -0
  48. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_rss_connector.py +197 -230
  49. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_schema.py +51 -0
  50. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_upsert.py +44 -0
  51. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_viewer_layout_contract.py +95 -1
  52. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_web_connector.py +23 -129
  53. agentgraph_server-0.7.2/.agents/skills/agentgraph/SKILL.md +0 -97
  54. agentgraph_server-0.7.2/agentgraph/connectors/registry.py +0 -86
  55. agentgraph_server-0.7.2/agentgraph/graph/delete.py +0 -40
  56. agentgraph_server-0.7.2/agentgraph_server.egg-info/requires.txt +0 -38
  57. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/.agents/skills/slack-auth/SKILL.md +0 -0
  58. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/LICENSE +0 -0
  59. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/__init__.py +0 -0
  60. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/auth/__init__.py +0 -0
  61. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/auth/credentials.py +0 -0
  62. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/backends/__init__.py +0 -0
  63. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/backends/sqlite/__init__.py +0 -0
  64. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/backends/sqlite/schema.sql +0 -0
  65. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/backends/sqlite/vector.py +0 -0
  66. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/cli_sync.py +0 -0
  67. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/config.py +0 -0
  68. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/connectors/__init__.py +0 -0
  69. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/connectors/match_patterns.py +0 -0
  70. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/core/__init__.py +0 -0
  71. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/core/context.py +0 -0
  72. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/core/runtime.py +0 -0
  73. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/demo.py +0 -0
  74. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/demo_fixtures/reliable-webhooks.md +0 -0
  75. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/demo_fixtures/retry-guidance.md +0 -0
  76. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/graph/__init__.py +0 -0
  77. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/graph/bookmark.py +0 -0
  78. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/graph/download.py +0 -0
  79. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/graph/embeddings.py +0 -0
  80. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/graph/expiration.py +0 -0
  81. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/graph/operations.py +0 -0
  82. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/graph/person.py +0 -0
  83. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/logging.py +0 -0
  84. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/mcp/__init__.py +0 -0
  85. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/perf.py +0 -0
  86. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/server/__init__.py +0 -0
  87. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/server/app.py +0 -0
  88. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/server/browse_api.py +0 -0
  89. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/server/observation.py +0 -0
  90. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/server/presentation.py +0 -0
  91. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/server/router.py +0 -0
  92. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/server/sync.py +0 -0
  93. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/server/sync_api.py +0 -0
  94. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph/server/uds.py +0 -0
  95. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph_server.egg-info/dependency_links.txt +0 -0
  96. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph_server.egg-info/entry_points.txt +0 -0
  97. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/agentgraph_server.egg-info/top_level.txt +0 -0
  98. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/setup.cfg +0 -0
  99. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_auth_status_methods.py +0 -0
  100. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_benchmarks.py +0 -0
  101. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_command_effects.py +0 -0
  102. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_config.py +0 -0
  103. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_connectors.py +0 -0
  104. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_discord_attachments.py +0 -0
  105. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_embeddings.py +0 -0
  106. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_feed_connectors.py +0 -0
  107. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_graph_operations.py +0 -0
  108. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_launch_demo.py +0 -0
  109. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_logging.py +0 -0
  110. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_match_patterns.py +0 -0
  111. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_query_parity.py +0 -0
  112. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_router.py +0 -0
  113. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_server_imports.py +0 -0
  114. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_server_lifespan.py +0 -0
  115. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_server_uds.py +0 -0
  116. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_slack_auth_artifacts.py +0 -0
  117. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_slack_oauth.py +0 -0
  118. {agentgraph_server-0.7.2 → agentgraph_server-0.9.0}/tests/test_sync.py +0 -0
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: agentgraph
3
+ description: Use AgentGraph through its CLI or MCP tools to search and traverse selected local context, retrieve full source-backed evidence, fetch missing or stale resources, and troubleshoot connector availability.
4
+ ---
5
+
6
+ # AgentGraph skill
7
+
8
+ AgentGraph is a local graph of selected messages, documents, people, feeds, pages,
9
+ and relationships. Use its CLI or connected MCP tools to investigate that context;
10
+ do not query its database or connector internals directly.
11
+
12
+ Prefer the connected MCP tools when AgentGraph is available through MCP. Use the
13
+ `agentgraph` CLI when MCP is unavailable or when a terminal script is the natural
14
+ interface. Do not query AgentGraph's SQLite database or connector internals directly.
15
+
16
+ ## Investigate
17
+
18
+ 1. Start with `search` for discovery. If the user supplied a graph ID, platform
19
+ reference, or indexed URL, use `get` directly instead.
20
+ 2. Read the full entity before making a source-backed claim. Search results and graph
21
+ traversals contain bounded snippets.
22
+ 3. Inspect direct edges or traverse only when relationships help answer the question.
23
+ 4. Resolve a stub or fetch a known resource when missing or stale content blocks the
24
+ answer, then repeat the read.
25
+ 5. Compare source dates and contents. Cite the source URL or entity ID and separate
26
+ source facts from conclusions drawn across sources.
27
+
28
+ Use search filters when the type, source, author, time window, observation, or
29
+ attachments are already known. A search without a query lists matching entities by
30
+ date; a query ranks them by relevance.
31
+
32
+ ## Guardrails
33
+
34
+ - A stub has neither a title nor content.
35
+ - Use `source_created_at` and `source_updated_at` for source chronology; local
36
+ `created_at` and `updated_at` describe the graph record.
37
+ - Browser observation alone updates `observed_at`; fetch, poll, and ingest do not
38
+ indicate that the user viewed a resource.
39
+ - Check connector or authentication state only when availability, freshness, or a
40
+ graph operation calls it into question.
41
+ - Confirm Person merges, deletion, credential removal, and removal of bookmark
42
+ protection with the user.
43
+ - A multi-target deletion resolves every target before changing the graph; it either
44
+ deletes the resolved set or leaves the graph unchanged.
45
+
46
+ ## References
47
+
48
+ - Read [references/commands.md](references/commands.md) for command syntax, MCP
49
+ mappings, filters, targets, stubs, and result limits.
50
+ - Read [references/data-model.md](references/data-model.md) for entity types,
51
+ relationships, attachments, timestamps, and retention.
52
+ - Read [references/operations.md](references/operations.md) for connector discovery,
53
+ authentication, fetching, downloads, polling, connector-owned commands, and server
54
+ transport.
@@ -3,7 +3,7 @@
3
3
  ## CLI
4
4
 
5
5
  ```bash
6
- agentgraph search ["<query>"] [--type <type>] [--platform <platform>] [--filter key=value] [--since 12h|30m|2d] [--mine] [--has-attachments] [--limit N] [--order-by created_at|updated_at|source_created_at|source_updated_at|observed_at|synced_at] [--min-score N] [--json]
6
+ agentgraph search [--type <type>] [--platform <platform>] [--filter key=value] [--since 12h|30m|2d] [--observed-since 12h|30m|2d] [--mine] [--has-attachments] [--limit N] [--order-by created_at|updated_at|source_created_at|source_updated_at|observed_at|synced_at] [--min-score N] [--json]
7
7
  agentgraph get <entity-id|platform/ref|url> [--resolve] [--json]
8
8
  agentgraph edges <entity-id|platform/ref> [--type <edge-type>] [--direction in|out|both] [--json]
9
9
  agentgraph traverse <entity-id|platform/ref> [--resolve] [--depth 0..4] [--json]
@@ -14,6 +14,11 @@ filter is applied as a hard constraint (`--limit` defaults to 10). Without one t
14
14
  is no ranking: the filters select the entities, `--order-by` sorts them newest first,
15
15
  `--min-score` is ignored, and `--limit` defaults to 50.
16
16
 
17
+ `--since` filters `updated_at`; `--observed-since` filters browser observation time
18
+ (`observed_at`), excluding never-observed entities. Both accept ISO timestamps or
19
+ relative durations and include the cutoff itself. When supplied together, both
20
+ must match. Neither flag changes ordering.
21
+
17
22
  `get` and `traverse` do not resolve stubs unless `--resolve` is supplied. Depth 0
18
23
  returns only the starting entity; depths 1 through 4 include that many relationship
19
24
  hops.
@@ -28,7 +33,7 @@ accept full UUIDs, unambiguous UUID prefixes, and platform references.
28
33
  ## MCP equivalents
29
34
 
30
35
  ```text
31
- agentgraph search ... -> search_entities_tool(query, entity_types, platform, filters, since, authored_by_me, has_attachments, limit, order_by, min_score, refresh)
36
+ agentgraph search ... -> search_entities_tool(query, entity_types, platform, filters, since, authored_by_me, has_attachments, limit, order_by, min_score, refresh, observed_since)
32
37
  agentgraph get ... -> get_entity_tool(entity_id, resolve)
33
38
  agentgraph edges ... -> get_edges_tool(entity_id, edge_type, direction)
34
39
  agentgraph traverse ... -> traverse_graph_tool(entity_id, max_depth, resolve)
@@ -38,6 +43,11 @@ MCP search defaults to `refresh=false`; set it only when fresh connector-owned
38
43
  presentation metadata is needed. It does not replace a targeted source fetch for
39
44
  stale content.
40
45
 
46
+ MCP tools return structured content: successes use `status="ok"` with a `data`
47
+ object; expected failures use `status="error"`, an error `code`, and `message`, with
48
+ the MCP error flag set. Search `data` includes `has_more`; entity payloads include
49
+ `is_stub` and, when known, `source_url`.
50
+
41
51
  ## Stubs
42
52
 
43
53
  An entity is a stub when it has neither a title nor content. With the CLI, pass
@@ -14,7 +14,12 @@
14
14
  | `Task` | Tracked work items such as Jira issues. Issue key, status, and assignee live in `metadata`. |
15
15
  | `Video` | Recorded videos such as Looms. The transcript is indexed as content and `metadata.web_url` links to the video. |
16
16
 
17
- The valid core model does not currently include `Project`.
17
+ Installed connectors may add shared entity type names such as `Project`. The complete
18
+ alphabetical catalog, including connector descriptions, is embedded in the
19
+ `search_entities_tool` MCP description. With the CLI, connector-specific declarations
20
+ are available from `agentgraph list-connectors --json`. Undeclared type strings remain
21
+ storable and searchable for compatibility, but do not receive automatic discovery or
22
+ reference routing.
18
23
 
19
24
  ## Relationships
20
25
 
@@ -10,10 +10,17 @@ agentgraph auth remove <provider> [--account <account-id>] [--json]
10
10
  ```
11
11
 
12
12
  Use `agentgraph list-connectors --json` to discover installed connector source names,
13
- valid platform values, URL ownership, polling delegation, and sync state. Do not rely
14
- on a hardcoded platform list. Use `--verify` only when a live provider check is
13
+ valid platform values, connector-declared entity types, URL ownership, polling
14
+ delegation, and sync state. Do not rely on a hardcoded platform list. MCP clients
15
+ receive the full installed entity type catalog in the `search_entities_tool`
16
+ description during tool discovery. Use `--verify` only when a live provider check is
15
17
  needed.
16
18
 
19
+ Connector `last_synced_at` values come from successful poll timestamps in `sync_state`,
20
+ aggregated across account-scoped cursors. A connector command may reset its own cursor
21
+ through the generic command-effects path; after a reset, its status is `never` until the
22
+ next successful poll.
23
+
17
24
  MCP equivalents are `list_connectors_tool`, `list_auth_providers_tool`,
18
25
  `authenticate_provider_tool`, and `remove_auth_provider_tool`.
19
26
 
@@ -68,13 +75,15 @@ forms are `run_connector_command_tool("web", ["observe", "<url-or-prefix>"])` an
68
75
  ```bash
69
76
  agentgraph download <entity-id|platform/ref> [--output <file-or-dir>] [--json]
70
77
  agentgraph bookmark <entity-id|platform/ref|url> [--remove] [--json]
71
- agentgraph delete <entity-id|platform/ref|url> [--json]
78
+ agentgraph delete <entity-id|platform/ref|url>... [--json]
72
79
  agentgraph unify-persons <primary-person-id> <duplicate-person-id>... [--json]
73
80
  ```
74
81
 
75
- Use `download_entity_tool`, `bookmark_entity_tool`, `delete_entity_tool`, and
76
- `unify_persons_tool` through MCP. Confirm identity before person unification. The
77
- first Person is canonical and keeps its ID. Deletion removes connected edges.
82
+ Use `download_entity_tool`, `bookmark_entity_tool`, `delete_entity_tool`,
83
+ `delete_entities_tool`, and `unify_persons_tool` through MCP. Confirm identity before
84
+ person unification. The first Person is canonical and keeps its ID. Multi-target
85
+ deletion resolves every target before deleting any of them, removes connected edges,
86
+ and returns compact references for the deleted entities.
78
87
 
79
88
  ## Server and skill setup
80
89
 
@@ -86,12 +95,17 @@ The server listens on a Unix socket (`AGENTGRAPH_SERVER_UDS_PATH`, default
86
95
  `~/.agentgraph/agentgraph.sock`) as well as TCP, because most agent sandboxes deny
87
96
  loopback TCP while permitting an allowlisted socket.
88
97
 
89
- `agentgraph poll` and connector or authentication commands that queue a poll or ingest
90
- always contact that service and have no local fallback; if sandboxed execution blocks
91
- one, request permission to contact the server or ask for the socket to be allowlisted,
92
- then retry. Reads and `fetch`/`download` follow `AGENTGRAPH_QUERY_TRANSPORT`
93
- (`auto` by default: socket, then TCP, then in-process), so they keep working when the
94
- server is unreachable.
98
+ CLI polling and connector or authentication commands that queue a poll or ingest
99
+ contact the local server. Reads and `fetch`/`download` follow
100
+ `AGENTGRAPH_QUERY_TRANSPORT` (`auto` by default: socket, then TCP, then in-process),
101
+ so they keep working when the server is unreachable. The MCP server can run polling
102
+ and ingest locally when it cannot queue them on the local server.
103
+
104
+ Run ordinary reads with the default transport. To diagnose connectivity, use
105
+ `AGENTGRAPH_QUERY_TRANSPORT=server agentgraph search x --limit 1`; retry an affected
106
+ read with `AGENTGRAPH_QUERY_TRANSPORT=in-process` only after an actual sandbox
107
+ connection failure. See the Coding agent sandboxes section of the configuration docs
108
+ for Unix-socket allowlist settings.
95
109
 
96
110
  ```bash
97
111
  agentgraph mcp-config
@@ -99,9 +113,11 @@ agentgraph mcp-serve
99
113
  agentgraph install-skill [agentgraph] [--target user|project] [--no-claude] [--force] [--json]
100
114
  ```
101
115
 
102
- `agentgraph mcp-config` prints local stdio setup for ChatGPT Desktop Work Mode and
103
- Claude Desktop. In ChatGPT Desktop, enter the printed executable in **Command to
104
- launch** and `mcp-serve` in **Arguments**; for Claude Desktop, add the printed JSON
105
- to its MCP configuration file.
116
+ `agentgraph mcp-config` prints local stdio setup for ChatGPT Desktop Work Mode, Codex,
117
+ Claude Desktop, and Claude Code. Prefer these MCP connections when they are available.
118
+ In ChatGPT Desktop, enter the printed executable in **Command to launch** and
119
+ `mcp-serve` in **Arguments**; for Claude Desktop, add the printed JSON to its MCP
120
+ configuration file. Claude Code uses the printed user-scoped command so the server is
121
+ available across projects.
106
122
 
107
123
  The skill installer is available only through the `agentgraph install-skill` CLI command.
@@ -1,18 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentgraph-server
3
- Version: 0.7.2
3
+ Version: 0.9.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
7
7
  License-File: LICENSE
8
8
  Requires-Dist: aiosqlite>=0.20.0
9
- Requires-Dist: agentgraph-connector-web<0.7,>=0.5.5
9
+ Requires-Dist: agentgraph-connector-web<0.8,>=0.7.0
10
10
  Requires-Dist: apscheduler>=3.11.2
11
11
  Requires-Dist: click<8.4
12
12
  Requires-Dist: fastapi>=0.135.2
13
13
  Requires-Dist: fastembed>=0.8.0
14
14
  Requires-Dist: httpx>=0.28.1
15
- Requires-Dist: mcp>=1.26.0
15
+ Requires-Dist: mcp<2,>=1.26.0
16
16
  Requires-Dist: numpy>=2.4.6
17
17
  Requires-Dist: pydantic>=2.12.5
18
18
  Requires-Dist: pydantic-settings>=2.13.1
@@ -22,21 +22,21 @@ Requires-Dist: sqlite-vec>=0.1.9
22
22
  Requires-Dist: typer>=0.24.1
23
23
  Requires-Dist: uvicorn>=0.42.0
24
24
  Provides-Extra: google
25
- Requires-Dist: agentgraph-connector-google<0.7,>=0.5.5; extra == "google"
25
+ Requires-Dist: agentgraph-connector-google<0.7,>=0.6.1; extra == "google"
26
26
  Provides-Extra: slack
27
- Requires-Dist: agentgraph-connector-slack<0.7,>=0.5.5; extra == "slack"
27
+ Requires-Dist: agentgraph-connector-slack<0.7,>=0.6.1; extra == "slack"
28
28
  Provides-Extra: discord
29
- Requires-Dist: agentgraph-connector-discord<0.7,>=0.5.5; extra == "discord"
29
+ Requires-Dist: agentgraph-connector-discord<0.7,>=0.6.1; extra == "discord"
30
30
  Provides-Extra: rss
31
- Requires-Dist: agentgraph-connector-rss<0.7,>=0.5.5; extra == "rss"
31
+ Requires-Dist: agentgraph-connector-rss<0.8,>=0.7.0; extra == "rss"
32
32
  Provides-Extra: web
33
- Requires-Dist: agentgraph-connector-web<0.7,>=0.5.5; extra == "web"
33
+ Requires-Dist: agentgraph-connector-web<0.8,>=0.7.0; extra == "web"
34
34
  Provides-Extra: all
35
- Requires-Dist: agentgraph-connector-web<0.7,>=0.5.5; extra == "all"
36
- Requires-Dist: agentgraph-connector-google<0.7,>=0.5.5; extra == "all"
37
- Requires-Dist: agentgraph-connector-slack<0.7,>=0.5.5; extra == "all"
38
- Requires-Dist: agentgraph-connector-discord<0.7,>=0.5.5; extra == "all"
39
- Requires-Dist: agentgraph-connector-rss<0.7,>=0.5.5; extra == "all"
35
+ Requires-Dist: agentgraph-connector-web<0.8,>=0.7.0; extra == "all"
36
+ Requires-Dist: agentgraph-connector-google<0.7,>=0.6.1; extra == "all"
37
+ Requires-Dist: agentgraph-connector-slack<0.7,>=0.6.1; extra == "all"
38
+ Requires-Dist: agentgraph-connector-discord<0.7,>=0.6.1; extra == "all"
39
+ Requires-Dist: agentgraph-connector-rss<0.8,>=0.7.0; extra == "all"
40
40
  Dynamic: license-file
41
41
 
42
42
  # AgentGraph
@@ -122,7 +122,7 @@ Connect the agent you already use:
122
122
  agentgraph mcp-config
123
123
  ```
124
124
 
125
- Use the printed instructions to connect ChatGPT Desktop Work Mode or Claude Desktop to the local stdio server. See [Install](docs-src/install.md) for the complete setup path.
125
+ Use the printed instructions to connect ChatGPT Desktop Work Mode, Codex, Claude Desktop, or Claude Code to the local stdio server. MCP is the preferred connection; the installed skill provides CLI fallback workflows. See [Install](docs-src/install.md) for the complete setup path.
126
126
 
127
127
  ## What AgentGraph is and is not
128
128
 
@@ -81,7 +81,7 @@ Connect the agent you already use:
81
81
  agentgraph mcp-config
82
82
  ```
83
83
 
84
- Use the printed instructions to connect ChatGPT Desktop Work Mode or Claude Desktop to the local stdio server. See [Install](docs-src/install.md) for the complete setup path.
84
+ Use the printed instructions to connect ChatGPT Desktop Work Mode, Codex, Claude Desktop, or Claude Code to the local stdio server. MCP is the preferred connection; the installed skill provides CLI fallback workflows. See [Install](docs-src/install.md) for the complete setup path.
85
85
 
86
86
  ## What AgentGraph is and is not
87
87
 
@@ -68,6 +68,7 @@ _LIST_PAGE_ORDER_BY = {
68
68
  }
69
69
  _COLUMN_FILTERS = {"platform", "platform_entity_id", "entity_type"}
70
70
  _FTS_DELETE_CHUNK_SIZE = 500
71
+ _PLATFORM_ENTITY_ID_LOOKUP_CHUNK_SIZE = 900
71
72
  _BUSY_TIMEOUT_MS = 5_000
72
73
  _SCHEMA_VERSION = 6
73
74
 
@@ -116,6 +117,7 @@ def _entity_predicates(
116
117
  since: datetime | None = None,
117
118
  authored_by: list[str] | None = None,
118
119
  has_attachments: bool = False,
120
+ observed_since: datetime | None = None,
119
121
  ) -> tuple[str, list[Any], str, list[Any]]:
120
122
  """Build the entity-selection predicates shared by every leg of a search.
121
123
 
@@ -143,6 +145,9 @@ def _entity_predicates(
143
145
  if since:
144
146
  clauses.append("e.updated_at >= ?")
145
147
  params.append(_utc_stamp(since))
148
+ if observed_since:
149
+ clauses.append("e.observed_at >= ?")
150
+ params.append(_utc_stamp(observed_since))
146
151
  if has_attachments:
147
152
  clauses.append(
148
153
  "json_extract(e.metadata, '$.attachments') IS NOT NULL"
@@ -865,11 +870,14 @@ class SQLiteBackend(StorageBackend):
865
870
  str(existing_row["content"]) if existing_row and existing_row["content"] else ""
866
871
  )
867
872
  fts_title = e.title if e.title is not None else existing_title
868
- fts_content = e.content if e.content is not None else existing_content
873
+ fts_content = (
874
+ e.content if e.content_searchable and e.content is not None else ""
875
+ )
869
876
  rewrite_fts = (
870
877
  existing_row is None
871
878
  or fts_title != existing_title
872
- or fts_content != existing_content
879
+ or (e.content_searchable and fts_content != existing_content)
880
+ or not e.content_searchable
873
881
  )
874
882
  embedding = embeddings.get(e.platform_entity_id)
875
883
  emb_blob = pack_embedding(embedding) if embedding else None
@@ -888,7 +896,10 @@ class SQLiteBackend(StorageBackend):
888
896
  target_title = e.title if e.title is not None else existing_row["title"]
889
897
  target_content = e.content if e.content is not None else existing_row["content"]
890
898
  target_embedding = (
891
- emb_blob if emb_blob is not None else existing_row["content_embedding"]
899
+ emb_blob if e.content_searchable and emb_blob is not None
900
+ else existing_row["content_embedding"]
901
+ if e.content_searchable
902
+ else None
892
903
  )
893
904
  target_source_created = (
894
905
  source_created
@@ -923,7 +934,7 @@ class SQLiteBackend(StorageBackend):
923
934
  entity_type = CASE WHEN entities.entity_type = 'Document' THEN EXCLUDED.entity_type ELSE entities.entity_type END,
924
935
  title = COALESCE(EXCLUDED.title, entities.title),
925
936
  content = COALESCE(EXCLUDED.content, entities.content),
926
- content_embedding = COALESCE(EXCLUDED.content_embedding, entities.content_embedding),
937
+ content_embedding = CASE WHEN ? THEN COALESCE(EXCLUDED.content_embedding, entities.content_embedding) ELSE NULL END,
927
938
  metadata = EXCLUDED.metadata,
928
939
  source_created_at = COALESCE(EXCLUDED.source_created_at, entities.source_created_at),
929
940
  source_updated_at = COALESCE(EXCLUDED.source_updated_at, entities.source_updated_at),
@@ -949,6 +960,7 @@ class SQLiteBackend(StorageBackend):
949
960
  local_updated,
950
961
  e.retention_policy,
951
962
  parent_id,
963
+ e.content_searchable,
952
964
  local_updated,
953
965
  ],
954
966
  )
@@ -1216,42 +1228,55 @@ class SQLiteBackend(StorageBackend):
1216
1228
 
1217
1229
  async def delete_entity(self, entity_id: str) -> EntityResult:
1218
1230
  """Delete one entity by internal ID. Edges cascade; FTS rows are removed explicitly."""
1231
+ deleted = await self.delete_entities([entity_id])
1232
+ return deleted[0]
1233
+
1234
+ async def delete_entities(self, entity_ids: list[str]) -> list[EntityResult]:
1235
+ """Atomically delete entities, their edges, and their FTS rows."""
1236
+ unique_ids = list(dict.fromkeys(entity_ids))
1237
+ if not unique_ids:
1238
+ raise ValueError("At least one entity ID is required")
1219
1239
  assert self._write_lock is not None
1220
1240
  async with self._write_lock:
1221
1241
  conn = self._conn_or_raise()
1222
1242
  await conn.execute("BEGIN IMMEDIATE")
1223
1243
  try:
1224
- await conn.execute(
1225
- """
1226
- UPDATE entities
1227
- SET retention_parent_id = NULL, updated_at = ?
1228
- WHERE retention_parent_id = ? AND bookmarked = 1
1229
- """,
1230
- [_now(), entity_id],
1231
- )
1232
- cursor = await conn.execute(
1233
- """
1234
- DELETE FROM entities
1235
- WHERE id = ?
1236
- RETURNING id, entity_type, platform, platform_entity_id,
1237
- title, content, metadata, created_at, updated_at,
1238
- source_created_at, source_updated_at, synced_at, observed_at,
1239
- retention_policy, retention_parent_id,
1240
- cumulative_observation_duration_ms, bookmarked
1241
- """,
1242
- [entity_id],
1243
- )
1244
- row = await cursor.fetchone()
1245
- if row is None:
1246
- raise ValueError(f"Entity {entity_id!r} not found")
1247
- await conn.execute(
1248
- "DELETE FROM entities_fts WHERE id NOT IN (SELECT id FROM entities)"
1249
- )
1244
+ rows: list[aiosqlite.Row] = []
1245
+ for start in range(0, len(unique_ids), _FTS_DELETE_CHUNK_SIZE):
1246
+ ids = unique_ids[start : start + _FTS_DELETE_CHUNK_SIZE]
1247
+ placeholders = ",".join("?" * len(ids))
1248
+ await conn.execute(
1249
+ f"""
1250
+ UPDATE entities
1251
+ SET retention_parent_id = NULL, updated_at = ?
1252
+ WHERE retention_parent_id IN ({placeholders}) AND bookmarked = 1
1253
+ """,
1254
+ [_now(), *ids],
1255
+ )
1256
+ cursor = await conn.execute(
1257
+ f"""
1258
+ DELETE FROM entities
1259
+ WHERE id IN ({placeholders})
1260
+ RETURNING id, entity_type, platform, platform_entity_id,
1261
+ title, content, metadata, created_at, updated_at,
1262
+ source_created_at, source_updated_at, synced_at, observed_at,
1263
+ retention_policy, retention_parent_id,
1264
+ cumulative_observation_duration_ms, bookmarked
1265
+ """,
1266
+ ids,
1267
+ )
1268
+ rows.extend(await cursor.fetchall())
1269
+ await conn.execute(
1270
+ f"DELETE FROM entities_fts WHERE id IN ({placeholders})", ids
1271
+ )
1272
+ if len(rows) != len(unique_ids):
1273
+ raise ValueError("One or more entities were not found")
1250
1274
  await conn.execute("COMMIT")
1251
1275
  except Exception:
1252
1276
  await conn.execute("ROLLBACK")
1253
1277
  raise
1254
- return _row_to_entity(row)
1278
+ deleted_by_id = {str(row["id"]): _row_to_entity(row) for row in rows}
1279
+ return [deleted_by_id[entity_id] for entity_id in unique_ids]
1255
1280
 
1256
1281
  # --- Read: entities ---
1257
1282
 
@@ -1269,6 +1294,7 @@ class SQLiteBackend(StorageBackend):
1269
1294
  has_attachments: bool = False,
1270
1295
  order_by: str | None = None,
1271
1296
  content_limit: int | None = None,
1297
+ observed_since: datetime | None = None,
1272
1298
  ) -> list[EntityResult]:
1273
1299
  """Select entities by predicate, ranked by relevance or ordered by date.
1274
1300
 
@@ -1286,6 +1312,7 @@ class SQLiteBackend(StorageBackend):
1286
1312
  since,
1287
1313
  authored_by,
1288
1314
  has_attachments,
1315
+ observed_since=observed_since,
1289
1316
  )
1290
1317
 
1291
1318
  if query_text is None or query_vec is None:
@@ -1540,6 +1567,30 @@ class SQLiteBackend(StorageBackend):
1540
1567
  )
1541
1568
  return _row_to_entity(row) if row else None
1542
1569
 
1570
+ async def get_existing_platform_entity_ids(
1571
+ self,
1572
+ platform: str,
1573
+ platform_entity_ids: list[str],
1574
+ ) -> set[str]:
1575
+ if not platform_entity_ids:
1576
+ return set()
1577
+
1578
+ existing: set[str] = set()
1579
+ for offset in range(0, len(platform_entity_ids), _PLATFORM_ENTITY_ID_LOOKUP_CHUNK_SIZE):
1580
+ chunk = platform_entity_ids[
1581
+ offset : offset + _PLATFORM_ENTITY_ID_LOOKUP_CHUNK_SIZE
1582
+ ]
1583
+ placeholders = ",".join("?" for _ in chunk)
1584
+ rows = await self._fetchall(
1585
+ f"""
1586
+ SELECT platform_entity_id FROM entities
1587
+ WHERE platform = ? AND platform_entity_id IN ({placeholders})
1588
+ """,
1589
+ [platform, *chunk],
1590
+ )
1591
+ existing.update(str(row["platform_entity_id"]) for row in rows)
1592
+ return existing
1593
+
1543
1594
  async def list_entities(
1544
1595
  self,
1545
1596
  entity_types: list[str] | None,
@@ -1986,6 +2037,9 @@ class SQLiteBackend(StorageBackend):
1986
2037
  [source, json.dumps(cursor), _now()],
1987
2038
  )
1988
2039
 
2040
+ async def clear_cursor(self, source: str) -> None:
2041
+ await self._execute("DELETE FROM sync_state WHERE source = ?", [source])
2042
+
1989
2043
  # --- Connector support ---
1990
2044
 
1991
2045
  async def increment_observation_duration(
@@ -2123,6 +2177,26 @@ class SQLiteBackend(StorageBackend):
2123
2177
  result[row["platform"]] = datetime.fromisoformat(val) if val else None
2124
2178
  return result
2125
2179
 
2180
+ async def get_sources_last_synced_at(
2181
+ self, sources: list[str]
2182
+ ) -> dict[str, datetime | None]:
2183
+ if not sources:
2184
+ return {}
2185
+ rows = await self._fetchall(
2186
+ "SELECT source, updated_at FROM sync_state"
2187
+ )
2188
+ result: dict[str, datetime | None] = dict.fromkeys(sources, None)
2189
+ for source in sources:
2190
+ matching = [
2191
+ row["updated_at"]
2192
+ for row in rows
2193
+ if row["source"] == source or row["source"].startswith(f"{source}:")
2194
+ ]
2195
+ values = [datetime.fromisoformat(value) for value in matching if value]
2196
+ if values:
2197
+ result[source] = max(values)
2198
+ return result
2199
+
2126
2200
  async def reset_synced_at(self, platform: str, platform_entity_id: str) -> None:
2127
2201
  await self._execute(
2128
2202
  "UPDATE entities SET synced_at = NULL WHERE platform = ? AND platform_entity_id = ?",
@@ -437,6 +437,13 @@ def connector_command(
437
437
  try:
438
438
  result = type(connector).run_cli_command(command_args)
439
439
  effects = type(connector).command_effects(command_args, result)
440
+ if effects.reset_cursors:
441
+ from agentgraph.cli_query import run_graph_operation
442
+ from agentgraph.connectors.command_effects import execute_cursor_resets
443
+
444
+ result["reset_cursors"] = run_graph_operation(
445
+ lambda: execute_cursor_resets(effects)
446
+ )
440
447
  if effects.delete_entities:
441
448
  from agentgraph.cli_query import run_graph_operation
442
449
  from agentgraph.connectors.command_effects import execute_deletions
@@ -484,7 +491,7 @@ def list_connectors(
484
491
  False, "--verify", help="Live-check connector credentials with provider APIs"
485
492
  ),
486
493
  ) -> None:
487
- """List installed connectors and their sync status."""
494
+ """List installed connectors, their resource entity types, and sync status."""
488
495
  from agentgraph.connectors.registry import bootstrap, get_all_connectors
489
496
  from agentgraph.connectors.status import connector_status_items
490
497
  from agentgraph.core.runtime import backend_context
@@ -505,6 +512,7 @@ def list_connectors(
505
512
  table = Table(title="Connectors", show_lines=True)
506
513
  table.add_column("Connector", style="bold", no_wrap=True)
507
514
  table.add_column("Description", ratio=1)
515
+ table.add_column("Types")
508
516
  table.add_column("Auth")
509
517
  table.add_column("Sync")
510
518
  table.add_column("Last sync", no_wrap=True)
@@ -513,6 +521,11 @@ def list_connectors(
513
521
  table.add_row(
514
522
  str(item["source"]),
515
523
  str(desc),
524
+ ", ".join(
525
+ str(definition["name"])
526
+ for definition in cast(list[dict[str, object]], item["entity_types"])
527
+ )
528
+ or "-",
516
529
  _connector_auth_label(item),
517
530
  str(item["sync"]),
518
531
  str(item["last_sync"]),
@@ -591,7 +604,7 @@ def search(
591
604
  None,
592
605
  "--since",
593
606
  "-s",
594
- help="Only results after this time: ISO timestamp or relative (12h, 30m, 2d)",
607
+ help="Only results updated at or after this time: ISO timestamp or relative (12h, 30m, 2d)",
595
608
  ),
596
609
  mine: bool = typer.Option(False, "--mine", "-m", help="Only entities authored by me"),
597
610
  has_attachments: bool = typer.Option(
@@ -613,6 +626,11 @@ def search(
613
626
  0.03, "--min-score", help="Minimum relevance score (0–1); ignored without a query"
614
627
  ),
615
628
  json: bool = typer.Option(False, "--json", help="Output as JSON"),
629
+ observed_since: str | None = typer.Option(
630
+ None,
631
+ "--observed-since",
632
+ help="Only results observed at or after this time: ISO timestamp or relative (12h, 30m, 2d)",
633
+ ),
616
634
  ) -> None:
617
635
  """Search the knowledge graph, or list entities matching filters alone."""
618
636
  from agentgraph.cli_query import cmd_search
@@ -623,6 +641,7 @@ def search(
623
641
  platform=platform,
624
642
  filters=dict(f.split("=", 1) for f in filter if "=" in f),
625
643
  since=since,
644
+ observed_since=observed_since,
626
645
  authored_by_me=mine,
627
646
  has_attachments=has_attachments,
628
647
  limit=limit,
@@ -725,13 +744,13 @@ def bookmark(
725
744
 
726
745
  @app.command("delete")
727
746
  def delete_cmd(
728
- target: str = typer.Argument(..., help="Entity ID, UUID prefix, platform ref, or URL"),
747
+ targets: list[str] = typer.Argument(..., help="Entity IDs, UUID prefixes, platform refs, or URLs"),
729
748
  json: bool = typer.Option(False, "--json", help="Output as JSON"),
730
749
  ) -> None:
731
- """Delete an entity from the graph."""
750
+ """Delete one or more entities from the graph."""
732
751
  from agentgraph.cli_query import cmd_delete
733
752
 
734
- cmd_delete(target=target, as_json=json)
753
+ cmd_delete(targets=targets, as_json=json)
735
754
 
736
755
 
737
756
  @app.command("unify-persons")
@@ -830,7 +849,7 @@ def mcp_config() -> None:
830
849
  typer.echo()
831
850
  typer.echo("Claude Code:")
832
851
  typer.echo(" Register the server from your terminal:\n")
833
- typer.echo(f" claude mcp add agentgraph -- {binary} mcp-serve")
852
+ typer.echo(f" claude mcp add --transport stdio --scope user agentgraph -- {binary} mcp-serve")
834
853
  typer.echo()
835
854
  typer.echo("Every client above runs the same server. Reads follow")
836
855
  typer.echo("AGENTGRAPH_QUERY_TRANSPORT: by default the local server when one is")
@@ -874,7 +893,13 @@ def install_skill(
874
893
 
875
894
  typer.echo(f"Installed AgentGraph skill '{result.skill}' to {result.destination}")
876
895
  if result.claude_destination is not None:
877
- typer.echo(f"Linked Claude skill to {result.claude_destination}")
896
+ if result.claude_linked:
897
+ typer.echo(f"Linked Claude skill to {result.claude_destination}")
898
+ else:
899
+ typer.echo(
900
+ f"Claude reads {result.claude_destination} already; "
901
+ "its skill directory is the same one."
902
+ )
878
903
 
879
904
 
880
905
  @app.command()
@@ -109,6 +109,7 @@ def cmd_search(
109
109
  authored_by_me: bool = False,
110
110
  has_attachments: bool = False,
111
111
  order_by: str | None = None,
112
+ observed_since: str | None = None,
112
113
  ) -> None:
113
114
  # An empty or whitespace-only argument is an absent query, not a query for
114
115
  # nothing: this keeps the limit default, the renderer, and the backend's
@@ -127,6 +128,7 @@ def cmd_search(
127
128
  platform,
128
129
  filters=filters,
129
130
  since=since,
131
+ observed_since=observed_since,
130
132
  authored_by_me=authored_by_me,
131
133
  has_attachments=has_attachments,
132
134
  order_by=order_by,
@@ -340,7 +342,22 @@ def cmd_bookmark(target: str, bookmarked: bool, as_json: bool) -> None:
340
342
  console.print(f"[green]{action}:[/green] {label} [{result['id'][:8]}]")
341
343
 
342
344
 
343
- def cmd_delete(target: str, as_json: bool) -> None:
345
+ def cmd_delete(targets: list[str], as_json: bool) -> None:
346
+ if len(targets) == 1:
347
+ _cmd_delete_one(targets[0], as_json)
348
+ return
349
+
350
+ async def operation(client: QueryClient) -> dict[str, Any]:
351
+ return await client.delete_many(targets)
352
+
353
+ result = _run_with_client(operation)
354
+ if as_json:
355
+ console.print_json(json.dumps(result, default=str))
356
+ return
357
+ console.print(f"[green]Deleted {result['deleted_count']} entities.[/green]")
358
+
359
+
360
+ def _cmd_delete_one(target: str, as_json: bool) -> None:
344
361
  async def operation(client: QueryClient) -> dict[str, Any]:
345
362
  return await client.delete(target)
346
363