exemplar-cli 0.1.1__tar.gz → 0.1.2__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.
- {exemplar_cli-0.1.1 → exemplar_cli-0.1.2}/PKG-INFO +49 -3
- exemplar_cli-0.1.2/README.md +78 -0
- exemplar_cli-0.1.2/exemplar_cli/doctor.py +166 -0
- {exemplar_cli-0.1.1 → exemplar_cli-0.1.2}/exemplar_cli/main.py +10 -2
- exemplar_cli-0.1.2/exemplar_cli/prompts.py +171 -0
- {exemplar_cli-0.1.1 → exemplar_cli-0.1.2}/exemplar_cli/skills.py +28 -24
- {exemplar_cli-0.1.1 → exemplar_cli-0.1.2}/pyproject.toml +3 -3
- exemplar_cli-0.1.1/README.md +0 -32
- {exemplar_cli-0.1.1 → exemplar_cli-0.1.2}/exemplar_cli/__init__.py +0 -0
- {exemplar_cli-0.1.1 → exemplar_cli-0.1.2}/exemplar_cli/_scopes.py +0 -0
- {exemplar_cli-0.1.1 → exemplar_cli-0.1.2}/exemplar_cli/memory.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: exemplar-cli
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary: Exemplar terminal CLI for skills and memory
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Exemplar terminal CLI for skills, prompts, and memory
|
|
5
5
|
License: LicenseRef-Proprietary
|
|
6
6
|
Author: Exemplar Dev LLC
|
|
7
7
|
Requires-Python: >=3.10,<4.0
|
|
@@ -10,17 +10,55 @@ Classifier: Programming Language :: Python :: 3
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.10
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
-
Requires-Dist: exemplar-core (>=0.1.
|
|
13
|
+
Requires-Dist: exemplar-core (>=0.1.2,<0.2)
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
|
|
16
16
|
# exemplar-cli
|
|
17
17
|
|
|
18
18
|
Terminal CLI for Exemplar **skills** and **memory**.
|
|
19
19
|
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
**Recommended** — isolated install with automatic PATH setup:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pipx install exemplar-cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Other options:
|
|
29
|
+
|
|
20
30
|
```bash
|
|
31
|
+
# System / python.org install (macOS, Linux)
|
|
21
32
|
pip install exemplar-cli
|
|
33
|
+
|
|
34
|
+
# User install (no sudo)
|
|
35
|
+
pip install --user exemplar-cli
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
After install, verify everything:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
exemplar doctor
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If you see `command not found: exemplar`, the package is installed but its `bin/`
|
|
45
|
+
directory is not on your `PATH`. `exemplar doctor` prints the exact fix for your
|
|
46
|
+
machine. Common cases:
|
|
47
|
+
|
|
48
|
+
| Install method | Scripts directory |
|
|
49
|
+
|----------------|-------------------|
|
|
50
|
+
| `pipx install` | `~/.local/bin` (pipx adds this automatically) |
|
|
51
|
+
| python.org macOS | `/Library/Frameworks/Python.framework/Versions/3.x/bin` |
|
|
52
|
+
| `pip install --user` | `~/Library/Python/3.x/bin` (macOS) or `~/.local/bin` (Linux) |
|
|
53
|
+
|
|
54
|
+
Open a **new terminal** after changing shell profile files.
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
```bash
|
|
22
59
|
export EXEMPLAR_API_KEY=eis_...
|
|
23
60
|
|
|
61
|
+
exemplar doctor
|
|
24
62
|
exemplar skills list
|
|
25
63
|
exemplar memory add "User prefers bullet points" --user-id u1 --type preference
|
|
26
64
|
exemplar memory search "formatting" --user-id u1
|
|
@@ -36,10 +74,18 @@ exemplar memory search "formatting" --user-id u1
|
|
|
36
74
|
|
|
37
75
|
## Commands
|
|
38
76
|
|
|
77
|
+
### Doctor
|
|
78
|
+
|
|
79
|
+
`exemplar doctor` — check package install, `PATH`, and required environment variables.
|
|
80
|
+
|
|
39
81
|
### Skills
|
|
40
82
|
|
|
41
83
|
`list`, `get`, `search`, `pull`, `push`, `init`, `export-zip`
|
|
42
84
|
|
|
85
|
+
### Prompts
|
|
86
|
+
|
|
87
|
+
`list`, `get`, `search`, `create`, `run`, `publish`, `delete`
|
|
88
|
+
|
|
43
89
|
### Memory
|
|
44
90
|
|
|
45
91
|
`add`, `list`, `get`, `search`, `recall`, `update`, `delete`, `delete-bulk`
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# exemplar-cli
|
|
2
|
+
|
|
3
|
+
Terminal CLI for Exemplar **skills** and **memory**.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
**Recommended** — isolated install with automatic PATH setup:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pipx install exemplar-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Other options:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# System / python.org install (macOS, Linux)
|
|
17
|
+
pip install exemplar-cli
|
|
18
|
+
|
|
19
|
+
# User install (no sudo)
|
|
20
|
+
pip install --user exemplar-cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
After install, verify everything:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
exemplar doctor
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If you see `command not found: exemplar`, the package is installed but its `bin/`
|
|
30
|
+
directory is not on your `PATH`. `exemplar doctor` prints the exact fix for your
|
|
31
|
+
machine. Common cases:
|
|
32
|
+
|
|
33
|
+
| Install method | Scripts directory |
|
|
34
|
+
|----------------|-------------------|
|
|
35
|
+
| `pipx install` | `~/.local/bin` (pipx adds this automatically) |
|
|
36
|
+
| python.org macOS | `/Library/Frameworks/Python.framework/Versions/3.x/bin` |
|
|
37
|
+
| `pip install --user` | `~/Library/Python/3.x/bin` (macOS) or `~/.local/bin` (Linux) |
|
|
38
|
+
|
|
39
|
+
Open a **new terminal** after changing shell profile files.
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
export EXEMPLAR_API_KEY=eis_...
|
|
45
|
+
|
|
46
|
+
exemplar doctor
|
|
47
|
+
exemplar skills list
|
|
48
|
+
exemplar memory add "User prefers bullet points" --user-id u1 --type preference
|
|
49
|
+
exemplar memory search "formatting" --user-id u1
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Environment
|
|
53
|
+
|
|
54
|
+
| Variable | Purpose |
|
|
55
|
+
|----------|---------|
|
|
56
|
+
| `EXEMPLAR_API_KEY` | Org API key (required) |
|
|
57
|
+
| `EXEMPLAR_BASE_URL` | API host override (optional) |
|
|
58
|
+
| `EXEMPLAR_ORGANIZATION_ID` | Org override for JWT auth (optional) |
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
### Doctor
|
|
63
|
+
|
|
64
|
+
`exemplar doctor` — check package install, `PATH`, and required environment variables.
|
|
65
|
+
|
|
66
|
+
### Skills
|
|
67
|
+
|
|
68
|
+
`list`, `get`, `search`, `pull`, `push`, `init`, `export-zip`
|
|
69
|
+
|
|
70
|
+
### Prompts
|
|
71
|
+
|
|
72
|
+
`list`, `get`, `search`, `create`, `run`, `publish`, `delete`
|
|
73
|
+
|
|
74
|
+
### Memory
|
|
75
|
+
|
|
76
|
+
`add`, `list`, `get`, `search`, `recall`, `update`, `delete`, `delete-bulk`
|
|
77
|
+
|
|
78
|
+
Memory commands accept scope flags: `--user-id`, `--agent-id`, `--session-id`, `--app-id`.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""exemplar doctor — diagnose CLI install, PATH, and environment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
8
|
+
import site
|
|
9
|
+
import sys
|
|
10
|
+
from importlib.metadata import PackageNotFoundError, distribution
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _path_entries() -> list[Path]:
|
|
15
|
+
entries: list[Path] = []
|
|
16
|
+
for entry in os.environ.get("PATH", "").split(os.pathsep):
|
|
17
|
+
if not entry:
|
|
18
|
+
continue
|
|
19
|
+
try:
|
|
20
|
+
entries.append(Path(entry).resolve())
|
|
21
|
+
except OSError:
|
|
22
|
+
continue
|
|
23
|
+
return entries
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _locate_installed_script() -> Path | None:
|
|
27
|
+
"""Find the exemplar console script when it is not on PATH."""
|
|
28
|
+
candidates: list[Path] = [
|
|
29
|
+
Path(sys.prefix) / "bin" / "exemplar",
|
|
30
|
+
Path(sys.executable).resolve().parent / "exemplar",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
user_base = site.USER_BASE
|
|
34
|
+
if user_base:
|
|
35
|
+
candidates.append(Path(user_base) / "bin" / "exemplar")
|
|
36
|
+
|
|
37
|
+
if sys.platform == "darwin":
|
|
38
|
+
version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
39
|
+
candidates.append(
|
|
40
|
+
Path(f"/Library/Frameworks/Python.framework/Versions/{version}/bin/exemplar")
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
dist = distribution("exemplar-cli")
|
|
45
|
+
for path in dist.files or ():
|
|
46
|
+
normalized = str(path).replace("\\", "/")
|
|
47
|
+
if normalized.endswith("/bin/exemplar") or normalized == "../../../bin/exemplar":
|
|
48
|
+
root = Path(dist.locate_file(""))
|
|
49
|
+
candidates.append((root / path).resolve())
|
|
50
|
+
except PackageNotFoundError:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
seen: set[Path] = set()
|
|
54
|
+
for candidate in candidates:
|
|
55
|
+
try:
|
|
56
|
+
resolved = candidate.resolve()
|
|
57
|
+
except OSError:
|
|
58
|
+
continue
|
|
59
|
+
if resolved in seen:
|
|
60
|
+
continue
|
|
61
|
+
seen.add(resolved)
|
|
62
|
+
if resolved.is_file():
|
|
63
|
+
return resolved
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _script_dir_on_path(script_dir: Path) -> bool:
|
|
68
|
+
try:
|
|
69
|
+
resolved = script_dir.resolve()
|
|
70
|
+
except OSError:
|
|
71
|
+
return False
|
|
72
|
+
return resolved in _path_entries()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _path_fix_lines(script_dir: Path) -> list[str]:
|
|
76
|
+
export_line = f'export PATH="{script_dir}:$PATH"'
|
|
77
|
+
return [
|
|
78
|
+
"Add one of the following to ~/.zprofile (macOS login shells) or ~/.zshrc:",
|
|
79
|
+
f" {export_line}",
|
|
80
|
+
"",
|
|
81
|
+
"Or symlink into a directory already on PATH:",
|
|
82
|
+
f" ln -sf {script_dir / 'exemplar'} ~/.local/bin/exemplar",
|
|
83
|
+
"",
|
|
84
|
+
"Recommended for CLI tools:",
|
|
85
|
+
" pipx install exemplar-cli",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _check_api_key() -> tuple[bool, str]:
|
|
90
|
+
key = os.environ.get("EXEMPLAR_API_KEY", "").strip()
|
|
91
|
+
if key:
|
|
92
|
+
return True, "EXEMPLAR_API_KEY is set"
|
|
93
|
+
return False, "EXEMPLAR_API_KEY is not set (required for skills/memory commands)"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main(argv: list[str] | None = None) -> int:
|
|
97
|
+
del argv # doctor accepts no subcommands yet
|
|
98
|
+
issues = 0
|
|
99
|
+
|
|
100
|
+
print("Exemplar doctor")
|
|
101
|
+
print("===============")
|
|
102
|
+
|
|
103
|
+
print(
|
|
104
|
+
f"Python: {platform.python_version()} "
|
|
105
|
+
f"({platform.machine()}, {sys.executable})"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
version = distribution("exemplar-cli").version
|
|
110
|
+
print(f"exemplar-cli package: installed ({version})")
|
|
111
|
+
except PackageNotFoundError:
|
|
112
|
+
issues += 1
|
|
113
|
+
print("exemplar-cli package: NOT INSTALLED")
|
|
114
|
+
print(" Install with: pipx install exemplar-cli")
|
|
115
|
+
print(" or: pip install exemplar-cli")
|
|
116
|
+
return 1
|
|
117
|
+
|
|
118
|
+
command = shutil.which("exemplar")
|
|
119
|
+
if command:
|
|
120
|
+
print(f"exemplar command: OK ({command})")
|
|
121
|
+
else:
|
|
122
|
+
issues += 1
|
|
123
|
+
script = _locate_installed_script()
|
|
124
|
+
print("exemplar command: NOT ON PATH")
|
|
125
|
+
if script:
|
|
126
|
+
print(f" Script installed at: {script}")
|
|
127
|
+
if not _script_dir_on_path(script.parent):
|
|
128
|
+
print(" Fix:")
|
|
129
|
+
for line in _path_fix_lines(script.parent):
|
|
130
|
+
print(f" {line}" if line else "")
|
|
131
|
+
else:
|
|
132
|
+
print(" Script directory is on PATH; open a new terminal or run:")
|
|
133
|
+
print(" source ~/.zprofile")
|
|
134
|
+
else:
|
|
135
|
+
print(" Could not locate the exemplar script.")
|
|
136
|
+
print(" Reinstall with: pipx install exemplar-cli")
|
|
137
|
+
|
|
138
|
+
api_ok, api_msg = _check_api_key()
|
|
139
|
+
if api_ok:
|
|
140
|
+
print(f"API key: OK — {api_msg}")
|
|
141
|
+
else:
|
|
142
|
+
issues += 1
|
|
143
|
+
print(f"API key: MISSING — {api_msg}")
|
|
144
|
+
print(" Set with: export EXEMPLAR_API_KEY=eis_...")
|
|
145
|
+
|
|
146
|
+
base_url = os.environ.get("EXEMPLAR_BASE_URL", "").strip()
|
|
147
|
+
if base_url:
|
|
148
|
+
print(f"API base URL: {base_url}")
|
|
149
|
+
else:
|
|
150
|
+
print("API base URL: default (https://api.exemplar.dev)")
|
|
151
|
+
|
|
152
|
+
org_id = os.environ.get("EXEMPLAR_ORGANIZATION_ID", "").strip()
|
|
153
|
+
if org_id:
|
|
154
|
+
print(f"Organization override: {org_id}")
|
|
155
|
+
|
|
156
|
+
print()
|
|
157
|
+
if issues:
|
|
158
|
+
print(f"{issues} issue(s) found.")
|
|
159
|
+
return 1
|
|
160
|
+
|
|
161
|
+
print("All checks passed.")
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
raise SystemExit(main())
|
|
@@ -4,7 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import sys
|
|
6
6
|
|
|
7
|
+
from exemplar_cli.doctor import main as doctor_main
|
|
7
8
|
from exemplar_cli.memory import main as memory_main
|
|
9
|
+
from exemplar_cli.prompts import main as prompts_main
|
|
8
10
|
from exemplar_cli.skills import main as skills_main
|
|
9
11
|
|
|
10
12
|
|
|
@@ -14,10 +16,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
14
16
|
return skills_main(args[1:])
|
|
15
17
|
if args and args[0] == "memory":
|
|
16
18
|
return memory_main(args[1:])
|
|
19
|
+
if args and args[0] == "prompts":
|
|
20
|
+
return prompts_main(args[1:])
|
|
21
|
+
if args and args[0] == "doctor":
|
|
22
|
+
return doctor_main(args[1:])
|
|
17
23
|
print(
|
|
18
|
-
"Usage: exemplar <skills|memory> <subcommand> ...\n"
|
|
24
|
+
"Usage: exemplar <skills|memory|prompts|doctor> <subcommand> ...\n"
|
|
19
25
|
" exemplar skills list|get|search|pull|push|init|export-zip\n"
|
|
20
|
-
" exemplar memory add|list|get|search|recall|update|delete|delete-bulk"
|
|
26
|
+
" exemplar memory add|list|get|search|recall|update|delete|delete-bulk\n"
|
|
27
|
+
" exemplar prompts list|get|search|create|run|publish|delete\n"
|
|
28
|
+
" exemplar doctor",
|
|
21
29
|
file=sys.stderr,
|
|
22
30
|
)
|
|
23
31
|
return 2
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""CLI: exemplar prompts — manage org prompt registry from the terminal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import asdict
|
|
9
|
+
|
|
10
|
+
from exemplar_core import prompts_from_env
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _record_dict(record) -> dict:
|
|
14
|
+
data = asdict(record)
|
|
15
|
+
if hasattr(record, "messages"):
|
|
16
|
+
data["messages"] = [{"role": m.role, "content": m.content} for m in record.messages]
|
|
17
|
+
return data
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _cmd_list(args: argparse.Namespace) -> int:
|
|
21
|
+
items = prompts_from_env().list(limit=args.limit, q=args.q)
|
|
22
|
+
if args.json:
|
|
23
|
+
print(json.dumps([item.__dict__ for item in items], indent=2))
|
|
24
|
+
return 0
|
|
25
|
+
for item in items:
|
|
26
|
+
tags = ",".join(item.tags) if item.tags else ""
|
|
27
|
+
suffix = f" [{tags}]" if tags else ""
|
|
28
|
+
print(f"{item.name}\t{item.prompt_id}\tv{item.current_version}{suffix}")
|
|
29
|
+
return 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _cmd_get(args: argparse.Namespace) -> int:
|
|
33
|
+
record = prompts_from_env().get(args.prompt_id)
|
|
34
|
+
if args.json:
|
|
35
|
+
print(json.dumps(_record_dict(record), indent=2))
|
|
36
|
+
return 0
|
|
37
|
+
for message in record.messages:
|
|
38
|
+
print(f"[{message.role}]\n{message.content}\n")
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _cmd_search(args: argparse.Namespace) -> int:
|
|
43
|
+
results = prompts_from_env().search(args.query, top_k=args.top_k)
|
|
44
|
+
if args.json:
|
|
45
|
+
print(json.dumps([item.__dict__ for item in results], indent=2))
|
|
46
|
+
return 0
|
|
47
|
+
for item in results:
|
|
48
|
+
print(f"{item.name}\t{item.title}")
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _cmd_create(args: argparse.Namespace) -> int:
|
|
53
|
+
messages = [{"role": "system", "content": args.system}]
|
|
54
|
+
if args.user:
|
|
55
|
+
messages.append({"role": "user", "content": args.user})
|
|
56
|
+
variables = [v.strip() for v in args.variables.split(",") if v.strip()] if args.variables else []
|
|
57
|
+
record = prompts_from_env().create(
|
|
58
|
+
name=args.name,
|
|
59
|
+
title=args.title,
|
|
60
|
+
description=args.description,
|
|
61
|
+
messages=messages,
|
|
62
|
+
variables=variables,
|
|
63
|
+
tags=[t.strip() for t in args.tags.split(",") if t.strip()] if args.tags else [],
|
|
64
|
+
)
|
|
65
|
+
if args.json:
|
|
66
|
+
print(json.dumps(_record_dict(record), indent=2))
|
|
67
|
+
else:
|
|
68
|
+
print(f"Created {record.name} ({record.prompt_id})")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _cmd_run(args: argparse.Namespace) -> int:
|
|
73
|
+
variables = {}
|
|
74
|
+
for pair in args.var or []:
|
|
75
|
+
if "=" not in pair:
|
|
76
|
+
print(f"expected KEY=VALUE, got {pair!r}", file=sys.stderr)
|
|
77
|
+
return 1
|
|
78
|
+
key, value = pair.split("=", 1)
|
|
79
|
+
variables[key.strip()] = value
|
|
80
|
+
result = prompts_from_env().run(args.prompt_id, variables=variables, model=args.model)
|
|
81
|
+
if args.json:
|
|
82
|
+
print(json.dumps(result, indent=2))
|
|
83
|
+
else:
|
|
84
|
+
print(result.get("content") or json.dumps(result))
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _cmd_publish(args: argparse.Namespace) -> int:
|
|
89
|
+
messages = [{"role": "system", "content": args.system}]
|
|
90
|
+
if args.user:
|
|
91
|
+
messages.append({"role": "user", "content": args.user})
|
|
92
|
+
record = prompts_from_env().publish_version(
|
|
93
|
+
args.prompt_id,
|
|
94
|
+
messages=messages,
|
|
95
|
+
change_notes=args.notes,
|
|
96
|
+
)
|
|
97
|
+
if args.json:
|
|
98
|
+
print(json.dumps(_record_dict(record), indent=2))
|
|
99
|
+
else:
|
|
100
|
+
print(f"Published v{record.current_version} for {record.name}")
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _cmd_delete(args: argparse.Namespace) -> int:
|
|
105
|
+
prompts_from_env().delete(args.prompt_id)
|
|
106
|
+
print(f"Deleted {args.prompt_id}")
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
111
|
+
parser = argparse.ArgumentParser(prog="exemplar prompts")
|
|
112
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
113
|
+
|
|
114
|
+
list_p = sub.add_parser("list", help="List org prompts")
|
|
115
|
+
list_p.add_argument("--q")
|
|
116
|
+
list_p.add_argument("--limit", type=int, default=100)
|
|
117
|
+
list_p.add_argument("--json", action="store_true")
|
|
118
|
+
list_p.set_defaults(func=_cmd_list)
|
|
119
|
+
|
|
120
|
+
get_p = sub.add_parser("get", help="Get prompt by ID")
|
|
121
|
+
get_p.add_argument("prompt_id")
|
|
122
|
+
get_p.add_argument("--json", action="store_true")
|
|
123
|
+
get_p.set_defaults(func=_cmd_get)
|
|
124
|
+
|
|
125
|
+
search_p = sub.add_parser("search", help="Search prompts")
|
|
126
|
+
search_p.add_argument("query")
|
|
127
|
+
search_p.add_argument("--top-k", type=int, default=20)
|
|
128
|
+
search_p.add_argument("--json", action="store_true")
|
|
129
|
+
search_p.set_defaults(func=_cmd_search)
|
|
130
|
+
|
|
131
|
+
create_p = sub.add_parser("create", help="Create a prompt")
|
|
132
|
+
create_p.add_argument("name")
|
|
133
|
+
create_p.add_argument("--title", required=True)
|
|
134
|
+
create_p.add_argument("--description", default="")
|
|
135
|
+
create_p.add_argument("--system", required=True, help="System message content")
|
|
136
|
+
create_p.add_argument("--user", help="Optional user message template")
|
|
137
|
+
create_p.add_argument("--variables", help="Comma-separated variable names")
|
|
138
|
+
create_p.add_argument("--tags", help="Comma-separated tags")
|
|
139
|
+
create_p.add_argument("--json", action="store_true")
|
|
140
|
+
create_p.set_defaults(func=_cmd_create)
|
|
141
|
+
|
|
142
|
+
run_p = sub.add_parser("run", help="Run a prompt with variables")
|
|
143
|
+
run_p.add_argument("prompt_id")
|
|
144
|
+
run_p.add_argument("--var", action="append", metavar="KEY=VALUE", help="Template variable")
|
|
145
|
+
run_p.add_argument("--model")
|
|
146
|
+
run_p.add_argument("--json", action="store_true")
|
|
147
|
+
run_p.set_defaults(func=_cmd_run)
|
|
148
|
+
|
|
149
|
+
publish_p = sub.add_parser("publish", help="Publish a new prompt version")
|
|
150
|
+
publish_p.add_argument("prompt_id")
|
|
151
|
+
publish_p.add_argument("--system", required=True)
|
|
152
|
+
publish_p.add_argument("--user")
|
|
153
|
+
publish_p.add_argument("--notes", default="CLI publish")
|
|
154
|
+
publish_p.add_argument("--json", action="store_true")
|
|
155
|
+
publish_p.set_defaults(func=_cmd_publish)
|
|
156
|
+
|
|
157
|
+
delete_p = sub.add_parser("delete", help="Delete a prompt")
|
|
158
|
+
delete_p.add_argument("prompt_id")
|
|
159
|
+
delete_p.set_defaults(func=_cmd_delete)
|
|
160
|
+
|
|
161
|
+
return parser
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main(argv: list[str] | None = None) -> int:
|
|
165
|
+
parser = build_parser()
|
|
166
|
+
args = parser.parse_args(argv)
|
|
167
|
+
return int(args.func(args))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
raise SystemExit(main())
|
|
@@ -56,60 +56,59 @@ def _cmd_pull(args: argparse.Namespace) -> int:
|
|
|
56
56
|
if args.all:
|
|
57
57
|
items = client.list(limit=500)
|
|
58
58
|
for item in items:
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
out = dest_root / item.name
|
|
60
|
+
client.export_folder(item.name, out)
|
|
61
|
+
print(f"Pulled {len(items)} skill folder(s) to {dest_root}")
|
|
61
62
|
return 0
|
|
62
63
|
|
|
63
64
|
if not args.name:
|
|
64
65
|
print("skill name required unless --all is set", file=sys.stderr)
|
|
65
66
|
return 1
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
out = dest_root / args.name
|
|
69
|
+
client.export_folder(args.name, out)
|
|
70
|
+
print(f"Pulled {args.name} to {out}")
|
|
69
71
|
return 0
|
|
70
72
|
|
|
71
73
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
def _resolve_skill_dir(path: Path) -> tuple[Path, Path]:
|
|
75
|
+
if path.is_dir():
|
|
76
|
+
return path, path / "SKILL.md"
|
|
77
|
+
return path.parent, path
|
|
76
78
|
|
|
77
79
|
|
|
78
80
|
def _cmd_push(args: argparse.Namespace) -> int:
|
|
79
81
|
path = Path(args.path)
|
|
80
|
-
|
|
81
|
-
skill_file = path / "SKILL.md"
|
|
82
|
-
else:
|
|
83
|
-
skill_file = path
|
|
82
|
+
skill_dir, skill_file = _resolve_skill_dir(path)
|
|
84
83
|
if not skill_file.is_file():
|
|
85
84
|
print(f"SKILL.md not found: {skill_file}", file=sys.stderr)
|
|
86
85
|
return 1
|
|
87
86
|
|
|
88
87
|
parsed = parse_skill_md(skill_file.read_text(encoding="utf-8"))
|
|
89
|
-
name = (parsed.get("name") or
|
|
88
|
+
name = (parsed.get("name") or skill_dir.name or path.stem).strip()
|
|
90
89
|
if not name:
|
|
91
90
|
print("could not determine skill name from frontmatter or path", file=sys.stderr)
|
|
92
91
|
return 1
|
|
93
92
|
|
|
94
93
|
skills = skills_from_env()
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"description": parsed.get("description") or "",
|
|
98
|
-
}
|
|
94
|
+
instructions = parsed.get("instructions") or ""
|
|
95
|
+
description = parsed.get("description") or ""
|
|
99
96
|
try:
|
|
100
97
|
existing = skills.get(name)
|
|
101
|
-
|
|
98
|
+
skills.update(
|
|
102
99
|
existing.skill_id,
|
|
103
|
-
instructions=
|
|
104
|
-
description=
|
|
100
|
+
instructions=instructions,
|
|
101
|
+
description=description or None,
|
|
105
102
|
)
|
|
103
|
+
record = skills.import_folder(existing.skill_id, skill_dir, prune=args.prune)
|
|
106
104
|
print(f"Updated {record.name} ({record.skill_id})")
|
|
107
105
|
except ValueError:
|
|
108
106
|
record = skills.create(
|
|
109
107
|
name=name,
|
|
110
|
-
instructions=
|
|
111
|
-
description=
|
|
108
|
+
instructions=instructions,
|
|
109
|
+
description=description or None,
|
|
112
110
|
)
|
|
111
|
+
record = skills.import_folder(record.skill_id, skill_dir, prune=args.prune)
|
|
113
112
|
print(f"Created {record.name} ({record.skill_id})")
|
|
114
113
|
return 0
|
|
115
114
|
|
|
@@ -163,15 +162,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
163
162
|
search_p.add_argument("--json", action="store_true")
|
|
164
163
|
search_p.set_defaults(func=_cmd_search)
|
|
165
164
|
|
|
166
|
-
pull_p = sub.add_parser("pull", help="Export skill(s)
|
|
165
|
+
pull_p = sub.add_parser("pull", help="Export skill folder(s) from registry")
|
|
167
166
|
pull_p.add_argument("name", nargs="?")
|
|
168
167
|
pull_p.add_argument("--all", action="store_true")
|
|
169
168
|
pull_p.add_argument("--dest")
|
|
170
169
|
pull_p.add_argument("--global", dest="global_dest", action="store_true")
|
|
171
170
|
pull_p.set_defaults(func=_cmd_pull)
|
|
172
171
|
|
|
173
|
-
push_p = sub.add_parser("push", help="Upload local
|
|
172
|
+
push_p = sub.add_parser("push", help="Upload local skill folder to registry")
|
|
174
173
|
push_p.add_argument("path")
|
|
174
|
+
push_p.add_argument(
|
|
175
|
+
"--prune",
|
|
176
|
+
action="store_true",
|
|
177
|
+
help="Remove remote files not present in local folder",
|
|
178
|
+
)
|
|
175
179
|
push_p.set_defaults(func=_cmd_push)
|
|
176
180
|
|
|
177
181
|
init_p = sub.add_parser("init", help="Scaffold local skill directory")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "exemplar-cli"
|
|
3
|
-
version = "0.1.
|
|
4
|
-
description = "Exemplar terminal CLI for skills and memory"
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = "Exemplar terminal CLI for skills, prompts, and memory"
|
|
5
5
|
authors = ["Exemplar Dev LLC"]
|
|
6
6
|
license = "LicenseRef-Proprietary"
|
|
7
7
|
readme = "README.md"
|
|
@@ -9,7 +9,7 @@ packages = [{ include = "exemplar_cli" }]
|
|
|
9
9
|
|
|
10
10
|
[tool.poetry.dependencies]
|
|
11
11
|
python = ">=3.10,<4.0"
|
|
12
|
-
exemplar-core = ">=0.1.
|
|
12
|
+
exemplar-core = ">=0.1.2,<0.2"
|
|
13
13
|
|
|
14
14
|
[tool.poetry.scripts]
|
|
15
15
|
exemplar = "exemplar_cli.main:main"
|
exemplar_cli-0.1.1/README.md
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# exemplar-cli
|
|
2
|
-
|
|
3
|
-
Terminal CLI for Exemplar **skills** and **memory**.
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
pip install exemplar-cli
|
|
7
|
-
export EXEMPLAR_API_KEY=eis_...
|
|
8
|
-
|
|
9
|
-
exemplar skills list
|
|
10
|
-
exemplar memory add "User prefers bullet points" --user-id u1 --type preference
|
|
11
|
-
exemplar memory search "formatting" --user-id u1
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Environment
|
|
15
|
-
|
|
16
|
-
| Variable | Purpose |
|
|
17
|
-
|----------|---------|
|
|
18
|
-
| `EXEMPLAR_API_KEY` | Org API key (required) |
|
|
19
|
-
| `EXEMPLAR_BASE_URL` | API host override (optional) |
|
|
20
|
-
| `EXEMPLAR_ORGANIZATION_ID` | Org override for JWT auth (optional) |
|
|
21
|
-
|
|
22
|
-
## Commands
|
|
23
|
-
|
|
24
|
-
### Skills
|
|
25
|
-
|
|
26
|
-
`list`, `get`, `search`, `pull`, `push`, `init`, `export-zip`
|
|
27
|
-
|
|
28
|
-
### Memory
|
|
29
|
-
|
|
30
|
-
`add`, `list`, `get`, `search`, `recall`, `update`, `delete`, `delete-bulk`
|
|
31
|
-
|
|
32
|
-
Memory commands accept scope flags: `--user-id`, `--agent-id`, `--session-id`, `--app-id`.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|