heyarchie-cli 0.1.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.
- archie_cli/__init__.py +12 -0
- archie_cli/__main__.py +35 -0
- archie_cli/auth.py +101 -0
- archie_cli/client.py +179 -0
- archie_cli/cmds/__init__.py +1 -0
- archie_cli/cmds/login.py +46 -0
- archie_cli/cmds/skills.py +233 -0
- archie_cli/cmds/workstreams.py +298 -0
- heyarchie_cli-0.1.0.dist-info/METADATA +101 -0
- heyarchie_cli-0.1.0.dist-info/RECORD +12 -0
- heyarchie_cli-0.1.0.dist-info/WHEEL +4 -0
- heyarchie_cli-0.1.0.dist-info/entry_points.txt +2 -0
archie_cli/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Archie Connect CLI — `archie` console script.
|
|
2
|
+
|
|
3
|
+
Surfaces the Connect platform as terminal commands. Read-friendly when
|
|
4
|
+
attached to a TTY (rich-rendered tables, colors); machine-friendly when
|
|
5
|
+
piped (`--json`).
|
|
6
|
+
|
|
7
|
+
Auth is bearer-only today: an Archie session JWT or an Archie OAuth JWT.
|
|
8
|
+
WorkOS-managed `sk_*` API keys are not yet validated server-side — see
|
|
9
|
+
`docs/connect/api-keys-flow.md` for the architectural decision pending.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
archie_cli/__main__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""``archie`` console-script entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from archie_cli import __version__
|
|
8
|
+
from archie_cli.cmds.login import login_cmd
|
|
9
|
+
from archie_cli.cmds.skills import skills_group
|
|
10
|
+
from archie_cli.cmds.workstreams import workstreams_group
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group(
|
|
14
|
+
help=(
|
|
15
|
+
"Archie Connect CLI.\n\n"
|
|
16
|
+
"Read-friendly when attached to a TTY; pipe-friendly with --json.\n"
|
|
17
|
+
"Auth: bearer token via --api-key, env ARCHIE_API_KEY, or `archie login`."
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
@click.version_option(__version__, prog_name="archie")
|
|
21
|
+
def cli() -> None:
|
|
22
|
+
"""archie — Connect CLI."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
cli.add_command(login_cmd)
|
|
26
|
+
cli.add_command(skills_group)
|
|
27
|
+
cli.add_command(workstreams_group)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> None:
|
|
31
|
+
cli()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__": # pragma: no cover
|
|
35
|
+
main()
|
archie_cli/auth.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Credential storage for the `archie` CLI.
|
|
2
|
+
|
|
3
|
+
Lookup order: explicit flag → env var → on-disk credentials → prompt the
|
|
4
|
+
user. The on-disk file at ``~/.archie/credentials.toml`` is chmod 600.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import stat
|
|
11
|
+
import sys
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import tomllib # py311+
|
|
17
|
+
except ImportError: # pragma: no cover
|
|
18
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
CREDENTIALS_PATH = Path.home() / ".archie" / "credentials.toml"
|
|
22
|
+
DEFAULT_PROFILE = "default"
|
|
23
|
+
DEFAULT_API_URL = "https://api.heyarchie.ai"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class Credentials:
|
|
28
|
+
"""Resolved CLI credentials."""
|
|
29
|
+
|
|
30
|
+
api_url: str
|
|
31
|
+
api_key: str
|
|
32
|
+
profile: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CredentialError(Exception):
|
|
36
|
+
"""Raised when credentials are missing or unreadable."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_credentials(
|
|
40
|
+
*,
|
|
41
|
+
profile: str = DEFAULT_PROFILE,
|
|
42
|
+
api_url_override: str | None = None,
|
|
43
|
+
api_key_override: str | None = None,
|
|
44
|
+
) -> Credentials:
|
|
45
|
+
"""Resolve credentials, preferring explicit overrides → env → on-disk."""
|
|
46
|
+
api_url = (
|
|
47
|
+
api_url_override
|
|
48
|
+
or os.environ.get("ARCHIE_API_URL")
|
|
49
|
+
or _from_disk(profile, "api_url")
|
|
50
|
+
or DEFAULT_API_URL
|
|
51
|
+
)
|
|
52
|
+
api_key = api_key_override or os.environ.get("ARCHIE_API_KEY") or _from_disk(profile, "api_key")
|
|
53
|
+
if not api_key:
|
|
54
|
+
raise CredentialError(
|
|
55
|
+
"No API key found. Set ARCHIE_API_KEY, run `archie login`, or pass --api-key."
|
|
56
|
+
)
|
|
57
|
+
return Credentials(api_url=api_url.rstrip("/"), api_key=api_key, profile=profile)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def save_credentials(api_url: str, api_key: str, *, profile: str = DEFAULT_PROFILE) -> Path:
|
|
61
|
+
"""Persist credentials to ``~/.archie/credentials.toml`` (chmod 600)."""
|
|
62
|
+
CREDENTIALS_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
existing: dict[str, dict[str, str]] = {}
|
|
64
|
+
if CREDENTIALS_PATH.exists():
|
|
65
|
+
try:
|
|
66
|
+
with CREDENTIALS_PATH.open("rb") as fh:
|
|
67
|
+
existing = tomllib.load(fh) # type: ignore[assignment]
|
|
68
|
+
except Exception: # pragma: no cover — corrupt file → overwrite
|
|
69
|
+
existing = {}
|
|
70
|
+
existing[profile] = {"api_url": api_url.rstrip("/"), "api_key": api_key}
|
|
71
|
+
text = _render_toml(existing)
|
|
72
|
+
CREDENTIALS_PATH.write_text(text)
|
|
73
|
+
os.chmod(CREDENTIALS_PATH, stat.S_IRUSR | stat.S_IWUSR)
|
|
74
|
+
return CREDENTIALS_PATH
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _from_disk(profile: str, key: str) -> str | None:
|
|
78
|
+
if not CREDENTIALS_PATH.exists():
|
|
79
|
+
return None
|
|
80
|
+
try:
|
|
81
|
+
with CREDENTIALS_PATH.open("rb") as fh:
|
|
82
|
+
data = tomllib.load(fh)
|
|
83
|
+
except Exception as exc: # pragma: no cover
|
|
84
|
+
print(f"warning: failed to read {CREDENTIALS_PATH}: {exc}", file=sys.stderr)
|
|
85
|
+
return None
|
|
86
|
+
section = data.get(profile)
|
|
87
|
+
if not isinstance(section, dict):
|
|
88
|
+
return None
|
|
89
|
+
value = section.get(key)
|
|
90
|
+
return str(value) if value else None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _render_toml(data: dict[str, dict[str, str]]) -> str:
|
|
94
|
+
out: list[str] = []
|
|
95
|
+
for profile, fields in sorted(data.items()):
|
|
96
|
+
out.append(f"[{profile}]")
|
|
97
|
+
for k in sorted(fields):
|
|
98
|
+
v = fields[k].replace('"', '\\"')
|
|
99
|
+
out.append(f'{k} = "{v}"')
|
|
100
|
+
out.append("")
|
|
101
|
+
return "\n".join(out)
|
archie_cli/client.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""HTTP client wrapper around the Archie Connect REST surface.
|
|
2
|
+
|
|
3
|
+
Handles bearer auth, response envelope unwrapping, and SSE streaming
|
|
4
|
+
for cycle event tails. Async — use via ``asyncio.run`` from each click
|
|
5
|
+
command (the click group is sync; commands hop into asyncio).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from collections.abc import AsyncIterator
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from archie_cli.auth import Credentials
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ArchieAPIError(Exception):
|
|
20
|
+
"""Raised when the API returns a non-2xx response."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, status_code: int, body: dict[str, Any] | str):
|
|
23
|
+
self.status_code = status_code
|
|
24
|
+
self.body = body
|
|
25
|
+
# Pull out the canonical error envelope shape if present.
|
|
26
|
+
message = ""
|
|
27
|
+
if isinstance(body, dict):
|
|
28
|
+
err = body.get("error") or {}
|
|
29
|
+
if isinstance(err, dict):
|
|
30
|
+
message = err.get("message") or err.get("code") or ""
|
|
31
|
+
else:
|
|
32
|
+
message = str(err)
|
|
33
|
+
if not message:
|
|
34
|
+
message = str(body)[:200]
|
|
35
|
+
super().__init__(f"HTTP {status_code}: {message}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ArchieClient:
|
|
39
|
+
"""Thin async wrapper over httpx.AsyncClient.
|
|
40
|
+
|
|
41
|
+
One client per command invocation; opens + closes around the call.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, creds: Credentials, *, timeout: float = 30.0):
|
|
45
|
+
self._creds = creds
|
|
46
|
+
self._timeout = timeout
|
|
47
|
+
|
|
48
|
+
async def __aenter__(self) -> ArchieClient:
|
|
49
|
+
self._client = httpx.AsyncClient(
|
|
50
|
+
base_url=self._creds.api_url,
|
|
51
|
+
timeout=self._timeout,
|
|
52
|
+
headers={
|
|
53
|
+
"Authorization": f"Bearer {self._creds.api_key}",
|
|
54
|
+
"User-Agent": f"heyarchie-cli/0.1.0 ({httpx.__version__})",
|
|
55
|
+
"Accept": "application/json",
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
61
|
+
await self._client.aclose()
|
|
62
|
+
|
|
63
|
+
async def invoke_skill(self, slug: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
64
|
+
"""POST /api/v1/skills/{slug}/invoke and return the parsed JSON body."""
|
|
65
|
+
resp = await self._client.post(
|
|
66
|
+
f"/api/v1/skills/{slug}/invoke",
|
|
67
|
+
json=payload,
|
|
68
|
+
)
|
|
69
|
+
return self._unwrap(resp)
|
|
70
|
+
|
|
71
|
+
async def workstreams_list(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
scope: str | None = None,
|
|
75
|
+
status: str | None = None,
|
|
76
|
+
limit: int = 20,
|
|
77
|
+
cursor: str | None = None,
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
payload: dict[str, Any] = {"limit": limit}
|
|
80
|
+
if scope:
|
|
81
|
+
payload["scope"] = scope
|
|
82
|
+
if status:
|
|
83
|
+
payload["status"] = status
|
|
84
|
+
if cursor:
|
|
85
|
+
payload["cursor"] = cursor
|
|
86
|
+
return await self.invoke_skill("workstream.list", payload)
|
|
87
|
+
|
|
88
|
+
async def workstreams_describe(self, workstream_id: str) -> dict[str, Any]:
|
|
89
|
+
return await self.invoke_skill("workstream.describe", {"workstream_id": workstream_id})
|
|
90
|
+
|
|
91
|
+
async def workstreams_run(
|
|
92
|
+
self,
|
|
93
|
+
workstream_id: str,
|
|
94
|
+
*,
|
|
95
|
+
client_id: str | None = None,
|
|
96
|
+
period: str | None = None,
|
|
97
|
+
runtime_vars: dict[str, Any] | None = None,
|
|
98
|
+
idempotency_key: str | None = None,
|
|
99
|
+
) -> dict[str, Any]:
|
|
100
|
+
payload: dict[str, Any] = {"workstream_id": workstream_id}
|
|
101
|
+
if client_id:
|
|
102
|
+
payload["client_id"] = client_id
|
|
103
|
+
if period:
|
|
104
|
+
payload["period"] = period
|
|
105
|
+
if runtime_vars:
|
|
106
|
+
payload["runtime_vars"] = runtime_vars
|
|
107
|
+
if idempotency_key:
|
|
108
|
+
payload["idempotency_key"] = idempotency_key
|
|
109
|
+
return await self.invoke_skill("workstream.run", payload)
|
|
110
|
+
|
|
111
|
+
async def workstreams_status(self, cycle_id: str) -> dict[str, Any]:
|
|
112
|
+
return await self.invoke_skill("workstream.status", {"cycle_id": cycle_id})
|
|
113
|
+
|
|
114
|
+
async def workstreams_cancel(
|
|
115
|
+
self,
|
|
116
|
+
cycle_id: str,
|
|
117
|
+
*,
|
|
118
|
+
reason: str | None = None,
|
|
119
|
+
idempotency_key: str | None = None,
|
|
120
|
+
) -> dict[str, Any]:
|
|
121
|
+
payload: dict[str, Any] = {"cycle_id": cycle_id}
|
|
122
|
+
if reason:
|
|
123
|
+
payload["reason"] = reason
|
|
124
|
+
if idempotency_key:
|
|
125
|
+
payload["idempotency_key"] = idempotency_key
|
|
126
|
+
return await self.invoke_skill("workstream.cancel", payload)
|
|
127
|
+
|
|
128
|
+
async def workstreams_events(self, cycle_id: str) -> AsyncIterator[dict[str, Any]]:
|
|
129
|
+
"""Stream SSE events from /api/v1/workstreams/cycles/{cycle_id}/events.
|
|
130
|
+
|
|
131
|
+
Yields one parsed JSON dict per SSE ``data:`` line. Filters out
|
|
132
|
+
comments and empty keepalives. Re-raises on transport failure.
|
|
133
|
+
"""
|
|
134
|
+
url = f"/api/v1/workstreams/cycles/{cycle_id}/events"
|
|
135
|
+
async with self._client.stream(
|
|
136
|
+
"GET",
|
|
137
|
+
url,
|
|
138
|
+
headers={"Accept": "text/event-stream"},
|
|
139
|
+
timeout=httpx.Timeout(60.0, read=None),
|
|
140
|
+
) as resp:
|
|
141
|
+
if resp.status_code >= 400:
|
|
142
|
+
body = await resp.aread()
|
|
143
|
+
try:
|
|
144
|
+
parsed: dict[str, Any] | str = json.loads(body)
|
|
145
|
+
except Exception:
|
|
146
|
+
parsed = body.decode("utf-8", errors="replace")
|
|
147
|
+
raise ArchieAPIError(resp.status_code, parsed)
|
|
148
|
+
buffer: list[str] = []
|
|
149
|
+
async for line in resp.aiter_lines():
|
|
150
|
+
if line == "" and buffer:
|
|
151
|
+
payload = "\n".join(buffer)
|
|
152
|
+
buffer = []
|
|
153
|
+
if not payload.startswith("data:"):
|
|
154
|
+
continue
|
|
155
|
+
data = payload[len("data:") :].strip()
|
|
156
|
+
if not data:
|
|
157
|
+
continue
|
|
158
|
+
try:
|
|
159
|
+
yield json.loads(data)
|
|
160
|
+
except json.JSONDecodeError:
|
|
161
|
+
yield {"raw": data}
|
|
162
|
+
elif line.startswith(":"):
|
|
163
|
+
# SSE comment / keepalive
|
|
164
|
+
continue
|
|
165
|
+
else:
|
|
166
|
+
buffer.append(line)
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def _unwrap(resp: httpx.Response) -> dict[str, Any]:
|
|
170
|
+
try:
|
|
171
|
+
body = resp.json()
|
|
172
|
+
except Exception:
|
|
173
|
+
body = resp.text
|
|
174
|
+
if resp.status_code >= 400:
|
|
175
|
+
raise ArchieAPIError(resp.status_code, body)
|
|
176
|
+
# Connect skill responses wrap the typed output under `output`.
|
|
177
|
+
if isinstance(body, dict) and "output" in body and isinstance(body["output"], dict):
|
|
178
|
+
return body["output"]
|
|
179
|
+
return body if isinstance(body, dict) else {"_raw": body}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Click subcommand groups for the `archie` CLI."""
|
archie_cli/cmds/login.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""``archie login`` — paste an API key, persist to ~/.archie/credentials.toml."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from archie_cli.auth import (
|
|
8
|
+
DEFAULT_API_URL,
|
|
9
|
+
DEFAULT_PROFILE,
|
|
10
|
+
save_credentials,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("login")
|
|
15
|
+
@click.option(
|
|
16
|
+
"--api-url",
|
|
17
|
+
default=DEFAULT_API_URL,
|
|
18
|
+
show_default=True,
|
|
19
|
+
help="API base URL. Override for staging or self-hosted tiers.",
|
|
20
|
+
)
|
|
21
|
+
@click.option(
|
|
22
|
+
"--api-key",
|
|
23
|
+
default=None,
|
|
24
|
+
help="Paste here, or omit and we'll prompt without echoing.",
|
|
25
|
+
)
|
|
26
|
+
@click.option(
|
|
27
|
+
"--profile",
|
|
28
|
+
default=DEFAULT_PROFILE,
|
|
29
|
+
show_default=True,
|
|
30
|
+
help="Save under a named profile (e.g. dev, staging).",
|
|
31
|
+
)
|
|
32
|
+
def login_cmd(api_url: str, api_key: str | None, profile: str) -> None:
|
|
33
|
+
"""Save an API key to ~/.archie/credentials.toml.
|
|
34
|
+
|
|
35
|
+
Today this expects an Archie session JWT or Archie OAuth JWT (sent as
|
|
36
|
+
a Bearer token). WorkOS-issued `sk_*` keys are not yet validated
|
|
37
|
+
server-side — see docs/connect/api-keys-flow.md.
|
|
38
|
+
"""
|
|
39
|
+
if not api_key:
|
|
40
|
+
api_key = click.prompt("API key", hide_input=True, confirmation_prompt=False)
|
|
41
|
+
api_key = api_key.strip()
|
|
42
|
+
if not api_key:
|
|
43
|
+
raise click.ClickException("API key cannot be empty.")
|
|
44
|
+
path = save_credentials(api_url=api_url, api_key=api_key, profile=profile)
|
|
45
|
+
click.echo(f"Saved credentials to {path} (profile={profile!r}).")
|
|
46
|
+
click.echo("Try: archie workstreams list")
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""``archie skills …`` — generic skill catalogue + invoke.
|
|
2
|
+
|
|
3
|
+
Extends the CLI beyond the 9 workstream-specific commands to the full
|
|
4
|
+
public skill catalogue (51 skills at the time of writing). Mirrors the
|
|
5
|
+
SDK shape: ``client.skills.list()`` + ``client.skills.invoke(slug, ...)``.
|
|
6
|
+
|
|
7
|
+
* ``archie skills list [--category X] [--search Q]`` — table of every
|
|
8
|
+
visible skill, with category + cluster + streaming flag.
|
|
9
|
+
* ``archie skills invoke <slug> [--input @file.json | --input '{json}']``
|
|
10
|
+
— POST the input to ``/api/v1/skills/{slug}/invoke`` and render the
|
|
11
|
+
unwrapped response.
|
|
12
|
+
|
|
13
|
+
Auth + ``--json`` + ``--profile`` flags share the same decorator as the
|
|
14
|
+
workstream commands.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import json as jsonlib
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import click
|
|
26
|
+
import httpx
|
|
27
|
+
from rich.console import Console
|
|
28
|
+
from rich.json import JSON
|
|
29
|
+
from rich.table import Table
|
|
30
|
+
|
|
31
|
+
from archie_cli.auth import CredentialError, load_credentials
|
|
32
|
+
from archie_cli.client import ArchieAPIError, ArchieClient
|
|
33
|
+
|
|
34
|
+
_console = Console()
|
|
35
|
+
_err_console = Console(stderr=True, style="bold red")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _global_options(fn):
|
|
39
|
+
"""Auth + output flags — same shape as workstreams.py to keep UX uniform."""
|
|
40
|
+
fn = click.option(
|
|
41
|
+
"--json",
|
|
42
|
+
"as_json",
|
|
43
|
+
is_flag=True,
|
|
44
|
+
default=False,
|
|
45
|
+
help="Emit raw JSON to stdout (machine-friendly).",
|
|
46
|
+
)(fn)
|
|
47
|
+
fn = click.option(
|
|
48
|
+
"--profile",
|
|
49
|
+
default=None,
|
|
50
|
+
help="Credentials profile in ~/.archie/credentials.toml (default: 'default').",
|
|
51
|
+
)(fn)
|
|
52
|
+
fn = click.option(
|
|
53
|
+
"--api-url",
|
|
54
|
+
default=None,
|
|
55
|
+
envvar="ARCHIE_API_URL",
|
|
56
|
+
help="API base URL. Falls back to env ARCHIE_API_URL or saved credentials.",
|
|
57
|
+
)(fn)
|
|
58
|
+
fn = click.option(
|
|
59
|
+
"--api-key",
|
|
60
|
+
default=None,
|
|
61
|
+
envvar="ARCHIE_API_KEY",
|
|
62
|
+
help="Bearer token. Falls back to env ARCHIE_API_KEY or saved credentials.",
|
|
63
|
+
)(fn)
|
|
64
|
+
return fn
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _resolve_creds(api_url: str | None, api_key: str | None, profile: str | None):
|
|
68
|
+
try:
|
|
69
|
+
return load_credentials(
|
|
70
|
+
profile=profile or "default",
|
|
71
|
+
api_url_override=api_url,
|
|
72
|
+
api_key_override=api_key,
|
|
73
|
+
)
|
|
74
|
+
except CredentialError as exc:
|
|
75
|
+
raise click.ClickException(str(exc)) from exc
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _run(coro):
|
|
79
|
+
try:
|
|
80
|
+
return asyncio.run(coro)
|
|
81
|
+
except ArchieAPIError as exc:
|
|
82
|
+
_err_console.print(str(exc))
|
|
83
|
+
raise click.exceptions.Exit(2) from exc
|
|
84
|
+
except KeyboardInterrupt:
|
|
85
|
+
_err_console.print("\nInterrupted.")
|
|
86
|
+
raise click.exceptions.Exit(130) from None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@click.group("skills")
|
|
90
|
+
def skills_group() -> None:
|
|
91
|
+
"""Browse and invoke the Archie skill catalogue."""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@skills_group.command("list")
|
|
95
|
+
@_global_options
|
|
96
|
+
@click.option("--category", default=None, help="Filter by category (e.g. transactions, tax).")
|
|
97
|
+
@click.option(
|
|
98
|
+
"--search",
|
|
99
|
+
"search",
|
|
100
|
+
default=None,
|
|
101
|
+
help="Case-insensitive substring match on slug or description.",
|
|
102
|
+
)
|
|
103
|
+
def list_cmd(
|
|
104
|
+
api_key: str | None,
|
|
105
|
+
api_url: str | None,
|
|
106
|
+
profile: str | None,
|
|
107
|
+
as_json: bool,
|
|
108
|
+
category: str | None,
|
|
109
|
+
search: str | None,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""List the skill catalogue visible to the caller."""
|
|
112
|
+
creds = _resolve_creds(api_url, api_key, profile)
|
|
113
|
+
|
|
114
|
+
async def go() -> list[dict[str, Any]]:
|
|
115
|
+
# The catalogue lives at /api/v1/skills (GET) — separate from the
|
|
116
|
+
# /api/v1/skills/{slug}/invoke surface. We hit it directly to avoid
|
|
117
|
+
# adding a bespoke client method just for the CLI.
|
|
118
|
+
async with httpx.AsyncClient(
|
|
119
|
+
base_url=creds.api_url,
|
|
120
|
+
headers={"Authorization": f"Bearer {creds.api_key}"},
|
|
121
|
+
timeout=15.0,
|
|
122
|
+
) as cli:
|
|
123
|
+
resp = await cli.get("/api/v1/skills")
|
|
124
|
+
if resp.status_code >= 400:
|
|
125
|
+
raise ArchieAPIError(
|
|
126
|
+
resp.status_code,
|
|
127
|
+
resp.json()
|
|
128
|
+
if resp.headers.get("content-type", "").startswith("application/json")
|
|
129
|
+
else resp.text,
|
|
130
|
+
)
|
|
131
|
+
body = resp.json()
|
|
132
|
+
return body.get("data") if isinstance(body, dict) else body
|
|
133
|
+
|
|
134
|
+
skills = _run(go()) or []
|
|
135
|
+
if category:
|
|
136
|
+
skills = [s for s in skills if s.get("category") == category]
|
|
137
|
+
if search:
|
|
138
|
+
needle = search.lower()
|
|
139
|
+
skills = [
|
|
140
|
+
s
|
|
141
|
+
for s in skills
|
|
142
|
+
if needle in (s.get("slug", "") + " " + s.get("description", "")).lower()
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
if as_json:
|
|
146
|
+
sys.stdout.write(jsonlib.dumps(skills, indent=2, default=str))
|
|
147
|
+
sys.stdout.write("\n")
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
if not skills:
|
|
151
|
+
_console.print("[dim]No skills match.[/dim]")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
table = Table(title=f"{len(skills)} skill(s)")
|
|
155
|
+
table.add_column("Slug", style="cyan")
|
|
156
|
+
table.add_column("Category", style="magenta")
|
|
157
|
+
table.add_column("Streams")
|
|
158
|
+
table.add_column("Description")
|
|
159
|
+
for s in sorted(skills, key=lambda x: x.get("slug", "")):
|
|
160
|
+
table.add_row(
|
|
161
|
+
s.get("slug", ""),
|
|
162
|
+
s.get("category", ""),
|
|
163
|
+
"yes" if s.get("streaming") else "no",
|
|
164
|
+
(s.get("description") or "")[:80],
|
|
165
|
+
)
|
|
166
|
+
_console.print(table)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@skills_group.command("invoke")
|
|
170
|
+
@_global_options
|
|
171
|
+
@click.argument("slug")
|
|
172
|
+
@click.option(
|
|
173
|
+
"--input",
|
|
174
|
+
"input_",
|
|
175
|
+
default="{}",
|
|
176
|
+
show_default=True,
|
|
177
|
+
help=(
|
|
178
|
+
"Skill input as inline JSON (e.g. '--input \\'{\"query\":\"...\"}\\'') "
|
|
179
|
+
"or a file path prefixed with @ (e.g. '--input @body.json'). "
|
|
180
|
+
"Default is empty object."
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
def invoke_cmd(
|
|
184
|
+
api_key: str | None,
|
|
185
|
+
api_url: str | None,
|
|
186
|
+
profile: str | None,
|
|
187
|
+
as_json: bool,
|
|
188
|
+
slug: str,
|
|
189
|
+
input_: str,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Invoke a skill by its public slug (e.g. ``ask_archie``, ``tax.advise``).
|
|
192
|
+
|
|
193
|
+
The slug accepts both the friendly name (``tax.advise``) and the
|
|
194
|
+
internal slug (``advise_tax``). The route translates either to
|
|
195
|
+
``/api/v1/skills/{slug}/invoke`` server-side.
|
|
196
|
+
"""
|
|
197
|
+
payload = _parse_input(input_)
|
|
198
|
+
creds = _resolve_creds(api_url, api_key, profile)
|
|
199
|
+
|
|
200
|
+
async def go() -> dict[str, Any]:
|
|
201
|
+
async with ArchieClient(creds) as cli:
|
|
202
|
+
return await cli.invoke_skill(slug, payload)
|
|
203
|
+
|
|
204
|
+
out = _run(go())
|
|
205
|
+
if as_json:
|
|
206
|
+
sys.stdout.write(jsonlib.dumps(out, indent=2, default=str))
|
|
207
|
+
sys.stdout.write("\n")
|
|
208
|
+
else:
|
|
209
|
+
_console.print(JSON.from_data(out))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _parse_input(value: str) -> dict[str, Any]:
|
|
213
|
+
"""``--input`` accepts inline JSON, ``@path``, or ``-`` for stdin."""
|
|
214
|
+
text = value
|
|
215
|
+
if value.startswith("@"):
|
|
216
|
+
path = Path(value[1:])
|
|
217
|
+
if not path.exists():
|
|
218
|
+
raise click.BadParameter(f"input file not found: {path}", param_hint="--input")
|
|
219
|
+
text = path.read_text()
|
|
220
|
+
elif value == "-":
|
|
221
|
+
text = sys.stdin.read()
|
|
222
|
+
try:
|
|
223
|
+
parsed = jsonlib.loads(text)
|
|
224
|
+
except jsonlib.JSONDecodeError as exc:
|
|
225
|
+
raise click.BadParameter(
|
|
226
|
+
f"--input is not valid JSON: {exc.msg} (at line {exc.lineno} col {exc.colno})",
|
|
227
|
+
param_hint="--input",
|
|
228
|
+
) from exc
|
|
229
|
+
if not isinstance(parsed, dict):
|
|
230
|
+
raise click.BadParameter(
|
|
231
|
+
"--input must be a JSON object (got non-object)", param_hint="--input"
|
|
232
|
+
)
|
|
233
|
+
return parsed
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""``archie workstreams …`` — list, describe, run, status, cancel, logs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json as jsonlib
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.json import JSON
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from archie_cli.auth import CredentialError, load_credentials
|
|
16
|
+
from archie_cli.client import ArchieAPIError, ArchieClient
|
|
17
|
+
|
|
18
|
+
_console = Console()
|
|
19
|
+
_err_console = Console(stderr=True, style="bold red")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _global_options(fn):
|
|
23
|
+
"""Attach the auth + output flags every workstream command needs."""
|
|
24
|
+
fn = click.option(
|
|
25
|
+
"--json",
|
|
26
|
+
"as_json",
|
|
27
|
+
is_flag=True,
|
|
28
|
+
default=False,
|
|
29
|
+
help="Emit raw JSON to stdout (machine-friendly).",
|
|
30
|
+
)(fn)
|
|
31
|
+
fn = click.option(
|
|
32
|
+
"--profile",
|
|
33
|
+
default=None,
|
|
34
|
+
help="Credentials profile in ~/.archie/credentials.toml (default: 'default').",
|
|
35
|
+
)(fn)
|
|
36
|
+
fn = click.option(
|
|
37
|
+
"--api-url",
|
|
38
|
+
default=None,
|
|
39
|
+
envvar="ARCHIE_API_URL",
|
|
40
|
+
help="API base URL. Falls back to env ARCHIE_API_URL or saved credentials.",
|
|
41
|
+
)(fn)
|
|
42
|
+
fn = click.option(
|
|
43
|
+
"--api-key",
|
|
44
|
+
default=None,
|
|
45
|
+
envvar="ARCHIE_API_KEY",
|
|
46
|
+
help="Bearer token. Falls back to env ARCHIE_API_KEY or saved credentials.",
|
|
47
|
+
)(fn)
|
|
48
|
+
return fn
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _resolve_creds(api_url: str | None, api_key: str | None, profile: str | None):
|
|
52
|
+
try:
|
|
53
|
+
return load_credentials(
|
|
54
|
+
profile=profile or "default",
|
|
55
|
+
api_url_override=api_url,
|
|
56
|
+
api_key_override=api_key,
|
|
57
|
+
)
|
|
58
|
+
except CredentialError as exc:
|
|
59
|
+
raise click.ClickException(str(exc)) from exc
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _emit(payload: Any, *, as_json: bool) -> None:
|
|
63
|
+
if as_json:
|
|
64
|
+
sys.stdout.write(jsonlib.dumps(payload, indent=2, default=str))
|
|
65
|
+
sys.stdout.write("\n")
|
|
66
|
+
else:
|
|
67
|
+
_console.print(JSON.from_data(payload))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _run(coro):
|
|
71
|
+
"""Bridge sync click handlers to async client calls."""
|
|
72
|
+
try:
|
|
73
|
+
return asyncio.run(coro)
|
|
74
|
+
except ArchieAPIError as exc:
|
|
75
|
+
_err_console.print(str(exc))
|
|
76
|
+
raise click.exceptions.Exit(2) from exc
|
|
77
|
+
except KeyboardInterrupt:
|
|
78
|
+
_err_console.print("\nInterrupted.")
|
|
79
|
+
raise click.exceptions.Exit(130) from None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@click.group("workstreams")
|
|
83
|
+
def workstreams_group() -> None:
|
|
84
|
+
"""Operate on Archie workstreams (templates, blueprints, cycles, tasks)."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@workstreams_group.command("list")
|
|
88
|
+
@_global_options
|
|
89
|
+
@click.option("--scope", type=click.Choice(["plan", "user", "team", "firm", "gallery"]))
|
|
90
|
+
@click.option("--status", type=click.Choice(["draft", "active", "archived"]))
|
|
91
|
+
@click.option("--limit", type=click.IntRange(1, 100), default=20, show_default=True)
|
|
92
|
+
@click.option("--cursor", default=None, help="Opaque pagination cursor from a previous response.")
|
|
93
|
+
def list_cmd(
|
|
94
|
+
api_key: str | None,
|
|
95
|
+
api_url: str | None,
|
|
96
|
+
profile: str | None,
|
|
97
|
+
as_json: bool,
|
|
98
|
+
scope: str | None,
|
|
99
|
+
status: str | None,
|
|
100
|
+
limit: int,
|
|
101
|
+
cursor: str | None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""List workstreams visible to the caller."""
|
|
104
|
+
creds = _resolve_creds(api_url, api_key, profile)
|
|
105
|
+
|
|
106
|
+
async def go():
|
|
107
|
+
async with ArchieClient(creds) as cli:
|
|
108
|
+
return await cli.workstreams_list(
|
|
109
|
+
scope=scope,
|
|
110
|
+
status=status,
|
|
111
|
+
limit=limit,
|
|
112
|
+
cursor=cursor,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
out = _run(go())
|
|
116
|
+
if as_json:
|
|
117
|
+
_emit(out, as_json=True)
|
|
118
|
+
return
|
|
119
|
+
items = out.get("items") or []
|
|
120
|
+
if not items:
|
|
121
|
+
_console.print("[dim]No workstreams visible.[/dim]")
|
|
122
|
+
return
|
|
123
|
+
table = Table(title=f"{len(items)} workstream(s)")
|
|
124
|
+
table.add_column("Permakey", style="cyan")
|
|
125
|
+
table.add_column("Name")
|
|
126
|
+
table.add_column("Mode", style="magenta")
|
|
127
|
+
table.add_column("Status")
|
|
128
|
+
for w in items:
|
|
129
|
+
table.add_row(
|
|
130
|
+
w.get("permakey", ""),
|
|
131
|
+
w.get("name", ""),
|
|
132
|
+
w.get("execution_mode", ""),
|
|
133
|
+
w.get("status", ""),
|
|
134
|
+
)
|
|
135
|
+
_console.print(table)
|
|
136
|
+
next_cursor = out.get("next_cursor")
|
|
137
|
+
if next_cursor:
|
|
138
|
+
_console.print(f"\n[dim]More results: --cursor {next_cursor}[/dim]")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@workstreams_group.command("describe")
|
|
142
|
+
@_global_options
|
|
143
|
+
@click.argument("workstream_id")
|
|
144
|
+
def describe_cmd(
|
|
145
|
+
api_key: str | None,
|
|
146
|
+
api_url: str | None,
|
|
147
|
+
profile: str | None,
|
|
148
|
+
as_json: bool,
|
|
149
|
+
workstream_id: str,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Describe a workstream (UUID, slug, or 7-char permakey)."""
|
|
152
|
+
creds = _resolve_creds(api_url, api_key, profile)
|
|
153
|
+
|
|
154
|
+
async def go():
|
|
155
|
+
async with ArchieClient(creds) as cli:
|
|
156
|
+
return await cli.workstreams_describe(workstream_id)
|
|
157
|
+
|
|
158
|
+
out = _run(go())
|
|
159
|
+
_emit(out, as_json=as_json)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@workstreams_group.command("run")
|
|
163
|
+
@_global_options
|
|
164
|
+
@click.argument("workstream_id")
|
|
165
|
+
@click.option("--client-id", default=None, help="Client UUID to run against.")
|
|
166
|
+
@click.option("--period", default=None, help="Period label (e.g. FY26-Q1).")
|
|
167
|
+
@click.option(
|
|
168
|
+
"--var",
|
|
169
|
+
"vars_",
|
|
170
|
+
multiple=True,
|
|
171
|
+
metavar="K=V",
|
|
172
|
+
help="Runtime variable. May be repeated. Use JSON for non-string values: --var qty='[1,2]'.",
|
|
173
|
+
)
|
|
174
|
+
@click.option(
|
|
175
|
+
"--idempotency-key",
|
|
176
|
+
default=None,
|
|
177
|
+
help="Caller-supplied stable token; safe-retry within 24h.",
|
|
178
|
+
)
|
|
179
|
+
def run_cmd(
|
|
180
|
+
api_key: str | None,
|
|
181
|
+
api_url: str | None,
|
|
182
|
+
profile: str | None,
|
|
183
|
+
as_json: bool,
|
|
184
|
+
workstream_id: str,
|
|
185
|
+
client_id: str | None,
|
|
186
|
+
period: str | None,
|
|
187
|
+
vars_: tuple[str, ...],
|
|
188
|
+
idempotency_key: str | None,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Start a workstream cycle and print the cycle id + URLs."""
|
|
191
|
+
runtime_vars: dict[str, Any] = {}
|
|
192
|
+
for v in vars_:
|
|
193
|
+
if "=" not in v:
|
|
194
|
+
raise click.BadParameter(f"--var must be K=V (got {v!r})")
|
|
195
|
+
key, _, val = v.partition("=")
|
|
196
|
+
try:
|
|
197
|
+
runtime_vars[key] = jsonlib.loads(val)
|
|
198
|
+
except jsonlib.JSONDecodeError:
|
|
199
|
+
runtime_vars[key] = val
|
|
200
|
+
creds = _resolve_creds(api_url, api_key, profile)
|
|
201
|
+
|
|
202
|
+
async def go():
|
|
203
|
+
async with ArchieClient(creds) as cli:
|
|
204
|
+
return await cli.workstreams_run(
|
|
205
|
+
workstream_id,
|
|
206
|
+
client_id=client_id,
|
|
207
|
+
period=period,
|
|
208
|
+
runtime_vars=runtime_vars or None,
|
|
209
|
+
idempotency_key=idempotency_key,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
out = _run(go())
|
|
213
|
+
_emit(out, as_json=as_json)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@workstreams_group.command("status")
|
|
217
|
+
@_global_options
|
|
218
|
+
@click.argument("cycle_id")
|
|
219
|
+
def status_cmd(
|
|
220
|
+
api_key: str | None,
|
|
221
|
+
api_url: str | None,
|
|
222
|
+
profile: str | None,
|
|
223
|
+
as_json: bool,
|
|
224
|
+
cycle_id: str,
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Show the current state of a cycle."""
|
|
227
|
+
creds = _resolve_creds(api_url, api_key, profile)
|
|
228
|
+
|
|
229
|
+
async def go():
|
|
230
|
+
async with ArchieClient(creds) as cli:
|
|
231
|
+
return await cli.workstreams_status(cycle_id)
|
|
232
|
+
|
|
233
|
+
out = _run(go())
|
|
234
|
+
_emit(out, as_json=as_json)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@workstreams_group.command("cancel")
|
|
238
|
+
@_global_options
|
|
239
|
+
@click.argument("cycle_id")
|
|
240
|
+
@click.option("--reason", default=None, help="Audit-trail reason (recommended).")
|
|
241
|
+
@click.option("--idempotency-key", default=None, help="Caller-supplied stable token.")
|
|
242
|
+
def cancel_cmd(
|
|
243
|
+
api_key: str | None,
|
|
244
|
+
api_url: str | None,
|
|
245
|
+
profile: str | None,
|
|
246
|
+
as_json: bool,
|
|
247
|
+
cycle_id: str,
|
|
248
|
+
reason: str | None,
|
|
249
|
+
idempotency_key: str | None,
|
|
250
|
+
) -> None:
|
|
251
|
+
"""Cancel a running cycle."""
|
|
252
|
+
creds = _resolve_creds(api_url, api_key, profile)
|
|
253
|
+
|
|
254
|
+
async def go():
|
|
255
|
+
async with ArchieClient(creds) as cli:
|
|
256
|
+
return await cli.workstreams_cancel(
|
|
257
|
+
cycle_id,
|
|
258
|
+
reason=reason,
|
|
259
|
+
idempotency_key=idempotency_key,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
out = _run(go())
|
|
263
|
+
_emit(out, as_json=as_json)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@workstreams_group.command("logs")
|
|
267
|
+
@_global_options
|
|
268
|
+
@click.argument("cycle_id")
|
|
269
|
+
@click.option("--follow/--no-follow", "follow", default=True, show_default=True)
|
|
270
|
+
def logs_cmd(
|
|
271
|
+
api_key: str | None,
|
|
272
|
+
api_url: str | None,
|
|
273
|
+
profile: str | None,
|
|
274
|
+
as_json: bool,
|
|
275
|
+
cycle_id: str,
|
|
276
|
+
follow: bool,
|
|
277
|
+
) -> None:
|
|
278
|
+
"""Stream cycle events via SSE.
|
|
279
|
+
|
|
280
|
+
Default is `--follow` (long-lived); use `--no-follow` to read whatever is
|
|
281
|
+
buffered and exit.
|
|
282
|
+
"""
|
|
283
|
+
creds = _resolve_creds(api_url, api_key, profile)
|
|
284
|
+
|
|
285
|
+
async def go():
|
|
286
|
+
async with ArchieClient(creds) as cli:
|
|
287
|
+
async for event in cli.workstreams_events(cycle_id):
|
|
288
|
+
if as_json:
|
|
289
|
+
sys.stdout.write(jsonlib.dumps(event, default=str) + "\n")
|
|
290
|
+
sys.stdout.flush()
|
|
291
|
+
else:
|
|
292
|
+
_console.print(JSON.from_data(event))
|
|
293
|
+
if not follow:
|
|
294
|
+
# Snapshot mode — exit after the first batch lull.
|
|
295
|
+
if event.get("kind") in {"finalization", "completed", "failed", "cancelled"}:
|
|
296
|
+
break
|
|
297
|
+
|
|
298
|
+
_run(go())
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: heyarchie-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line interface for the Archie Connect platform — workstreams, cycles, skills.
|
|
5
|
+
Project-URL: Homepage, https://developers.heyarchie.ai
|
|
6
|
+
Project-URL: Documentation, https://developers.heyarchie.ai/docs/cli
|
|
7
|
+
Project-URL: Source, https://github.com/heyarchie-ai/archie-platform-v2/tree/alpha/apps/cli
|
|
8
|
+
Author-email: Archie <hey@archie.ai>
|
|
9
|
+
License: Proprietary
|
|
10
|
+
Keywords: archie,cli,connect,workstreams
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: click<9.0,>=8.1
|
|
19
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
20
|
+
Requires-Dist: rich<14.0,>=13.7
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# archie CLI
|
|
28
|
+
|
|
29
|
+
Command-line interface for the Archie Connect platform.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# from PyPI (when published)
|
|
35
|
+
pipx install heyarchie-cli
|
|
36
|
+
|
|
37
|
+
# or from this repo for development
|
|
38
|
+
pip install -e apps/cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`pipx` is recommended; it installs `archie` as a self-contained executable on
|
|
42
|
+
your PATH without touching system Python packages.
|
|
43
|
+
|
|
44
|
+
## Authenticate
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
archie login --api-url https://api.heyarchie.ai
|
|
48
|
+
# Then paste your bearer token at the prompt (input is hidden).
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This writes `~/.archie/credentials.toml` (chmod 600) under the `[default]`
|
|
52
|
+
profile. Override per-call with `--profile`, `--api-key`, or env vars
|
|
53
|
+
`ARCHIE_API_KEY` / `ARCHIE_API_URL`.
|
|
54
|
+
|
|
55
|
+
> Today the bearer must be an **Archie session JWT** or **Archie OAuth JWT**.
|
|
56
|
+
> WorkOS-issued `sk_*` API keys are not yet validated server-side — see
|
|
57
|
+
> [`docs/connect/api-keys-flow.md`](../../docs/connect/api-keys-flow.md) for
|
|
58
|
+
> the architectural decision pending.
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
archie login Save bearer credentials.
|
|
64
|
+
archie workstreams list [--scope ...] List visible workstreams.
|
|
65
|
+
archie workstreams describe <id> Show metadata + step graph.
|
|
66
|
+
archie workstreams run <id> [--var k=v] Start a cycle.
|
|
67
|
+
archie workstreams status <cycle_id> Show cycle progress.
|
|
68
|
+
archie workstreams cancel <cycle_id> Cancel a running cycle.
|
|
69
|
+
archie workstreams logs <cycle_id> Stream SSE events live.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`workstream_id` accepts a UUID, a workstream slug, or a 7-char permakey;
|
|
73
|
+
the API resolves all three.
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# List workstreams
|
|
79
|
+
archie workstreams list
|
|
80
|
+
|
|
81
|
+
# JSON output (pipe-friendly)
|
|
82
|
+
archie workstreams list --json | jq '.items[].permakey'
|
|
83
|
+
|
|
84
|
+
# Start a cycle and watch it run
|
|
85
|
+
CYCLE=$(archie workstreams run abc1234 --period 2026-Q1 --json | jq -r .cycle_id)
|
|
86
|
+
archie workstreams logs "$CYCLE"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Environment variables
|
|
90
|
+
|
|
91
|
+
| Variable | Purpose | Default |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `ARCHIE_API_URL` | API base URL | `https://api.heyarchie.ai` |
|
|
94
|
+
| `ARCHIE_API_KEY` | Bearer token | (none) |
|
|
95
|
+
|
|
96
|
+
CLI flags take precedence over env, which takes precedence over the
|
|
97
|
+
on-disk credentials file.
|
|
98
|
+
|
|
99
|
+
## Where this lives
|
|
100
|
+
|
|
101
|
+
`apps/cli/` in the `archie-platform-v2` monorepo. PRs welcome.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
archie_cli/__init__.py,sha256=-0B5hLpuva9c1HFjodsV1uRhOseWbFqpuwkNM6VD4tk,459
|
|
2
|
+
archie_cli/__main__.py,sha256=GX-a-7hRx5xSAkTmZBErQ2w5XxGxDafNSAZzPzgitHk,804
|
|
3
|
+
archie_cli/auth.py,sha256=5Z-1VOVLkJTNrKSdwEsyMTtyUhpx2juQvjsLYTrfUXM,3221
|
|
4
|
+
archie_cli/client.py,sha256=pbPLq2DphIx1tq8b2CLX_K8y2spNxND-HIdcV4doVWo,6453
|
|
5
|
+
archie_cli/cmds/__init__.py,sha256=bPwqo_P2WUBxzLSQyrijoLWpEZjLSF6MrnPyD91CCc4,52
|
|
6
|
+
archie_cli/cmds/login.py,sha256=UHJbl0rl4caqktaHzy-QryPVHeqReL81B_0gzzV9hp0,1424
|
|
7
|
+
archie_cli/cmds/skills.py,sha256=BH-VJ4j8eHwk1RUNdButZ_COX2WIDuPRWZdNSONOCp8,7299
|
|
8
|
+
archie_cli/cmds/workstreams.py,sha256=ol87X60M69pJopL165FH-ahmy4ibfq3m1WiqBNa6pfY,8794
|
|
9
|
+
heyarchie_cli-0.1.0.dist-info/METADATA,sha256=xLSVzTAlVHh-uRFWjs0M14reuiqFmgol70NFixE06c8,3284
|
|
10
|
+
heyarchie_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
heyarchie_cli-0.1.0.dist-info/entry_points.txt,sha256=pL_BexbbOi-N7AtfHCfIQN00nDsgPTqV0iNruhChUzo,52
|
|
12
|
+
heyarchie_cli-0.1.0.dist-info/RECORD,,
|