@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,465 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
Base URL: `https://opentil.ai/api/v1`
|
|
4
|
+
|
|
5
|
+
All requests require a Bearer token:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Authorization: Bearer $OPENTIL_TOKEN
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## POST /entries -- Create Entry
|
|
12
|
+
|
|
13
|
+
### Request Body
|
|
14
|
+
|
|
15
|
+
All fields are nested under `entry`. Additionally, `tag_names` and `category_name` are accepted at the `entry` level as convenience parameters.
|
|
16
|
+
|
|
17
|
+
| Field | Type | Required | Description |
|
|
18
|
+
|-------|------|----------|-------------|
|
|
19
|
+
| `content` | string | yes | Markdown body (max 100,000 chars) |
|
|
20
|
+
| `title` | string | no | Entry title (max 200 chars). Auto-generates slug if omitted. |
|
|
21
|
+
| `slug` | string | no | Custom URL slug. Auto-generated from title if omitted. |
|
|
22
|
+
| `tag_names` | array | no | 1-3 lowercase tags, e.g. `["go", "concurrency"]` |
|
|
23
|
+
| `category_name` | string | no | Category name. Only include if the user explicitly specifies one. |
|
|
24
|
+
| `category_id` | integer | no | Category ID (alternative to `category_name`). |
|
|
25
|
+
| `published` | boolean | no | `false` for draft (default), `true` to publish immediately. |
|
|
26
|
+
| `published_at` | datetime | no | ISO 8601 timestamp. Only relevant when publishing. |
|
|
27
|
+
| `visibility` | string | no | `public` (default), `unlisted`, or `private` |
|
|
28
|
+
| `summary` | string | no | AI-generated summary for listing pages (max 500 chars) |
|
|
29
|
+
| `meta_description` | string | no | SEO meta description |
|
|
30
|
+
| `meta_image` | string | no | URL for social sharing image |
|
|
31
|
+
| `lang` | string | no | Language code (see Supported Languages below) |
|
|
32
|
+
|
|
33
|
+
### Supported Languages
|
|
34
|
+
|
|
35
|
+
`en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `es`, `fr`, `de`, `pt-BR`, `pt`, `ru`, `ar`, `bs`, `da`, `nb`, `pl`, `th`, `tr`, `it`
|
|
36
|
+
|
|
37
|
+
### 201 Response (EntrySerializer)
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"id": "1234567890",
|
|
42
|
+
"title": "Go interfaces are satisfied implicitly",
|
|
43
|
+
"slug": "go-interfaces-are-satisfied-implicitly",
|
|
44
|
+
"content": "In Go, a type implements an interface...",
|
|
45
|
+
"content_html": "<p>In Go, a type implements an interface...</p>",
|
|
46
|
+
"published": false,
|
|
47
|
+
"published_at": null,
|
|
48
|
+
"first_published_at": null,
|
|
49
|
+
"visibility": "public",
|
|
50
|
+
"hidden": false,
|
|
51
|
+
"summary": null,
|
|
52
|
+
"meta_description": null,
|
|
53
|
+
"meta_image": null,
|
|
54
|
+
"lang": "en",
|
|
55
|
+
"views_count": 0,
|
|
56
|
+
"unique_views_count": 0,
|
|
57
|
+
"category_id": null,
|
|
58
|
+
"category": null,
|
|
59
|
+
"tag_names": ["go", "interfaces"],
|
|
60
|
+
"source": "human",
|
|
61
|
+
"agent_name": "Claude Code",
|
|
62
|
+
"agent_model": "Claude Opus 4.6",
|
|
63
|
+
"url": "https://opentil.ai/@username/go-interfaces-are-satisfied-implicitly",
|
|
64
|
+
"created_at": "2026-02-10T14:30:22Z",
|
|
65
|
+
"updated_at": "2026-02-10T14:30:22Z"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Use the `url` field for the Review link in result messages.
|
|
70
|
+
|
|
71
|
+
## GET /entries -- List Entries
|
|
72
|
+
|
|
73
|
+
List entries for the authenticated user. Requires `read:entries` scope.
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
GET /entries?status=published&per_page=20&q=go
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| Param | Description |
|
|
80
|
+
|-------|-------------|
|
|
81
|
+
| `status` | `published`, `draft`, or `scheduled` |
|
|
82
|
+
| `q` | Search by title (case-insensitive partial match) |
|
|
83
|
+
| `tag` | Filter by tag slug |
|
|
84
|
+
| `category_id` | Filter by category ID |
|
|
85
|
+
| `uncategorized` | `true` to filter uncategorized entries |
|
|
86
|
+
| `per_page` | Results per page (max 100, default 20) |
|
|
87
|
+
| `page` | Page number |
|
|
88
|
+
|
|
89
|
+
### Response (EntryListSerializer)
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"data": [
|
|
94
|
+
{
|
|
95
|
+
"id": "1234567890",
|
|
96
|
+
"title": "Go interfaces are satisfied implicitly",
|
|
97
|
+
"slug": "go-interfaces-are-satisfied-implicitly",
|
|
98
|
+
"excerpt": "In Go, a type implements an interface by implementing...",
|
|
99
|
+
"published": true,
|
|
100
|
+
"published_at": "2026-02-10T14:30:22Z",
|
|
101
|
+
"first_published_at": "2026-02-10T14:30:22Z",
|
|
102
|
+
"visibility": "public",
|
|
103
|
+
"views_count": 42,
|
|
104
|
+
"unique_views_count": 35,
|
|
105
|
+
"category_id": null,
|
|
106
|
+
"category_name": null,
|
|
107
|
+
"tag_names": ["go", "interfaces"],
|
|
108
|
+
"source": "human",
|
|
109
|
+
"agent_name": "Claude Code",
|
|
110
|
+
"agent_model": "Claude Opus 4.6",
|
|
111
|
+
"created_at": "2026-02-10T14:30:22Z",
|
|
112
|
+
"updated_at": "2026-02-10T14:30:22Z"
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"meta": {
|
|
116
|
+
"current_page": 1,
|
|
117
|
+
"total_pages": 1,
|
|
118
|
+
"total_count": 1,
|
|
119
|
+
"per_page": 20
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## GET /entries/drafts
|
|
125
|
+
|
|
126
|
+
Shorthand for listing draft entries. Requires `read:entries` scope.
|
|
127
|
+
|
|
128
|
+
Returns the same response format as `GET /entries` but filtered to drafts, ordered by `updated_at` descending.
|
|
129
|
+
|
|
130
|
+
## GET /entries/:id -- Show Entry
|
|
131
|
+
|
|
132
|
+
Fetch a single entry. Requires `read:entries` scope.
|
|
133
|
+
|
|
134
|
+
Returns the full EntrySerializer response (same as 201 response above).
|
|
135
|
+
|
|
136
|
+
## PATCH /entries/:id -- Update Entry
|
|
137
|
+
|
|
138
|
+
Update an entry. Requires `write:entries` scope.
|
|
139
|
+
|
|
140
|
+
### Request Body
|
|
141
|
+
|
|
142
|
+
Same fields as POST, all optional. Only include fields that are changing.
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"entry": {
|
|
147
|
+
"title": "Updated title",
|
|
148
|
+
"content": "Updated content...",
|
|
149
|
+
"tag_names": ["go", "interfaces", "type-system"]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 200 Response
|
|
155
|
+
|
|
156
|
+
Returns the full EntrySerializer response with updated fields.
|
|
157
|
+
|
|
158
|
+
## DELETE /entries/:id -- Delete Entry
|
|
159
|
+
|
|
160
|
+
Permanently delete an entry. Requires `delete:entries` scope.
|
|
161
|
+
|
|
162
|
+
### 200 Response
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"message": "Entry deleted"
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## POST /entries/:id/publish
|
|
171
|
+
|
|
172
|
+
Publish a draft entry. Requires `write:entries` scope. No request body needed.
|
|
173
|
+
|
|
174
|
+
Returns the full EntrySerializer response with `published: true`.
|
|
175
|
+
|
|
176
|
+
## POST /entries/:id/unpublish
|
|
177
|
+
|
|
178
|
+
Unpublish a published entry (revert to draft). Requires `write:entries` scope. No request body needed.
|
|
179
|
+
|
|
180
|
+
Returns the full EntrySerializer response with `published: false`.
|
|
181
|
+
|
|
182
|
+
## Error Handling
|
|
183
|
+
|
|
184
|
+
### Error Response Format
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"error": {
|
|
189
|
+
"type": "validation_error",
|
|
190
|
+
"code": "validation_failed",
|
|
191
|
+
"message": "Validation failed",
|
|
192
|
+
"details": [{"field": "title", "message": "Title can't be blank"}]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Error Codes
|
|
198
|
+
|
|
199
|
+
| Status | Code | Action |
|
|
200
|
+
|--------|------|--------|
|
|
201
|
+
| 401 | `unauthorized` | Token invalid or expired. Save locally (for capture commands). Then follow the inline re-authentication flow defined in SKILL.md Error Handling — prompt to reconnect if token is from `~/.til/credentials`, or show env var guidance if from `$OPENTIL_TOKEN`. |
|
|
202
|
+
| 403 | `insufficient_scope` | Token lacks required scope. Show which scope is needed. |
|
|
203
|
+
| 404 | `not_found` | Entry does not exist or belongs to another user. |
|
|
204
|
+
| 422 | `validation_failed` | Parse `details` array, auto-fix, and retry once. Save locally if retry fails. |
|
|
205
|
+
| 429 | `rate_limited` | Rate limit exceeded. Save locally. Retry after `X-RateLimit-Reset`. |
|
|
206
|
+
| 5xx | -- | Server error. Save locally. |
|
|
207
|
+
|
|
208
|
+
### 422 Auto-Fix Retry Logic
|
|
209
|
+
|
|
210
|
+
When a 422 is returned, inspect the `details` array and attempt to fix:
|
|
211
|
+
|
|
212
|
+
1. `title` too long -> truncate to 200 chars
|
|
213
|
+
2. `lang` invalid -> fall back to `en`
|
|
214
|
+
3. `slug` already taken -> append `-2` (or increment)
|
|
215
|
+
4. `tag_names` invalid -> remove offending tags, keep valid ones
|
|
216
|
+
5. `content` too long -> truncate to 100,000 chars
|
|
217
|
+
|
|
218
|
+
After fixing, retry the POST **once**. If the retry also returns 422, save locally and report the error.
|
|
219
|
+
|
|
220
|
+
## GET /site -- Site Info
|
|
221
|
+
|
|
222
|
+
Fetch the authenticated user's site details. Requires `read:entries` scope.
|
|
223
|
+
|
|
224
|
+
### Response (SiteDetailSerializer)
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"username": "hong",
|
|
229
|
+
"title": "Hong's TIL",
|
|
230
|
+
"bio": "Learning something new every day",
|
|
231
|
+
"timezone": "Asia/Shanghai",
|
|
232
|
+
"locale": "en",
|
|
233
|
+
"entries_count": 28,
|
|
234
|
+
"published_entries_count": 15,
|
|
235
|
+
"categories_count": 3,
|
|
236
|
+
"custom_domain": null,
|
|
237
|
+
"domain_verified": false,
|
|
238
|
+
"discoverable": true,
|
|
239
|
+
"theme_slug": "default",
|
|
240
|
+
"theme_mode": "auto",
|
|
241
|
+
"last_posted_at": "2026-02-10T14:30:22Z",
|
|
242
|
+
"created_at": "2025-06-01T00:00:00Z",
|
|
243
|
+
"updated_at": "2026-02-10T14:30:22Z"
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
- `entries_count`: total entries (including drafts)
|
|
248
|
+
- `published_entries_count`: published entries only
|
|
249
|
+
|
|
250
|
+
Used by `/til status` to display site info.
|
|
251
|
+
|
|
252
|
+
## GET /tags -- List Tags
|
|
253
|
+
|
|
254
|
+
List tags for the authenticated user's site. Requires `read:entries` scope.
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
GET /tags?sort=popular&per_page=20
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
| Param | Description |
|
|
261
|
+
|-------|-------------|
|
|
262
|
+
| `sort` | `popular` (default, by taggings count) or `alphabetical` |
|
|
263
|
+
| `per_page` | Results per page (max 100, default 20) |
|
|
264
|
+
| `page` | Page number |
|
|
265
|
+
| `with_entries` | `true` to only return tags that have entries |
|
|
266
|
+
|
|
267
|
+
### Response (TagSerializer)
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"data": [
|
|
272
|
+
{
|
|
273
|
+
"id": "123",
|
|
274
|
+
"name": "go",
|
|
275
|
+
"slug": "go",
|
|
276
|
+
"taggings_count": 8,
|
|
277
|
+
"created_at": "2025-06-15T10:00:00Z"
|
|
278
|
+
}
|
|
279
|
+
],
|
|
280
|
+
"meta": {
|
|
281
|
+
"current_page": 1,
|
|
282
|
+
"total_pages": 1,
|
|
283
|
+
"total_count": 12,
|
|
284
|
+
"per_page": 20
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Note: `taggings_count` is the global usage count across all entries on the site.
|
|
290
|
+
|
|
291
|
+
Used by `/til tags` to display tag usage.
|
|
292
|
+
|
|
293
|
+
## GET /categories -- List Categories
|
|
294
|
+
|
|
295
|
+
List categories (topics) for the authenticated user's site. Requires `read:entries` scope.
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
GET /categories
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Response (CategorySerializer)
|
|
302
|
+
|
|
303
|
+
```json
|
|
304
|
+
{
|
|
305
|
+
"data": [
|
|
306
|
+
{
|
|
307
|
+
"id": "456",
|
|
308
|
+
"name": "Backend",
|
|
309
|
+
"slug": "backend",
|
|
310
|
+
"description": "Server-side topics",
|
|
311
|
+
"entries_count": 12,
|
|
312
|
+
"position": 0,
|
|
313
|
+
"created_at": "2025-06-15T10:00:00Z"
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Note: `entries_count` is the site-level count of entries in that category.
|
|
320
|
+
|
|
321
|
+
Used by `/til categories` to display category listing.
|
|
322
|
+
|
|
323
|
+
## Rate Limits
|
|
324
|
+
|
|
325
|
+
- Authenticated: 5,000 requests/hour
|
|
326
|
+
- Unauthenticated: 60 requests/hour
|
|
327
|
+
|
|
328
|
+
Rate limit info is returned in response headers:
|
|
329
|
+
|
|
330
|
+
| Header | Description |
|
|
331
|
+
|--------|-------------|
|
|
332
|
+
| `X-RateLimit-Limit` | Maximum requests per window |
|
|
333
|
+
| `X-RateLimit-Remaining` | Requests remaining in current window |
|
|
334
|
+
| `X-RateLimit-Reset` | Unix timestamp when the window resets |
|
|
335
|
+
|
|
336
|
+
When `429` is received, save the draft locally and inform the user. Do not retry automatically -- the user's workflow should not be blocked by rate limits.
|
|
337
|
+
|
|
338
|
+
## Device Flow (OAuth)
|
|
339
|
+
|
|
340
|
+
These endpoints do not require a Bearer token. Used by `/til auth` to obtain a token via browser authorization.
|
|
341
|
+
|
|
342
|
+
### POST /oauth/device/code
|
|
343
|
+
|
|
344
|
+
Create a device authorization code.
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
POST /api/v1/oauth/device/code
|
|
348
|
+
Content-Type: application/json
|
|
349
|
+
|
|
350
|
+
{ "scopes": ["read", "write"] }
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**200 Response:**
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
{
|
|
357
|
+
"device_code": "uuid-string",
|
|
358
|
+
"user_code": "XXXX-YYYY",
|
|
359
|
+
"verification_uri": "https://opentil.ai/device",
|
|
360
|
+
"expires_in": 900,
|
|
361
|
+
"interval": 5
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
| Field | Description |
|
|
366
|
+
|-------|-------------|
|
|
367
|
+
| `device_code` | Opaque code used to poll for the token |
|
|
368
|
+
| `user_code` | Human-readable code displayed to the user |
|
|
369
|
+
| `verification_uri` | URL where the user authorizes the device |
|
|
370
|
+
| `expires_in` | Seconds until the device code expires |
|
|
371
|
+
| `interval` | Minimum polling interval in seconds |
|
|
372
|
+
|
|
373
|
+
### POST /oauth/device/token
|
|
374
|
+
|
|
375
|
+
Poll for an access token after the user authorizes.
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
POST /api/v1/oauth/device/token
|
|
379
|
+
Content-Type: application/json
|
|
380
|
+
|
|
381
|
+
{ "device_code": "uuid-string", "grant_type": "urn:ietf:params:oauth:grant-type:device_code" }
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**200 Response (authorized):**
|
|
385
|
+
|
|
386
|
+
```json
|
|
387
|
+
{
|
|
388
|
+
"access_token": "til_xxx...",
|
|
389
|
+
"token_type": "bearer",
|
|
390
|
+
"scope": "read write"
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**400 Response (pending):**
|
|
395
|
+
|
|
396
|
+
```json
|
|
397
|
+
{
|
|
398
|
+
"error": {
|
|
399
|
+
"code": "authorization_pending",
|
|
400
|
+
"message": "The user has not yet authorized this device"
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Error codes:**
|
|
406
|
+
|
|
407
|
+
| Code | Meaning | Action |
|
|
408
|
+
|------|---------|--------|
|
|
409
|
+
| `authorization_pending` | User hasn't authorized yet | Continue polling |
|
|
410
|
+
| `slow_down` | Polling too fast | Increase interval by 5 seconds |
|
|
411
|
+
| `expired_token` | Device code expired | Stop polling, show timeout message |
|
|
412
|
+
| `invalid_grant` | Invalid device code | Stop polling, show error |
|
|
413
|
+
|
|
414
|
+
## Credential Storage
|
|
415
|
+
|
|
416
|
+
After a successful device flow, credentials are stored locally in `~/.til/credentials` as YAML.
|
|
417
|
+
|
|
418
|
+
### File Format
|
|
419
|
+
|
|
420
|
+
```yaml
|
|
421
|
+
active: personal
|
|
422
|
+
profiles:
|
|
423
|
+
personal:
|
|
424
|
+
token: til_abc...
|
|
425
|
+
nickname: hong
|
|
426
|
+
site_url: https://opentil.ai/@hong
|
|
427
|
+
host: https://opentil.ai
|
|
428
|
+
work:
|
|
429
|
+
token: til_xyz...
|
|
430
|
+
nickname: hong-corp
|
|
431
|
+
site_url: https://opentil.ai/@hong-corp
|
|
432
|
+
host: https://opentil.ai
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Field Reference
|
|
436
|
+
|
|
437
|
+
| Field | Level | Description |
|
|
438
|
+
|-------|-------|-------------|
|
|
439
|
+
| `active` | top | Name of the currently active profile |
|
|
440
|
+
| `profiles` | top | Map of profile name → profile object |
|
|
441
|
+
| `token` | profile | Bearer token (starts with `til_`) |
|
|
442
|
+
| `nickname` | profile | Username from `GET /site` response (`username` field) |
|
|
443
|
+
| `site_url` | profile | Public site URL, e.g. `https://opentil.ai/@hong` |
|
|
444
|
+
| `host` | profile | API host, e.g. `https://opentil.ai` |
|
|
445
|
+
|
|
446
|
+
### Backward Compatibility
|
|
447
|
+
|
|
448
|
+
Old format (`~/.til/credentials` containing only a plain text token):
|
|
449
|
+
|
|
450
|
+
```
|
|
451
|
+
til_abc123...
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
On first read, detect the old format (file content starts with `til_` and contains no YAML structure). Migrate automatically:
|
|
455
|
+
|
|
456
|
+
1. Read the token string
|
|
457
|
+
2. `GET /site` with the token to fetch `username`
|
|
458
|
+
- On success: use `username` as profile name, populate `nickname` and `site_url`
|
|
459
|
+
- On failure (401/network): use `default` as profile name, leave `nickname` and `site_url` empty
|
|
460
|
+
3. Write back as YAML with the single profile set as `active`
|
|
461
|
+
4. Preserve file permissions (`chmod 600`)
|
|
462
|
+
|
|
463
|
+
### File Permissions
|
|
464
|
+
|
|
465
|
+
Always set `~/.til/credentials` to `chmod 600` (owner read/write only) after any write operation. Create `~/.til/` directory with `chmod 700` if it doesn't exist.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Auto-Detection Guide
|
|
2
|
+
|
|
3
|
+
This document details how the Agent proactively detects TIL-worthy moments during work sessions.
|
|
4
|
+
|
|
5
|
+
## Trigger Examples
|
|
6
|
+
|
|
7
|
+
### Debugging uncovered a non-obvious root cause
|
|
8
|
+
|
|
9
|
+
Good example: "The memory leak was caused by a goroutine referencing a closure variable that held the entire HTTP request body, not just the header field we needed."
|
|
10
|
+
|
|
11
|
+
Bad example: "Fixed the null pointer error by adding a nil check." (Obvious fix, no insight.)
|
|
12
|
+
|
|
13
|
+
### Language/framework behavior contradicts common assumptions
|
|
14
|
+
|
|
15
|
+
Good example: "Python's `defaultdict` calls the factory function even when you're just reading a key with `d[key]` -- it doesn't distinguish reads from writes."
|
|
16
|
+
|
|
17
|
+
Bad example: "JavaScript has both `==` and `===`." (Well-known, not surprising.)
|
|
18
|
+
|
|
19
|
+
### Refactoring revealed a superior pattern
|
|
20
|
+
|
|
21
|
+
Good example: "Replacing the chain of `if-else` handlers with a strategy map reduced the function from 80 lines to 15 and made adding new handlers a one-line change."
|
|
22
|
+
|
|
23
|
+
Bad example: "Renamed variables to be more descriptive." (Cosmetic, no pattern insight.)
|
|
24
|
+
|
|
25
|
+
### Performance optimization with measurable results
|
|
26
|
+
|
|
27
|
+
Good example: "Adding a compound index on (user_id, created_at) reduced the dashboard query from 2.3s to 12ms."
|
|
28
|
+
|
|
29
|
+
Bad example: "Used caching to make things faster." (Vague, no specifics.)
|
|
30
|
+
|
|
31
|
+
### Obscure but useful tool flag or API parameter
|
|
32
|
+
|
|
33
|
+
Good example: "git diff --word-diff=color shows inline character-level changes instead of full-line diffs, perfect for reviewing prose changes."
|
|
34
|
+
|
|
35
|
+
Bad example: "Used git log to see commit history." (Basic, widely known.)
|
|
36
|
+
|
|
37
|
+
### Two technologies interacting unexpectedly
|
|
38
|
+
|
|
39
|
+
Good example: "When using PostgreSQL's `jsonb_path_query` with a Rails `where` clause, the query planner can't use the GIN index because Rails wraps the expression in a type cast."
|
|
40
|
+
|
|
41
|
+
Bad example: "Used Redis with Rails for caching." (Standard pattern, no surprise.)
|
|
42
|
+
|
|
43
|
+
### Upgrade/migration breaking changes
|
|
44
|
+
|
|
45
|
+
Good example: "Ruby 3.2 changed `Struct` keyword arguments to be required by default -- all existing `Struct.new` calls with optional keyword args silently broke."
|
|
46
|
+
|
|
47
|
+
Bad example: "Updated Node from v18 to v20." (Fact, no insight.)
|
|
48
|
+
|
|
49
|
+
## What NOT to Detect
|
|
50
|
+
|
|
51
|
+
Do not suggest TIL capture for:
|
|
52
|
+
|
|
53
|
+
- Standard usage of tools/APIs (reading docs, running commands)
|
|
54
|
+
- Configuration that works as documented
|
|
55
|
+
- Bugs caused by typos or simple mistakes
|
|
56
|
+
- Widely known best practices (use environment variables, write tests, etc.)
|
|
57
|
+
- Anything the user already seems to know well
|
|
58
|
+
- Tasks where the user is actively frustrated or stressed -- wait for resolution
|
|
59
|
+
|
|
60
|
+
## Rate Limiting State Machine
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
[IDLE] ---(TIL-worthy moment detected)---> [EVALUATING]
|
|
64
|
+
^ |
|
|
65
|
+
| (check constraints)
|
|
66
|
+
| |
|
|
67
|
+
| +-------+-------+
|
|
68
|
+
| | |
|
|
69
|
+
| (constraints (constraints
|
|
70
|
+
| not met) met)
|
|
71
|
+
| | |
|
|
72
|
+
| v v
|
|
73
|
+
+----(stay idle)----------[IDLE] [SUGGESTED]
|
|
74
|
+
|
|
|
75
|
+
+---------+---------+
|
|
76
|
+
| |
|
|
77
|
+
(user accepts) (user declines
|
|
78
|
+
| or ignores)
|
|
79
|
+
v v
|
|
80
|
+
[CAPTURED] [DONE_FOR_SESSION]
|
|
81
|
+
|
|
|
82
|
+
v
|
|
83
|
+
[DONE_FOR_SESSION]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Constraints checked in EVALUATING state:**
|
|
87
|
+
|
|
88
|
+
1. Has a suggestion already been made this session? → If yes, stay IDLE
|
|
89
|
+
2. Is the user in the middle of active problem-solving? → If yes, stay IDLE
|
|
90
|
+
3. Is this a natural pause point (resolution or task boundary)? → If no, stay IDLE
|
|
91
|
+
|
|
92
|
+
Once in DONE_FOR_SESSION, the agent never suggests again until a new session starts.
|
|
93
|
+
|
|
94
|
+
## Suggestion Format
|
|
95
|
+
|
|
96
|
+
Append the suggestion at the end of your normal response. Never interrupt the workflow with a standalone suggestion message.
|
|
97
|
+
|
|
98
|
+
**Template:**
|
|
99
|
+
```
|
|
100
|
+
💡 TIL: [concise title of the insight]
|
|
101
|
+
Tags: [tag1, tag2] · Capture? (yes/no)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Example** (debugging root cause):
|
|
105
|
+
```
|
|
106
|
+
...so the memory leak was caused by the goroutine holding a reference to the entire request body.
|
|
107
|
+
|
|
108
|
+
💡 TIL: Goroutine closures can silently retain large objects, causing memory leaks
|
|
109
|
+
Tags: go, concurrency · Capture? (yes/no)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Example** (performance optimization):
|
|
113
|
+
```
|
|
114
|
+
...the compound index on (user_id, created_at) reduced the query from 2.3s to 12ms.
|
|
115
|
+
|
|
116
|
+
💡 TIL: Compound indexes with the right column order can yield 100x+ query speedups
|
|
117
|
+
Tags: postgresql, indexing · Capture? (yes/no)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Example** (migration breaking change):
|
|
121
|
+
```
|
|
122
|
+
...Ruby 3.2 changed Struct keyword arguments to be required by default.
|
|
123
|
+
|
|
124
|
+
💡 TIL: Ruby 3.2 makes Struct keyword args required by default, silently breaking existing code
|
|
125
|
+
Tags: ruby, migration · Capture? (yes/no)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Single Confirmation Flow
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Agent: [normal response content]
|
|
132
|
+
|
|
133
|
+
💡 TIL: [concise title]
|
|
134
|
+
Tags: [tags] · Capture? (yes/no)
|
|
135
|
+
|
|
136
|
+
User: yes / y / ok / sure
|
|
137
|
+
|
|
138
|
+
Agent: [Generates full entry: title, body, tags, lang]
|
|
139
|
+
[POST to API or save locally]
|
|
140
|
+
[Show result message]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The suggestion itself is the candidate. When the user says yes, the agent generates the full entry directly — no extract flow, no draft review step.
|
|
144
|
+
|
|
145
|
+
If the user ignores the suggestion, says "no", or continues with another topic, treat it as decline. Move on and do not ask again this session.
|