@pokelo/cli 0.1.3 → 0.2.0

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.
package/README.md CHANGED
@@ -20,7 +20,7 @@ From this monorepo (before publish / local dev):
20
20
  ```bash
21
21
  pnpm --filter @pokelo/cli exec pokelo --help
22
22
  # or
23
- node tools/pokelo-cli/bin/pokelo.mjs --help
23
+ node tools/pokelo-cli/bin/pokelo --help
24
24
 
25
25
  cd tools/pokelo-cli && pnpm link --global
26
26
  pokelo --help
@@ -70,9 +70,9 @@ pokelo auth status
70
70
  ### Config priority
71
71
 
72
72
  ```text
73
- CLI flags (--url, --token, --project)
74
- > environment (POKELO_URL, POKELO_TOKEN, POKELO_PROJECT)
75
- > ~/.config/pokelo/config.json
73
+ CLI flags (--url, --token, --project, --workspace)
74
+ > environment (POKELO_URL, POKELO_TOKEN, POKELO_PROJECT, POKELO_WORKSPACE)
75
+ > ~/.config/pokelo/config.json (url, token, defaultProjectId, defaultWorkspaceId)
76
76
  > legacy ~/.pokelo-cli.json (read-only migrate of url/token)
77
77
  ```
78
78
 
@@ -83,8 +83,13 @@ Create a Personal Access Token in the Pokelo UI (Settings → MCP / PAT). Prefix
83
83
  ## Commands
84
84
 
85
85
  ```bash
86
- pokelo projects list
86
+ pokelo workspace list
87
+ pokelo workspace show <id>
88
+ pokelo workspace use <id> # save as default (written to config.json)
89
+
90
+ pokelo projects list [--workspace <id>]
87
91
  pokelo projects get <project-id-or-slug>
92
+ pokelo projects create --name "My Project" [--workspace <id>] [--description "..."]
88
93
 
89
94
  pokelo search "how does auth work" --project <ref> --limit 5
90
95
 
@@ -93,6 +98,10 @@ pokelo docs get <document-id-or-slug> --project <ref>
93
98
  pokelo docs create --title "Note" --type Note --content "# Hi" --project <ref>
94
99
  pokelo docs update <id> --content-file ./body.md --project <ref>
95
100
 
101
+ pokelo docs versions <id> --project <ref> --limit 20
102
+ pokelo docs version <id> 7 --project <ref> --raw
103
+ pokelo docs restore <id> 7 --project <ref> --expect-head 9 --include-title
104
+
96
105
  pokelo assets list --project <ref>
97
106
  pokelo assets get <asset-id> --project <ref>
98
107
  pokelo assets upload ./shot.png --project <ref> --description "screenshot"
@@ -103,7 +112,18 @@ pokelo assets delete <asset-id> --project <ref>
103
112
 
104
113
  `assets upload` uses `POST …/assets/base64` for files ≤ 8 MiB; larger files use init → PUT → complete.
105
114
 
106
- Stdout is always JSON. Errors go to stderr.
115
+ ### Version history
116
+
117
+ `docs versions` lists snapshots newest first (any project member, viewers included). `docs version <n>`
118
+ returns one snapshot with its markdown body; `--raw` prints just the body. `docs restore <n>` makes an old
119
+ snapshot the new head — content always, title only with `--include-title` — and requires write access
120
+ (owner/admin/member). Pass `--expect-head <n>` (the `versionNo` you last saw) to get a `CONFLICT` error
121
+ instead of overwriting somebody else's newer edit.
122
+
123
+ The version subcommands also accept the explicit `<project> <document>` positional form, e.g.
124
+ `pokelo docs versions khirby article-ideas-d692f34a`.
125
+
126
+ Stdout is always JSON, except `docs version --raw`. Errors go to stderr.
107
127
 
108
128
  Exit codes: `0` ok, `1` usage/config, `2` API/network.
109
129
 
@@ -111,6 +131,9 @@ Exit codes: `0` ok, `1` usage/config, `2` API/network.
111
131
 
112
132
  ```bash
113
133
  pokelo auth login --url https://pokelo.example.com --token "$TOKEN"
134
+ pokelo workspace list # discover workspaces
135
+ pokelo workspace use <workspace-id> # set default (optional)
136
+ pokelo projects list # list projects (filtered by default workspace if set)
114
137
  pokelo search "jak działa auth" --project khirby --limit 5
115
138
  pokelo docs get <document-id> --project khirby
116
139
  pokelo assets upload ./diagram.png --project khirby
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pokelo/cli",
3
- "version": "0.1.3",
4
- "description": "Official Pokelo CLI \u2014 public REST client for agents and developers",
3
+ "version": "0.2.0",
4
+ "description": "Official Pokelo CLI \u2014 public REST client for agents and developers (multi-workspace)",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "pokelo": "bin/pokelo"
package/skill/SKILL.md CHANGED
@@ -1,98 +1,306 @@
1
1
  ---
2
2
  name: pokelo-cli
3
3
  description: >-
4
- Uses the Pokelo CLI (`pokelo` binary from npm package `@pokelo/cli`) over the
5
- public REST API for auth, projects, semantic search, documents, and assets.
6
- Use when working with Pokelo knowledge bases from the shell, provisioning
7
- agents without MCP, or when the user mentions pokelo CLI, @pokelo/cli, or
8
- PAT-based Pokelo access.
9
- disable-model-invocation: false
4
+ Use the official Pokelo CLI (`pokelo` binary from npm package `@pokelo/cli`)
5
+ to access Pokelo through the public REST API using Personal Access Tokens.
6
+ Trigger when working with Pokelo knowledge bases from the shell, provisioning
7
+ agents without MCP, configuring self-hosted Pokelo instances, searching or
8
+ reading project knowledge, creating or updating Pokelo documents, managing
9
+ assets, or when the user mentions Pokelo CLI, `pokelo`, `@pokelo/cli`,
10
+ Pokelo PAT access, `POKELO_URL`, or `POKELO_TOKEN`.
10
11
  ---
11
12
 
12
13
  # Pokelo CLI
13
14
 
14
- Official shell client for Pokelo. Speaks **public REST `/v1` + Personal Access Token** — never MCP.
15
+ Use the official `pokelo` shell client for Pokelo.
15
16
 
16
- | Concept | Value |
17
- |---------|--------|
18
- | npm package | `@pokelo/cli` |
19
- | Command on PATH | `pokelo` |
20
- | Config | flags > `POKELO_*` env > `~/.config/pokelo/config.json` |
17
+ The CLI talks to the **public REST `/v1` API using a Personal Access Token**.
21
18
 
22
- ## 1. Ensure installed (idempotent)
19
+ **Never use MCP as the transport for this skill.**
23
20
 
24
- Run before the first `pokelo` call in a session:
21
+ ## Core rules
22
+
23
+ * Use the existing `pokelo` binary when available.
24
+ * If `pokelo` is not available on `PATH`, install it with `npm install -g @pokelo/cli`.
25
+ * Never reinstall the CLI when `pokelo` is already available.
26
+ * Use the public REST API only.
27
+ * Never echo, print, log, or expose access tokens.
28
+ * Prefer environment variables in CI, sandboxes, and ephemeral agent environments.
29
+ * Support self-hosted Pokelo instances by treating the instance URL as part of auth configuration.
30
+ * Prefer semantic search before listing large document trees.
31
+ * Fetch the full document only when search snippets are insufficient.
32
+ * Perform write operations only when the user explicitly asks to create, update, upload, or delete something.
33
+ * After creating or updating a Pokelo document, return its direct URL when the CLI response provides one.
34
+
35
+ ## 1. Ensure CLI availability
36
+
37
+ Before the first `pokelo` call in a session:
38
+
39
+ ```bash
40
+ command -v pokelo >/dev/null 2>&1 || npm install -g @pokelo/cli
41
+ ```
42
+
43
+ Then use:
25
44
 
26
45
  ```bash
27
- # From package install or checkout:
28
- bash skill/scripts/ensure-pokelo.sh
29
- # Or: command -v pokelo || npm install -g @pokelo/cli
46
+ pokelo ...
30
47
  ```
31
48
 
32
- Or manually:
49
+ Do not use helper install scripts.
33
50
 
34
- 1. If `command -v pokelo` succeeds → **do not install**; use it.
35
- 2. Else: `npm install -g @pokelo/cli` (exposes the `pokelo` binary).
36
- 3. In this monorepo only, if npm package is unpublished: use
37
- `node tools/pokelo-cli/bin/pokelo.mjs` or symlink it to `~/.local/bin/pokelo`.
51
+ Do not reinstall `@pokelo/cli` if `pokelo` already exists on `PATH`.
38
52
 
39
- Never reinstall when `pokelo` is already available.
53
+ If installation fails, stop and report the installation error instead of falling back to MCP.
40
54
 
41
55
  ## 2. Auth
42
56
 
43
- Prefer env in CI / ephemeral sandboxes (no disk write):
57
+ Pokelo may be self-hosted, so always treat the instance URL and token as configuration.
58
+
59
+ Configuration priority:
60
+
61
+ ```text
62
+ explicit CLI flags
63
+ > POKELO_* environment variables
64
+ > ~/.config/pokelo/config.json
65
+ > defaults
66
+ ```
67
+
68
+ ### Ephemeral / CI / sandbox
69
+
70
+ Prefer environment variables:
44
71
 
45
72
  ```bash
46
- export POKELO_URL=https://<host> # or http://localhost:3001 for local API
47
- export POKELO_TOKEN=mcp_… # Personal Access Token
48
- export POKELO_PROJECT=<slug-or-uuid> # optional default
73
+ export POKELO_URL=https://pokelo.example.com
74
+ export POKELO_TOKEN=...
75
+ export POKELO_PROJECT=<slug-or-uuid> # optional
49
76
  ```
50
77
 
51
- Or persist config (agents, headless):
78
+ ### Persistent agent configuration
52
79
 
53
80
  ```bash
54
- pokelo auth login --url "$POKELO_URL" --token-stdin <<< "$POKELO_TOKEN"
55
- # or: pokelo auth login --url … --token "$TOKEN"
56
- pokelo auth status # never prints the full token
81
+ pokelo auth login \
82
+ --url "$POKELO_URL" \
83
+ --token-stdin <<< "$POKELO_TOKEN"
57
84
  ```
58
85
 
59
- Local Vite (`:5173`) does **not** proxy `/v1` — point `POKELO_URL` at the API (`:3001`), not the frontend.
86
+ Direct token form:
87
+
88
+ ```bash
89
+ pokelo auth login \
90
+ --url https://pokelo.example.com \
91
+ --token "$TOKEN"
92
+ ```
93
+
94
+ Use `--token-stdin` when practical because process arguments may be visible to other processes.
95
+
96
+ Verify:
97
+
98
+ ```bash
99
+ pokelo auth status
100
+ ```
101
+
102
+ `auth status` must never reveal the full token.
103
+
104
+ ## 3. Resolve the workspace (when needed)
105
+
106
+ Some commands accept an optional `--workspace <id>` filter. Resolution order:
107
+
108
+ 1. `--workspace <id>` flag (or `-w <id>`)
109
+ 2. `POKELO_WORKSPACE` environment variable
110
+ 3. `defaultWorkspaceId` in `~/.config/pokelo/config.json`
111
+ 4. Auto-detect: if exactly one workspace, use it; otherwise error
60
112
 
61
- ## 3. Commands (stdout = JSON)
113
+ Set the default workspace for all future commands:
114
+
115
+ ```bash
116
+ pokelo workspace list # list available workspaces
117
+ pokelo workspace use <id> # save as defaultWorkspaceId
118
+ ```
119
+
120
+ Show or verify a specific workspace:
121
+
122
+ ```bash
123
+ pokelo workspace show <id>
124
+ ```
125
+
126
+ ## 4. Resolve the project
127
+
128
+ When the project is unknown or ambiguous:
129
+
130
+ ```bash
131
+ pokelo projects list [--workspace <id>]
132
+ ```
133
+
134
+ Then:
62
135
 
63
136
  ```bash
64
- pokelo projects list
65
137
  pokelo projects get <project-ref>
138
+ ```
139
+
140
+ Create a project (workspace admin required):
141
+
142
+ ```bash
143
+ pokelo projects create --name "My Project" [--workspace <id>] [--description "..."]
144
+ ```
66
145
 
67
- pokelo search "<query>" --project <ref> [--limit n]
146
+ Prefer an already known project slug or `POKELO_PROJECT` when available.
68
147
 
69
- pokelo docs list --project <ref>
70
- pokelo docs get <doc-ref> --project <ref>
71
- pokelo docs create --title "…" [--type] [--content|--content-file] [--parent] --project <ref>
72
- pokelo docs update <doc-ref> [--title] [--content|--content-file] --project <ref>
148
+ Do not repeatedly list projects when the correct project is already known.
73
149
 
74
- pokelo assets list --project <ref>
75
- pokelo assets get <asset-id> --project <ref>
76
- pokelo assets upload <file> [--name] [--mime] [--visibility] [--document] [--description] --project <ref>
77
- pokelo assets download <asset-id> --out <path> --project <ref>
78
- pokelo assets update <asset-id> [--name] [--description] … --project <ref>
79
- pokelo assets delete <asset-id> --project <ref>
150
+ ## 4. Read knowledge
151
+
152
+ Use semantic search first:
153
+
154
+ ```bash
155
+ pokelo search "<query>" --project <project-ref> --limit 10
80
156
  ```
81
157
 
82
- Exit codes: `0` ok, `1` usage/config, `2` API/network. Errors on stderr.
158
+ Typical workflow:
159
+
160
+ ```text
161
+ search
162
+ -> inspect matches
163
+ -> docs get when more context is needed
164
+ -> answer
165
+ ```
166
+
167
+ Fetch the full document only when needed:
168
+
169
+ ```bash
170
+ pokelo docs get <document-ref> --project <project-ref>
171
+ ```
172
+
173
+ List documents only when semantic search is not sufficient:
174
+
175
+ ```bash
176
+ pokelo docs list --project <project-ref>
177
+ ```
178
+
179
+ Avoid dumping large document trees into model context unless necessary.
180
+
181
+ ## 5. Write knowledge
182
+
183
+ Only write when explicitly requested or when the user clearly asks to persist durable project knowledge.
184
+
185
+ ### Create
186
+
187
+ ```bash
188
+ pokelo docs create \
189
+ --title "Document title" \
190
+ --project <project-ref> \
191
+ --content-file ./document.md
192
+ ```
193
+
194
+ Optional fields may include:
195
+
196
+ ```text
197
+ --type
198
+ --parent
199
+ --content
200
+ --content-file
201
+ ```
202
+
203
+ ### Update
204
+
205
+ ```bash
206
+ pokelo docs update <document-ref> \
207
+ --project <project-ref> \
208
+ --content-file ./document.md
209
+ ```
210
+
211
+ Prefer updating an existing relevant document instead of creating a duplicate.
212
+
213
+ After successful create or update, return the direct Pokelo document URL from the CLI response when available.
214
+
215
+ Do not store:
216
+
217
+ * credentials,
218
+ * API keys,
219
+ * passwords,
220
+ * access tokens,
221
+ * temporary debugging output,
222
+ * unconfirmed speculation.
223
+
224
+ ### Version history
225
+
226
+ Every write to a document creates a snapshot. Use history to inspect what changed and to undo a bad edit
227
+ instead of retyping the old body.
228
+
229
+ ```bash
230
+ pokelo docs versions <document-ref> --project <project-ref> [--page <n>] [--limit <n>]
231
+ pokelo docs version <document-ref> <version-no> --project <project-ref> [--raw]
232
+ pokelo docs restore <document-ref> <version-no> --project <project-ref> [--expect-head <n>] [--include-title]
233
+ ```
234
+
235
+ Notes:
236
+
237
+ * `versions` returns snapshots newest first with `versionNo`, `createdAt`, `createdBy`, `createdVia`
238
+ (`editor` / `api` / `mcp` / `assist` / `import` / `sync` / `conflict_resolve` / `restore` / `system`),
239
+ `contentSize` and `isCurrent` — not the body.
240
+ * `version` returns one snapshot including `contentMd`; `--raw` prints only the markdown body.
241
+ * `restore` creates a NEW head version from an old one — nothing is deleted. Content is always restored,
242
+ the title only with `--include-title`. Requires project write access (owner / admin / member).
243
+ * Read the current `versionNo` first and pass it as `--expect-head`; a `CONFLICT` error then means somebody
244
+ edited the document in the meantime — re-read the history instead of retrying blindly.
245
+ * Restore only when the user asked to undo or roll back.
246
+
247
+ ## 6. Assets
248
+
249
+ Use asset commands when the user asks to work with files stored in Pokelo.
250
+
251
+ ```bash
252
+ pokelo assets list --project <project-ref>
253
+ pokelo assets get <asset-id> --project <project-ref>
254
+ pokelo assets upload <file> --project <project-ref> [options]
255
+ pokelo assets download <asset-id> --out <path> --project <project-ref>
256
+ pokelo assets update <asset-id> --project <project-ref> [options]
257
+ pokelo assets delete <asset-id> --project <project-ref>
258
+ ```
259
+
260
+ Do not expose storage implementation details unless debugging the CLI itself.
261
+
262
+ ## 7. Output handling
263
+
264
+ Treat stdout as machine-readable JSON unless documented otherwise (`docs version --raw` prints markdown).
265
+
266
+ Rules:
267
+
268
+ * parse stdout,
269
+ * send errors to stderr,
270
+ * use exit status to distinguish success from failure,
271
+ * do not rely on decorative terminal output,
272
+ * never expose secrets in errors.
273
+
274
+ Expected exit codes:
275
+
276
+ ```text
277
+ 0 = success
278
+ 1 = usage or configuration error
279
+ 2 = API or network error
280
+ ```
83
281
 
84
- `assets upload`: ≤ 8 MiB → `POST …/base64`; larger → init → PUT → complete.
282
+ ## 8. Recommended agent workflow
85
283
 
86
- ## 4. Agent workflow
284
+ 1. Check whether `pokelo` exists.
285
+ 2. If not, install `@pokelo/cli`.
286
+ 3. Resolve auth from flags, environment, or persisted config.
287
+ 4. Resolve the project.
288
+ 5. Run semantic search.
289
+ 6. Fetch the full document if needed.
290
+ 7. Answer from retrieved knowledge.
291
+ 8. Write only when requested.
292
+ 9. Return Pokelo document URLs after successful creates or updates.
87
293
 
88
- 1. Ensure `pokelo` is installed (step 1).
89
- 2. Confirm auth (`auth status` or env vars).
90
- 3. Resolve project (`projects list` / known slug).
91
- 4. `search` → if snippets are thin, `docs get`.
92
- 5. Write only when asked (`docs create/update`, `assets upload`).
93
- 6. Do **not** echo tokens; do **not** call MCP from this skill.
294
+ ## 9. Prohibited behavior
94
295
 
95
- ## 5. Extra detail
296
+ Do not:
96
297
 
97
- - Command cheat sheet: [reference.md](reference.md)
98
- - This directory is the copy shipped inside the `@pokelo/cli` npm package (`skill/`).
298
+ * reinstall `@pokelo/cli` when `pokelo` already exists,
299
+ * use helper installer scripts,
300
+ * call the Pokelo MCP endpoint,
301
+ * construct MCP JSON-RPC requests,
302
+ * fall back from REST to MCP,
303
+ * print or expose PATs,
304
+ * list large document sets when semantic search is enough,
305
+ * create duplicate documentation when an existing document should be updated,
306
+ * perform writes without user intent.
package/src/cli.mjs CHANGED
@@ -4,6 +4,8 @@ import { runAuth } from './commands/auth.mjs';
4
4
  import { runDocs } from './commands/docs.mjs';
5
5
  import { runProjects } from './commands/projects.mjs';
6
6
  import { runSearch } from './commands/search.mjs';
7
+ import { runSearchWorkspace } from './commands/search-workspace.mjs';
8
+ import { runWorkspace } from './commands/workspace.mjs';
7
9
 
8
10
  function printHelp() {
9
11
  process.stdout.write(
@@ -15,15 +17,24 @@ Usage:
15
17
  pokelo auth login
16
18
  pokelo auth status
17
19
 
18
- pokelo projects list
20
+ pokelo workspace list
21
+ pokelo workspace show <id>
22
+ pokelo workspace use <id>
23
+
24
+ pokelo projects list [--workspace <id>]
19
25
  pokelo projects get <project-id-or-slug>
26
+ pokelo projects create --name <name> [--workspace <id>] [--description <text>]
20
27
 
21
28
  pokelo search "<query>" [--project <ref>] [--limit <n>]
29
+ pokelo search-workspace "<query>" [--per-project-limit <n>]
22
30
 
23
31
  pokelo docs list [--project <ref>]
24
32
  pokelo docs get <document-id-or-slug> [--project <ref>]
25
33
  pokelo docs create --title <t> [--type] [--content|--content-file] [--parent] [--project]
26
34
  pokelo docs update <document-id-or-slug> [options]
35
+ pokelo docs versions [<project>] <document> [--page <n>] [--limit <n>]
36
+ pokelo docs version [<project>] <document> <version-no> [--raw]
37
+ pokelo docs restore [<project>] <document> <version-no> [--expect-head <n>] [--include-title]
27
38
 
28
39
  pokelo assets list [--project] [--type] [--visibility] [--document]
29
40
  pokelo assets get <asset-id> [--project]
@@ -33,14 +44,16 @@ Usage:
33
44
  pokelo assets delete <asset-id> [--project]
34
45
 
35
46
  Global options:
36
- --project, -p <ref> Project UUID or slug (or POKELO_PROJECT / config default)
37
- --url <url> Instance URL (overrides env/config)
38
- --token <token> Personal Access Token (overrides env/config; prefer env/--token-stdin)
39
- --json Accepted for compatibility (stdout is always JSON)
40
- --help, -h Show help
47
+ --project, -p <ref> Project UUID or slug (or POKELO_PROJECT / config default)
48
+ --workspace, -w <id> Workspace UUID (or POKELO_WORKSPACE / config defaultWorkspaceId)
49
+ --url <url> Instance URL (overrides env/config)
50
+ --token <token> Personal Access Token (overrides env/config; prefer env/--token-stdin)
51
+ --json Accepted for compatibility (stdout is always JSON, except docs version --raw)
52
+ --raw docs version: print the markdown body instead of JSON
53
+ --help, -h Show help
41
54
 
42
55
  Config (priority: flags > env > file):
43
- POKELO_URL, POKELO_TOKEN, POKELO_PROJECT
56
+ POKELO_URL, POKELO_TOKEN, POKELO_PROJECT, POKELO_WORKSPACE
44
57
  ~/.config/pokelo/config.json
45
58
 
46
59
  Exit codes: 0 ok, 1 usage/config, 2 API/network error
@@ -79,6 +92,14 @@ export function parseArgs(argv) {
79
92
  flags.tree = true;
80
93
  continue;
81
94
  }
95
+ if (arg === '--raw') {
96
+ flags.raw = true;
97
+ continue;
98
+ }
99
+ if (arg === '--include-title') {
100
+ flags.includeTitle = true;
101
+ continue;
102
+ }
82
103
 
83
104
  const eq = arg.match(/^--([a-zA-Z0-9-]+)=(.*)$/);
84
105
  if (eq) {
@@ -90,6 +111,10 @@ export function parseArgs(argv) {
90
111
  flags.project = needValue(argv, ++i, arg);
91
112
  continue;
92
113
  }
114
+ if (arg === '--workspace' || arg === '-w') {
115
+ flags.workspace = needValue(argv, ++i, arg);
116
+ continue;
117
+ }
93
118
  if (arg === '--limit' || arg === '-l') {
94
119
  flags.limit = parseInt(needValue(argv, ++i, arg), 10);
95
120
  continue;
@@ -98,6 +123,17 @@ export function parseArgs(argv) {
98
123
  flags.page = parseInt(needValue(argv, ++i, arg), 10);
99
124
  continue;
100
125
  }
126
+ if (arg === '--expect-head') {
127
+ flags.expectHead = parseInt(needValue(argv, ++i, arg), 10);
128
+ continue;
129
+ }
130
+ if (arg === '--per-project-limit') {
131
+ const raw = needValue(argv, ++i, arg);
132
+ const n = Number(raw);
133
+ if (!Number.isInteger(n) || n < 1) fail(`--per-project-limit must be a positive integer, got: ${raw}`);
134
+ flags['per-project-limit'] = n;
135
+ continue;
136
+ }
101
137
  if (arg === '--url') {
102
138
  flags.url = needValue(argv, ++i, arg);
103
139
  continue;
@@ -180,8 +216,10 @@ function needValue(argv, index, flag) {
180
216
  function setFlag(flags, name, value) {
181
217
  const map = {
182
218
  project: 'project',
219
+ workspace: 'workspace',
183
220
  limit: 'limit',
184
221
  page: 'page',
222
+ 'expect-head': 'expectHead',
185
223
  url: 'url',
186
224
  token: 'token',
187
225
  title: 'title',
@@ -190,6 +228,7 @@ function setFlag(flags, name, value) {
190
228
  'content-file': 'contentFile',
191
229
  parent: 'parent',
192
230
  status: 'status',
231
+ 'per-project-limit': 'per-project-limit',
193
232
  search: 'search',
194
233
  name: 'name',
195
234
  mime: 'mime',
@@ -203,8 +242,12 @@ function setFlag(flags, name, value) {
203
242
  };
204
243
  const key = map[name];
205
244
  if (!key) fail(`unknown option: --${name}`);
206
- if (key === 'limit' || key === 'page') {
245
+ if (key === 'limit' || key === 'page' || key === 'expectHead') {
207
246
  flags[key] = parseInt(value, 10);
247
+ } else if (key === 'per-project-limit') {
248
+ const n = Number(value);
249
+ if (!Number.isInteger(n) || n < 1) fail(`--per-project-limit must be a positive integer, got: ${value}`);
250
+ flags[key] = n;
208
251
  } else {
209
252
  flags[key] = value;
210
253
  }
@@ -231,12 +274,18 @@ export async function main(argv) {
231
274
  case 'auth':
232
275
  await runAuth(rest, flags);
233
276
  break;
277
+ case 'workspace':
278
+ await runWorkspace(rest, flags);
279
+ break;
234
280
  case 'projects':
235
281
  await runProjects(rest, flags);
236
282
  break;
237
283
  case 'search':
238
284
  await runSearch(rest, flags);
239
285
  break;
286
+ case 'search-workspace':
287
+ await runSearchWorkspace(rest, flags);
288
+ break;
240
289
  case 'docs':
241
290
  await runDocs(rest, flags);
242
291
  break;
@@ -26,6 +26,33 @@ function resolveContent(flags) {
26
26
  return undefined;
27
27
  }
28
28
 
29
+ /**
30
+ * Version subcommands accept both `<document>` (project from --project/config) and the
31
+ * explicit `<project> <document>` form. `trailing` counts the args that follow the document.
32
+ *
33
+ * @param {string[]} rest - positionals after the subcommand
34
+ * @param {number} trailing
35
+ * @returns {{ projectRef: string | null, docRef: string | undefined, tail: string[] }}
36
+ */
37
+ function splitVersionRefs(rest, trailing) {
38
+ if (rest.length > 1 + trailing) {
39
+ return { projectRef: rest[0], docRef: rest[1], tail: rest.slice(2) };
40
+ }
41
+ return { projectRef: null, docRef: rest[0], tail: rest.slice(1) };
42
+ }
43
+
44
+ function requireVersionNo(value) {
45
+ const parsed = typeof value === 'string' ? parseInt(value, 10) : NaN;
46
+ if (!Number.isInteger(parsed) || parsed < 1) {
47
+ fail('<version-no> must be a positive integer (see: pokelo docs versions)');
48
+ }
49
+ return parsed;
50
+ }
51
+
52
+ function versionsPath(project, docRef) {
53
+ return `/projects/${encodeURIComponent(project)}/documents/${encodeURIComponent(docRef)}/versions`;
54
+ }
55
+
29
56
  /**
30
57
  * @param {string[]} positional
31
58
  * @param {Record<string, unknown>} flags
@@ -34,15 +61,66 @@ export async function runDocs(positional, flags) {
34
61
  const sub = positional[0];
35
62
  if (!sub) {
36
63
  fail(
37
- 'Usage: pokelo docs <list|get|create|update>\n' +
64
+ 'Usage: pokelo docs <list|get|create|update|versions|version|restore>\n' +
38
65
  ' pokelo docs list [--project <ref>]\n' +
39
66
  ' pokelo docs get <id-or-slug> [--project <ref>]\n' +
40
67
  ' pokelo docs create --title <t> [--type] [--content|--content-file] [--parent] [--project]\n' +
41
- ' pokelo docs update <id-or-slug> [--title] [--content|--content-file] [--project]',
68
+ ' pokelo docs update <id-or-slug> [--title] [--content|--content-file] [--project]\n' +
69
+ ' pokelo docs versions [<project>] <document> [--page <n>] [--limit <n>]\n' +
70
+ ' pokelo docs version [<project>] <document> <version-no> [--raw]\n' +
71
+ ' pokelo docs restore [<project>] <document> <version-no> [--expect-head <n>] [--include-title]',
42
72
  );
43
73
  }
44
74
 
45
75
  const config = resolveConfig(flags);
76
+ const rest = positional.slice(1);
77
+
78
+ if (sub === 'versions' || sub === 'version' || sub === 'restore') {
79
+ const trailing = sub === 'versions' ? 0 : 1;
80
+ const { projectRef, docRef, tail } = splitVersionRefs(rest, trailing);
81
+ if (!docRef) {
82
+ fail(
83
+ sub === 'versions'
84
+ ? 'Usage: pokelo docs versions [<project>] <document-id-or-slug>'
85
+ : `Usage: pokelo docs ${sub} [<project>] <document-id-or-slug> <version-no>`,
86
+ );
87
+ }
88
+ const project = projectRef ?? requireProject(config, flags);
89
+ const base = versionsPath(project, docRef);
90
+
91
+ if (sub === 'versions') {
92
+ const data = await apiRequest(config, 'GET', base, {
93
+ query: {
94
+ page: typeof flags.page === 'number' ? flags.page : undefined,
95
+ limit: typeof flags.limit === 'number' ? flags.limit : undefined,
96
+ },
97
+ });
98
+ writeJson(data);
99
+ return;
100
+ }
101
+
102
+ const versionNo = requireVersionNo(tail[0]);
103
+
104
+ if (sub === 'version') {
105
+ const data = await apiRequest(config, 'GET', `${base}/${versionNo}`);
106
+ if (flags.raw === true && flags.json !== true) {
107
+ process.stdout.write(`${data?.contentMd ?? ''}\n`);
108
+ return;
109
+ }
110
+ writeJson(data);
111
+ return;
112
+ }
113
+
114
+ const body = {};
115
+ if (typeof flags.expectHead === 'number' && Number.isInteger(flags.expectHead)) {
116
+ body.expectedHeadVersionNo = flags.expectHead;
117
+ }
118
+ if (flags.includeTitle === true) body.includeTitle = true;
119
+ const data = await apiRequest(config, 'POST', `${base}/${versionNo}/restore`, { body });
120
+ writeJson(data);
121
+ return;
122
+ }
123
+
46
124
  const project = requireProject(config, flags);
47
125
 
48
126
  if (sub === 'list') {
@@ -1,4 +1,4 @@
1
- import { resolveConfig } from '../config.mjs';
1
+ import { resolveConfig, resolveWorkspaceId } from '../config.mjs';
2
2
  import { fail, writeJson } from '../exit.mjs';
3
3
  import { apiRequest } from '../http.mjs';
4
4
 
@@ -11,12 +11,17 @@ export async function runProjects(positional, flags) {
11
11
  const config = resolveConfig(flags);
12
12
 
13
13
  if (sub === 'list' || !sub) {
14
+ const workspaceId =
15
+ (typeof flags.workspace === 'string' && flags.workspace.trim()) ||
16
+ config.defaultWorkspaceId ||
17
+ undefined;
14
18
  const data = await apiRequest(config, 'GET', '/projects', {
15
19
  query: {
16
20
  page: typeof flags.page === 'number' ? flags.page : undefined,
17
21
  limit: typeof flags.limit === 'number' ? flags.limit : undefined,
18
22
  q: typeof flags.search === 'string' ? flags.search : undefined,
19
23
  status: typeof flags.status === 'string' ? flags.status : undefined,
24
+ workspaceId,
20
25
  },
21
26
  });
22
27
  writeJson(data);
@@ -31,5 +36,19 @@ export async function runProjects(positional, flags) {
31
36
  return;
32
37
  }
33
38
 
34
- fail(`unknown projects command: ${sub}\nUsage: pokelo projects <list|get>`);
39
+ if (sub === 'create') {
40
+ const name = typeof flags.name === 'string' ? flags.name.trim() : '';
41
+ if (!name) fail('Usage: pokelo projects create --name <name> [--workspace <id>]');
42
+ // Order: --workspace / env / config → auto if exactly one membership → error
43
+ const workspaceId = await resolveWorkspaceId(config, apiRequest, { requireWorkspace: true });
44
+ const body = { workspaceId, name };
45
+ if (typeof flags.description === 'string' && flags.description.trim()) {
46
+ body.description = flags.description.trim();
47
+ }
48
+ const data = await apiRequest(config, 'POST', '/projects', { body });
49
+ writeJson(data);
50
+ return;
51
+ }
52
+
53
+ fail(`unknown projects command: ${sub}\nUsage: pokelo projects <list|get|create>`);
35
54
  }
@@ -0,0 +1,22 @@
1
+ import { resolveConfig } from '../config.mjs';
2
+ import { fail, writeJson } from '../exit.mjs';
3
+ import { apiRequest } from '../http.mjs';
4
+
5
+ /**
6
+ * @param {string[]} positional
7
+ * @param {Record<string, unknown>} flags
8
+ */
9
+ export async function runSearchWorkspace(positional, flags) {
10
+ const query = positional[0];
11
+ if (!query) fail('Usage: pokelo search-workspace "<query>" [--per-project-limit <n>]');
12
+ const config = resolveConfig(flags);
13
+ const perProjectLimit =
14
+ typeof flags['per-project-limit'] === 'number' ? flags['per-project-limit'] : undefined;
15
+ const data = await apiRequest(config, 'POST', '/documents/search-workspace', {
16
+ body: {
17
+ query,
18
+ ...(perProjectLimit !== undefined ? { perProjectLimit } : {}),
19
+ },
20
+ });
21
+ writeJson(data);
22
+ }
@@ -0,0 +1,53 @@
1
+ import { loadPersistedConfig, resolveConfig, saveConfig, xdgConfigPath } from '../config.mjs';
2
+ import { fail, writeJson } from '../exit.mjs';
3
+ import { apiRequest } from '../http.mjs';
4
+
5
+ /**
6
+ * @param {string[]} positional
7
+ * @param {Record<string, unknown>} flags
8
+ */
9
+ export async function runWorkspace(positional, flags) {
10
+ const sub = positional[0];
11
+
12
+ if (!sub || sub === 'list') {
13
+ const config = resolveConfig(flags);
14
+ const data = await apiRequest(config, 'GET', '/workspaces');
15
+ writeJson(data);
16
+ return;
17
+ }
18
+
19
+ if (sub === 'show') {
20
+ const id = positional[1];
21
+ if (!id) fail('Usage: pokelo workspace show <id>');
22
+ const config = resolveConfig(flags);
23
+ const data = await apiRequest(config, 'GET', `/workspaces/${encodeURIComponent(id)}`);
24
+ writeJson(data);
25
+ return;
26
+ }
27
+
28
+ if (sub === 'use') {
29
+ const id = positional[1];
30
+ if (!id) fail('Usage: pokelo workspace use <id>');
31
+ const config = resolveConfig(flags);
32
+ // Verify the workspace is accessible before saving
33
+ const data = await apiRequest(config, 'GET', `/workspaces/${encodeURIComponent(id)}`);
34
+ const existing = loadPersistedConfig();
35
+ const path = saveConfig({
36
+ url: config.url,
37
+ token: config.token,
38
+ defaultProjectId: existing?.defaultProjectId ?? null,
39
+ defaultWorkspaceId: id,
40
+ });
41
+ writeJson({
42
+ ok: true,
43
+ defaultWorkspaceId: id,
44
+ workspaceName: data?.name ?? id,
45
+ configPath: path,
46
+ });
47
+ return;
48
+ }
49
+
50
+ fail(
51
+ `unknown workspace command: ${sub}\nUsage:\n pokelo workspace list\n pokelo workspace show <id>\n pokelo workspace use <id>`,
52
+ );
53
+ }
package/src/config.mjs CHANGED
@@ -6,7 +6,7 @@ import { fail } from './exit.mjs';
6
6
 
7
7
  const LEGACY_CONFIG_PATH = join(homedir(), '.pokelo-cli.json');
8
8
 
9
- /** @typedef {{ url: string, token: string, defaultProjectId?: string | null }} PokeloConfig */
9
+ /** @typedef {{ url: string, token: string, defaultProjectId?: string | null, defaultWorkspaceId?: string | null }} PokeloConfig */
10
10
 
11
11
  export function xdgConfigPath() {
12
12
  const xdg = process.env.XDG_CONFIG_HOME?.trim();
@@ -77,13 +77,17 @@ function sanitizeFileConfig(obj) {
77
77
  typeof obj.defaultProjectId === 'string' && obj.defaultProjectId.trim()
78
78
  ? obj.defaultProjectId.trim()
79
79
  : null;
80
+ const defaultWorkspaceId =
81
+ typeof obj.defaultWorkspaceId === 'string' && obj.defaultWorkspaceId.trim()
82
+ ? obj.defaultWorkspaceId.trim()
83
+ : null;
80
84
  if (!url && !token) return null;
81
- return { url, token, defaultProjectId };
85
+ return { url, token, defaultProjectId, defaultWorkspaceId };
82
86
  }
83
87
 
84
88
  /**
85
89
  * Resolve runtime config.
86
- * @param {{ url?: string, token?: string, project?: string }} flags
90
+ * @param {{ url?: string, token?: string, project?: string, workspace?: string }} flags
87
91
  */
88
92
  export function resolveConfig(flags = {}) {
89
93
  const file = loadPersistedConfig();
@@ -102,6 +106,11 @@ export function resolveConfig(flags = {}) {
102
106
  process.env.POKELO_PROJECT?.trim() ||
103
107
  file?.defaultProjectId ||
104
108
  null;
109
+ const workspace =
110
+ (typeof flags.workspace === 'string' && flags.workspace.trim()) ||
111
+ process.env.POKELO_WORKSPACE?.trim() ||
112
+ file?.defaultWorkspaceId ||
113
+ null;
105
114
 
106
115
  if (!urlRaw) fail('missing Pokelo URL (set --url, POKELO_URL, or run: pokelo auth login)');
107
116
  if (!token) fail('missing access token (set --token, POKELO_TOKEN, or run: pokelo auth login)');
@@ -110,9 +119,37 @@ export function resolveConfig(flags = {}) {
110
119
  url: normalizeApiBaseUrl(urlRaw),
111
120
  token,
112
121
  defaultProjectId: project,
122
+ defaultWorkspaceId: workspace,
113
123
  };
114
124
  }
115
125
 
126
+ /**
127
+ * Resolve workspace context from flags + config + optional auto-detection.
128
+ * Resolution order: --workspace flag → config defaultWorkspaceId → auto (single membership) → error.
129
+ * @param {{ url: string, token: string, defaultWorkspaceId: string | null }} config
130
+ * @param {import('./http.mjs').apiRequest} apiRequest
131
+ * @param {{ workspace?: string, requireWorkspace?: boolean }} opts
132
+ * @returns {Promise<string | null>}
133
+ */
134
+ export async function resolveWorkspaceId(config, apiRequest, opts = {}) {
135
+ if (config.defaultWorkspaceId) return config.defaultWorkspaceId;
136
+ if (!opts.requireWorkspace) return null;
137
+
138
+ // Auto-detect: list workspaces and pick if exactly 1 membership
139
+ let data;
140
+ try {
141
+ data = await apiRequest(config, 'GET', '/workspaces');
142
+ } catch {
143
+ fail('workspace_id required: use --workspace, POKELO_WORKSPACE, or: pokelo workspace use <id>');
144
+ }
145
+ const items = Array.isArray(data?.items) ? data.items : [];
146
+ if (items.length === 1) return items[0].id;
147
+ if (items.length === 0) fail('no workspaces found; contact your admin');
148
+ fail(
149
+ `multiple workspaces found — specify one:\n pokelo workspace use <id>\n --workspace <id>\n\nAvailable workspaces:\n${items.map((w) => ` ${w.id} ${w.name}`).join('\n')}`,
150
+ );
151
+ }
152
+
116
153
  /**
117
154
  * Persist config to XDG path. Never logs the token.
118
155
  * @param {{ url: string, token: string, defaultProjectId?: string | null }} cfg
@@ -124,6 +161,7 @@ export function saveConfig(cfg) {
124
161
  url: normalizeApiBaseUrl(cfg.url),
125
162
  token: String(cfg.token).trim(),
126
163
  defaultProjectId: cfg.defaultProjectId?.trim() || null,
164
+ defaultWorkspaceId: cfg.defaultWorkspaceId?.trim() || null,
127
165
  };
128
166
  writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
129
167
  try {
@@ -1,59 +0,0 @@
1
- # Pokelo CLI reference
2
-
3
- ## Package vs binary
4
-
5
- ```text
6
- npm i -g @pokelo/cli → installs package @pokelo/cli
7
- → binary name: pokelo
8
- ```
9
-
10
- ## Config priority
11
-
12
- ```text
13
- --url / --token / --project
14
- > POKELO_URL / POKELO_TOKEN / POKELO_PROJECT
15
- > ~/.config/pokelo/config.json
16
- > legacy ~/.pokelo-cli.json (read for migration on login)
17
- ```
18
-
19
- URL forms `https://host` and `https://host/v1` both normalize to `…/v1`.
20
-
21
- ## Auth
22
-
23
- | Command | Notes |
24
- |---------|--------|
25
- | `pokelo auth login --url U --token T` | Persist config; never echo T |
26
- | `pokelo auth login --url U --token-stdin` | Token from stdin |
27
- | `pokelo auth login` | Interactive TTY only |
28
- | `pokelo auth status` | Masked preview `mcp_…****` |
29
-
30
- ## Projects / search / docs
31
-
32
- | Command | REST |
33
- |---------|------|
34
- | `projects list` | `GET /projects` |
35
- | `projects get <ref>` | `GET /projects/:id` |
36
- | `search "q" -p <ref> -l n` | `POST /projects/:id/documents/search` |
37
- | `docs list -p <ref>` | `GET /projects/:id/documents` |
38
- | `docs get <ref> -p <ref>` | `GET /projects/:id/documents/:id` |
39
- | `docs create --title …` | `POST /projects/:id/documents` |
40
- | `docs update <ref> …` | `PATCH /projects/:id/documents/:id` |
41
-
42
- ## Assets
43
-
44
- | Command | REST |
45
- |---------|------|
46
- | `assets list` | `GET /projects/:id/assets` |
47
- | `assets get <id>` | `GET /projects/:id/assets/:id` |
48
- | `assets upload <file>` | `POST …/base64` or uploads+complete |
49
- | `assets download <id> --out F` | get + downloadUrl / content redirect |
50
- | `assets update <id>` | `PATCH /projects/:id/assets/:id` |
51
- | `assets delete <id>` | `DELETE /projects/:id/assets/:id` |
52
-
53
- Filters for list: `--type`, `--source`, `--visibility`, `--document`, `--page`, `--limit`.
54
-
55
- ## Safety
56
-
57
- - Never print the full PAT.
58
- - Prefer `--token-stdin` or env over `--token` when possible.
59
- - Do not commit `~/.config/pokelo/config.json`.
@@ -1,47 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Ensure the `pokelo` binary is on PATH. No-op if already available.
3
- # npm package name: @pokelo/cli → CLI command: pokelo
4
- set -euo pipefail
5
-
6
- if command -v pokelo >/dev/null 2>&1; then
7
- echo "pokelo already available: $(command -v pokelo)"
8
- exit 0
9
- fi
10
-
11
- if command -v npm >/dev/null 2>&1; then
12
- echo "Installing @pokelo/cli (provides \`pokelo\` binary)…"
13
- if npm install -g @pokelo/cli; then
14
- if command -v pokelo >/dev/null 2>&1; then
15
- echo "Installed: $(command -v pokelo)"
16
- exit 0
17
- fi
18
- echo "Package installed but \`pokelo\` not on PATH; check npm global bin." >&2
19
- else
20
- echo "npm install -g @pokelo/cli failed (package may be unpublished)." >&2
21
- fi
22
- fi
23
-
24
- # Monorepo / package-local fallbacks
25
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
- CANDIDATES=(
27
- # skill shipped inside tools/pokelo-cli/skill/scripts
28
- "$SCRIPT_DIR/../../bin/pokelo.mjs"
29
- # .cursor/skills/pokelo-cli/scripts → repo tools/pokelo-cli
30
- "$SCRIPT_DIR/../../../tools/pokelo-cli/bin/pokelo.mjs"
31
- )
32
-
33
- mkdir -p "${HOME}/.local/bin"
34
- for candidate in "${CANDIDATES[@]}"; do
35
- if [[ -f "$candidate" ]]; then
36
- ln -sfn "$candidate" "${HOME}/.local/bin/pokelo"
37
- export PATH="${HOME}/.local/bin:${PATH}"
38
- if command -v pokelo >/dev/null 2>&1; then
39
- echo "Linked local CLI: $candidate → ~/.local/bin/pokelo"
40
- exit 0
41
- fi
42
- fi
43
- done
44
-
45
- echo "Error: could not locate or install pokelo." >&2
46
- echo "Fix: npm install -g @pokelo/cli OR symlink tools/pokelo-cli/bin/pokelo.mjs" >&2
47
- exit 1