agentgraph-server 0.5.0__py3-none-any.whl
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.
- agentgraph/__init__.py +1 -0
- agentgraph/auth/__init__.py +0 -0
- agentgraph/auth/credentials.py +224 -0
- agentgraph/backends/__init__.py +50 -0
- agentgraph/backends/sqlite/__init__.py +1 -0
- agentgraph/backends/sqlite/backend.py +1471 -0
- agentgraph/backends/sqlite/vector.py +142 -0
- agentgraph/cli.py +721 -0
- agentgraph/cli_query.py +519 -0
- agentgraph/config.py +90 -0
- agentgraph/connectors/__init__.py +0 -0
- agentgraph/connectors/base.py +455 -0
- agentgraph/connectors/registry.py +78 -0
- agentgraph/connectors/status.py +244 -0
- agentgraph/core/__init__.py +0 -0
- agentgraph/core/context.py +26 -0
- agentgraph/core/runtime.py +36 -0
- agentgraph/core/storage.py +240 -0
- agentgraph/graph/__init__.py +1 -0
- agentgraph/graph/bookmark.py +87 -0
- agentgraph/graph/delete.py +17 -0
- agentgraph/graph/download.py +35 -0
- agentgraph/graph/embeddings.py +58 -0
- agentgraph/graph/fetch.py +53 -0
- agentgraph/graph/gc.py +26 -0
- agentgraph/graph/link.py +63 -0
- agentgraph/graph/person.py +40 -0
- agentgraph/graph/query.py +244 -0
- agentgraph/graph/upsert.py +49 -0
- agentgraph/logging.py +78 -0
- agentgraph/mcp/__init__.py +0 -0
- agentgraph/mcp/server.py +811 -0
- agentgraph/perf.py +43 -0
- agentgraph/server/__init__.py +0 -0
- agentgraph/server/app.py +133 -0
- agentgraph/server/cli_api.py +708 -0
- agentgraph/server/dwell.py +79 -0
- agentgraph/server/graph_api.py +46 -0
- agentgraph/server/router.py +47 -0
- agentgraph/server/sync.py +247 -0
- agentgraph/skills.py +93 -0
- agentgraph_server-0.5.0.data/data/.agents/skills/graph/SKILL.md +159 -0
- agentgraph_server-0.5.0.data/data/.agents/skills/slack-auth/SKILL.md +92 -0
- agentgraph_server-0.5.0.dist-info/METADATA +286 -0
- agentgraph_server-0.5.0.dist-info/RECORD +49 -0
- agentgraph_server-0.5.0.dist-info/WHEEL +5 -0
- agentgraph_server-0.5.0.dist-info/entry_points.txt +2 -0
- agentgraph_server-0.5.0.dist-info/licenses/LICENSE +21 -0
- agentgraph_server-0.5.0.dist-info/top_level.txt +1 -0
agentgraph/cli.py
ADDED
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
"""AgentGraph CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json as _json
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from typing import TYPE_CHECKING, cast
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from agentgraph.connectors.base import BaseConnector
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(
|
|
17
|
+
name="agentgraph",
|
|
18
|
+
help="Local knowledge graph for AI agents.",
|
|
19
|
+
no_args_is_help=True,
|
|
20
|
+
)
|
|
21
|
+
auth_app = typer.Typer(
|
|
22
|
+
help="Show auth provider state or manage connector authentication.",
|
|
23
|
+
invoke_without_command=True,
|
|
24
|
+
no_args_is_help=False,
|
|
25
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
26
|
+
)
|
|
27
|
+
app.add_typer(auth_app, name="auth")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@contextmanager
|
|
31
|
+
def _readable_credentials() -> Iterator[None]:
|
|
32
|
+
"""Report a damaged credentials file as an error instead of a traceback."""
|
|
33
|
+
from agentgraph.auth.credentials import CredentialsFileError
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
yield
|
|
37
|
+
except CredentialsFileError as exc:
|
|
38
|
+
typer.echo(str(exc), err=True)
|
|
39
|
+
raise typer.Exit(code=1) from exc
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _status_label(status: str) -> str:
|
|
43
|
+
return {"ok": "authenticated", "missing": "not authenticated", "invalid": "INVALID"}.get(
|
|
44
|
+
status, status
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _remove_auth_credentials(provider: str, account_id: str | None) -> dict[str, object]:
|
|
49
|
+
from agentgraph.auth.credentials import remove_platform, remove_platform_account
|
|
50
|
+
|
|
51
|
+
removed = (
|
|
52
|
+
remove_platform_account(provider, account_id)
|
|
53
|
+
if account_id is not None
|
|
54
|
+
else remove_platform(provider)
|
|
55
|
+
)
|
|
56
|
+
result: dict[str, object] = {
|
|
57
|
+
"provider": provider,
|
|
58
|
+
"removed": removed,
|
|
59
|
+
}
|
|
60
|
+
if account_id is not None:
|
|
61
|
+
result["account_id"] = account_id
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _parse_auth_args(
|
|
66
|
+
args: list[str],
|
|
67
|
+
*,
|
|
68
|
+
account: str | None,
|
|
69
|
+
json: bool,
|
|
70
|
+
verify: bool,
|
|
71
|
+
add: bool,
|
|
72
|
+
provider_args: list[str] | None = None,
|
|
73
|
+
) -> tuple[list[str], str | None, bool, bool, bool, list[str], str | None]:
|
|
74
|
+
positionals: list[str] = []
|
|
75
|
+
parsed_account = account
|
|
76
|
+
parsed_json = json
|
|
77
|
+
parsed_verify = verify
|
|
78
|
+
parsed_add = add
|
|
79
|
+
parsed_provider_args = list(provider_args or [])
|
|
80
|
+
error: str | None = None
|
|
81
|
+
i = 0
|
|
82
|
+
while i < len(args):
|
|
83
|
+
arg = args[i]
|
|
84
|
+
if arg == "--json":
|
|
85
|
+
parsed_json = True
|
|
86
|
+
elif arg == "--verify":
|
|
87
|
+
parsed_verify = True
|
|
88
|
+
elif arg == "--add":
|
|
89
|
+
parsed_add = True
|
|
90
|
+
elif arg == "--account":
|
|
91
|
+
if i + 1 >= len(args):
|
|
92
|
+
error = "--account requires an account ID"
|
|
93
|
+
break
|
|
94
|
+
parsed_account = args[i + 1]
|
|
95
|
+
i += 1
|
|
96
|
+
elif arg.startswith("--account="):
|
|
97
|
+
parsed_account = arg.split("=", 1)[1]
|
|
98
|
+
elif arg.startswith("-"):
|
|
99
|
+
if not positionals or positionals[0] in ("status", "remove"):
|
|
100
|
+
error = f"Unknown option for auth: {arg}"
|
|
101
|
+
break
|
|
102
|
+
parsed_provider_args.append(arg)
|
|
103
|
+
else:
|
|
104
|
+
if len(positionals) == 1 and positionals[0] not in ("status", "remove"):
|
|
105
|
+
parsed_provider_args.append(arg)
|
|
106
|
+
else:
|
|
107
|
+
positionals.append(arg)
|
|
108
|
+
i += 1
|
|
109
|
+
return (
|
|
110
|
+
positionals,
|
|
111
|
+
parsed_account,
|
|
112
|
+
parsed_json,
|
|
113
|
+
parsed_verify,
|
|
114
|
+
parsed_add,
|
|
115
|
+
parsed_provider_args,
|
|
116
|
+
error,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@auth_app.callback()
|
|
121
|
+
def auth(
|
|
122
|
+
auth_args: list[str] | None = typer.Argument(
|
|
123
|
+
None,
|
|
124
|
+
help="Use 'status', 'remove <provider>', or an auth provider such as google, slack, or discord",
|
|
125
|
+
),
|
|
126
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
127
|
+
verify: bool = typer.Option(
|
|
128
|
+
False, "--verify", help="Live-check credentials with provider APIs"
|
|
129
|
+
),
|
|
130
|
+
add: bool = typer.Option(
|
|
131
|
+
False, "--add", help="Add another authenticated account for this provider"
|
|
132
|
+
),
|
|
133
|
+
account: str | None = typer.Option(
|
|
134
|
+
None, "--account", help="Re-authenticate a specific account ID"
|
|
135
|
+
),
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Show auth status or authenticate a provider."""
|
|
138
|
+
(
|
|
139
|
+
args,
|
|
140
|
+
parsed_account,
|
|
141
|
+
parsed_json,
|
|
142
|
+
parsed_verify,
|
|
143
|
+
parsed_add,
|
|
144
|
+
parsed_provider_args,
|
|
145
|
+
parse_error,
|
|
146
|
+
) = _parse_auth_args(
|
|
147
|
+
auth_args or [],
|
|
148
|
+
account=account,
|
|
149
|
+
json=json,
|
|
150
|
+
verify=verify,
|
|
151
|
+
add=add,
|
|
152
|
+
)
|
|
153
|
+
if parse_error is not None:
|
|
154
|
+
typer.echo(parse_error, err=True)
|
|
155
|
+
raise typer.Exit(code=1)
|
|
156
|
+
|
|
157
|
+
target = args[0] if args else None
|
|
158
|
+
if target == "remove":
|
|
159
|
+
if len(args) > 2:
|
|
160
|
+
typer.echo(f"Unexpected argument for auth remove: {args[2]}", err=True)
|
|
161
|
+
raise typer.Exit(code=1)
|
|
162
|
+
if len(args) < 2:
|
|
163
|
+
typer.echo(
|
|
164
|
+
"Usage: agentgraph auth remove <provider> [--account <account-id>] [--json]",
|
|
165
|
+
err=True,
|
|
166
|
+
)
|
|
167
|
+
raise typer.Exit(code=1)
|
|
168
|
+
provider = args[1]
|
|
169
|
+
with _readable_credentials():
|
|
170
|
+
result = _remove_auth_credentials(provider, parsed_account)
|
|
171
|
+
|
|
172
|
+
if parsed_json:
|
|
173
|
+
typer.echo(_json.dumps(result, indent=2))
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
if result["removed"]:
|
|
177
|
+
if parsed_account is None:
|
|
178
|
+
typer.echo(f"Removed stored credentials for {provider}.")
|
|
179
|
+
else:
|
|
180
|
+
typer.echo(f"Removed stored credentials for {provider} account {parsed_account}.")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
if parsed_account is None:
|
|
184
|
+
typer.echo(f"No stored credentials found for {provider}.", err=True)
|
|
185
|
+
else:
|
|
186
|
+
typer.echo(
|
|
187
|
+
f"No stored credentials found for {provider} account {parsed_account}.",
|
|
188
|
+
err=True,
|
|
189
|
+
)
|
|
190
|
+
raise typer.Exit(code=1)
|
|
191
|
+
|
|
192
|
+
if len(args) > 1:
|
|
193
|
+
typer.echo(f"Unexpected argument for auth: {args[1]}", err=True)
|
|
194
|
+
raise typer.Exit(code=1)
|
|
195
|
+
|
|
196
|
+
if target not in (None, "status"):
|
|
197
|
+
from agentgraph.connectors.registry import bootstrap, get_all_connectors
|
|
198
|
+
from agentgraph.connectors.status import (
|
|
199
|
+
auth_provider_connectors,
|
|
200
|
+
run_auth_provider_flow,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
bootstrap()
|
|
204
|
+
all_connectors = get_all_connectors()
|
|
205
|
+
grouped = auth_provider_connectors(all_connectors)
|
|
206
|
+
seen = {label: connectors[0] for label, connectors in grouped.items()}
|
|
207
|
+
|
|
208
|
+
if target not in seen:
|
|
209
|
+
available = ", ".join(["status", *sorted(seen)])
|
|
210
|
+
typer.echo(f"Unknown auth target '{target}'. Available: {available}", err=True)
|
|
211
|
+
raise typer.Exit(code=1)
|
|
212
|
+
|
|
213
|
+
with _readable_credentials():
|
|
214
|
+
try:
|
|
215
|
+
run_auth_provider_flow(
|
|
216
|
+
all_connectors,
|
|
217
|
+
target,
|
|
218
|
+
account_id=parsed_account,
|
|
219
|
+
add=parsed_add,
|
|
220
|
+
args=parsed_provider_args,
|
|
221
|
+
)
|
|
222
|
+
except ValueError as exc:
|
|
223
|
+
typer.echo(str(exc), err=True)
|
|
224
|
+
raise typer.Exit(code=2) from exc
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
from agentgraph.connectors.registry import bootstrap, get_all_connectors
|
|
228
|
+
from agentgraph.connectors.status import auth_provider_status_items
|
|
229
|
+
|
|
230
|
+
bootstrap()
|
|
231
|
+
with _readable_credentials():
|
|
232
|
+
items = asyncio.run(auth_provider_status_items(get_all_connectors(), verify=parsed_verify))
|
|
233
|
+
|
|
234
|
+
if parsed_json:
|
|
235
|
+
typer.echo(_json.dumps(items, indent=2))
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
for item in items:
|
|
239
|
+
status = str(item["auth_status"])
|
|
240
|
+
detail = item["auth_detail"]
|
|
241
|
+
auth_state = _status_label(status)
|
|
242
|
+
if detail:
|
|
243
|
+
auth_state = (
|
|
244
|
+
f"{auth_state} ({detail})" if status != "ok" else f"{auth_state} as {detail}"
|
|
245
|
+
)
|
|
246
|
+
connectors = ", ".join(str(source) for source in cast(list[object], item["connectors"]))
|
|
247
|
+
typer.echo(f" {item['provider']:<12} {item['description']}")
|
|
248
|
+
typer.echo(f" {'':<12} auth: {auth_state} | connectors: {connectors}")
|
|
249
|
+
accounts = cast(list[dict[str, object]], item.get("accounts") or [])
|
|
250
|
+
for account_row in accounts:
|
|
251
|
+
account_status = str(account_row["auth_status"])
|
|
252
|
+
account_auth = _status_label(account_status)
|
|
253
|
+
account_detail = account_row.get("auth_detail")
|
|
254
|
+
if account_detail:
|
|
255
|
+
account_auth = (
|
|
256
|
+
f"{account_auth} ({account_detail})"
|
|
257
|
+
if account_status != "ok"
|
|
258
|
+
else f"{account_auth} as {account_detail}"
|
|
259
|
+
)
|
|
260
|
+
typer.echo(
|
|
261
|
+
f" {'':<12} account: {account_row['label']} [{account_row['account_id']}]"
|
|
262
|
+
f" | method: {account_row.get('auth_method') or 'unknown'} | {account_auth}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@app.command(
|
|
267
|
+
"connector",
|
|
268
|
+
context_settings={
|
|
269
|
+
"allow_extra_args": True,
|
|
270
|
+
"ignore_unknown_options": True,
|
|
271
|
+
"help_option_names": [],
|
|
272
|
+
},
|
|
273
|
+
)
|
|
274
|
+
def connector_command(
|
|
275
|
+
ctx: typer.Context,
|
|
276
|
+
source: str | None = typer.Argument(None, help="Connector source, e.g. rss"),
|
|
277
|
+
args: list[str] | None = typer.Argument(None, help="Connector-owned command and arguments"),
|
|
278
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
279
|
+
help: bool = typer.Option(False, "--help", help="Show this message and exit."),
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Run a connector-owned command."""
|
|
282
|
+
from agentgraph.connectors.registry import bootstrap, get_connector
|
|
283
|
+
|
|
284
|
+
if source is None:
|
|
285
|
+
from typer.rich_utils import rich_format_help
|
|
286
|
+
|
|
287
|
+
rich_format_help(obj=ctx.command, ctx=ctx, markup_mode="rich")
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
bootstrap()
|
|
291
|
+
connector = get_connector(source)
|
|
292
|
+
if connector is None:
|
|
293
|
+
typer.echo(f"Unknown connector '{source}'", err=True)
|
|
294
|
+
raise typer.Exit(code=1)
|
|
295
|
+
|
|
296
|
+
if help:
|
|
297
|
+
typer.echo(type(connector).cli_help())
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
command_args = args or []
|
|
301
|
+
try:
|
|
302
|
+
result = type(connector).run_cli_command(command_args)
|
|
303
|
+
effects = type(connector).command_effects(command_args, result)
|
|
304
|
+
if effects.poll:
|
|
305
|
+
from agentgraph.cli_query import queue_connector_poll
|
|
306
|
+
|
|
307
|
+
result["poll"] = queue_connector_poll(connector.source)
|
|
308
|
+
except NotImplementedError as exc:
|
|
309
|
+
typer.echo(str(exc), err=True)
|
|
310
|
+
raise typer.Exit(code=1) from exc
|
|
311
|
+
except (OSError, ValueError) as exc:
|
|
312
|
+
typer.echo(str(exc), err=True)
|
|
313
|
+
raise typer.Exit(code=1) from exc
|
|
314
|
+
|
|
315
|
+
if json:
|
|
316
|
+
typer.echo(_json.dumps(result, indent=2))
|
|
317
|
+
return
|
|
318
|
+
typer.echo(type(connector).format_cli_result(result))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@app.command()
|
|
322
|
+
def connectors(
|
|
323
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
324
|
+
verify: bool = typer.Option(
|
|
325
|
+
False, "--verify", help="Live-check connector credentials with provider APIs"
|
|
326
|
+
),
|
|
327
|
+
) -> None:
|
|
328
|
+
"""List installed connectors and their sync status."""
|
|
329
|
+
from agentgraph.connectors.registry import bootstrap, get_all_connectors
|
|
330
|
+
from agentgraph.connectors.status import connector_status_items
|
|
331
|
+
from agentgraph.core.runtime import backend_context
|
|
332
|
+
|
|
333
|
+
bootstrap()
|
|
334
|
+
all_connectors = get_all_connectors()
|
|
335
|
+
|
|
336
|
+
async def _gather() -> list[dict[str, object]]:
|
|
337
|
+
async with backend_context() as backend:
|
|
338
|
+
return await connector_status_items(all_connectors, backend, verify=verify)
|
|
339
|
+
|
|
340
|
+
items = asyncio.run(_gather())
|
|
341
|
+
|
|
342
|
+
if json:
|
|
343
|
+
typer.echo(_json.dumps(items, indent=2))
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
for item in items:
|
|
347
|
+
sync = str(item["sync"])
|
|
348
|
+
last_sync = str(item["last_sync"])
|
|
349
|
+
desc = item["description"] or item["source"]
|
|
350
|
+
typer.echo(f" {item['source']:<12} {desc}")
|
|
351
|
+
status = item["auth_status"]
|
|
352
|
+
if status is None:
|
|
353
|
+
typer.echo(f" {'':<12} sync: {sync} | last sync: {last_sync}")
|
|
354
|
+
continue
|
|
355
|
+
status_label = str(status)
|
|
356
|
+
detail = item["auth_detail"]
|
|
357
|
+
auth = _status_label(status_label)
|
|
358
|
+
if detail:
|
|
359
|
+
auth = f"{auth} ({detail})" if status_label != "ok" else f"{auth} as {detail}"
|
|
360
|
+
typer.echo(
|
|
361
|
+
f" {'':<12} auth: {auth} via {item['auth_provider']} | sync: {sync} | last sync: {last_sync}"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@app.command()
|
|
366
|
+
def serve(
|
|
367
|
+
reload: bool = typer.Option(False, "--reload", "-r", help="Auto-reload on code changes"),
|
|
368
|
+
) -> None:
|
|
369
|
+
"""Start the AgentGraph backend server."""
|
|
370
|
+
import uvicorn
|
|
371
|
+
|
|
372
|
+
from agentgraph.config import get_settings
|
|
373
|
+
from agentgraph.logging import configure_logging
|
|
374
|
+
|
|
375
|
+
settings = get_settings()
|
|
376
|
+
configure_logging(settings.log_level, settings.log_file)
|
|
377
|
+
uvicorn.run(
|
|
378
|
+
"agentgraph.server.app:app",
|
|
379
|
+
host=settings.server_host,
|
|
380
|
+
port=settings.server_port,
|
|
381
|
+
reload=reload,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@app.command()
|
|
386
|
+
def search(
|
|
387
|
+
query: str = typer.Argument(..., help="Search query"),
|
|
388
|
+
type: list[str] = typer.Option([], "--type", "-t", help="Filter by entity type"),
|
|
389
|
+
platform: str | None = typer.Option(
|
|
390
|
+
None, "--platform", "-p", help="Scope to a single platform (e.g. slack, discord)"
|
|
391
|
+
),
|
|
392
|
+
limit: int = typer.Option(10, "--limit", "-n", help="Maximum results"),
|
|
393
|
+
min_score: float = typer.Option(0.03, "--min-score", help="Minimum relevance score (0–1)"),
|
|
394
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
395
|
+
) -> None:
|
|
396
|
+
"""Search the knowledge graph."""
|
|
397
|
+
from agentgraph.cli_query import cmd_search
|
|
398
|
+
|
|
399
|
+
cmd_search(
|
|
400
|
+
query=query,
|
|
401
|
+
entity_types=type,
|
|
402
|
+
platform=platform,
|
|
403
|
+
limit=limit,
|
|
404
|
+
min_score=min_score,
|
|
405
|
+
as_json=json,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@app.command()
|
|
410
|
+
def get(
|
|
411
|
+
entity_id: str = typer.Argument(..., help="Entity ID, platform ref, or URL"),
|
|
412
|
+
resolve: bool = typer.Option(
|
|
413
|
+
False, "--resolve", "-r", help="Fetch from source if entity is a stub (no content)"
|
|
414
|
+
),
|
|
415
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
416
|
+
) -> None:
|
|
417
|
+
"""Fetch full details for an existing entity."""
|
|
418
|
+
from agentgraph.cli_query import cmd_get
|
|
419
|
+
|
|
420
|
+
cmd_get(entity_id=entity_id, as_json=json, resolve=resolve)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@app.command()
|
|
424
|
+
def edges(
|
|
425
|
+
entity_id: str = typer.Argument(..., help="Entity ID"),
|
|
426
|
+
type: str = typer.Option("", "--type", "-t", help="Filter by edge type"),
|
|
427
|
+
direction: str = typer.Option("both", "--direction", "-d", help="in | out | both"),
|
|
428
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
429
|
+
) -> None:
|
|
430
|
+
"""List edges for an entity."""
|
|
431
|
+
from agentgraph.cli_query import cmd_edges
|
|
432
|
+
|
|
433
|
+
cmd_edges(entity_id=entity_id, edge_type=type or None, direction=direction, as_json=json)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@app.command()
|
|
437
|
+
def traverse(
|
|
438
|
+
entity_id: str = typer.Argument(..., help="Start entity ID"),
|
|
439
|
+
depth: int = typer.Option(2, "--depth", "-d", help="Maximum traversal depth"),
|
|
440
|
+
resolve: bool = typer.Option(
|
|
441
|
+
False, "--resolve", "-r", help="Fetch stub nodes from source before returning"
|
|
442
|
+
),
|
|
443
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
444
|
+
) -> None:
|
|
445
|
+
"""Traverse the graph from an entity."""
|
|
446
|
+
from agentgraph.cli_query import cmd_traverse
|
|
447
|
+
|
|
448
|
+
cmd_traverse(entity_id=entity_id, max_depth=depth, as_json=json, resolve=resolve)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@app.command()
|
|
452
|
+
def fetch(
|
|
453
|
+
platform: str = typer.Argument(..., help="Platform name (e.g. gdocs, slack, discord, rss)"),
|
|
454
|
+
resource_id: str = typer.Argument(..., help="Platform-specific entity ID"),
|
|
455
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
456
|
+
) -> None:
|
|
457
|
+
"""Trigger a connector fetch for a platform entity."""
|
|
458
|
+
from agentgraph.cli_query import cmd_fetch
|
|
459
|
+
|
|
460
|
+
cmd_fetch(platform=platform, resource_id=resource_id, as_json=json)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@app.command("fetch-entity")
|
|
464
|
+
def fetch_entity_cmd(
|
|
465
|
+
entity_id: str = typer.Argument(..., help="Internal entity UUID"),
|
|
466
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
467
|
+
) -> None:
|
|
468
|
+
"""Trigger a connector re-fetch for an entity by its internal ID."""
|
|
469
|
+
from agentgraph.cli_query import cmd_fetch_entity
|
|
470
|
+
|
|
471
|
+
cmd_fetch_entity(entity_id=entity_id, as_json=json)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@app.command()
|
|
475
|
+
def download(
|
|
476
|
+
entity_id: str = typer.Argument(..., help="Entity ID, UUID prefix, or platform ref"),
|
|
477
|
+
output: str | None = typer.Option(None, "--output", "-o", help="Output file path or directory"),
|
|
478
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
479
|
+
) -> None:
|
|
480
|
+
"""Download an entity's source file using connector auth."""
|
|
481
|
+
from agentgraph.cli_query import cmd_download
|
|
482
|
+
|
|
483
|
+
cmd_download(entity_id=entity_id, output_path=output, as_json=json)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@app.command()
|
|
487
|
+
def bookmark(
|
|
488
|
+
target: str = typer.Argument(..., help="Entity ID, UUID prefix, platform ref, or URL"),
|
|
489
|
+
remove: bool = typer.Option(
|
|
490
|
+
False, "--remove", help="Remove bookmark protection instead of adding it"
|
|
491
|
+
),
|
|
492
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
493
|
+
) -> None:
|
|
494
|
+
"""Set or remove bookmark protection for an entity or URL."""
|
|
495
|
+
from agentgraph.cli_query import cmd_bookmark
|
|
496
|
+
|
|
497
|
+
cmd_bookmark(target=target, bookmarked=not remove, as_json=json)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@app.command("delete")
|
|
501
|
+
def delete_cmd(
|
|
502
|
+
target: str = typer.Argument(..., help="Entity ID, UUID prefix, platform ref, or URL"),
|
|
503
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
504
|
+
) -> None:
|
|
505
|
+
"""Delete an entity from the graph."""
|
|
506
|
+
from agentgraph.cli_query import cmd_delete
|
|
507
|
+
|
|
508
|
+
cmd_delete(target=target, as_json=json)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@app.command("unify-persons")
|
|
512
|
+
def unify_persons_cmd(
|
|
513
|
+
primary_entity_id: str = typer.Argument(..., help="Person entity to keep"),
|
|
514
|
+
duplicate_entity_ids: list[str] = typer.Argument(
|
|
515
|
+
..., help="Duplicate Person entities to merge into the primary"
|
|
516
|
+
),
|
|
517
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
518
|
+
) -> None:
|
|
519
|
+
"""Merge duplicate Person entities that refer to the same human."""
|
|
520
|
+
from agentgraph.cli_query import cmd_unify_persons
|
|
521
|
+
|
|
522
|
+
cmd_unify_persons(
|
|
523
|
+
primary_entity_id=primary_entity_id,
|
|
524
|
+
duplicate_entity_ids=duplicate_entity_ids,
|
|
525
|
+
as_json=json,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@app.command()
|
|
530
|
+
def onboard() -> None:
|
|
531
|
+
"""Interactive setup: authenticate with each installed connector."""
|
|
532
|
+
from agentgraph.connectors.registry import bootstrap, get_all_connectors
|
|
533
|
+
|
|
534
|
+
bootstrap()
|
|
535
|
+
# Dedup by auth_label so multi-connector platforms (e.g. Google) appear once.
|
|
536
|
+
seen: dict[str, BaseConnector] = {}
|
|
537
|
+
for connector in get_all_connectors():
|
|
538
|
+
label: str = getattr(connector, "auth_label", None) or connector.source
|
|
539
|
+
if label not in seen:
|
|
540
|
+
seen[label] = connector
|
|
541
|
+
|
|
542
|
+
steps = list(seen.items())
|
|
543
|
+
total = len(steps)
|
|
544
|
+
|
|
545
|
+
typer.echo("=== AgentGraph Setup ===\n")
|
|
546
|
+
|
|
547
|
+
for i, (label, connector) in enumerate(steps, 1):
|
|
548
|
+
prompt: str = getattr(connector, "onboard_prompt", None) or f"Set up {label}?"
|
|
549
|
+
description: str = getattr(connector, "auth_description", None) or label.title()
|
|
550
|
+
typer.echo(f"Step {i}/{total}: {description}")
|
|
551
|
+
if typer.confirm(f" {prompt}", default=True):
|
|
552
|
+
type(connector).run_auth_flow()
|
|
553
|
+
else:
|
|
554
|
+
typer.echo(" Skipped.")
|
|
555
|
+
if i < total:
|
|
556
|
+
typer.echo()
|
|
557
|
+
|
|
558
|
+
typer.echo("\nSetup complete. Run `agentgraph serve` to start the server.")
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@app.command()
|
|
562
|
+
def mcp_config() -> None:
|
|
563
|
+
"""Print MCP client setup instructions."""
|
|
564
|
+
import json
|
|
565
|
+
import sys
|
|
566
|
+
|
|
567
|
+
binary = sys.argv[0]
|
|
568
|
+
|
|
569
|
+
config = {
|
|
570
|
+
"mcpServers": {
|
|
571
|
+
"agentgraph": {
|
|
572
|
+
"command": binary,
|
|
573
|
+
"args": ["mcp-serve"],
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
typer.echo("\nFor stdio MCP clients, add this to your MCP client config:\n")
|
|
579
|
+
typer.echo(" Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json")
|
|
580
|
+
typer.echo(" Claude Code: ~/.claude/mcp.json (or .claude/mcp.json in your project)\n")
|
|
581
|
+
typer.echo(json.dumps(config, indent=2))
|
|
582
|
+
typer.echo()
|
|
583
|
+
typer.echo("For ChatGPT developer mode, do not use the stdio JSON above.")
|
|
584
|
+
typer.echo("Run a streamable HTTP MCP server:")
|
|
585
|
+
typer.echo(f" {binary} mcp-serve --transport streamable-http --port 8808")
|
|
586
|
+
typer.echo("Local endpoint:")
|
|
587
|
+
typer.echo(" http://127.0.0.1:8808/mcp")
|
|
588
|
+
typer.echo("ChatGPT requires a reachable HTTPS URL. Use Secure MCP Tunnel, ngrok,")
|
|
589
|
+
typer.echo("or Cloudflare Tunnel, then create an app/connector in ChatGPT Developer mode")
|
|
590
|
+
typer.echo("with the public URL ending in /mcp, for example:")
|
|
591
|
+
typer.echo(" https://your-tunnel.example/mcp")
|
|
592
|
+
typer.echo()
|
|
593
|
+
typer.echo("SSE is also supported by the server if your MCP client needs it:")
|
|
594
|
+
typer.echo(f" {binary} mcp-serve --transport sse --port 8808")
|
|
595
|
+
typer.echo(" http://127.0.0.1:8808/sse")
|
|
596
|
+
typer.echo()
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@app.command()
|
|
600
|
+
def install_skill(
|
|
601
|
+
skill: str = typer.Argument("graph", help="Bundled skill to install"),
|
|
602
|
+
target: str = typer.Option("user", "--target", help="Install target: user or project"),
|
|
603
|
+
force: bool = typer.Option(False, "--force", help="Overwrite an existing installed skill"),
|
|
604
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
605
|
+
) -> None:
|
|
606
|
+
"""Install a bundled AgentGraph skill into an agent skill directory."""
|
|
607
|
+
from agentgraph.skills import SkillInstallError
|
|
608
|
+
from agentgraph.skills import install_skill as install_agentgraph_skill
|
|
609
|
+
|
|
610
|
+
if target not in ("user", "project"):
|
|
611
|
+
typer.echo("Target must be 'user' or 'project'", err=True)
|
|
612
|
+
raise typer.Exit(code=1)
|
|
613
|
+
|
|
614
|
+
try:
|
|
615
|
+
result = install_agentgraph_skill(skill, target=target, force=force)
|
|
616
|
+
except SkillInstallError as exc:
|
|
617
|
+
typer.echo(str(exc), err=True)
|
|
618
|
+
raise typer.Exit(code=1) from exc
|
|
619
|
+
|
|
620
|
+
if json:
|
|
621
|
+
typer.echo(_json.dumps(result.to_dict(), indent=2))
|
|
622
|
+
return
|
|
623
|
+
|
|
624
|
+
typer.echo(f"Installed AgentGraph skill '{result.skill}' to {result.destination}")
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
@app.command()
|
|
628
|
+
def mcp_serve(
|
|
629
|
+
transport: str = typer.Option(
|
|
630
|
+
"stdio", "--transport", help="Transport: stdio, sse, or streamable-http"
|
|
631
|
+
),
|
|
632
|
+
port: int = typer.Option(8808, "--port", help="Port for sse / streamable-http transports"),
|
|
633
|
+
host: str = typer.Option(
|
|
634
|
+
"127.0.0.1", "--host", help="Host to bind for sse / streamable-http transports"
|
|
635
|
+
),
|
|
636
|
+
) -> None:
|
|
637
|
+
"""Start the AgentGraph MCP server."""
|
|
638
|
+
import asyncio
|
|
639
|
+
|
|
640
|
+
from agentgraph.core.context import set_backend
|
|
641
|
+
from agentgraph.core.runtime import create_backend
|
|
642
|
+
|
|
643
|
+
backend = create_backend()
|
|
644
|
+
asyncio.run(backend.initialize())
|
|
645
|
+
set_backend(backend)
|
|
646
|
+
|
|
647
|
+
from agentgraph.mcp.server import mcp
|
|
648
|
+
|
|
649
|
+
if transport in ("sse", "streamable-http"):
|
|
650
|
+
mcp.settings.port = port
|
|
651
|
+
mcp.settings.host = host
|
|
652
|
+
# Clear DNS rebinding protection when binding to a non-localhost address
|
|
653
|
+
# (the singleton is initialized with host=127.0.0.1 which enables it by default)
|
|
654
|
+
if host not in ("127.0.0.1", "localhost", "::1"):
|
|
655
|
+
mcp.settings.transport_security = None
|
|
656
|
+
mcp.run(transport=transport) # type: ignore[arg-type]
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
@app.command()
|
|
660
|
+
def poll(
|
|
661
|
+
source: str | None = typer.Argument(
|
|
662
|
+
None, help="Connector source to poll (e.g. slack, gmail, rss). Omit to poll all."
|
|
663
|
+
),
|
|
664
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
665
|
+
) -> None:
|
|
666
|
+
"""Trigger a background poll for one or all connectors."""
|
|
667
|
+
from agentgraph.cli_query import cmd_poll
|
|
668
|
+
|
|
669
|
+
cmd_poll(source=source, as_json=json)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
@app.command()
|
|
673
|
+
def ingest(
|
|
674
|
+
source: str = typer.Argument(..., help="Connector source to ingest (e.g. gmail, rss)"),
|
|
675
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
676
|
+
) -> None:
|
|
677
|
+
"""Run a one-shot bulk ingest for a connector (all data within the retention window)."""
|
|
678
|
+
from agentgraph.cli_query import cmd_ingest
|
|
679
|
+
|
|
680
|
+
cmd_ingest(source=source, as_json=json)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
@app.command()
|
|
684
|
+
def query(
|
|
685
|
+
entity_type: str = typer.Option(..., "--type", "-t", help="Entity type to query"),
|
|
686
|
+
filter: list[str] = typer.Option(
|
|
687
|
+
[], "--filter", "-f", help="key=value filters (column or metadata)"
|
|
688
|
+
),
|
|
689
|
+
since: str | None = typer.Option(
|
|
690
|
+
None,
|
|
691
|
+
"--since",
|
|
692
|
+
"-s",
|
|
693
|
+
help="Only results after this time: ISO timestamp or relative (12h, 30m, 2d)",
|
|
694
|
+
),
|
|
695
|
+
mine: bool = typer.Option(False, "--mine", "-m", help="Only entities authored by me"),
|
|
696
|
+
has_attachments: bool = typer.Option(
|
|
697
|
+
False, "--has-attachments", help="Only Message entities that have file/image attachments"
|
|
698
|
+
),
|
|
699
|
+
limit: int = typer.Option(50, "--limit", "-n", help="Maximum results"),
|
|
700
|
+
order_by: str = typer.Option(
|
|
701
|
+
"created_at",
|
|
702
|
+
"--order-by",
|
|
703
|
+
"-o",
|
|
704
|
+
help="Column to sort by (created_at, updated_at, last_accessed)",
|
|
705
|
+
),
|
|
706
|
+
json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
707
|
+
) -> None:
|
|
708
|
+
"""Query entities by type and filters."""
|
|
709
|
+
from agentgraph.cli_query import cmd_query
|
|
710
|
+
|
|
711
|
+
parsed_filters = dict(f.split("=", 1) for f in filter if "=" in f)
|
|
712
|
+
cmd_query(
|
|
713
|
+
entity_type=entity_type,
|
|
714
|
+
filters=parsed_filters,
|
|
715
|
+
limit=limit,
|
|
716
|
+
order_by=order_by,
|
|
717
|
+
since=since,
|
|
718
|
+
authored_by_me=mine,
|
|
719
|
+
has_attachments=has_attachments,
|
|
720
|
+
as_json=json,
|
|
721
|
+
)
|