til
Capture and manage TIL (Today I Learned) entries on OpenTIL. Use /til <content> to capture, /til to extract insights from conversation, or /til list|publish|edit|search|delete|status|sync|tags|categories|batch to manage entries -- all without leaving the CLI.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-til
Repository
Skill path: skills/biao29/til
Capture and manage TIL (Today I Learned) entries on OpenTIL. Use /til <content> to capture, /til to extract insights from conversation, or /til list|publish|edit|search|delete|status|sync|tags|categories|batch to manage entries -- all without leaving the CLI.
Open repositoryBest for
Primary workflow: Write Technical Docs.
Technical facets: Full Stack, Tech Writer.
Target audience: everyone.
License: MIT.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install til into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding til to shared team environments
- Use til for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: til
description: >
Capture and manage TIL (Today I Learned) entries on OpenTIL.
Use /til <content> to capture, /til to extract insights from conversation,
or /til list|publish|edit|search|delete|status|sync|tags|categories|batch to
manage entries -- all without leaving the CLI.
homepage: https://opentil.ai
license: MIT
metadata:
author: opentil
version: "1.11.0"
primaryEnv: OPENTIL_TOKEN
---
# til
Capture and manage "Today I Learned" entries on OpenTIL -- from drafting to publishing, all within the CLI.
## Setup
1. Go to https://opentil.ai/dashboard/settings/tokens and create a Personal Access Token with `read:entries`, `write:entries`, and `delete:entries` scopes
2. Copy the token (starts with `til_`)
3. Set the environment variable:
```bash
export OPENTIL_TOKEN="til_xxx"
```
### Token Resolution
Token resolution order:
1. `$OPENTIL_TOKEN` environment variable (overrides all profiles)
2. `~/.til/credentials` file — active profile's token (created by `/til auth`)
If neither is set, entries are saved locally to `~/.til/drafts/`.
### Credential File Format
`~/.til/credentials` stores named profiles in YAML:
```yaml
active: personal
profiles:
personal:
token: til_abc...
nickname: hong
site_url: https://opentil.ai/@hong
host: https://opentil.ai
work:
token: til_xyz...
nickname: hong-corp
site_url: https://opentil.ai/@hong-corp
host: https://opentil.ai
```
- `active`: name of the currently active profile
- `profiles`: map of profile name → credentials
- Each profile stores: `token`, `nickname` (from API), `site_url`, `host`
**Backward compatibility**: If `~/.til/credentials` contains a plain text token (old format), silently migrate it to a `default` profile in YAML format and write back.
## Subcommand Routing
The first word after `/til` determines the action. Reserved words route to management subcommands; anything else is treated as content to capture.
| Invocation | Action |
|------------|--------|
| `/til list [drafts\|published\|all]` | List entries (default: drafts) |
| `/til publish [<id> \| last]` | Publish an entry |
| `/til unpublish <id>` | Unpublish (revert to draft) |
| `/til edit <id> [instructions]` | AI-assisted edit |
| `/til search <keyword>` | Search entries by title |
| `/til delete <id>` | Delete entry (with confirmation) |
| `/til status` | Show site status and connection info |
| `/til sync` | Sync local drafts to OpenTIL |
| `/til tags` | List site tags with usage counts |
| `/til categories` | List site categories |
| `/til batch <topics>` | Batch-capture multiple TIL entries |
| `/til auth` | Connect OpenTIL account (browser auth) |
| `/til auth switch [name]` | Switch active profile (by profile name or @nickname) |
| `/til auth list` | List all profiles |
| `/til auth remove <name>` | Remove a profile |
| `/til auth rename <old> <new>` | Rename a profile |
| `/til <anything else>` | Capture content as a new TIL |
| `/til` | Extract insights from conversation (multi-candidate) |
Reserved words: `list`, `publish`, `unpublish`, `edit`, `search`, `delete`, `status`, `sync`, `tags`, `categories`, `batch`, `auth`.
## Reference Loading
⚠️ DO NOT read reference files unless specified below. SKILL.md contains enough inline context for most operations.
### On subcommand dispatch (load before execution):
| Subcommand | References to load |
|------------|--------------------|
| `/til <content>` | none |
| `/til` (extract from conversation) | none |
| `/til list\|status\|tags\|categories` | [references/management.md](references/management.md) |
| `/til publish\|unpublish\|edit\|search\|delete\|batch` | [references/management.md](references/management.md) |
| `/til sync` | [references/management.md](references/management.md), [references/local-drafts.md](references/local-drafts.md) |
| `/til auth` | [references/management.md](references/management.md), [references/api.md](references/api.md) |
| `/til auth switch\|list\|remove\|rename` | [references/management.md](references/management.md) |
### On-demand (load only when the situation arises):
| Trigger | Reference to load |
|---------|-------------------|
| API returns non-2xx after inline error handling is insufficient | [references/api.md](references/api.md) |
| Auto-detection context (proactive TIL suggestion) | [references/auto-detection.md](references/auto-detection.md) |
| No token found (first-run local fallback) | [references/local-drafts.md](references/local-drafts.md) |
## API Quick Reference
**Create and publish an entry:**
```bash
curl -X POST "https://opentil.ai/api/v1/entries" \
-H "Authorization: Bearer $OPENTIL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"entry": {
"title": "Go interfaces are satisfied implicitly",
"content": "In Go, a type implements an interface...",
"summary": "Go types implement interfaces implicitly by implementing their methods, with no explicit declaration needed.",
"tag_names": ["go", "interfaces"],
"published": true,
"lang": "en"
}
}'
```
**Key create parameters:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `content` | string | yes | Markdown body (max 100,000 chars) |
| `title` | string | no | Entry title (max 200 chars). Auto-generates slug. |
| `tag_names` | array | no | 1-3 lowercase tags, e.g. `["go", "concurrency"]` |
| `published` | boolean | no | `false` for draft (default), `true` to publish immediately |
| `lang` | string | no | Language code: `en`, `zh-CN`, `zh-TW`, `ja`, `ko`, etc. |
| `slug` | string | no | Custom URL slug. Auto-generated from title if omitted. |
| `visibility` | string | no | `public` (default), `unlisted`, or `private` |
| `summary` | string | no | AI-generated summary for listing pages (max 500 chars) |
**Management endpoints:**
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/entries?status=draft&q=keyword` | GET | List/search entries |
| `/entries/:id` | GET | Get a single entry |
| `/entries/:id` | PATCH | Update entry fields |
| `/entries/:id` | DELETE | Permanently delete entry |
| `/entries/:id/publish` | POST | Publish a draft |
| `/entries/:id/unpublish` | POST | Revert to draft |
| `/site` | GET | Site info (username, entry counts, etc.) |
| `/tags?sort=popular` | GET | List tags with usage counts |
| `/categories` | GET | List categories with entry counts |
> Full parameter list, response format, and error handling: see references/api.md
## Execution Flow
Every `/til` invocation follows this flow:
1. **Generate** -- craft the TIL entry (title, body, summary, tags, lang)
2. **Check token** -- resolve token (env var → active profile in `~/.til/credentials`)
- If `~/.til/credentials` exists in old plain-text format, migrate to YAML `default` profile first
- **Found** -> POST to API with `published: true` -> show published URL
- **Not found** -> save to `~/.til/drafts/` -> show first-run guide with connect prompt
- **401 response** -> save locally -> inline re-authentication (see Error Handling):
- Token from `~/.til/credentials` (active profile) or no prior token: prompt to reconnect via device flow → on success, update the active profile's token and auto-retry the original operation
- Token from `$OPENTIL_TOKEN` env var: cannot auto-fix — guide user to update/unset the variable
3. **Show identity** -- when ≥2 profiles are configured, include `Account: @nickname (profile_name)` in result messages so the user always knows which account was used
4. **Never lose content** -- the entry is always persisted somewhere
5. **On API failure** -> save locally as draft (fallback unchanged)
## `/til <content>` -- Explicit Capture
The user's input is **raw material** -- a seed, not the final entry. Generate a complete TIL from it:
- Short input (a sentence or phrase) -> expand into a full entry with context and examples
- Long input (a paragraph or more) -> refine and structure, but preserve the user's intent
**Steps:**
1. Treat the user's input as a seed -- craft a complete title + body from it
2. Generate a concise title (5-15 words) in the same language as the content
3. Write a self-contained Markdown body (see Content Guidelines below)
4. Generate a summary (see Summary Guidelines below)
5. Infer 1-3 lowercase tags from technical domain (e.g. `rails`, `postgresql`, `go`)
6. Detect language -> set `lang` (`en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `es`, `fr`, `de`, `pt-BR`, `pt`, `ru`, `ar`, `bs`, `da`, `nb`, `pl`, `th`, `tr`, `it`)
7. Follow Execution Flow above (check token -> POST or save locally)
No confirmation needed -- the user explicitly asked to capture. Execute directly.
## `/til` -- Extract from Conversation
When `/til` is used without arguments, analyze the current conversation for learnable insights.
**Steps:**
1. Scan the conversation for knowledge worth preserving -- surprising facts, useful techniques, debugging breakthroughs, "aha" moments
2. Identify **all** TIL-worthy insights (not just one), up to 5
3. Branch based on count:
**0 insights:**
```
No clear TIL insights found in this conversation.
```
**1 insight:** Generate the full draft (title, body, tags), show it, ask for confirmation. On confirmation -> follow Execution Flow.
**2+ insights:** Show a numbered list (max 5), let the user choose:
```
Found 3 TIL-worthy insights:
1. Go interfaces are satisfied implicitly
2. PostgreSQL JSONB arrays don't support GIN @>
3. CSS :has() enables parent selection
Which to capture? (1/2/3/all/none)
```
- Single number -> generate draft for that insight, show confirmation, proceed
- Comma-separated list (e.g. `1,3`) -> generate drafts for selected, show all for confirmation, POST sequentially
- `all` -> generate drafts for each, show all for confirmation, POST sequentially
- `none` -> cancel
4. For each selected insight, generate a standalone TIL entry following Content Guidelines
5. **Show the generated entry to the user and ask for confirmation before proceeding**
6. On confirmation -> follow Execution Flow above (check token -> POST or save locally)
## Auto-Detection
When working alongside a user, proactively detect moments worth capturing as TIL entries.
### When to Suggest
Suggest when the conversation produces a genuine "aha" moment — something surprising, non-obvious, or worth remembering. Examples:
- Debugging uncovered a non-obvious root cause
- A language/framework behavior contradicted common assumptions
- Refactoring revealed a clearly superior pattern
- Performance optimization yielded measurable improvement
- An obscure but useful tool flag or API parameter was discovered
- Two technologies interacting produced unexpected behavior
Do NOT suggest for: standard tool usage, documented behavior, typo-caused bugs, or widely known best practices.
### Rate Limiting
1. **Once per session** — after suggesting once (accepted or declined), never suggest again
2. **Natural pauses only** — suggest at resolution points or task boundaries, never mid-problem-solving
3. **Respect rejection** — if declined, move on without persuasion
### Suggestion Format
Append at the end of your normal response. Never interrupt workflow.
**Template:**
```
💡 TIL: [concise title of the insight]
Tags: [tag1, tag2] · Capture? (yes/no)
```
**Example** (at the end of a debugging response):
```
...so the fix is to close the channel before the goroutine exits.
💡 TIL: Unclosed Go channels in goroutines cause silent memory leaks
Tags: go, concurrency · Capture? (yes/no)
```
### Capture Flow
Auto-detected TILs bypass the extract flow. The suggestion itself is the candidate.
1. User replies `yes` / `y` / `ok` / `sure` → agent generates full entry (title, body, tags, lang) from the suggested insight → follows Execution Flow (POST or save locally)
2. User replies `no` / ignores / continues other topic → move on, do not ask again
Non-affirmative responses (continuing the conversation about something else) are treated as implicit decline.
> Detailed trigger examples, state machine, and anti-patterns: see references/auto-detection.md
## Management Subcommands
Management subcommands require a token. There is no local fallback -- management operations need the API.
### `/til list [drafts|published|all]`
List entries. Default filter: `drafts`.
- API: `GET /entries?status=<filter>&per_page=10`
- Display as a compact table with short IDs (last 8 chars, prefixed with `...`)
- Show pagination info at the bottom
### `/til publish [<id> | last]`
Publish a draft entry.
- `last` resolves to the most recently created entry in this session (tracked via `last_created_entry_id` set on every successful POST)
- Fetch the entry first, show title/tags, ask for confirmation
- On success, display the published URL
- If already published, show informational message (not an error)
### `/til unpublish <id>`
Revert a published entry to draft.
- Fetch the entry first, confirm before unpublishing
- If already a draft, show informational message
### `/til edit <id> [instructions]`
AI-assisted editing of an existing entry.
- Fetch the full entry via `GET /entries/:id`
- Apply changes based on instructions (or ask what to change if none given)
- Show a diff preview of proposed changes
- On confirmation, `PATCH /entries/:id` with only the changed fields
### `/til search <keyword>`
Search entries by title.
- API: `GET /entries?q=<keyword>&per_page=10`
- Same compact table format as `list`
### `/til delete <id>`
Permanently delete an entry.
- Fetch the entry, show title and status
- Double-confirm: "This cannot be undone. Type 'delete' to confirm."
- On confirmation, `DELETE /entries/:id`
### `/til status`
Show site status and connection info. **Works without a token** (degraded display).
- With token: `GET /site` -> show username, entry breakdown (total/published/drafts), token status, local draft count, dashboard link
- Without token: show "not connected", local draft count, setup link
### `/til sync`
Explicitly sync local drafts from `~/.til/drafts/` to OpenTIL. Requires token.
- List pending drafts, POST each one, delete local file on success
- Show summary with success/failure per draft
### `/til tags`
List site tags sorted by usage count (top 20). Requires token.
- API: `GET /tags?sort=popular&per_page=20&with_entries=true`
- Show as compact table with tag name and entry count
### `/til categories`
List site categories. Requires token.
- API: `GET /categories`
- Show as compact table with name, entry count, and description
### `/til batch <topics>`
Batch-capture multiple TIL entries in one invocation. Requires explicit topic list.
- User lists topics separated by newlines, semicolons, or markdown list items (`-` / `1.`)
- Generate a draft for each -> show all drafts for confirmation -> POST sequentially
- On partial failure, show per-entry success/failure (same format as `/til sync`)
### ID Resolution
- In listings, show IDs in short form: `...` + last 8 characters
- Accept both short and full IDs as input
- Resolve short IDs by suffix match against the current listing
- If ambiguous (multiple matches), ask for clarification
### Session State
Track the following session state (not persisted across sessions):
- `last_created_entry_id` -- set on every successful `POST /entries` (201). Used by `/til publish last`.
- `active_profile` -- the profile name resolved at first token access. Reflects the `active` field from `~/.til/credentials` (or `$OPENTIL_TOKEN` override). Used for identity display and draft attribution.
> Detailed subcommand flows, display formats, and error handling: see references/management.md
## Agent Identity
Three layers of attribution signal distinguish human-initiated from agent-initiated TILs.
### Layer 1: HTTP Headers
Include these headers on every API call:
```
X-OpenTIL-Source: human | agent
X-OpenTIL-Agent: <your agent display name>
X-OpenTIL-Model: <human-readable model name>
```
- Source: `/til <content>` and `/til` -> `human`; Auto-detected -> `agent`
- Agent: use your tool's display name (e.g. `Claude Code`, `Cursor`, `GitHub Copilot`). Do not use a slug.
- Model: use a human-readable model name (e.g. `Claude Opus 4.6`, `GPT-4o`, `Gemini 2.5 Pro`). Do not use a model ID.
- Agent and Model are optional -- omit them if you are unsure.
### Layer 2: Tag Convention
- Auto-detected TILs: automatically add `agent-assisted` to the tag list
- `/til <content>` and `/til`: do **not** add the tag (unless the Agent substantially rewrote the content)
### Layer 3: Attribution Rendering (Backend)
Agent-initiated TILs are visually marked on OpenTIL automatically based on the
`source` field. No content modification needed -- the backend renders attribution
in the display layer.
- Public page: shows `✨ via {agent_name}`, or `✨ AI` when agent_name is absent
- Tooltip (hover): shows `{agent_name} · {model}` when both are present
- Dashboard: shows ✨ badge + agent_name, or "Agent" when agent_name is absent
Do NOT append any footer or attribution text to the content body.
### Summary
| Dimension | `/til <content>` | `/til` | Auto-detected |
|-----------|-----------------|--------|---------------|
| Trigger | User explicit | User command | Agent proactive |
| Confirmations | 0 (direct publish) | 1 (review before publish) | 1 (suggest → capture) |
| Source header | `human` | `human` | `agent` |
| Agent header | Yes | Yes | Yes |
| Model header | Yes | Yes | Yes |
| `agent-assisted` tag | No | No | Yes |
| Attribution | Automatic (backend) | Automatic (backend) | Automatic (backend) |
## Content Guidelines
Every TIL entry must follow these rules:
- **Self-contained**: The reader must understand the entry without any conversation context. Never write "as we discussed", "the above error", "this project's config", etc.
- **Desensitized**: Remove project names, company details, colleague names, internal URLs, and proprietary business logic. Generalize specifics: "our User model" -> "a model", "the production server" -> "a production environment", "the Acme payment service" -> "a payment gateway".
- **Universally valuable**: Write to StackOverflow-answer standards. A stranger searching for this topic should find the entry immediately useful. Content only useful to the author belongs in private notes, not TIL.
- **Factual tone**: State facts, show examples, explain why. Avoid first-person narrative ("I was debugging...", "I discovered..."). Exception: brief situational context is fine ("When upgrading Rails from 7.2 to 8.0...").
- **One insight per entry**: Each TIL teaches exactly ONE thing. If there are multiple insights, create separate entries.
- **Concrete examples**: Include code snippets, commands, or specific data whenever relevant. Avoid vague descriptions.
- **Title**: 5-15 words. Descriptive, same language as content. No "TIL:" prefix.
- **Content**: Use the most efficient format for the knowledge — tables for comparisons, code blocks for examples, lists for enumerations, math (`$inline$` / `$$display$$`) for formulas with fractions/subscripts/superscripts/greek letters, Mermaid diagrams (` ```mermaid `) for flows/states/sequences that text cannot clearly express. Simple expressions like `O(n)` stay as inline code; use math only when notation complexity warrants it. Only use prose when explaining causation or context. Never pad content; if one sentence suffices, don't write a paragraph.
- **Tags**: 1-3 lowercase tags from the technical domain (`go`, `rails`, `postgresql`, `css`, `linux`). No generic tags like `programming` or `til`.
- **Lang**: Detect from content. Chinese -> `zh-CN`, Traditional Chinese -> `zh-TW`, English -> `en`, Japanese -> `ja`, Korean -> `ko`.
- **Category**: Do not auto-infer `category_name` -- only include it if the user explicitly specifies a category/topic.
- **Summary**: 1-2 sentences, plain text (no markdown). Max 500 chars and must be shorter than the content body. Same language as content. Self-contained: the reader should understand the core takeaway from the summary alone. Be specific about what the reader will learn, not meta ("this article discusses..."). No first person, no meta-descriptions. Omit if the content is already very short (under ~200 chars) -- the excerpt fallback is sufficient.
## Result Messages
### API Success (token configured, 201)
```
Published to OpenTIL
Title: Go interfaces are satisfied implicitly
Tags: go, interfaces
URL: https://opentil.ai/@username/go-interfaces-are-satisfied-implicitly
```
When ≥2 profiles are configured, add an `Account` line:
```
Published to OpenTIL
Account: @hong (personal)
Title: Go interfaces are satisfied implicitly
Tags: go, interfaces
URL: https://opentil.ai/@hong/go-interfaces-are-satisfied-implicitly
```
Single-profile users see no `Account` line — keep the output clean.
Extract the `url` field from the API response for the URL.
### Sync Local Drafts
After the first successful API call, check `~/.til/drafts/` for pending files. If any exist, offer to sync:
```
Draft saved to OpenTIL
Title: Go interfaces are satisfied implicitly
Tags: go, interfaces
Review: https://opentil.ai/@username/go-interfaces-are-satisfied-implicitly
Found 3 local drafts from before. Sync them to OpenTIL?
```
On confirmation, POST each draft to the API. Delete the local file after each successful sync. Keep files that fail. Show summary:
```
Synced 3 local drafts to OpenTIL
+ Go defer runs in LIFO order
+ PostgreSQL JSONB indexes support GIN operators
+ CSS :has() selector enables parent selection
```
If the user declines, keep the local files and do not ask again in this session.
### First Run (no token)
Save the draft locally, then proactively offer to connect. This is NOT an error -- the user successfully captured a TIL.
```
TIL captured
Title: Go interfaces are satisfied implicitly
Tags: go, interfaces
File: ~/.til/drafts/20260210-143022-go-interfaces.md
Connect to OpenTIL to publish entries online.
Connect now? (y/n)
```
- `y` → run inline device flow (same as `/til auth`) → on success, sync the just-saved draft + any other pending drafts in `~/.til/drafts/`
- `n` → show manual setup instructions (see Manual Setup Instructions below)
Only show the connect prompt on the **first** local save in this session. On subsequent saves, use the short form (no prompt):
```
TIL captured
Title: Go interfaces are satisfied implicitly
Tags: go, interfaces
File: ~/.til/drafts/20260210-143022-go-interfaces.md
```
## Error Handling
**On ANY API failure, always save the draft locally first.** Never let user content be lost.
**422 -- Validation error:** Analyze the error response, fix the issue (e.g. truncate title to 200 chars, correct lang code), and retry. Only save locally if the retry also fails.
**401 -- Token invalid or expired (token from `~/.til/credentials` active profile):**
```
TIL captured (saved locally)
File: ~/.til/drafts/20260210-143022-go-interfaces.md
Token expired for @hong (personal). Reconnect now? (y/n)
```
- `y` → run inline device flow (same as `/til auth`) → on success, update the active profile's token in `~/.til/credentials` and auto-retry the original POST (publish the just-saved draft, then delete the local file)
- `n` → show manual setup instructions (see Manual Setup Instructions below)
When only one profile exists, omit the `@nickname (profile)` from the message.
**401 -- Token invalid or expired (token from `$OPENTIL_TOKEN` env var):**
The env var takes priority over `~/.til/credentials`, so saving a new token via device flow would not help — the env var would still be used. Guide the user instead:
```
TIL captured (saved locally)
File: ~/.til/drafts/20260210-143022-go-interfaces.md
Your $OPENTIL_TOKEN is expired or invalid. To fix:
• Update the variable with a new token, or
• unset OPENTIL_TOKEN, then run /til auth
Create a new token: https://opentil.ai/dashboard/settings/tokens
```
**Network failure or 5xx:**
```
TIL captured (saved locally -- API unavailable)
File: ~/.til/drafts/20260210-143022-go-interfaces.md
```
> Full error codes, 422 auto-fix logic, and rate limit details: see references/api.md
### Re-authentication Safeguards
| Rule | Behavior |
|------|----------|
| No retry loops | If re-auth succeeds but the retry still returns 401 → stop and show the error. Do not re-authenticate again. |
| Batch-aware | During batch/sync operations, re-authenticate at most once. On success, continue processing remaining items with the new token. |
| Respect refusal | If the user declines re-authentication (`n`), do not prompt again for the rest of this session. Use the short local-save format silently. |
| Env var awareness | When the active token comes from `$OPENTIL_TOKEN`, never attempt device flow — it cannot override the env var. Always show the env var guidance instead. |
| Profile-aware re-auth | On successful re-authentication, update the corresponding profile's token in `~/.til/credentials`. Do not create a new profile. |
### Manual Setup Instructions
When the user declines inline authentication (answers `n`), show:
```
Or set up manually:
1. Visit https://opentil.ai/dashboard/settings/tokens
2. Create a token (select read + write + delete scopes)
3. Add to shell profile:
export OPENTIL_TOKEN="til_..."
```
## Local Draft Fallback
When the API is unavailable or no token is configured, drafts are saved locally to `~/.til/drafts/`.
**File format:** `YYYYMMDD-HHMMSS-<slug>.md`
```markdown
---
title: "Go interfaces are satisfied implicitly"
tags: [go, interfaces]
lang: en
summary: "Go types implement interfaces implicitly by implementing their methods, with no explicit declaration needed."
profile: personal
---
In Go, a type implements an interface...
```
The `profile` field records the active profile name at save time, ensuring sync uses the correct account's token. Omitted when no profiles are configured (backward-compatible).
> Full directory structure, metadata fields, and sync protocol: see references/local-drafts.md
## Notes
- **UI language adaptation**: All prompts, result messages, and error messages in this document are written in English as canonical examples. At runtime, adapt them to match the user's language in the current session (e.g. if the user writes in Chinese, display messages in Chinese). Entry content language (`lang` field) is independent -- it is always detected from the content itself.
- Entries are published immediately by default (`published: true`) -- use `/til unpublish <id>` to revert to draft
- The API auto-generates a URL slug from the title
- Tags are created automatically if they don't exist on the site
- Content is rendered to HTML server-side (GFM Markdown with syntax highlighting, KaTeX math, and Mermaid diagrams)
- Management subcommands (`list`, `publish`, `edit`, `search`, `delete`, `tags`, `categories`, `sync`, `batch`) require a token -- no local fallback. Exception: `status` and `auth` (including `auth switch`, `auth list`, `auth remove`, `auth rename`) work without a token.
- Scope errors map to specific scopes: `list`/`search`/`tags`/`categories` need `read:entries`, `publish`/`unpublish`/`edit`/`sync`/`batch` need `write:entries`, `delete` needs `delete:entries`. `status` uses `read:entries` when available but works without a token.
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/management.md
```markdown
# Management Subcommands Reference
Detailed reference for TIL entry management via `/til` subcommands.
## Prerequisites
- **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.
- **No local fallback**: Unlike `/til <content>` which can save locally, management commands need live API access.
- **Missing token**: Proactively offer to connect (except for `status` and `auth`):
```
Token required.
Connect now? (y/n)
```
- `y` → run inline device flow (same as `/til auth`) → on success, execute the original management command
- `n` → show manual setup instructions:
```
Or set up manually:
1. Visit https://opentil.ai/dashboard/settings/tokens
2. Create a token (select read + write + delete scopes)
3. Add to shell profile:
export OPENTIL_TOKEN="til_..."
```
## Scope Requirements
| Subcommand | Required Scope | API Calls |
|------------|---------------|-----------|
| `list` | `read:entries` | `GET /entries` |
| `search` | `read:entries` | `GET /entries?q=...` |
| `publish` | `write:entries` | `POST /entries/:id/publish` |
| `unpublish` | `write:entries` | `POST /entries/:id/unpublish` |
| `edit` | `read:entries` + `write:entries` | `GET /entries/:id` + `PATCH /entries/:id` |
| `delete` | `delete:entries` | `DELETE /entries/:id` |
| `status` | `read:entries` (optional) | `GET /site` |
| `sync` | `write:entries` | `POST /entries` (per draft) |
| `tags` | `read:entries` | `GET /tags?sort=popular` |
| `categories` | `read:entries` | `GET /categories` |
| `batch` | `write:entries` | `POST /entries` (per topic) |
| `auth switch` | none | local file only (+ `GET /site` to verify) |
| `auth list` | none | local file only |
| `auth remove` | none | local file only |
| `auth rename` | none | local file only |
When a 403 `insufficient_scope` error is returned, map the subcommand to the needed scope:
```
Permission denied — your token needs the <scope> scope.
Regenerate at: https://opentil.ai/dashboard/settings/tokens
```
## ID Format and Resolution
### Display Format
In list/search output, show entry IDs in short form: `...` prefix + last 8 characters.
```
...a1b2c3d4 Draft Go interfaces are satisfied implicitly
```
### Input Resolution
Users can provide short or full IDs. Resolve by suffix match:
1. If the input matches an entry ID exactly → use it
2. If the input is a suffix of exactly one entry ID from the current listing → use it
3. If the input matches multiple entries → ask the user to be more specific
4. If no match → return "Entry not found"
For `publish last` — resolve via session state (see below).
## Session State
Track `last_created_entry_id` in the current session:
- **Set** on every successful `POST /entries` (201 response) — capture the `id` from the response
- **Used by** `publish last` — resolves to this ID
- **Cleared** when session ends (not persisted across sessions)
If `publish last` is used but no entry was created in this session:
```
No entry created in this session. Use /til publish <id> instead.
```
## Subcommand Details
### `/til list [drafts|published|all]`
**Default filter**: `drafts` (most common use case — review and publish drafts).
**API call**: `GET /entries?status=<filter>&per_page=10`
- `drafts` → `status=draft`
- `published` → `status=published`
- `all` → omit `status` param
**Display format** (compact table):
```
Your drafts (3):
ID Status Title
...a1b2c3d4 Draft Go interfaces are satisfied implicitly
...e5f6g7h8 Draft Ruby supports pattern matching
...i9j0k1l2 Draft CSS :has() enables parent selection
Page 1 of 1 · 3 entries
```
**Empty state**:
```
No drafts found. Create one with /til <content>.
```
For published:
```
No published entries found.
```
### `/til publish [<id> | last]`
**Resolution**:
- `last` → use `last_created_entry_id` from session state
- `<id>` → resolve via ID resolution algorithm
**Flow**:
1. `GET /entries/:id` — fetch the entry to show what will be published
2. Show confirmation:
```
Publish this entry?
Title: Go interfaces are satisfied implicitly
Tags: go, interfaces
Confirm? (y/n)
```
3. On confirmation → `POST /entries/:id/publish`
4. Show result:
```
Published
Title: Go interfaces are satisfied implicitly
URL: https://opentil.ai/@username/go-interfaces-are-satisfied-implicitly
```
**Already published**: Informational, not an error.
```
Already published.
Title: Go interfaces are satisfied implicitly
URL: https://opentil.ai/@username/go-interfaces-are-satisfied-implicitly
```
### `/til unpublish <id>`
**Flow**:
1. `GET /entries/:id` — fetch the entry
2. Show confirmation:
```
Unpublish this entry? It will become a draft.
Title: Go interfaces are satisfied implicitly
```
3. On confirmation → `POST /entries/:id/unpublish`
4. Show result:
```
Unpublished — entry is now a draft.
Title: Go interfaces are satisfied implicitly
```
**Already a draft**: Informational, not an error.
```
Already a draft.
Title: Go interfaces are satisfied implicitly
```
### `/til edit <id> [instructions]`
**Flow**:
1. `GET /entries/:id` — fetch the full entry
2. Apply AI-assisted changes based on instructions (or ask what to change if no instructions given)
3. Show diff preview:
```
Proposed changes to "Go interfaces are satisfied implicitly":
Title: Go interfaces are satisfied implicitly (unchanged)
Content diff:
- In Go, a type implements an interface by implementing its methods.
+ In Go, a type satisfies an interface by implementing all of its methods.
+ No explicit "implements" declaration is needed.
Tags: go, interfaces → go, interfaces, type-system
Apply changes?
```
4. On confirmation → `PATCH /entries/:id` with only the changed fields
5. Show result:
```
Updated
Title: Go interfaces are satisfied implicitly
URL: https://opentil.ai/@username/go-interfaces-are-satisfied-implicitly
```
### `/til search <keyword>`
**API call**: `GET /entries?q=<keyword>&per_page=10`
**Display format**: Same compact table as `list`.
```
Search results for "go" (2):
ID Status Title
...a1b2c3d4 Published Go interfaces are satisfied implicitly
...i9j0k1l2 Draft Go concurrency with goroutines
2 entries found
```
**No results**:
```
No entries matching "go" found.
```
### `/til delete <id>`
**Flow**:
1. `GET /entries/:id` — fetch the entry
2. Double-confirm (this cannot be undone):
```
Delete this entry? This cannot be undone.
Title: Go interfaces are satisfied implicitly
Status: Draft
Type "delete" to confirm:
```
3. On confirmation → `DELETE /entries/:id`
4. Show result:
```
Deleted.
Title: Go interfaces are satisfied implicitly
```
### `/til status`
Show site status and connection info. **Special: works without a token** (degraded display).
**With token (≥2 profiles)** -- `GET /site`:
```
OpenTIL Status
Profile: personal (active)
Site: @hong (opentil.ai/@hong)
Entries: 28 total (15 published, 13 drafts)
Token: til_...a3f2 ✓
Local: 1 draft pending sync
Profiles: 2 configured (/til auth list)
Manage: https://opentil.ai/dashboard
```
**With token (single profile)** -- `GET /site`:
```
OpenTIL Status
Site: @hong (opentil.ai/@hong)
Entries: 28 total (15 published, 13 drafts)
Token: til_...a3f2 ✓
Local: 1 draft pending sync
Manage: https://opentil.ai/dashboard
```
**With token (env var override)** -- `GET /site`:
```
OpenTIL Status
Site: @hong (opentil.ai/@hong)
Entries: 28 total (15 published, 13 drafts)
Token: til_...a3f2 ✓ (env override)
Local: 1 draft pending sync
Manage: https://opentil.ai/dashboard
```
- `Profile` line: only shown when ≥2 profiles exist. Shows `name (active)`.
- `Site` line: `@username` + public URL
- `Entries` line: `entries_count` (total), `published_entries_count` (published), difference = drafts
- `Token` line: last 4 chars of the resolved token + `✓`. Append `(env override)` when token comes from `$OPENTIL_TOKEN`.
- `Local` line: count of `*.md` files in `~/.til/drafts/`
- `Profiles` line: only shown when ≥2 profiles exist. Shows count + hint to `/til auth list`.
- `Manage` link: dashboard URL
**Without token:**
```
OpenTIL Status
Site: (not connected)
Token: not configured
Local: 3 drafts pending sync
Run /til auth to connect
```
**Token set but API error** (401, network failure):
```
OpenTIL Status
Site: (unable to connect)
Token: til_...a3f2 ✗
Local: 0 drafts
Check token: https://opentil.ai/dashboard/settings/tokens
```
### `/til auth`
Connect an OpenTIL account via Device Flow (browser-based authorization). **Works without a token.**
**Flow:**
1. **Check existing connection**
- Resolve token (env var → active profile in `~/.til/credentials`)
- If `~/.til/credentials` exists in old plain-text format, migrate to YAML `default` profile first
- If token found, `GET /site` to verify:
- Valid: `"Already connected as @{username}. Re-authorize? (y/n)"`
- `y` → continue to new authorization
- `n` → end
- Invalid (401) → continue to new authorization
- If no token → continue to new authorization
2. **Create device code**
- `POST /api/v1/oauth/device/code` with `{ "scopes": ["read", "write"] }`
- Response: `{ device_code, user_code, verification_uri, expires_in, interval }`
3. **Open browser + display**
- Open `{verification_uri}?user_code={user_code}` via `open` (macOS) or `xdg-open` (Linux)
- Display:
```
Opening browser to connect...
If browser didn't open, visit:
https://opentil.ai/device
Enter code: XXXX-YYYY
Waiting for authorization...
```
4. **Poll for token**
- Use a bash script to poll in a single command (not multiple turns):
- Every `{interval}` seconds, `POST /api/v1/oauth/device/token`
- `authorization_pending` → continue polling
- `slow_down` → increase interval by 5 seconds
- `expired_token` → timeout
- 200 → extract `access_token`
- Hard timeout: 300 seconds (5 minutes)
5. **On success — save as named profile**
- Create `~/.til/` directory if it doesn't exist
- `GET /site` with the new token to fetch `username` (nickname)
- Determine profile name: use the API-returned `username` as the default profile name
- If a profile with the same name already exists and its token differs, append a numeric suffix (`hong-2`, `hong-3`, etc.)
- If re-authorizing the current active profile (same nickname), update the existing profile's token in-place
- Write `~/.til/credentials` in YAML format (`chmod 600`):
- Set `active` to the new profile name
- Add/update the profile under `profiles` with `token`, `nickname`, `site_url`, `host`
- Display:
```
✓ Connected as @hong
Profile "hong" saved to ~/.til/credentials
```
- If other profiles already exist, and this is a new profile (not re-auth), ask whether to switch:
- `"Switch to @hong (hong)? (y/n)"` — default yes
- `y` or new profile → set as active
- `n` → keep current active profile
- Check `~/.til/drafts/` for local drafts
- If drafts exist: `"Found N local drafts. Sync now? (y/n)"`
6. **On timeout**
```
Authorization timed out. Run /til auth to try again.
```
7. **On network error**
```
Unable to reach OpenTIL. Check your connection and try again.
```
**Edge cases:**
| Scenario | Handling |
|----------|----------|
| Already has valid token | Confirm before re-authorizing |
| Token expired/invalid | Proceed directly to new authorization, no confirmation |
| `~/.til/` directory doesn't exist | Create automatically |
| Browser didn't open | Display fallback URL + manual code entry |
| User cancels in browser | Polling times out, show timeout message |
| Token obtained + local drafts exist | Offer to sync |
| Old plain-text credentials file | Migrate to YAML `default` profile before proceeding |
| Re-auth same account | Update existing profile's token in-place |
| Auth new account, profiles exist | Ask whether to switch to new profile |
### `/til auth switch [name]`
Switch the active profile. **Works without a token** (operates on local `~/.til/credentials` only).
**No argument — interactive selection:**
```
Profiles:
1. personal @hong opentil.ai/@hong (active)
2. work @hong-corp opentil.ai/@hong-corp
Switch to: (1/2)
```
User picks a number → update `active` field in `~/.til/credentials` → verify token with `GET /site`:
- Valid: `Switched to @hong-corp (work)`
- Invalid (401): `Switched to @hong-corp (work) — token expired, run /til auth to reconnect`
**With argument:**
`/til auth switch work` or `/til auth switch hong-corp` → directly switch, no interactive prompt.
Name resolution order:
1. Exact match on profile name → use it
2. Exact match on nickname (with or without `@` prefix) → use that profile
3. No match → show error with available profiles
Examples:
- `/til auth switch work` → matches profile name "work"
- `/til auth switch hong-corp` → matches nickname "hong-corp"
- `/til auth switch @hong-corp` → matches nickname "hong-corp" (strips `@`)
- Match found → switch and verify (same as above)
- No match:
```
Profile "xyz" not found.
Available profiles:
* personal @hong opentil.ai/@hong
Use /til auth to add a new account.
```
**No profiles configured:**
```
No profiles configured. Run /til auth to connect.
```
### `/til auth list`
List all configured profiles. **Works without a token.**
```
Profiles:
* personal @hong opentil.ai/@hong
work @hong-corp opentil.ai/@hong-corp
```
- `*` marks the active profile
- Columns: profile name, `@nickname`, site URL
**No profiles:**
```
No profiles configured. Run /til auth to connect.
```
**With env var override:**
```
Profiles:
* personal @hong opentil.ai/@hong
work @hong-corp opentil.ai/@hong-corp
Token override: $OPENTIL_TOKEN is set (overrides active profile)
```
### `/til auth remove <name>`
Remove a profile from `~/.til/credentials`. **Works without a token.**
**Cannot remove active profile (when other profiles exist):**
```
Cannot remove "personal" — it is the active profile.
Switch to another profile first: /til auth switch <name>
```
**Last remaining profile:** Can be removed even if active (special case — returns to "not connected" state).
**Confirmation:**
```
Remove profile "work" (@hong-corp)? (y/n)
```
- `y` → remove from `profiles` map, write back `~/.til/credentials`
- `n` → cancel
When the last profile is removed, `~/.til/credentials` is cleared (empty `profiles` map, no `active` field).
**Profile not found:**
```
Profile "work" not found. Use /til auth list to see profiles.
```
### `/til auth rename <old> <new>`
Rename a profile. **Works without a token.**
```
Renamed "personal" → "home"
```
- If the renamed profile is the active profile, update the `active` field accordingly
- If `<new>` already exists:
```
Profile "home" already exists. Choose a different name.
```
- If `<old>` not found:
```
Profile "personal" not found. Use /til auth list to see profiles.
```
### `/til sync`
Explicitly sync local drafts from `~/.til/drafts/` to OpenTIL. Requires token.
**Flow:**
1. List `*.md` files in `~/.til/drafts/`
2. If no files: `No local drafts to sync.`
3. Show what will be synced and ask for confirmation:
```
Found 2 local drafts:
1. go-interfaces.md
2. rails-solid-queue.md
Sync to OpenTIL? (y/n)
```
4. On confirmation, for each file: parse frontmatter, POST to API (with correct attribution headers), delete local file on success
5. Show results:
**All synced:**
```
Synced 2 local drafts
✓ go-interfaces.md
✓ rails-solid-queue.md
```
**Partial failure:**
```
Synced 1 of 2 local drafts
✓ go-interfaces.md
✗ rails-solid-queue.md (validation error)
Kept at: ~/.til/drafts/20260210-150415-rails-solid-queue.md
```
### `/til tags`
List site tags sorted by usage count. Requires token.
**API call:** `GET /tags?sort=popular&per_page=20&with_entries=true`
**Display format:**
```
Your tags (12):
Tag Entries
go 8
postgresql 5
rails 4
css 3
linux 2
...
Showing top 20 · 12 total tags
```
**Empty state:**
```
No tags yet. Tags are created automatically when you publish entries.
```
### `/til categories`
List site categories. Requires token.
**API call:** `GET /categories`
**Display format:**
```
Your categories (3):
Name Entries Description
Backend 12 Server-side topics
Frontend 8 Client-side development
DevOps 5 Infrastructure and deployment
3 categories
```
**Empty state:**
```
No categories yet. Create them at: https://opentil.ai/dashboard/topics
```
### `/til batch <topics>`
Batch-capture multiple TIL entries in one invocation. Requires an explicit topic list (no implicit extraction — use `/til` without arguments for that).
**Input formats** -- user provides topics separated by newlines, semicolons, markdown list items (`-`), or numbered list (`1.`):
```
/til batch
- Go channels block when buffer is full
- CSS grid fr unit distributes remaining space
- PostgreSQL EXPLAIN ANALYZE shows actual vs estimated rows
```
**Flow:**
1. Parse the input into separate topics
2. For each topic, generate a complete TIL entry (title, body, tags, lang)
3. Show all drafts as a numbered list for review:
```
Generated 3 drafts:
1. Go channels block when buffer is full
Tags: go, concurrency
2. CSS grid fr unit distributes remaining space
Tags: css, grid
3. PostgreSQL EXPLAIN ANALYZE shows actual vs estimated rows
Tags: postgresql, performance
Which to send? (1/2/3/all/none)
```
4. On confirmation, POST each selected entry sequentially
5. Show summary:
```
Captured 3 TILs
✓ Go channels block when buffer is full
✓ CSS grid fr unit distributes remaining space
✓ PostgreSQL EXPLAIN ANALYZE shows actual vs estimated rows
```
**Partial failure:**
```
Captured 2 of 3 TILs
✓ Go channels block when buffer is full
✗ CSS grid fr unit distributes remaining space (validation error)
✓ PostgreSQL EXPLAIN ANALYZE shows actual vs estimated rows
```
Failed entries are saved locally to `~/.til/drafts/` (same as normal capture fallback).
## Error Handling
### Missing Token
See Prerequisites above — proactively offer to connect via device flow.
### 401 -- Token Invalid or Expired
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:
**Token from `~/.til/credentials` (active profile):**
```
Token expired. Reconnect now? (y/n)
```
When ≥2 profiles exist, include the profile identity: `Token expired for @hong (personal). Reconnect now? (y/n)`
- `y` → run inline device flow → on success, update the active profile's token in `~/.til/credentials` and auto-retry the original management command
- `n` → show manual setup instructions (same as Prerequisites section)
**Token from `$OPENTIL_TOKEN` env var:**
```
Your $OPENTIL_TOKEN is expired or invalid. To fix:
• Update the variable with a new token, or
• unset OPENTIL_TOKEN, then run /til auth
Create a new token: https://opentil.ai/dashboard/settings/tokens
```
**Safeguards** (same as SKILL.md):
- If re-auth succeeds but retry still returns 401 → stop and show the error
- During batch operations, re-authenticate at most once, then continue remaining items
- If the user declines (`n`), do not prompt again this session
### Insufficient Scope (403)
```
Permission denied — your token needs the <scope> scope.
Regenerate at: https://opentil.ai/dashboard/settings/tokens
```
### Entry Not Found (404)
```
Entry not found: <id>
Use /til list to see your entries.
```
### Already in Target State
Not errors — show informational message (see publish/unpublish sections above).
### Network Errors
```
API unavailable. Try again later.
```
Management subcommands do not have a local fallback — they require API access.
```
### references/local-drafts.md
```markdown
# Local Drafts & Sync Protocol
When the API is unavailable or no token is configured, TIL entries are saved locally. This document covers the full local draft lifecycle.
## Directory Structure
```
~/.til/
drafts/
20260210-143022-go-interfaces.md
20260210-150415-postgresql-gin-index.md
20260211-091200-css-has-selector.md
```
All platforms use `~/.til/drafts/`. Create the directory if it does not exist.
## File Format
Filename: `YYYYMMDD-HHMMSS-<slug>.md`
The slug is derived from the title (lowercase, hyphens, no special chars, max 50 chars).
```markdown
---
title: "Go interfaces are satisfied implicitly"
tags: [go, interfaces]
lang: en
summary: "Go types implement interfaces implicitly by implementing their methods, with no explicit declaration needed."
source: human
agent_name: Claude Code
agent_model: Claude Opus 4.6
profile: personal
---
In Go, a type implements an interface by implementing its methods.
There is no explicit `implements` keyword...
```
### Frontmatter Fields
| Field | Type | Description |
|-------|------|-------------|
| `title` | string | Entry title |
| `tags` | array | Tag list |
| `lang` | string | Language code |
| `summary` | string | AI-generated summary for listing pages (max 500 chars) |
| `source` | string | `human` (from `/til`) or `agent` (from auto-detection) |
| `agent_name` | string | Agent display name, e.g. `Claude Code` (optional) |
| `agent_model` | string | Human-readable model name, e.g. `Claude Opus 4.6` (optional) |
| `profile` | string | Active profile name at save time (optional). Used during sync to determine which account's token to use. Omitted when no profiles are configured. |
The `source`, `agent_name`, and `agent_model` fields preserve attribution so that when syncing to the API, the correct headers and tags can be applied.
The `profile` field ensures drafts are synced to the correct account in multi-profile setups.
## Sync Protocol
When a successful API call is made (201 response), check for pending local drafts:
### Step 1: Detect Pending Drafts
```
List files in ~/.til/drafts/ matching *.md
```
If no files exist, skip sync entirely.
### Step 2: Offer to Sync
```
Found 3 local drafts from before. Sync them to OpenTIL?
```
Wait for user confirmation. If the user declines, do not ask again this session.
### Step 3: Sync Each Draft
For each `.md` file in `~/.til/drafts/`:
1. Parse the frontmatter (title, tags, lang, source, agent_name, agent_model, profile)
2. **Resolve token for this draft** (profile matching):
- If `$OPENTIL_TOKEN` is set → always use it (env var overrides all profiles)
- If `profile` field is present → look up that profile's token in `~/.til/credentials`
- Profile found → use its token
- Profile not found → skip this draft, report: `Skipped: profile "work" not found (/til auth list)`
- If `profile` field is absent (old drafts) → use the current active profile's token
3. Read the content body (everything after the second `---`)
4. POST to API (using the resolved token):
- Set `published: false`
- Set `X-OpenTIL-Source` header based on `source` field
- Set `X-OpenTIL-Agent` header from `agent_name` field (if present)
- Set `X-OpenTIL-Model` header from `agent_model` field (if present)
- Add `agent-assisted` tag if `source` is `agent`
5. On 201 success: delete the local file
6. On failure: keep the local file, record the error
### Step 4: Report Results
**All succeeded:**
```
Synced 3 local drafts to OpenTIL
+ Go defer runs in LIFO order
+ PostgreSQL JSONB indexes support GIN operators
+ CSS :has() selector enables parent selection
```
**Partial failure:**
```
Synced 2 of 3 local drafts
+ Go defer runs in LIFO order
+ PostgreSQL JSONB indexes support GIN operators
x CSS :has() selector enables parent selection (validation error)
Kept at: ~/.til/drafts/20260210-143022-css-has-selector.md
```
## First-Run Guide Template
On the first local save in a session (when no token is found):
```
TIL captured
Title: Go interfaces are satisfied implicitly
Tags: go, interfaces
File: ~/.til/drafts/20260210-143022-go-interfaces.md
Sync to OpenTIL? Run: /til auth
```
On subsequent local saves in the same session, use the short form:
```
TIL captured
Title: Go interfaces are satisfied implicitly
Tags: go, interfaces
File: ~/.til/drafts/20260210-143022-go-interfaces.md
```
Track "first save shown" as session state. Reset on each new session.
```
### references/api.md
```markdown
# API Reference
Base URL: `https://opentil.ai/api/v1`
All requests require a Bearer token:
```
Authorization: Bearer $OPENTIL_TOKEN
```
## POST /entries -- Create Entry
### Request Body
All fields are nested under `entry`. Additionally, `tag_names` and `category_name` are accepted at the `entry` level as convenience parameters.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `content` | string | yes | Markdown body (max 100,000 chars) |
| `title` | string | no | Entry title (max 200 chars). Auto-generates slug if omitted. |
| `slug` | string | no | Custom URL slug. Auto-generated from title if omitted. |
| `tag_names` | array | no | 1-3 lowercase tags, e.g. `["go", "concurrency"]` |
| `category_name` | string | no | Category name. Only include if the user explicitly specifies one. |
| `category_id` | integer | no | Category ID (alternative to `category_name`). |
| `published` | boolean | no | `false` for draft (default), `true` to publish immediately. |
| `published_at` | datetime | no | ISO 8601 timestamp. Only relevant when publishing. |
| `visibility` | string | no | `public` (default), `unlisted`, or `private` |
| `summary` | string | no | AI-generated summary for listing pages (max 500 chars) |
| `meta_description` | string | no | SEO meta description |
| `meta_image` | string | no | URL for social sharing image |
| `lang` | string | no | Language code (see Supported Languages below) |
### Supported Languages
`en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `es`, `fr`, `de`, `pt-BR`, `pt`, `ru`, `ar`, `bs`, `da`, `nb`, `pl`, `th`, `tr`, `it`
### 201 Response (EntrySerializer)
```json
{
"id": "1234567890",
"title": "Go interfaces are satisfied implicitly",
"slug": "go-interfaces-are-satisfied-implicitly",
"content": "In Go, a type implements an interface...",
"content_html": "<p>In Go, a type implements an interface...</p>",
"published": false,
"published_at": null,
"first_published_at": null,
"visibility": "public",
"hidden": false,
"summary": null,
"meta_description": null,
"meta_image": null,
"lang": "en",
"views_count": 0,
"unique_views_count": 0,
"category_id": null,
"category": null,
"tag_names": ["go", "interfaces"],
"source": "human",
"agent_name": "Claude Code",
"agent_model": "Claude Opus 4.6",
"url": "https://opentil.ai/@username/go-interfaces-are-satisfied-implicitly",
"created_at": "2026-02-10T14:30:22Z",
"updated_at": "2026-02-10T14:30:22Z"
}
```
Use the `url` field for the Review link in result messages.
## GET /entries -- List Entries
List entries for the authenticated user. Requires `read:entries` scope.
```
GET /entries?status=published&per_page=20&q=go
```
| Param | Description |
|-------|-------------|
| `status` | `published`, `draft`, or `scheduled` |
| `q` | Search by title (case-insensitive partial match) |
| `tag` | Filter by tag slug |
| `category_id` | Filter by category ID |
| `uncategorized` | `true` to filter uncategorized entries |
| `per_page` | Results per page (max 100, default 20) |
| `page` | Page number |
### Response (EntryListSerializer)
```json
{
"data": [
{
"id": "1234567890",
"title": "Go interfaces are satisfied implicitly",
"slug": "go-interfaces-are-satisfied-implicitly",
"excerpt": "In Go, a type implements an interface by implementing...",
"published": true,
"published_at": "2026-02-10T14:30:22Z",
"first_published_at": "2026-02-10T14:30:22Z",
"visibility": "public",
"views_count": 42,
"unique_views_count": 35,
"category_id": null,
"category_name": null,
"tag_names": ["go", "interfaces"],
"source": "human",
"agent_name": "Claude Code",
"agent_model": "Claude Opus 4.6",
"created_at": "2026-02-10T14:30:22Z",
"updated_at": "2026-02-10T14:30:22Z"
}
],
"meta": {
"current_page": 1,
"total_pages": 1,
"total_count": 1,
"per_page": 20
}
}
```
## GET /entries/drafts
Shorthand for listing draft entries. Requires `read:entries` scope.
Returns the same response format as `GET /entries` but filtered to drafts, ordered by `updated_at` descending.
## GET /entries/:id -- Show Entry
Fetch a single entry. Requires `read:entries` scope.
Returns the full EntrySerializer response (same as 201 response above).
## PATCH /entries/:id -- Update Entry
Update an entry. Requires `write:entries` scope.
### Request Body
Same fields as POST, all optional. Only include fields that are changing.
```json
{
"entry": {
"title": "Updated title",
"content": "Updated content...",
"tag_names": ["go", "interfaces", "type-system"]
}
}
```
### 200 Response
Returns the full EntrySerializer response with updated fields.
## DELETE /entries/:id -- Delete Entry
Permanently delete an entry. Requires `delete:entries` scope.
### 200 Response
```json
{
"message": "Entry deleted"
}
```
## POST /entries/:id/publish
Publish a draft entry. Requires `write:entries` scope. No request body needed.
Returns the full EntrySerializer response with `published: true`.
## POST /entries/:id/unpublish
Unpublish a published entry (revert to draft). Requires `write:entries` scope. No request body needed.
Returns the full EntrySerializer response with `published: false`.
## Error Handling
### Error Response Format
```json
{
"error": {
"type": "validation_error",
"code": "validation_failed",
"message": "Validation failed",
"details": [{"field": "title", "message": "Title can't be blank"}]
}
}
```
### Error Codes
| Status | Code | Action |
|--------|------|--------|
| 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`. |
| 403 | `insufficient_scope` | Token lacks required scope. Show which scope is needed. |
| 404 | `not_found` | Entry does not exist or belongs to another user. |
| 422 | `validation_failed` | Parse `details` array, auto-fix, and retry once. Save locally if retry fails. |
| 429 | `rate_limited` | Rate limit exceeded. Save locally. Retry after `X-RateLimit-Reset`. |
| 5xx | -- | Server error. Save locally. |
### 422 Auto-Fix Retry Logic
When a 422 is returned, inspect the `details` array and attempt to fix:
1. `title` too long -> truncate to 200 chars
2. `lang` invalid -> fall back to `en`
3. `slug` already taken -> append `-2` (or increment)
4. `tag_names` invalid -> remove offending tags, keep valid ones
5. `content` too long -> truncate to 100,000 chars
After fixing, retry the POST **once**. If the retry also returns 422, save locally and report the error.
## GET /site -- Site Info
Fetch the authenticated user's site details. Requires `read:entries` scope.
### Response (SiteDetailSerializer)
```json
{
"username": "hong",
"title": "Hong's TIL",
"bio": "Learning something new every day",
"timezone": "Asia/Shanghai",
"locale": "en",
"entries_count": 28,
"published_entries_count": 15,
"categories_count": 3,
"custom_domain": null,
"domain_verified": false,
"discoverable": true,
"theme_slug": "default",
"theme_mode": "auto",
"last_posted_at": "2026-02-10T14:30:22Z",
"created_at": "2025-06-01T00:00:00Z",
"updated_at": "2026-02-10T14:30:22Z"
}
```
- `entries_count`: total entries (including drafts)
- `published_entries_count`: published entries only
Used by `/til status` to display site info.
## GET /tags -- List Tags
List tags for the authenticated user's site. Requires `read:entries` scope.
```
GET /tags?sort=popular&per_page=20
```
| Param | Description |
|-------|-------------|
| `sort` | `popular` (default, by taggings count) or `alphabetical` |
| `per_page` | Results per page (max 100, default 20) |
| `page` | Page number |
| `with_entries` | `true` to only return tags that have entries |
### Response (TagSerializer)
```json
{
"data": [
{
"id": "123",
"name": "go",
"slug": "go",
"taggings_count": 8,
"created_at": "2025-06-15T10:00:00Z"
}
],
"meta": {
"current_page": 1,
"total_pages": 1,
"total_count": 12,
"per_page": 20
}
}
```
Note: `taggings_count` is the global usage count across all entries on the site.
Used by `/til tags` to display tag usage.
## GET /categories -- List Categories
List categories (topics) for the authenticated user's site. Requires `read:entries` scope.
```
GET /categories
```
### Response (CategorySerializer)
```json
{
"data": [
{
"id": "456",
"name": "Backend",
"slug": "backend",
"description": "Server-side topics",
"entries_count": 12,
"position": 0,
"created_at": "2025-06-15T10:00:00Z"
}
]
}
```
Note: `entries_count` is the site-level count of entries in that category.
Used by `/til categories` to display category listing.
## Rate Limits
- Authenticated: 5,000 requests/hour
- Unauthenticated: 60 requests/hour
Rate limit info is returned in response headers:
| Header | Description |
|--------|-------------|
| `X-RateLimit-Limit` | Maximum requests per window |
| `X-RateLimit-Remaining` | Requests remaining in current window |
| `X-RateLimit-Reset` | Unix timestamp when the window resets |
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.
## Device Flow (OAuth)
These endpoints do not require a Bearer token. Used by `/til auth` to obtain a token via browser authorization.
### POST /oauth/device/code
Create a device authorization code.
```
POST /api/v1/oauth/device/code
Content-Type: application/json
{ "scopes": ["read", "write"] }
```
**200 Response:**
```json
{
"device_code": "uuid-string",
"user_code": "XXXX-YYYY",
"verification_uri": "https://opentil.ai/device",
"expires_in": 900,
"interval": 5
}
```
| Field | Description |
|-------|-------------|
| `device_code` | Opaque code used to poll for the token |
| `user_code` | Human-readable code displayed to the user |
| `verification_uri` | URL where the user authorizes the device |
| `expires_in` | Seconds until the device code expires |
| `interval` | Minimum polling interval in seconds |
### POST /oauth/device/token
Poll for an access token after the user authorizes.
```
POST /api/v1/oauth/device/token
Content-Type: application/json
{ "device_code": "uuid-string", "grant_type": "urn:ietf:params:oauth:grant-type:device_code" }
```
**200 Response (authorized):**
```json
{
"access_token": "til_xxx...",
"token_type": "bearer",
"scope": "read write"
}
```
**400 Response (pending):**
```json
{
"error": {
"code": "authorization_pending",
"message": "The user has not yet authorized this device"
}
}
```
**Error codes:**
| Code | Meaning | Action |
|------|---------|--------|
| `authorization_pending` | User hasn't authorized yet | Continue polling |
| `slow_down` | Polling too fast | Increase interval by 5 seconds |
| `expired_token` | Device code expired | Stop polling, show timeout message |
| `invalid_grant` | Invalid device code | Stop polling, show error |
## Credential Storage
After a successful device flow, credentials are stored locally in `~/.til/credentials` as YAML.
### File Format
```yaml
active: personal
profiles:
personal:
token: til_abc...
nickname: hong
site_url: https://opentil.ai/@hong
host: https://opentil.ai
work:
token: til_xyz...
nickname: hong-corp
site_url: https://opentil.ai/@hong-corp
host: https://opentil.ai
```
### Field Reference
| Field | Level | Description |
|-------|-------|-------------|
| `active` | top | Name of the currently active profile |
| `profiles` | top | Map of profile name → profile object |
| `token` | profile | Bearer token (starts with `til_`) |
| `nickname` | profile | Username from `GET /site` response (`username` field) |
| `site_url` | profile | Public site URL, e.g. `https://opentil.ai/@hong` |
| `host` | profile | API host, e.g. `https://opentil.ai` |
### Backward Compatibility
Old format (`~/.til/credentials` containing only a plain text token):
```
til_abc123...
```
On first read, detect the old format (file content starts with `til_` and contains no YAML structure). Migrate automatically:
1. Read the token string
2. `GET /site` with the token to fetch `username`
- On success: use `username` as profile name, populate `nickname` and `site_url`
- On failure (401/network): use `default` as profile name, leave `nickname` and `site_url` empty
3. Write back as YAML with the single profile set as `active`
4. Preserve file permissions (`chmod 600`)
### File Permissions
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.
```
### references/auto-detection.md
```markdown
# Auto-Detection Guide
This document details how the Agent proactively detects TIL-worthy moments during work sessions.
## Trigger Examples
### Debugging uncovered a non-obvious root cause
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."
Bad example: "Fixed the null pointer error by adding a nil check." (Obvious fix, no insight.)
### Language/framework behavior contradicts common assumptions
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."
Bad example: "JavaScript has both `==` and `===`." (Well-known, not surprising.)
### Refactoring revealed a superior pattern
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."
Bad example: "Renamed variables to be more descriptive." (Cosmetic, no pattern insight.)
### Performance optimization with measurable results
Good example: "Adding a compound index on (user_id, created_at) reduced the dashboard query from 2.3s to 12ms."
Bad example: "Used caching to make things faster." (Vague, no specifics.)
### Obscure but useful tool flag or API parameter
Good example: "git diff --word-diff=color shows inline character-level changes instead of full-line diffs, perfect for reviewing prose changes."
Bad example: "Used git log to see commit history." (Basic, widely known.)
### Two technologies interacting unexpectedly
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."
Bad example: "Used Redis with Rails for caching." (Standard pattern, no surprise.)
### Upgrade/migration breaking changes
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."
Bad example: "Updated Node from v18 to v20." (Fact, no insight.)
## What NOT to Detect
Do not suggest TIL capture for:
- Standard usage of tools/APIs (reading docs, running commands)
- Configuration that works as documented
- Bugs caused by typos or simple mistakes
- Widely known best practices (use environment variables, write tests, etc.)
- Anything the user already seems to know well
- Tasks where the user is actively frustrated or stressed -- wait for resolution
## Rate Limiting State Machine
```
[IDLE] ---(TIL-worthy moment detected)---> [EVALUATING]
^ |
| (check constraints)
| |
| +-------+-------+
| | |
| (constraints (constraints
| not met) met)
| | |
| v v
+----(stay idle)----------[IDLE] [SUGGESTED]
|
+---------+---------+
| |
(user accepts) (user declines
| or ignores)
v v
[CAPTURED] [DONE_FOR_SESSION]
|
v
[DONE_FOR_SESSION]
```
**Constraints checked in EVALUATING state:**
1. Has a suggestion already been made this session? → If yes, stay IDLE
2. Is the user in the middle of active problem-solving? → If yes, stay IDLE
3. Is this a natural pause point (resolution or task boundary)? → If no, stay IDLE
Once in DONE_FOR_SESSION, the agent never suggests again until a new session starts.
## Suggestion Format
Append the suggestion at the end of your normal response. Never interrupt the workflow with a standalone suggestion message.
**Template:**
```
💡 TIL: [concise title of the insight]
Tags: [tag1, tag2] · Capture? (yes/no)
```
**Example** (debugging root cause):
```
...so the memory leak was caused by the goroutine holding a reference to the entire request body.
💡 TIL: Goroutine closures can silently retain large objects, causing memory leaks
Tags: go, concurrency · Capture? (yes/no)
```
**Example** (performance optimization):
```
...the compound index on (user_id, created_at) reduced the query from 2.3s to 12ms.
💡 TIL: Compound indexes with the right column order can yield 100x+ query speedups
Tags: postgresql, indexing · Capture? (yes/no)
```
**Example** (migration breaking change):
```
...Ruby 3.2 changed Struct keyword arguments to be required by default.
💡 TIL: Ruby 3.2 makes Struct keyword args required by default, silently breaking existing code
Tags: ruby, migration · Capture? (yes/no)
```
## Single Confirmation Flow
```
Agent: [normal response content]
💡 TIL: [concise title]
Tags: [tags] · Capture? (yes/no)
User: yes / y / ok / sure
Agent: [Generates full entry: title, body, tags, lang]
[POST to API or save locally]
[Show result message]
```
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.
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.
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### _meta.json
```json
{
"owner": "biao29",
"slug": "til",
"displayName": "OpenTIL",
"latest": {
"version": "1.11.0",
"publishedAt": 1770992521017,
"commit": "https://github.com/openclaw/skills/commit/06c7e35e715508ef925cce23bad6d7cd4de4b76a"
},
"history": [
{
"version": "1.7.0",
"publishedAt": 1770830012530,
"commit": "https://github.com/openclaw/skills/commit/575380b60af6808228f54ca74131229825b194a5"
}
]
}
```