@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.
@@ -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.