Skip to content

Commit c6366de

Browse files
authored
Merge pull request #86 from redis/feature/token-cli-json-output
Add JSON output for token CLI token commands
2 parents c067307 + f8a475c commit c6366de

File tree

2 files changed

+386
-53
lines changed

2 files changed

+386
-53
lines changed

agent_memory_server/cli.py

Lines changed: 178 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import importlib
6+
import json
67
import sys
78
from datetime import UTC, datetime, timedelta
89

@@ -305,7 +306,26 @@ def token():
305306
@token.command()
306307
@click.option("--description", "-d", required=True, help="Token description")
307308
@click.option("--expires-days", "-e", type=int, help="Token expiration in days")
308-
def add(description: str, expires_days: int | None):
309+
@click.option(
310+
"--format",
311+
"output_format",
312+
type=click.Choice(["text", "json"]),
313+
default="text",
314+
show_default=True,
315+
help="Output format.",
316+
)
317+
@click.option(
318+
"--token",
319+
"provided_token",
320+
type=str,
321+
help="Use a pre-generated token instead of generating a new one.",
322+
)
323+
def add(
324+
description: str,
325+
expires_days: int | None,
326+
output_format: str,
327+
provided_token: str | None,
328+
) -> None:
309329
"""Add a new authentication token."""
310330
import asyncio
311331

@@ -315,9 +335,9 @@ def add(description: str, expires_days: int | None):
315335
async def create_token():
316336
redis = await get_redis_conn()
317337

318-
# Generate token
319-
token = generate_token()
320-
token_hash = hash_token(token)
338+
# Determine token value
339+
token_value = provided_token or generate_token()
340+
token_hash = hash_token(token_value)
321341

322342
# Calculate expiration
323343
now = datetime.now(UTC)
@@ -344,20 +364,43 @@ async def create_token():
344364
list_key = Keys.auth_tokens_list_key()
345365
await redis.sadd(list_key, token_hash)
346366

367+
return token_value, token_info
368+
369+
token, token_info = asyncio.run(create_token())
370+
371+
if output_format == "json":
372+
data = {
373+
"token": token,
374+
"description": token_info.description,
375+
"created_at": token_info.created_at.isoformat(),
376+
"expires_at": token_info.expires_at.isoformat()
377+
if token_info.expires_at
378+
else None,
379+
"hash": token_info.token_hash,
380+
}
381+
click.echo(json.dumps(data))
382+
else:
383+
expires_at = token_info.expires_at
347384
click.echo("Token created successfully!")
348385
click.echo(f"Token: {token}")
349-
click.echo(f"Description: {description}")
386+
click.echo(f"Description: {token_info.description}")
350387
if expires_at:
351388
click.echo(f"Expires: {expires_at.isoformat()}")
352389
else:
353390
click.echo("Expires: Never")
354391
click.echo("\nWARNING: Save this token securely. It will not be shown again.")
355392

356-
asyncio.run(create_token())
357-
358393

359394
@token.command()
360-
def list():
395+
@click.option(
396+
"--format",
397+
"output_format",
398+
type=click.Choice(["text", "json"]),
399+
default="text",
400+
show_default=True,
401+
help="Output format.",
402+
)
403+
def list(output_format: str):
361404
"""List all authentication tokens."""
362405
import asyncio
363406

@@ -371,12 +414,16 @@ async def list_tokens():
371414
list_key = Keys.auth_tokens_list_key()
372415
token_hashes = await redis.smembers(list_key)
373416

417+
tokens_data = []
418+
374419
if not token_hashes:
375-
click.echo("No tokens found.")
376-
return
420+
if output_format == "text":
421+
click.echo("No tokens found.")
422+
return tokens_data
377423

378-
click.echo("Authentication Tokens:")
379-
click.echo("=" * 50)
424+
if output_format == "text":
425+
click.echo("Authentication Tokens:")
426+
click.echo("=" * 50)
380427

381428
for token_hash in token_hashes:
382429
key = Keys.auth_token_key(token_hash)
@@ -390,27 +437,62 @@ async def list_tokens():
390437
try:
391438
token_info = TokenInfo.model_validate_json(token_data)
392439

393-
# Mask the token hash for display
394-
masked_hash = token_hash[:8] + "..." + token_hash[-8:]
395-
396-
click.echo(f"Token: {masked_hash}")
397-
click.echo(f"Description: {token_info.description}")
398-
click.echo(f"Created: {token_info.created_at.isoformat()}")
399-
if token_info.expires_at:
400-
click.echo(f"Expires: {token_info.expires_at.isoformat()}")
401-
else:
402-
click.echo("Expires: Never")
403-
click.echo("-" * 30)
440+
tokens_data.append(
441+
{
442+
"hash": token_hash,
443+
"description": token_info.description,
444+
"created_at": token_info.created_at.isoformat(),
445+
"expires_at": token_info.expires_at.isoformat()
446+
if token_info.expires_at
447+
else None,
448+
"status": (
449+
"Never Expires"
450+
if not token_info.expires_at
451+
else (
452+
"EXPIRED"
453+
if datetime.now(UTC) > token_info.expires_at
454+
else "Active"
455+
)
456+
),
457+
}
458+
)
459+
460+
if output_format == "text":
461+
# Mask the token hash for display
462+
masked_hash = token_hash[:8] + "..." + token_hash[-8:]
463+
464+
click.echo(f"Token: {masked_hash}")
465+
click.echo(f"Description: {token_info.description}")
466+
click.echo(f"Created: {token_info.created_at.isoformat()}")
467+
if token_info.expires_at:
468+
click.echo(f"Expires: {token_info.expires_at.isoformat()}")
469+
else:
470+
click.echo("Expires: Never")
471+
click.echo("-" * 30)
404472

405473
except Exception as e:
406-
click.echo(f"Error processing token {token_hash}: {e}")
474+
if output_format == "text":
475+
click.echo(f"Error processing token {token_hash}: {e}")
476+
477+
return tokens_data
478+
479+
tokens_data = asyncio.run(list_tokens())
407480

408-
asyncio.run(list_tokens())
481+
if output_format == "json":
482+
click.echo(json.dumps(tokens_data))
409483

410484

411485
@token.command()
412486
@click.argument("token_hash")
413-
def show(token_hash: str):
487+
@click.option(
488+
"--format",
489+
"output_format",
490+
type=click.Choice(["text", "json"]),
491+
default="text",
492+
show_default=True,
493+
help="Output format.",
494+
)
495+
def show(token_hash: str, output_format: str):
414496
"""Show details for a specific token."""
415497
import asyncio
416498

@@ -430,45 +512,90 @@ async def show_token():
430512
matching_hashes = [h for h in token_hashes if h.startswith(token_hash)]
431513

432514
if not matching_hashes:
433-
click.echo(f"No token found matching '{token_hash}'")
434-
return
515+
if output_format == "json":
516+
click.echo(
517+
json.dumps({"error": f"No token found matching '{token_hash}'"})
518+
)
519+
sys.exit(1)
520+
else:
521+
click.echo(f"No token found matching '{token_hash}'")
522+
sys.exit(1)
435523
if len(matching_hashes) > 1:
436-
click.echo(f"Multiple tokens match '{token_hash}':")
437-
for h in matching_hashes:
438-
click.echo(f" {h[:8]}...{h[-8:]}")
439-
return
524+
if output_format == "json":
525+
click.echo(
526+
json.dumps(
527+
{
528+
"error": f"Multiple tokens match '{token_hash}'",
529+
"matches": [
530+
f"{h[:8]}...{h[-8:]}" for h in matching_hashes
531+
],
532+
}
533+
)
534+
)
535+
sys.exit(1)
536+
else:
537+
click.echo(f"Multiple tokens match '{token_hash}':")
538+
for h in matching_hashes:
539+
click.echo(f" {h[:8]}...{h[-8:]}")
540+
sys.exit(1)
440541
token_hash = matching_hashes[0]
441542

442543
key = Keys.auth_token_key(token_hash)
443544
token_data = await redis.get(key)
444545

445546
if not token_data:
446-
click.echo(f"Token not found: {token_hash}")
447-
return
547+
if output_format == "json":
548+
click.echo(json.dumps({"error": f"Token not found: {token_hash}"}))
549+
sys.exit(1)
550+
else:
551+
click.echo(f"Token not found: {token_hash}")
552+
sys.exit(1)
448553

449554
try:
450555
token_info = TokenInfo.model_validate_json(token_data)
451556

452-
click.echo("Token Details:")
453-
click.echo("=" * 30)
454-
click.echo(f"Hash: {token_hash}")
455-
click.echo(f"Description: {token_info.description}")
456-
click.echo(f"Created: {token_info.created_at.isoformat()}")
457-
if token_info.expires_at:
458-
click.echo(f"Expires: {token_info.expires_at.isoformat()}")
459-
# Check if expired
460-
if datetime.now(UTC) > token_info.expires_at:
461-
click.echo("Status: EXPIRED")
462-
else:
463-
click.echo("Status: Active")
557+
if token_info.expires_at and datetime.now(UTC) > token_info.expires_at:
558+
status = "EXPIRED"
464559
else:
465-
click.echo("Expires: Never")
466-
click.echo("Status: Active")
560+
status = "Active"
467561

468-
except Exception as e:
469-
click.echo(f"Error processing token: {e}")
562+
return token_hash, token_info, status
470563

471-
asyncio.run(show_token())
564+
except Exception as e:
565+
if output_format == "json":
566+
click.echo(
567+
json.dumps({"error": f"Failed to parse token data: {str(e)}"})
568+
)
569+
sys.exit(1)
570+
else:
571+
click.echo(f"Error processing token: {e}", err=True)
572+
sys.exit(1)
573+
574+
result = asyncio.run(show_token())
575+
token_hash, token_info, status = result
576+
577+
if output_format == "json":
578+
data = {
579+
"hash": token_hash,
580+
"description": token_info.description,
581+
"created_at": token_info.created_at.isoformat(),
582+
"expires_at": token_info.expires_at.isoformat()
583+
if token_info.expires_at
584+
else None,
585+
"status": status,
586+
}
587+
click.echo(json.dumps(data))
588+
else:
589+
click.echo("Token Details:")
590+
click.echo("=" * 30)
591+
click.echo(f"Hash: {token_hash}")
592+
click.echo(f"Description: {token_info.description}")
593+
click.echo(f"Created: {token_info.created_at.isoformat()}")
594+
if token_info.expires_at:
595+
click.echo(f"Expires: {token_info.expires_at.isoformat()}")
596+
else:
597+
click.echo("Expires: Never")
598+
click.echo(f"Status: {status}")
472599

473600

474601
@token.command()

0 commit comments

Comments
 (0)