exemplar-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.1
2
+ Name: exemplar-cli
3
+ Version: 0.1.0
4
+ Summary: Exemplar terminal CLI for skills and memory
5
+ License: LicenseRef-Proprietary
6
+ Author: Exemplar Dev LLC
7
+ Requires-Python: >=3.10,<3.14
8
+ Classifier: License :: Other/Proprietary License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Dist: exemplar-core (>=0.1.0,<0.2)
14
+ Description-Content-Type: text/markdown
15
+
16
+ # exemplar-cli
17
+
18
+ Terminal CLI for Exemplar **skills** and **memory**.
19
+
20
+ ```bash
21
+ pip install exemplar-cli
22
+ export EXEMPLAR_API_KEY=eis_...
23
+
24
+ exemplar skills list
25
+ exemplar memory add "User prefers bullet points" --user-id u1 --type preference
26
+ exemplar memory search "formatting" --user-id u1
27
+ ```
28
+
29
+ ## Environment
30
+
31
+ | Variable | Purpose |
32
+ |----------|---------|
33
+ | `EXEMPLAR_API_KEY` | Org API key (required) |
34
+ | `EXEMPLAR_BASE_URL` | API host override (optional) |
35
+ | `EXEMPLAR_ORGANIZATION_ID` | Org override for JWT auth (optional) |
36
+
37
+ ## Commands
38
+
39
+ ### Skills
40
+
41
+ `list`, `get`, `search`, `pull`, `push`, `init`, `export-zip`
42
+
43
+ ### Memory
44
+
45
+ `add`, `list`, `get`, `search`, `recall`, `update`, `delete`, `delete-bulk`
46
+
47
+ Memory commands accept scope flags: `--user-id`, `--agent-id`, `--session-id`, `--app-id`.
48
+
@@ -0,0 +1,32 @@
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
@@ -0,0 +1,23 @@
1
+ """Shared CLI helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from exemplar_core.memory.models import MemoryScopes
8
+
9
+
10
+ def add_scope_arguments(parser: argparse.ArgumentParser) -> None:
11
+ parser.add_argument("--user-id", dest="user_id")
12
+ parser.add_argument("--agent-id", dest="agent_id")
13
+ parser.add_argument("--session-id", dest="session_id")
14
+ parser.add_argument("--app-id", dest="app_id")
15
+
16
+
17
+ def scopes_from_args(args: argparse.Namespace) -> MemoryScopes:
18
+ return MemoryScopes(
19
+ user_id=args.user_id,
20
+ agent_id=args.agent_id,
21
+ session_id=args.session_id,
22
+ app_id=args.app_id,
23
+ )
@@ -0,0 +1,27 @@
1
+ """Exemplar CLI entrypoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from exemplar_cli.memory import main as memory_main
8
+ from exemplar_cli.skills import main as skills_main
9
+
10
+
11
+ def main(argv: list[str] | None = None) -> int:
12
+ args = list(argv if argv is not None else sys.argv[1:])
13
+ if args and args[0] == "skills":
14
+ return skills_main(args[1:])
15
+ if args and args[0] == "memory":
16
+ return memory_main(args[1:])
17
+ print(
18
+ "Usage: exemplar <skills|memory> <subcommand> ...\n"
19
+ " exemplar skills list|get|search|pull|push|init|export-zip\n"
20
+ " exemplar memory add|list|get|search|recall|update|delete|delete-bulk",
21
+ file=sys.stderr,
22
+ )
23
+ return 2
24
+
25
+
26
+ if __name__ == "__main__":
27
+ raise SystemExit(main())
@@ -0,0 +1,196 @@
1
+ """CLI: exemplar memory — manage long-term memory 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 memory_from_env
11
+ from exemplar_cli._scopes import add_scope_arguments, scopes_from_args
12
+
13
+
14
+ def _memory(args: argparse.Namespace):
15
+ scopes = scopes_from_args(args)
16
+ return memory_from_env(
17
+ user_id=scopes.user_id,
18
+ agent_id=scopes.agent_id,
19
+ session_id=scopes.session_id,
20
+ app_id=scopes.app_id,
21
+ )
22
+
23
+
24
+ def _record_dict(record) -> dict:
25
+ data = asdict(record)
26
+ if record.scopes is not None:
27
+ data["scopes"] = record.scopes.to_api_dict()
28
+ return data
29
+
30
+
31
+ def _cmd_add(args: argparse.Namespace) -> int:
32
+ try:
33
+ result = _memory(args).add(args.content, memory_type=args.type)
34
+ except ValueError as exc:
35
+ print(str(exc), file=sys.stderr)
36
+ return 1
37
+ if args.json:
38
+ print(json.dumps(result, indent=2))
39
+ else:
40
+ print(f"Added memory {result.get('memoryId', '')}")
41
+ return 0
42
+
43
+
44
+ def _cmd_list(args: argparse.Namespace) -> int:
45
+ records = _memory(args).list(
46
+ skip=args.skip,
47
+ limit=args.limit,
48
+ memory_type=args.type,
49
+ )
50
+ if args.json:
51
+ print(json.dumps([_record_dict(r) for r in records], indent=2))
52
+ return 0
53
+ for record in records:
54
+ print(f"{record.memory_id}\t[{record.memory_type}]\t{record.content}")
55
+ return 0
56
+
57
+
58
+ def _cmd_get(args: argparse.Namespace) -> int:
59
+ record = _memory(args).get(args.memory_id)
60
+ if args.json:
61
+ print(json.dumps(_record_dict(record), indent=2))
62
+ return 0
63
+ print(record.content)
64
+ return 0
65
+
66
+
67
+ def _cmd_search(args: argparse.Namespace) -> int:
68
+ records = _memory(args).search(
69
+ args.query,
70
+ top_k=args.top_k,
71
+ threshold=args.threshold,
72
+ )
73
+ if args.json:
74
+ print(json.dumps([_record_dict(r) for r in records], indent=2))
75
+ return 0
76
+ for record in records:
77
+ score = f"{record.score:.2f}\t" if record.score is not None else ""
78
+ print(f"{score}{record.memory_id}\t[{record.memory_type}]\t{record.content}")
79
+ return 0
80
+
81
+
82
+ def _cmd_recall(args: argparse.Namespace) -> int:
83
+ text = _memory(args).recall(
84
+ args.query,
85
+ top_k=args.top_k,
86
+ threshold=args.threshold,
87
+ )
88
+ print(text)
89
+ return 0
90
+
91
+
92
+ def _cmd_update(args: argparse.Namespace) -> int:
93
+ if args.content is None and args.type is None:
94
+ print("provide --content and/or --type", file=sys.stderr)
95
+ return 1
96
+ try:
97
+ record = _memory(args).update(
98
+ args.memory_id,
99
+ content=args.content,
100
+ memory_type=args.type,
101
+ )
102
+ except ValueError as exc:
103
+ print(str(exc), file=sys.stderr)
104
+ return 1
105
+ if args.json:
106
+ print(json.dumps(_record_dict(record), indent=2))
107
+ else:
108
+ print(f"Updated {record.memory_id}")
109
+ return 0
110
+
111
+
112
+ def _cmd_delete(args: argparse.Namespace) -> int:
113
+ _memory(args).delete(args.memory_id)
114
+ print(f"Deleted {args.memory_id}")
115
+ return 0
116
+
117
+
118
+ def _cmd_delete_bulk(args: argparse.Namespace) -> int:
119
+ try:
120
+ deleted = _memory(args).delete_bulk(memory_type=args.type)
121
+ except ValueError as exc:
122
+ print(str(exc), file=sys.stderr)
123
+ return 1
124
+ print(f"Deleted {deleted} memory(s)")
125
+ return 0
126
+
127
+
128
+ def build_parser() -> argparse.ArgumentParser:
129
+ parser = argparse.ArgumentParser(prog="exemplar memory")
130
+ sub = parser.add_subparsers(dest="command", required=True)
131
+
132
+ add_p = sub.add_parser("add", help="Add a memory")
133
+ add_p.add_argument("content")
134
+ add_p.add_argument("--type", default="fact", dest="type")
135
+ add_p.add_argument("--json", action="store_true")
136
+ add_scope_arguments(add_p)
137
+ add_p.set_defaults(func=_cmd_add)
138
+
139
+ list_p = sub.add_parser("list", help="List memories")
140
+ list_p.add_argument("--type")
141
+ list_p.add_argument("--skip", type=int, default=0)
142
+ list_p.add_argument("--limit", type=int, default=100)
143
+ list_p.add_argument("--json", action="store_true")
144
+ add_scope_arguments(list_p)
145
+ list_p.set_defaults(func=_cmd_list)
146
+
147
+ get_p = sub.add_parser("get", help="Get a memory by ID")
148
+ get_p.add_argument("memory_id")
149
+ get_p.add_argument("--json", action="store_true")
150
+ add_scope_arguments(get_p)
151
+ get_p.set_defaults(func=_cmd_get)
152
+
153
+ search_p = sub.add_parser("search", help="Search memories")
154
+ search_p.add_argument("query")
155
+ search_p.add_argument("--top-k", type=int, default=10)
156
+ search_p.add_argument("--threshold", type=float, default=0.1)
157
+ search_p.add_argument("--json", action="store_true")
158
+ add_scope_arguments(search_p)
159
+ search_p.set_defaults(func=_cmd_search)
160
+
161
+ recall_p = sub.add_parser("recall", help="Print formatted recall for prompts")
162
+ recall_p.add_argument("query")
163
+ recall_p.add_argument("--top-k", type=int, default=5)
164
+ recall_p.add_argument("--threshold", type=float, default=0.1)
165
+ add_scope_arguments(recall_p)
166
+ recall_p.set_defaults(func=_cmd_recall)
167
+
168
+ update_p = sub.add_parser("update", help="Update a memory")
169
+ update_p.add_argument("memory_id")
170
+ update_p.add_argument("--content")
171
+ update_p.add_argument("--type")
172
+ update_p.add_argument("--json", action="store_true")
173
+ add_scope_arguments(update_p)
174
+ update_p.set_defaults(func=_cmd_update)
175
+
176
+ delete_p = sub.add_parser("delete", help="Delete a memory")
177
+ delete_p.add_argument("memory_id")
178
+ add_scope_arguments(delete_p)
179
+ delete_p.set_defaults(func=_cmd_delete)
180
+
181
+ bulk_p = sub.add_parser("delete-bulk", help="Bulk delete by scope filters")
182
+ bulk_p.add_argument("--type")
183
+ add_scope_arguments(bulk_p)
184
+ bulk_p.set_defaults(func=_cmd_delete_bulk)
185
+
186
+ return parser
187
+
188
+
189
+ def main(argv: list[str] | None = None) -> int:
190
+ parser = build_parser()
191
+ args = parser.parse_args(argv)
192
+ return int(args.func(args))
193
+
194
+
195
+ if __name__ == "__main__":
196
+ raise SystemExit(main())
@@ -0,0 +1,201 @@
1
+ """CLI: exemplar skills — manage org skill registry from the terminal."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from exemplar_core import skills_from_env
11
+ from exemplar_core.skills.skill_md import parse_skill_md, skill_md_template
12
+
13
+
14
+ def _default_dest(global_flag: bool) -> Path:
15
+ if global_flag:
16
+ return Path.home() / ".cursor" / "skills"
17
+ return Path(".cursor") / "skills"
18
+
19
+
20
+ def _cmd_list(args: argparse.Namespace) -> int:
21
+ skills = skills_from_env()
22
+ items = skills.list(tag=args.tag, limit=args.limit)
23
+ if args.json:
24
+ print(json.dumps([item.__dict__ for item in items], indent=2))
25
+ return 0
26
+ for item in items:
27
+ tags = ",".join(item.tags) if item.tags else ""
28
+ suffix = f" [{tags}]" if tags else ""
29
+ print(f"{item.name}\t{item.skill_id}{suffix}")
30
+ return 0
31
+
32
+
33
+ def _cmd_get(args: argparse.Namespace) -> int:
34
+ record = skills_from_env().get(args.name)
35
+ if args.json:
36
+ print(json.dumps(record.__dict__, indent=2))
37
+ return 0
38
+ print(record.instructions)
39
+ return 0
40
+
41
+
42
+ def _cmd_search(args: argparse.Namespace) -> int:
43
+ results = skills_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.description}")
49
+ return 0
50
+
51
+
52
+ def _cmd_pull(args: argparse.Namespace) -> int:
53
+ client = skills_from_env()
54
+ dest_root = Path(args.dest) if args.dest else _default_dest(args.global_dest)
55
+
56
+ if args.all:
57
+ items = client.list(limit=500)
58
+ for item in items:
59
+ _write_skill_md(client, item.name, dest_root / item.name)
60
+ print(f"Pulled {len(items)} skill(s) to {dest_root}")
61
+ return 0
62
+
63
+ if not args.name:
64
+ print("skill name required unless --all is set", file=sys.stderr)
65
+ return 1
66
+
67
+ _write_skill_md(client, args.name, dest_root / args.name)
68
+ print(f"Pulled {args.name} to {dest_root / args.name / 'SKILL.md'}")
69
+ return 0
70
+
71
+
72
+ def _write_skill_md(client, name: str, dest_dir: Path) -> None:
73
+ dest_dir.mkdir(parents=True, exist_ok=True)
74
+ content = client.export_skill_md(name)
75
+ (dest_dir / "SKILL.md").write_text(content, encoding="utf-8")
76
+
77
+
78
+ def _cmd_push(args: argparse.Namespace) -> int:
79
+ path = Path(args.path)
80
+ if path.is_dir():
81
+ skill_file = path / "SKILL.md"
82
+ else:
83
+ skill_file = path
84
+ if not skill_file.is_file():
85
+ print(f"SKILL.md not found: {skill_file}", file=sys.stderr)
86
+ return 1
87
+
88
+ parsed = parse_skill_md(skill_file.read_text(encoding="utf-8"))
89
+ name = (parsed.get("name") or path.parent.name or path.stem).strip()
90
+ if not name:
91
+ print("could not determine skill name from frontmatter or path", file=sys.stderr)
92
+ return 1
93
+
94
+ skills = skills_from_env()
95
+ body = {
96
+ "instructions": parsed.get("instructions") or "",
97
+ "description": parsed.get("description") or "",
98
+ }
99
+ try:
100
+ existing = skills.get(name)
101
+ record = skills.update(
102
+ existing.skill_id,
103
+ instructions=body["instructions"],
104
+ description=body["description"],
105
+ )
106
+ print(f"Updated {record.name} ({record.skill_id})")
107
+ except ValueError:
108
+ record = skills.create(
109
+ name=name,
110
+ instructions=body["instructions"],
111
+ description=body.get("description"),
112
+ )
113
+ print(f"Created {record.name} ({record.skill_id})")
114
+ return 0
115
+
116
+
117
+ def _cmd_init(args: argparse.Namespace) -> int:
118
+ dest_root = Path(args.dest) if args.dest else _default_dest(args.global_dest)
119
+ dest_dir = dest_root / args.name
120
+ dest_dir.mkdir(parents=True, exist_ok=True)
121
+ skill_path = dest_dir / "SKILL.md"
122
+ if skill_path.exists() and not args.force:
123
+ print(f"already exists: {skill_path}", file=sys.stderr)
124
+ return 1
125
+ skill_path.write_text(
126
+ skill_md_template(
127
+ name=args.name,
128
+ description=args.description or "",
129
+ instructions=args.instructions or f"# {args.name}\n",
130
+ ),
131
+ encoding="utf-8",
132
+ )
133
+ print(f"Created {skill_path}")
134
+ return 0
135
+
136
+
137
+ def _cmd_export_zip(args: argparse.Namespace) -> int:
138
+ payload = skills_from_env().export_zip(args.name)
139
+ out = Path(args.output) if args.output else Path(f"{args.name}.zip")
140
+ out.write_bytes(payload)
141
+ print(f"Wrote {out}")
142
+ return 0
143
+
144
+
145
+ def build_parser() -> argparse.ArgumentParser:
146
+ parser = argparse.ArgumentParser(prog="exemplar skills")
147
+ sub = parser.add_subparsers(dest="command", required=True)
148
+
149
+ list_p = sub.add_parser("list", help="List org skills")
150
+ list_p.add_argument("--tag")
151
+ list_p.add_argument("--limit", type=int, default=100)
152
+ list_p.add_argument("--json", action="store_true")
153
+ list_p.set_defaults(func=_cmd_list)
154
+
155
+ get_p = sub.add_parser("get", help="Get skill instructions")
156
+ get_p.add_argument("name")
157
+ get_p.add_argument("--json", action="store_true")
158
+ get_p.set_defaults(func=_cmd_get)
159
+
160
+ search_p = sub.add_parser("search", help="Search skills")
161
+ search_p.add_argument("query")
162
+ search_p.add_argument("--top-k", type=int, default=20)
163
+ search_p.add_argument("--json", action="store_true")
164
+ search_p.set_defaults(func=_cmd_search)
165
+
166
+ pull_p = sub.add_parser("pull", help="Export skill(s) to local SKILL.md")
167
+ pull_p.add_argument("name", nargs="?")
168
+ pull_p.add_argument("--all", action="store_true")
169
+ pull_p.add_argument("--dest")
170
+ pull_p.add_argument("--global", dest="global_dest", action="store_true")
171
+ pull_p.set_defaults(func=_cmd_pull)
172
+
173
+ push_p = sub.add_parser("push", help="Upload local SKILL.md to registry")
174
+ push_p.add_argument("path")
175
+ push_p.set_defaults(func=_cmd_push)
176
+
177
+ init_p = sub.add_parser("init", help="Scaffold local skill directory")
178
+ init_p.add_argument("name")
179
+ init_p.add_argument("--description", default="")
180
+ init_p.add_argument("--instructions", default="")
181
+ init_p.add_argument("--dest")
182
+ init_p.add_argument("--global", dest="global_dest", action="store_true")
183
+ init_p.add_argument("--force", action="store_true")
184
+ init_p.set_defaults(func=_cmd_init)
185
+
186
+ zip_p = sub.add_parser("export-zip", help="Download skill zip bundle")
187
+ zip_p.add_argument("name")
188
+ zip_p.add_argument("-o", "--output")
189
+ zip_p.set_defaults(func=_cmd_export_zip)
190
+
191
+ return parser
192
+
193
+
194
+ def main(argv: list[str] | None = None) -> int:
195
+ parser = build_parser()
196
+ args = parser.parse_args(argv)
197
+ return int(args.func(args))
198
+
199
+
200
+ if __name__ == "__main__":
201
+ raise SystemExit(main())
@@ -0,0 +1,26 @@
1
+ [tool.poetry]
2
+ name = "exemplar-cli"
3
+ version = "0.1.0"
4
+ description = "Exemplar terminal CLI for skills and memory"
5
+ authors = ["Exemplar Dev LLC"]
6
+ license = "LicenseRef-Proprietary"
7
+ readme = "README.md"
8
+ packages = [{ include = "exemplar_cli" }]
9
+
10
+ [tool.poetry.dependencies]
11
+ python = ">=3.10,<3.14"
12
+ exemplar-core = ">=0.1.0,<0.2"
13
+
14
+ [tool.poetry.scripts]
15
+ exemplar = "exemplar_cli.main:main"
16
+
17
+ [tool.poetry.group.dev.dependencies]
18
+ pytest = ">=8.0"
19
+
20
+ [build-system]
21
+ requires = ["poetry-core>=1.0.0"]
22
+ build-backend = "poetry.core.masonry.api"
23
+
24
+ [tool.pytest.ini_options]
25
+ testpaths = ["tests"]
26
+ pythonpath = ["."]