primethink-cli 1.3.0__tar.gz → 1.3.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.
- {primethink_cli-1.3.0/primethink_cli.egg-info → primethink_cli-1.3.2}/PKG-INFO +1 -1
- primethink_cli-1.3.2/docs/superpowers/specs/2026-08-16-install-attached-skills-design.md +184 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/primethink.py +16 -5
- {primethink_cli-1.3.0 → primethink_cli-1.3.2/primethink_cli.egg-info}/PKG-INFO +1 -1
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/primethink_cli.egg-info/SOURCES.txt +1 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/pyproject.toml +1 -1
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/tests/test_cli.py +7 -2
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/DEVELOPER.md +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/LICENSE +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/MANIFEST.in +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/README.md +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/SPECS.md +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/USER_GUIDE.md +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/docs/cli-reference.md +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/docs/install.md +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/docs/primethink_help_llms.txt +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/install/homebrew/primethink-cli.rb +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/install/install.cmd +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/install/install.ps1 +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/install/install.sh +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/primethink_cli.egg-info/dependency_links.txt +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/primethink_cli.egg-info/entry_points.txt +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/primethink_cli.egg-info/requires.txt +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/primethink_cli.egg-info/top_level.txt +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/primethink_mcp.py +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/scripts/build.sh +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/scripts/deploy.sh +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/scripts/publish.sh +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/setup.cfg +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/setup.py +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/skills/__init__.py +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/skills/primethink-cli/SKILL.md +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/tests/test_agent_commands.py +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/tests/test_env_config.py +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/tests/test_install_developer_skill.py +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/tests/test_install_skill.py +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/tests/test_live_app.py +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/tests/test_mcp_server.py +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/tests/test_search_commands.py +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/tests/test_task_commands.py +0 -0
- {primethink_cli-1.3.0 → primethink_cli-1.3.2}/tests/test_whoami.py +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Design: Install skills attached to a chat or agent
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-16
|
|
4
|
+
**Status:** Approved for planning
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
|
|
8
|
+
Add CLI commands that download and install every skill attached to a PrimeThink
|
|
9
|
+
chat and/or agent into a local Claude-Code-style skills directory. The primary
|
|
10
|
+
use case is a sandboxed agent turn, where `PT_CHAT_UUID` / `PT_AGENT_ID` are in
|
|
11
|
+
the environment and a single zero-argument command should fetch everything the
|
|
12
|
+
current chat and agent carry.
|
|
13
|
+
|
|
14
|
+
## Background: how skills are modeled in the API
|
|
15
|
+
|
|
16
|
+
Skills are collections, in two flavors:
|
|
17
|
+
|
|
18
|
+
1. **`skill` collections** — `type == "skill"`. The collection's document tree
|
|
19
|
+
*is* the skill's file tree; it is downloaded through the same per-collection
|
|
20
|
+
document endpoints that `pt collections sync-from` uses.
|
|
21
|
+
2. **GitHub-backed skills** — `type == "external_source"` with
|
|
22
|
+
`external_source_type == "github_skill"`. `external_source_value` holds a
|
|
23
|
+
**full GitHub URL** pointing at the skill (repo root, or a
|
|
24
|
+
`/tree/<ref>/<path>` / `/blob/<ref>/<path>` URL naming a subdirectory).
|
|
25
|
+
|
|
26
|
+
Attachment discovery:
|
|
27
|
+
|
|
28
|
+
- **Chat:** `GET /api/v1/chats/{chat_id_or_uuid}/collections` — the path
|
|
29
|
+
parameter accepts an integer ID or a chat UUID. Returns
|
|
30
|
+
`List[BaseCollectionResponse]` including `type`, `external_source_type`,
|
|
31
|
+
`external_source_value`, and the link `status`.
|
|
32
|
+
- **Agent:** `GET /api/v1/virtual-assistants/{va_id}/collections` — integer ID
|
|
33
|
+
only. Returns `BaseCollectionResponse` plus per-agent link fields `hidden`
|
|
34
|
+
and `status`.
|
|
35
|
+
|
|
36
|
+
There is no server-side `?type=` filter; filtering is client-side, matching
|
|
37
|
+
what the Deep1 backend itself does internally (`_load_skills`).
|
|
38
|
+
|
|
39
|
+
Both endpoints authenticate with the standard token, so the CLI's existing
|
|
40
|
+
`_resolve_connection` / `PT_TOKEN` flow works unchanged.
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
Three commands sharing one implementation:
|
|
45
|
+
|
|
46
|
+
### `pt install-skills` (top-level)
|
|
47
|
+
|
|
48
|
+
Installs skills from the current chat **and** its agent by default.
|
|
49
|
+
|
|
50
|
+
- Chat default: `PT_CHAT_UUID` env var, falling back to `PT_CHAT_ID`.
|
|
51
|
+
- Agent default: `PT_AGENT_ID` env var.
|
|
52
|
+
- `--chat-id TEXT` / `--agent-id INT` — explicit overrides.
|
|
53
|
+
- `--chat-only` / `--agent-only` — restrict to one side (mutually exclusive).
|
|
54
|
+
- Standard installer options: `--user` (default, `~/.claude/skills`),
|
|
55
|
+
`--project` (`./.claude/skills`), `--dir PATH`, `--force`.
|
|
56
|
+
- Standard connection options (`_connection_options`).
|
|
57
|
+
- Errors cleanly (exit 1) if, after env fallback, neither a chat nor an agent
|
|
58
|
+
can be resolved for the requested sides.
|
|
59
|
+
|
|
60
|
+
### `pt chat install-skills [CHAT_ID]`
|
|
61
|
+
|
|
62
|
+
Chat side only. `CHAT_ID` argument (ID or UUID) is optional, falling back to
|
|
63
|
+
`PT_CHAT_UUID` / `PT_CHAT_ID`. Uses `profile_short=False` (the chat group
|
|
64
|
+
reserves `-p` for `--path`). Same installer/connection options as above minus
|
|
65
|
+
the agent-related flags.
|
|
66
|
+
|
|
67
|
+
### `pt agent install-skills [AGENT_ID]`
|
|
68
|
+
|
|
69
|
+
Agent side only. `AGENT_ID` (int) optional, falling back to `PT_AGENT_ID`.
|
|
70
|
+
Same installer/connection options minus the chat-related flags.
|
|
71
|
+
|
|
72
|
+
## Filtering semantics
|
|
73
|
+
|
|
74
|
+
From each collections listing, keep a collection when:
|
|
75
|
+
|
|
76
|
+
- `type == "skill"`, **or**
|
|
77
|
+
- `type == "external_source"` and `external_source_type == "github_skill"`.
|
|
78
|
+
|
|
79
|
+
Additionally:
|
|
80
|
+
|
|
81
|
+
- **Agent links:** require `status == "active"` and `hidden` falsy — this
|
|
82
|
+
replicates how Deep1 computes "the skills this agent actually has".
|
|
83
|
+
- **Chat links:** require the link `status` to be `active` when present.
|
|
84
|
+
- When both chat and agent are processed, de-duplicate by collection `id`
|
|
85
|
+
(first occurrence wins; chat side is processed first).
|
|
86
|
+
|
|
87
|
+
Non-skill collections (`collection`, `db`, other external-source types) are
|
|
88
|
+
ignored silently.
|
|
89
|
+
|
|
90
|
+
## Install pipeline
|
|
91
|
+
|
|
92
|
+
Destination base directory resolves via the existing `_skill_install_base`
|
|
93
|
+
(`--user` / `--project` / `--dir`). Each skill installs into its own
|
|
94
|
+
subdirectory. If the destination subdirectory already exists, the skill is
|
|
95
|
+
**skipped with a notice** unless `--force` is given.
|
|
96
|
+
|
|
97
|
+
### `skill` collections
|
|
98
|
+
|
|
99
|
+
- Reuse the existing remote-tree machinery (`_list_remote_tree` and the
|
|
100
|
+
download helpers behind `pt collections sync-from`) to walk the collection's
|
|
101
|
+
document tree and download every file into
|
|
102
|
+
`<skills-dir>/<slug>/`, where `<slug>` is the sanitized collection name
|
|
103
|
+
(reusing `_sanitize_filename`-style slugging; kebab-case).
|
|
104
|
+
- After download, **warn** (do not fail) if there is no `SKILL.md` at the
|
|
105
|
+
skill's root.
|
|
106
|
+
|
|
107
|
+
### `github_skill` collections
|
|
108
|
+
|
|
109
|
+
- Parse `external_source_value` as a GitHub URL:
|
|
110
|
+
- `https://github.com/{owner}/{repo}` → repo root, ref `main`, path empty.
|
|
111
|
+
- `https://github.com/{owner}/{repo}/tree/{ref}/{path...}` (or `/blob/`) →
|
|
112
|
+
that ref and subdirectory.
|
|
113
|
+
- Anything else (non-GitHub host, malformed) → per-skill failure.
|
|
114
|
+
- Download via the existing `_download_github_archive` and extract via
|
|
115
|
+
`_extract_github_subdirectory`, which already enforce SKILL.md presence,
|
|
116
|
+
path safety (no traversal, no symlinks), and file-count/size caps. Note:
|
|
117
|
+
the extractor currently rejects an empty source path, so supporting
|
|
118
|
+
repo-root URLs requires a small extension to it (treat empty path as "the
|
|
119
|
+
archive's single top-level directory", keeping the SKILL.md marker check).
|
|
120
|
+
- Destination folder name: last path segment of the skill path, or the repo
|
|
121
|
+
name for root URLs.
|
|
122
|
+
|
|
123
|
+
## Failure semantics
|
|
124
|
+
|
|
125
|
+
Consistent with the sync commands: **per-skill failures are non-fatal**. Each
|
|
126
|
+
skill prints an install/skip/fail line as it is processed; the command ends
|
|
127
|
+
with a summary such as `Installed 3, skipped 1, failed 1`.
|
|
128
|
+
|
|
129
|
+
Exit code 1 when:
|
|
130
|
+
|
|
131
|
+
- discovery itself fails (the collections request errors), or
|
|
132
|
+
- at least one skill was requested and **all** of them failed.
|
|
133
|
+
|
|
134
|
+
Exit code 0 otherwise (including "no skills attached", which prints a notice).
|
|
135
|
+
|
|
136
|
+
## Error handling details
|
|
137
|
+
|
|
138
|
+
- Unresolvable chat/agent (no arg, no env var): clear message naming the env
|
|
139
|
+
vars and flags, exit 1.
|
|
140
|
+
- `--chat-only` together with `--agent-only`: usage error.
|
|
141
|
+
- API errors during discovery: standard `_api_request` error path.
|
|
142
|
+
- Download errors, bad GitHub URLs, unsafe archives: recorded as that skill's
|
|
143
|
+
failure; processing continues.
|
|
144
|
+
|
|
145
|
+
## Testing
|
|
146
|
+
|
|
147
|
+
All API traffic is mocked via `primethink.requests.request` (repo convention):
|
|
148
|
+
|
|
149
|
+
- Discovery: chat and agent collections responses; client-side filter drops
|
|
150
|
+
non-skill types, inactive links, and hidden agent links.
|
|
151
|
+
- Dedup: same collection attached to both chat and agent installs once.
|
|
152
|
+
- `skill` collection install: mocked directory listing + file downloads;
|
|
153
|
+
correct tree on disk; SKILL.md warning path.
|
|
154
|
+
- `github_skill` install: URL parsing cases (root, `/tree/`, `/blob/`,
|
|
155
|
+
malformed); archive extraction reusing the zip-fixture pattern from the
|
|
156
|
+
developer-skill tests.
|
|
157
|
+
- Env-var defaults (`PT_CHAT_UUID`, `PT_CHAT_ID`, `PT_AGENT_ID`) and explicit
|
|
158
|
+
overrides; missing-both error.
|
|
159
|
+
- Skip-existing vs `--force`.
|
|
160
|
+
- Failure semantics: partial failure exits 0 with summary; total failure
|
|
161
|
+
exits 1.
|
|
162
|
+
- All three command entry points share behavior.
|
|
163
|
+
|
|
164
|
+
## Documentation updates (same change)
|
|
165
|
+
|
|
166
|
+
- `docs/cli-reference.md` — full reference for all three commands.
|
|
167
|
+
- `skills/primethink-cli/SKILL.md` — command map + sandbox workflow note.
|
|
168
|
+
- `README.md` — commands-at-a-glance table.
|
|
169
|
+
- `USER_GUIDE.md` — walkthrough section.
|
|
170
|
+
- `SPECS.md` — the two collections-listing endpoints.
|
|
171
|
+
- `CHANGELOG.md` — entry under `[Unreleased]`.
|
|
172
|
+
|
|
173
|
+
**MCP / agent-tools:** deliberately not exposed. Like `pt install-skill`, this
|
|
174
|
+
is a local-filesystem installer, which the MCP server and LangChain plugin do
|
|
175
|
+
not surface.
|
|
176
|
+
|
|
177
|
+
## Out of scope
|
|
178
|
+
|
|
179
|
+
- Server-side `?type=` filter on the collections endpoints (five-line backend
|
|
180
|
+
change; not needed for v1 since client-side filtering is the established
|
|
181
|
+
pattern).
|
|
182
|
+
- Updating/refreshing already-installed skills beyond `--force` overwrite.
|
|
183
|
+
- Installing skills from collections not attached to a chat/agent (a direct
|
|
184
|
+
`pt collections install-skill <id>` could be a follow-up).
|
|
@@ -119,6 +119,17 @@ def _api_call(method, url, token, timeout=REQUEST_TIMEOUT, **kwargs):
|
|
|
119
119
|
return response, None
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
def _upload_part(path, fileobj):
|
|
123
|
+
"""Multipart tuple for a document upload: (field, (filename, fp, mimetype)).
|
|
124
|
+
|
|
125
|
+
Sends the BASENAME (not the local path) and an explicit part
|
|
126
|
+
Content-Type — requests omits the part header otherwise, which some
|
|
127
|
+
servers reject (and older PrimeThink APIs answered 500 to).
|
|
128
|
+
"""
|
|
129
|
+
guessed = mimetypes.guess_type(path)[0] or 'application/octet-stream'
|
|
130
|
+
return ('documents', (os.path.basename(str(path)), fileobj, guessed))
|
|
131
|
+
|
|
132
|
+
|
|
122
133
|
def _api_request(method, url, token, ok_status=(200,), timeout=REQUEST_TIMEOUT, **kwargs):
|
|
123
134
|
"""Make an authenticated API request; print an error and exit on failure."""
|
|
124
135
|
response, error = _api_call(method, url, token, timeout=timeout, **kwargs)
|
|
@@ -194,7 +205,7 @@ def _upload_file_groups(local_path, file_list, path, upload_batch):
|
|
|
194
205
|
files_data = []
|
|
195
206
|
for file in dir_files:
|
|
196
207
|
try:
|
|
197
|
-
files_data.append((
|
|
208
|
+
files_data.append(_upload_part(file, stack.enter_context(open(file, 'rb'))))
|
|
198
209
|
except OSError as e:
|
|
199
210
|
error_count += 1
|
|
200
211
|
click.echo(f" Error opening {file}: {e}")
|
|
@@ -416,7 +427,7 @@ def cli():
|
|
|
416
427
|
@cli.command()
|
|
417
428
|
def version():
|
|
418
429
|
"""Display the version of PrimeThink CLI."""
|
|
419
|
-
click.echo("PrimeThink CLI v1.3.
|
|
430
|
+
click.echo("PrimeThink CLI v1.3.2")
|
|
420
431
|
|
|
421
432
|
|
|
422
433
|
@cli.command()
|
|
@@ -802,7 +813,7 @@ def live_app_new(directory, framework, tailwind, flowbite, repo_url, ref):
|
|
|
802
813
|
)
|
|
803
814
|
click.echo(f"Created PrimeThink Live App at {directory}")
|
|
804
815
|
click.echo(f"Template: {template_id} ({framework}, tailwind={'yes' if tailwind else 'no'}, flowbite={'yes' if flowbite else 'no'})")
|
|
805
|
-
click.echo("Next: read the generated README.md and
|
|
816
|
+
click.echo("Next: read the generated README.md and learn how to build Live Apps.")
|
|
806
817
|
|
|
807
818
|
|
|
808
819
|
# ============================================================================
|
|
@@ -1324,7 +1335,7 @@ def upload_files(chat_id, files, path, profile, api_url):
|
|
|
1324
1335
|
|
|
1325
1336
|
params = {'path': path} if path else {}
|
|
1326
1337
|
with contextlib.ExitStack() as stack:
|
|
1327
|
-
files_data = [(
|
|
1338
|
+
files_data = [_upload_part(fp, stack.enter_context(open(fp, 'rb'))) for fp in files]
|
|
1328
1339
|
response = _api_request(
|
|
1329
1340
|
'POST', f"{api_url}/api/v1/chats/{chat_id}/documents", token,
|
|
1330
1341
|
files=files_data, params=params
|
|
@@ -1463,7 +1474,7 @@ def upload_files(collection_id, files, path, profile, api_url):
|
|
|
1463
1474
|
|
|
1464
1475
|
form_data = {'path': path} if path else None
|
|
1465
1476
|
with contextlib.ExitStack() as stack:
|
|
1466
|
-
files_data = [(
|
|
1477
|
+
files_data = [_upload_part(fp, stack.enter_context(open(fp, 'rb'))) for fp in files]
|
|
1467
1478
|
response = _api_request(
|
|
1468
1479
|
'POST', f"{api_url}/api/v1/collections/{collection_id}/documents", token,
|
|
1469
1480
|
files=files_data, data=form_data
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
1
3
|
import pytest
|
|
2
4
|
import os
|
|
3
5
|
import json
|
|
@@ -51,7 +53,8 @@ def test_version_command(runner):
|
|
|
51
53
|
"""Test version command."""
|
|
52
54
|
result = runner.invoke(cli, ["version"])
|
|
53
55
|
assert result.exit_code == 0
|
|
54
|
-
|
|
56
|
+
# Version-agnostic: the hardcoded number went stale at every release.
|
|
57
|
+
assert re.match(r"PrimeThink CLI v\d+\.\d+\.\d+", result.output)
|
|
55
58
|
|
|
56
59
|
@patch('primethink.CONFIG_DIR', Path('/tmp/.primethink'))
|
|
57
60
|
@patch('primethink.CONFIG_FILE', Path('/tmp/.primethink/config.json'))
|
|
@@ -553,8 +556,10 @@ def test_chat_sync_prefer_local(mock_request, mock_config_fn, runner, tmp_path):
|
|
|
553
556
|
assert upload_call.kwargs['params'] == {}
|
|
554
557
|
files_sent = upload_call.kwargs['files']
|
|
555
558
|
assert len(files_sent) == 1
|
|
556
|
-
field_name, file_obj = files_sent[0]
|
|
559
|
+
field_name, (filename, file_obj, ctype) = files_sent[0]
|
|
557
560
|
assert field_name == 'documents'
|
|
561
|
+
assert filename == 'shared.txt', "part filename must be the basename, not the local path"
|
|
562
|
+
assert ctype == 'text/plain', "part must carry an explicit Content-Type"
|
|
558
563
|
assert Path(file_obj.name).name == 'shared.txt'
|
|
559
564
|
|
|
560
565
|
@patch('primethink.get_active_config')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|