@opentil/cli 1.11.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/dist/index.js +620 -0
- package/package.json +44 -0
- package/templates/claude-md-section.md +5 -0
- package/templates/cursor-rule.md +4 -0
- package/templates/hooks.json +26 -0
- package/templates/skill/SKILL.md +635 -0
- package/templates/skill/references/api.md +465 -0
- package/templates/skill/references/auto-detection.md +145 -0
- package/templates/skill/references/local-drafts.md +142 -0
- package/templates/skill/references/management.md +779 -0
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
# Management Subcommands Reference
|
|
2
|
+
|
|
3
|
+
Detailed reference for TIL entry management via `/til` subcommands.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- **Token required**: All management subcommands require a token (env var or active profile in `~/.til/credentials`), except `/til status`, `/til auth`, and profile management commands (`auth switch|list|remove|rename`) which work without a token. There is no local fallback — management operations are API-only.
|
|
8
|
+
- **No local fallback**: Unlike `/til <content>` which can save locally, management commands need live API access.
|
|
9
|
+
- **Missing token**: Proactively offer to connect (except for `status` and `auth`):
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Token required.
|
|
13
|
+
|
|
14
|
+
Connect now? (y/n)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `y` → run inline device flow (same as `/til auth`) → on success, execute the original management command
|
|
18
|
+
- `n` → show manual setup instructions:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Or set up manually:
|
|
22
|
+
1. Visit https://opentil.ai/dashboard/settings/tokens
|
|
23
|
+
2. Create a token (select read + write + delete scopes)
|
|
24
|
+
3. Add to shell profile:
|
|
25
|
+
export OPENTIL_TOKEN="til_..."
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Scope Requirements
|
|
29
|
+
|
|
30
|
+
| Subcommand | Required Scope | API Calls |
|
|
31
|
+
|------------|---------------|-----------|
|
|
32
|
+
| `list` | `read:entries` | `GET /entries` |
|
|
33
|
+
| `search` | `read:entries` | `GET /entries?q=...` |
|
|
34
|
+
| `publish` | `write:entries` | `POST /entries/:id/publish` |
|
|
35
|
+
| `unpublish` | `write:entries` | `POST /entries/:id/unpublish` |
|
|
36
|
+
| `edit` | `read:entries` + `write:entries` | `GET /entries/:id` + `PATCH /entries/:id` |
|
|
37
|
+
| `delete` | `delete:entries` | `DELETE /entries/:id` |
|
|
38
|
+
| `status` | `read:entries` (optional) | `GET /site` |
|
|
39
|
+
| `sync` | `write:entries` | `POST /entries` (per draft) |
|
|
40
|
+
| `tags` | `read:entries` | `GET /tags?sort=popular` |
|
|
41
|
+
| `categories` | `read:entries` | `GET /categories` |
|
|
42
|
+
| `batch` | `write:entries` | `POST /entries` (per topic) |
|
|
43
|
+
| `auth switch` | none | local file only (+ `GET /site` to verify) |
|
|
44
|
+
| `auth list` | none | local file only |
|
|
45
|
+
| `auth remove` | none | local file only |
|
|
46
|
+
| `auth rename` | none | local file only |
|
|
47
|
+
|
|
48
|
+
When a 403 `insufficient_scope` error is returned, map the subcommand to the needed scope:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Permission denied — your token needs the <scope> scope.
|
|
52
|
+
|
|
53
|
+
Regenerate at: https://opentil.ai/dashboard/settings/tokens
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## ID Format and Resolution
|
|
57
|
+
|
|
58
|
+
### Display Format
|
|
59
|
+
|
|
60
|
+
In list/search output, show entry IDs in short form: `...` prefix + last 8 characters.
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
...a1b2c3d4 Draft Go interfaces are satisfied implicitly
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Input Resolution
|
|
67
|
+
|
|
68
|
+
Users can provide short or full IDs. Resolve by suffix match:
|
|
69
|
+
|
|
70
|
+
1. If the input matches an entry ID exactly → use it
|
|
71
|
+
2. If the input is a suffix of exactly one entry ID from the current listing → use it
|
|
72
|
+
3. If the input matches multiple entries → ask the user to be more specific
|
|
73
|
+
4. If no match → return "Entry not found"
|
|
74
|
+
|
|
75
|
+
For `publish last` — resolve via session state (see below).
|
|
76
|
+
|
|
77
|
+
## Session State
|
|
78
|
+
|
|
79
|
+
Track `last_created_entry_id` in the current session:
|
|
80
|
+
|
|
81
|
+
- **Set** on every successful `POST /entries` (201 response) — capture the `id` from the response
|
|
82
|
+
- **Used by** `publish last` — resolves to this ID
|
|
83
|
+
- **Cleared** when session ends (not persisted across sessions)
|
|
84
|
+
|
|
85
|
+
If `publish last` is used but no entry was created in this session:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
No entry created in this session. Use /til publish <id> instead.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Subcommand Details
|
|
92
|
+
|
|
93
|
+
### `/til list [drafts|published|all]`
|
|
94
|
+
|
|
95
|
+
**Default filter**: `drafts` (most common use case — review and publish drafts).
|
|
96
|
+
|
|
97
|
+
**API call**: `GET /entries?status=<filter>&per_page=10`
|
|
98
|
+
|
|
99
|
+
- `drafts` → `status=draft`
|
|
100
|
+
- `published` → `status=published`
|
|
101
|
+
- `all` → omit `status` param
|
|
102
|
+
|
|
103
|
+
**Display format** (compact table):
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Your drafts (3):
|
|
107
|
+
|
|
108
|
+
ID Status Title
|
|
109
|
+
...a1b2c3d4 Draft Go interfaces are satisfied implicitly
|
|
110
|
+
...e5f6g7h8 Draft Ruby supports pattern matching
|
|
111
|
+
...i9j0k1l2 Draft CSS :has() enables parent selection
|
|
112
|
+
|
|
113
|
+
Page 1 of 1 · 3 entries
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Empty state**:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
No drafts found. Create one with /til <content>.
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
For published:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
No published entries found.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `/til publish [<id> | last]`
|
|
129
|
+
|
|
130
|
+
**Resolution**:
|
|
131
|
+
- `last` → use `last_created_entry_id` from session state
|
|
132
|
+
- `<id>` → resolve via ID resolution algorithm
|
|
133
|
+
|
|
134
|
+
**Flow**:
|
|
135
|
+
1. `GET /entries/:id` — fetch the entry to show what will be published
|
|
136
|
+
2. Show confirmation:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
Publish this entry?
|
|
140
|
+
|
|
141
|
+
Title: Go interfaces are satisfied implicitly
|
|
142
|
+
Tags: go, interfaces
|
|
143
|
+
|
|
144
|
+
Confirm? (y/n)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
3. On confirmation → `POST /entries/:id/publish`
|
|
148
|
+
4. Show result:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
Published
|
|
152
|
+
|
|
153
|
+
Title: Go interfaces are satisfied implicitly
|
|
154
|
+
URL: https://opentil.ai/@username/go-interfaces-are-satisfied-implicitly
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Already published**: Informational, not an error.
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
Already published.
|
|
161
|
+
|
|
162
|
+
Title: Go interfaces are satisfied implicitly
|
|
163
|
+
URL: https://opentil.ai/@username/go-interfaces-are-satisfied-implicitly
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### `/til unpublish <id>`
|
|
167
|
+
|
|
168
|
+
**Flow**:
|
|
169
|
+
1. `GET /entries/:id` — fetch the entry
|
|
170
|
+
2. Show confirmation:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
Unpublish this entry? It will become a draft.
|
|
174
|
+
|
|
175
|
+
Title: Go interfaces are satisfied implicitly
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
3. On confirmation → `POST /entries/:id/unpublish`
|
|
179
|
+
4. Show result:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
Unpublished — entry is now a draft.
|
|
183
|
+
|
|
184
|
+
Title: Go interfaces are satisfied implicitly
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Already a draft**: Informational, not an error.
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
Already a draft.
|
|
191
|
+
|
|
192
|
+
Title: Go interfaces are satisfied implicitly
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `/til edit <id> [instructions]`
|
|
196
|
+
|
|
197
|
+
**Flow**:
|
|
198
|
+
1. `GET /entries/:id` — fetch the full entry
|
|
199
|
+
2. Apply AI-assisted changes based on instructions (or ask what to change if no instructions given)
|
|
200
|
+
3. Show diff preview:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
Proposed changes to "Go interfaces are satisfied implicitly":
|
|
204
|
+
|
|
205
|
+
Title: Go interfaces are satisfied implicitly (unchanged)
|
|
206
|
+
|
|
207
|
+
Content diff:
|
|
208
|
+
- In Go, a type implements an interface by implementing its methods.
|
|
209
|
+
+ In Go, a type satisfies an interface by implementing all of its methods.
|
|
210
|
+
+ No explicit "implements" declaration is needed.
|
|
211
|
+
|
|
212
|
+
Tags: go, interfaces → go, interfaces, type-system
|
|
213
|
+
|
|
214
|
+
Apply changes?
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
4. On confirmation → `PATCH /entries/:id` with only the changed fields
|
|
218
|
+
5. Show result:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
Updated
|
|
222
|
+
|
|
223
|
+
Title: Go interfaces are satisfied implicitly
|
|
224
|
+
URL: https://opentil.ai/@username/go-interfaces-are-satisfied-implicitly
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### `/til search <keyword>`
|
|
228
|
+
|
|
229
|
+
**API call**: `GET /entries?q=<keyword>&per_page=10`
|
|
230
|
+
|
|
231
|
+
**Display format**: Same compact table as `list`.
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
Search results for "go" (2):
|
|
235
|
+
|
|
236
|
+
ID Status Title
|
|
237
|
+
...a1b2c3d4 Published Go interfaces are satisfied implicitly
|
|
238
|
+
...i9j0k1l2 Draft Go concurrency with goroutines
|
|
239
|
+
|
|
240
|
+
2 entries found
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**No results**:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
No entries matching "go" found.
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### `/til delete <id>`
|
|
250
|
+
|
|
251
|
+
**Flow**:
|
|
252
|
+
1. `GET /entries/:id` — fetch the entry
|
|
253
|
+
2. Double-confirm (this cannot be undone):
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
Delete this entry? This cannot be undone.
|
|
257
|
+
|
|
258
|
+
Title: Go interfaces are satisfied implicitly
|
|
259
|
+
Status: Draft
|
|
260
|
+
|
|
261
|
+
Type "delete" to confirm:
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
3. On confirmation → `DELETE /entries/:id`
|
|
265
|
+
4. Show result:
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
Deleted.
|
|
269
|
+
|
|
270
|
+
Title: Go interfaces are satisfied implicitly
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### `/til status`
|
|
274
|
+
|
|
275
|
+
Show site status and connection info. **Special: works without a token** (degraded display).
|
|
276
|
+
|
|
277
|
+
**With token (≥2 profiles)** -- `GET /site`:
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
OpenTIL Status
|
|
281
|
+
|
|
282
|
+
Profile: personal (active)
|
|
283
|
+
Site: @hong (opentil.ai/@hong)
|
|
284
|
+
Entries: 28 total (15 published, 13 drafts)
|
|
285
|
+
Token: til_...a3f2 ✓
|
|
286
|
+
Local: 1 draft pending sync
|
|
287
|
+
Profiles: 2 configured (/til auth list)
|
|
288
|
+
|
|
289
|
+
Manage: https://opentil.ai/dashboard
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**With token (single profile)** -- `GET /site`:
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
OpenTIL Status
|
|
296
|
+
|
|
297
|
+
Site: @hong (opentil.ai/@hong)
|
|
298
|
+
Entries: 28 total (15 published, 13 drafts)
|
|
299
|
+
Token: til_...a3f2 ✓
|
|
300
|
+
Local: 1 draft pending sync
|
|
301
|
+
|
|
302
|
+
Manage: https://opentil.ai/dashboard
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**With token (env var override)** -- `GET /site`:
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
OpenTIL Status
|
|
309
|
+
|
|
310
|
+
Site: @hong (opentil.ai/@hong)
|
|
311
|
+
Entries: 28 total (15 published, 13 drafts)
|
|
312
|
+
Token: til_...a3f2 ✓ (env override)
|
|
313
|
+
Local: 1 draft pending sync
|
|
314
|
+
|
|
315
|
+
Manage: https://opentil.ai/dashboard
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
- `Profile` line: only shown when ≥2 profiles exist. Shows `name (active)`.
|
|
319
|
+
- `Site` line: `@username` + public URL
|
|
320
|
+
- `Entries` line: `entries_count` (total), `published_entries_count` (published), difference = drafts
|
|
321
|
+
- `Token` line: last 4 chars of the resolved token + `✓`. Append `(env override)` when token comes from `$OPENTIL_TOKEN`.
|
|
322
|
+
- `Local` line: count of `*.md` files in `~/.til/drafts/`
|
|
323
|
+
- `Profiles` line: only shown when ≥2 profiles exist. Shows count + hint to `/til auth list`.
|
|
324
|
+
- `Manage` link: dashboard URL
|
|
325
|
+
|
|
326
|
+
**Without token:**
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
OpenTIL Status
|
|
330
|
+
|
|
331
|
+
Site: (not connected)
|
|
332
|
+
Token: not configured
|
|
333
|
+
Local: 3 drafts pending sync
|
|
334
|
+
|
|
335
|
+
Run /til auth to connect
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Token set but API error** (401, network failure):
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
OpenTIL Status
|
|
342
|
+
|
|
343
|
+
Site: (unable to connect)
|
|
344
|
+
Token: til_...a3f2 ✗
|
|
345
|
+
Local: 0 drafts
|
|
346
|
+
|
|
347
|
+
Check token: https://opentil.ai/dashboard/settings/tokens
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### `/til auth`
|
|
351
|
+
|
|
352
|
+
Connect an OpenTIL account via Device Flow (browser-based authorization). **Works without a token.**
|
|
353
|
+
|
|
354
|
+
**Flow:**
|
|
355
|
+
|
|
356
|
+
1. **Check existing connection**
|
|
357
|
+
- Resolve token (env var → active profile in `~/.til/credentials`)
|
|
358
|
+
- If `~/.til/credentials` exists in old plain-text format, migrate to YAML `default` profile first
|
|
359
|
+
- If token found, `GET /site` to verify:
|
|
360
|
+
- Valid: `"Already connected as @{username}. Re-authorize? (y/n)"`
|
|
361
|
+
- `y` → continue to new authorization
|
|
362
|
+
- `n` → end
|
|
363
|
+
- Invalid (401) → continue to new authorization
|
|
364
|
+
- If no token → continue to new authorization
|
|
365
|
+
|
|
366
|
+
2. **Create device code**
|
|
367
|
+
- `POST /api/v1/oauth/device/code` with `{ "scopes": ["read", "write"] }`
|
|
368
|
+
- Response: `{ device_code, user_code, verification_uri, expires_in, interval }`
|
|
369
|
+
|
|
370
|
+
3. **Open browser + display**
|
|
371
|
+
- Open `{verification_uri}?user_code={user_code}` via `open` (macOS) or `xdg-open` (Linux)
|
|
372
|
+
- Display:
|
|
373
|
+
|
|
374
|
+
```
|
|
375
|
+
Opening browser to connect...
|
|
376
|
+
|
|
377
|
+
If browser didn't open, visit:
|
|
378
|
+
https://opentil.ai/device
|
|
379
|
+
Enter code: XXXX-YYYY
|
|
380
|
+
|
|
381
|
+
Waiting for authorization...
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
4. **Poll for token**
|
|
385
|
+
- Use a bash script to poll in a single command (not multiple turns):
|
|
386
|
+
- Every `{interval}` seconds, `POST /api/v1/oauth/device/token`
|
|
387
|
+
- `authorization_pending` → continue polling
|
|
388
|
+
- `slow_down` → increase interval by 5 seconds
|
|
389
|
+
- `expired_token` → timeout
|
|
390
|
+
- 200 → extract `access_token`
|
|
391
|
+
- Hard timeout: 300 seconds (5 minutes)
|
|
392
|
+
|
|
393
|
+
5. **On success — save as named profile**
|
|
394
|
+
- Create `~/.til/` directory if it doesn't exist
|
|
395
|
+
- `GET /site` with the new token to fetch `username` (nickname)
|
|
396
|
+
- Determine profile name: use the API-returned `username` as the default profile name
|
|
397
|
+
- If a profile with the same name already exists and its token differs, append a numeric suffix (`hong-2`, `hong-3`, etc.)
|
|
398
|
+
- If re-authorizing the current active profile (same nickname), update the existing profile's token in-place
|
|
399
|
+
- Write `~/.til/credentials` in YAML format (`chmod 600`):
|
|
400
|
+
- Set `active` to the new profile name
|
|
401
|
+
- Add/update the profile under `profiles` with `token`, `nickname`, `site_url`, `host`
|
|
402
|
+
- Display:
|
|
403
|
+
|
|
404
|
+
```
|
|
405
|
+
✓ Connected as @hong
|
|
406
|
+
Profile "hong" saved to ~/.til/credentials
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
- If other profiles already exist, and this is a new profile (not re-auth), ask whether to switch:
|
|
410
|
+
- `"Switch to @hong (hong)? (y/n)"` — default yes
|
|
411
|
+
- `y` or new profile → set as active
|
|
412
|
+
- `n` → keep current active profile
|
|
413
|
+
|
|
414
|
+
- Check `~/.til/drafts/` for local drafts
|
|
415
|
+
- If drafts exist: `"Found N local drafts. Sync now? (y/n)"`
|
|
416
|
+
|
|
417
|
+
6. **On timeout**
|
|
418
|
+
|
|
419
|
+
```
|
|
420
|
+
Authorization timed out. Run /til auth to try again.
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
7. **On network error**
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
Unable to reach OpenTIL. Check your connection and try again.
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Edge cases:**
|
|
430
|
+
|
|
431
|
+
| Scenario | Handling |
|
|
432
|
+
|----------|----------|
|
|
433
|
+
| Already has valid token | Confirm before re-authorizing |
|
|
434
|
+
| Token expired/invalid | Proceed directly to new authorization, no confirmation |
|
|
435
|
+
| `~/.til/` directory doesn't exist | Create automatically |
|
|
436
|
+
| Browser didn't open | Display fallback URL + manual code entry |
|
|
437
|
+
| User cancels in browser | Polling times out, show timeout message |
|
|
438
|
+
| Token obtained + local drafts exist | Offer to sync |
|
|
439
|
+
| Old plain-text credentials file | Migrate to YAML `default` profile before proceeding |
|
|
440
|
+
| Re-auth same account | Update existing profile's token in-place |
|
|
441
|
+
| Auth new account, profiles exist | Ask whether to switch to new profile |
|
|
442
|
+
|
|
443
|
+
### `/til auth switch [name]`
|
|
444
|
+
|
|
445
|
+
Switch the active profile. **Works without a token** (operates on local `~/.til/credentials` only).
|
|
446
|
+
|
|
447
|
+
**No argument — interactive selection:**
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
Profiles:
|
|
451
|
+
1. personal @hong opentil.ai/@hong (active)
|
|
452
|
+
2. work @hong-corp opentil.ai/@hong-corp
|
|
453
|
+
|
|
454
|
+
Switch to: (1/2)
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
User picks a number → update `active` field in `~/.til/credentials` → verify token with `GET /site`:
|
|
458
|
+
- Valid: `Switched to @hong-corp (work)`
|
|
459
|
+
- Invalid (401): `Switched to @hong-corp (work) — token expired, run /til auth to reconnect`
|
|
460
|
+
|
|
461
|
+
**With argument:**
|
|
462
|
+
|
|
463
|
+
`/til auth switch work` or `/til auth switch hong-corp` → directly switch, no interactive prompt.
|
|
464
|
+
|
|
465
|
+
Name resolution order:
|
|
466
|
+
1. Exact match on profile name → use it
|
|
467
|
+
2. Exact match on nickname (with or without `@` prefix) → use that profile
|
|
468
|
+
3. No match → show error with available profiles
|
|
469
|
+
|
|
470
|
+
Examples:
|
|
471
|
+
- `/til auth switch work` → matches profile name "work"
|
|
472
|
+
- `/til auth switch hong-corp` → matches nickname "hong-corp"
|
|
473
|
+
- `/til auth switch @hong-corp` → matches nickname "hong-corp" (strips `@`)
|
|
474
|
+
|
|
475
|
+
- Match found → switch and verify (same as above)
|
|
476
|
+
- No match:
|
|
477
|
+
|
|
478
|
+
```
|
|
479
|
+
Profile "xyz" not found.
|
|
480
|
+
|
|
481
|
+
Available profiles:
|
|
482
|
+
* personal @hong opentil.ai/@hong
|
|
483
|
+
|
|
484
|
+
Use /til auth to add a new account.
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**No profiles configured:**
|
|
488
|
+
|
|
489
|
+
```
|
|
490
|
+
No profiles configured. Run /til auth to connect.
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### `/til auth list`
|
|
494
|
+
|
|
495
|
+
List all configured profiles. **Works without a token.**
|
|
496
|
+
|
|
497
|
+
```
|
|
498
|
+
Profiles:
|
|
499
|
+
* personal @hong opentil.ai/@hong
|
|
500
|
+
work @hong-corp opentil.ai/@hong-corp
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
- `*` marks the active profile
|
|
504
|
+
- Columns: profile name, `@nickname`, site URL
|
|
505
|
+
|
|
506
|
+
**No profiles:**
|
|
507
|
+
|
|
508
|
+
```
|
|
509
|
+
No profiles configured. Run /til auth to connect.
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**With env var override:**
|
|
513
|
+
|
|
514
|
+
```
|
|
515
|
+
Profiles:
|
|
516
|
+
* personal @hong opentil.ai/@hong
|
|
517
|
+
work @hong-corp opentil.ai/@hong-corp
|
|
518
|
+
|
|
519
|
+
Token override: $OPENTIL_TOKEN is set (overrides active profile)
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### `/til auth remove <name>`
|
|
523
|
+
|
|
524
|
+
Remove a profile from `~/.til/credentials`. **Works without a token.**
|
|
525
|
+
|
|
526
|
+
**Cannot remove active profile (when other profiles exist):**
|
|
527
|
+
|
|
528
|
+
```
|
|
529
|
+
Cannot remove "personal" — it is the active profile.
|
|
530
|
+
Switch to another profile first: /til auth switch <name>
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**Last remaining profile:** Can be removed even if active (special case — returns to "not connected" state).
|
|
534
|
+
|
|
535
|
+
**Confirmation:**
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
Remove profile "work" (@hong-corp)? (y/n)
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
- `y` → remove from `profiles` map, write back `~/.til/credentials`
|
|
542
|
+
- `n` → cancel
|
|
543
|
+
|
|
544
|
+
When the last profile is removed, `~/.til/credentials` is cleared (empty `profiles` map, no `active` field).
|
|
545
|
+
|
|
546
|
+
**Profile not found:**
|
|
547
|
+
|
|
548
|
+
```
|
|
549
|
+
Profile "work" not found. Use /til auth list to see profiles.
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### `/til auth rename <old> <new>`
|
|
553
|
+
|
|
554
|
+
Rename a profile. **Works without a token.**
|
|
555
|
+
|
|
556
|
+
```
|
|
557
|
+
Renamed "personal" → "home"
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
- If the renamed profile is the active profile, update the `active` field accordingly
|
|
561
|
+
- If `<new>` already exists:
|
|
562
|
+
|
|
563
|
+
```
|
|
564
|
+
Profile "home" already exists. Choose a different name.
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
- If `<old>` not found:
|
|
568
|
+
|
|
569
|
+
```
|
|
570
|
+
Profile "personal" not found. Use /til auth list to see profiles.
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### `/til sync`
|
|
574
|
+
|
|
575
|
+
Explicitly sync local drafts from `~/.til/drafts/` to OpenTIL. Requires token.
|
|
576
|
+
|
|
577
|
+
**Flow:**
|
|
578
|
+
|
|
579
|
+
1. List `*.md` files in `~/.til/drafts/`
|
|
580
|
+
2. If no files: `No local drafts to sync.`
|
|
581
|
+
3. Show what will be synced and ask for confirmation:
|
|
582
|
+
|
|
583
|
+
```
|
|
584
|
+
Found 2 local drafts:
|
|
585
|
+
|
|
586
|
+
1. go-interfaces.md
|
|
587
|
+
2. rails-solid-queue.md
|
|
588
|
+
|
|
589
|
+
Sync to OpenTIL? (y/n)
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
4. On confirmation, for each file: parse frontmatter, POST to API (with correct attribution headers), delete local file on success
|
|
593
|
+
5. Show results:
|
|
594
|
+
|
|
595
|
+
**All synced:**
|
|
596
|
+
|
|
597
|
+
```
|
|
598
|
+
Synced 2 local drafts
|
|
599
|
+
✓ go-interfaces.md
|
|
600
|
+
✓ rails-solid-queue.md
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Partial failure:**
|
|
604
|
+
```
|
|
605
|
+
Synced 1 of 2 local drafts
|
|
606
|
+
✓ go-interfaces.md
|
|
607
|
+
✗ rails-solid-queue.md (validation error)
|
|
608
|
+
Kept at: ~/.til/drafts/20260210-150415-rails-solid-queue.md
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### `/til tags`
|
|
612
|
+
|
|
613
|
+
List site tags sorted by usage count. Requires token.
|
|
614
|
+
|
|
615
|
+
**API call:** `GET /tags?sort=popular&per_page=20&with_entries=true`
|
|
616
|
+
|
|
617
|
+
**Display format:**
|
|
618
|
+
|
|
619
|
+
```
|
|
620
|
+
Your tags (12):
|
|
621
|
+
|
|
622
|
+
Tag Entries
|
|
623
|
+
go 8
|
|
624
|
+
postgresql 5
|
|
625
|
+
rails 4
|
|
626
|
+
css 3
|
|
627
|
+
linux 2
|
|
628
|
+
...
|
|
629
|
+
|
|
630
|
+
Showing top 20 · 12 total tags
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
**Empty state:**
|
|
634
|
+
```
|
|
635
|
+
No tags yet. Tags are created automatically when you publish entries.
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### `/til categories`
|
|
639
|
+
|
|
640
|
+
List site categories. Requires token.
|
|
641
|
+
|
|
642
|
+
**API call:** `GET /categories`
|
|
643
|
+
|
|
644
|
+
**Display format:**
|
|
645
|
+
|
|
646
|
+
```
|
|
647
|
+
Your categories (3):
|
|
648
|
+
|
|
649
|
+
Name Entries Description
|
|
650
|
+
Backend 12 Server-side topics
|
|
651
|
+
Frontend 8 Client-side development
|
|
652
|
+
DevOps 5 Infrastructure and deployment
|
|
653
|
+
|
|
654
|
+
3 categories
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
**Empty state:**
|
|
658
|
+
```
|
|
659
|
+
No categories yet. Create them at: https://opentil.ai/dashboard/topics
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### `/til batch <topics>`
|
|
663
|
+
|
|
664
|
+
Batch-capture multiple TIL entries in one invocation. Requires an explicit topic list (no implicit extraction — use `/til` without arguments for that).
|
|
665
|
+
|
|
666
|
+
**Input formats** -- user provides topics separated by newlines, semicolons, markdown list items (`-`), or numbered list (`1.`):
|
|
667
|
+
|
|
668
|
+
```
|
|
669
|
+
/til batch
|
|
670
|
+
- Go channels block when buffer is full
|
|
671
|
+
- CSS grid fr unit distributes remaining space
|
|
672
|
+
- PostgreSQL EXPLAIN ANALYZE shows actual vs estimated rows
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
**Flow:**
|
|
676
|
+
|
|
677
|
+
1. Parse the input into separate topics
|
|
678
|
+
2. For each topic, generate a complete TIL entry (title, body, tags, lang)
|
|
679
|
+
3. Show all drafts as a numbered list for review:
|
|
680
|
+
|
|
681
|
+
```
|
|
682
|
+
Generated 3 drafts:
|
|
683
|
+
|
|
684
|
+
1. Go channels block when buffer is full
|
|
685
|
+
Tags: go, concurrency
|
|
686
|
+
2. CSS grid fr unit distributes remaining space
|
|
687
|
+
Tags: css, grid
|
|
688
|
+
3. PostgreSQL EXPLAIN ANALYZE shows actual vs estimated rows
|
|
689
|
+
Tags: postgresql, performance
|
|
690
|
+
|
|
691
|
+
Which to send? (1/2/3/all/none)
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
4. On confirmation, POST each selected entry sequentially
|
|
695
|
+
5. Show summary:
|
|
696
|
+
|
|
697
|
+
```
|
|
698
|
+
Captured 3 TILs
|
|
699
|
+
|
|
700
|
+
✓ Go channels block when buffer is full
|
|
701
|
+
✓ CSS grid fr unit distributes remaining space
|
|
702
|
+
✓ PostgreSQL EXPLAIN ANALYZE shows actual vs estimated rows
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
**Partial failure:**
|
|
706
|
+
|
|
707
|
+
```
|
|
708
|
+
Captured 2 of 3 TILs
|
|
709
|
+
|
|
710
|
+
✓ Go channels block when buffer is full
|
|
711
|
+
✗ CSS grid fr unit distributes remaining space (validation error)
|
|
712
|
+
✓ PostgreSQL EXPLAIN ANALYZE shows actual vs estimated rows
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
Failed entries are saved locally to `~/.til/drafts/` (same as normal capture fallback).
|
|
716
|
+
|
|
717
|
+
## Error Handling
|
|
718
|
+
|
|
719
|
+
### Missing Token
|
|
720
|
+
|
|
721
|
+
See Prerequisites above — proactively offer to connect via device flow.
|
|
722
|
+
|
|
723
|
+
### 401 -- Token Invalid or Expired
|
|
724
|
+
|
|
725
|
+
Management commands have no local fallback, so the user cannot proceed without a valid token. Apply the same token-source-aware flow as SKILL.md:
|
|
726
|
+
|
|
727
|
+
**Token from `~/.til/credentials` (active profile):**
|
|
728
|
+
|
|
729
|
+
```
|
|
730
|
+
Token expired. Reconnect now? (y/n)
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
When ≥2 profiles exist, include the profile identity: `Token expired for @hong (personal). Reconnect now? (y/n)`
|
|
734
|
+
|
|
735
|
+
- `y` → run inline device flow → on success, update the active profile's token in `~/.til/credentials` and auto-retry the original management command
|
|
736
|
+
- `n` → show manual setup instructions (same as Prerequisites section)
|
|
737
|
+
|
|
738
|
+
**Token from `$OPENTIL_TOKEN` env var:**
|
|
739
|
+
|
|
740
|
+
```
|
|
741
|
+
Your $OPENTIL_TOKEN is expired or invalid. To fix:
|
|
742
|
+
• Update the variable with a new token, or
|
|
743
|
+
• unset OPENTIL_TOKEN, then run /til auth
|
|
744
|
+
|
|
745
|
+
Create a new token: https://opentil.ai/dashboard/settings/tokens
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
**Safeguards** (same as SKILL.md):
|
|
749
|
+
- If re-auth succeeds but retry still returns 401 → stop and show the error
|
|
750
|
+
- During batch operations, re-authenticate at most once, then continue remaining items
|
|
751
|
+
- If the user declines (`n`), do not prompt again this session
|
|
752
|
+
|
|
753
|
+
### Insufficient Scope (403)
|
|
754
|
+
|
|
755
|
+
```
|
|
756
|
+
Permission denied — your token needs the <scope> scope.
|
|
757
|
+
|
|
758
|
+
Regenerate at: https://opentil.ai/dashboard/settings/tokens
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### Entry Not Found (404)
|
|
762
|
+
|
|
763
|
+
```
|
|
764
|
+
Entry not found: <id>
|
|
765
|
+
|
|
766
|
+
Use /til list to see your entries.
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### Already in Target State
|
|
770
|
+
|
|
771
|
+
Not errors — show informational message (see publish/unpublish sections above).
|
|
772
|
+
|
|
773
|
+
### Network Errors
|
|
774
|
+
|
|
775
|
+
```
|
|
776
|
+
API unavailable. Try again later.
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
Management subcommands do not have a local fallback — they require API access.
|