Files
jackwener__opencli/clis/devto/user.js
jakevin 68485cc54e feat(devto): surface article id on listings + new read <id> (#1292)
* feat(devto): surface article id + published_at on listings, add `read <id>`

Agent-native gap: devto listings (`top`/`tag`/`user`) didn't include the
article `id`, so an agent couldn't round-trip from a listing into a body
read. They also dropped `reading_time` and `published_at`, which are cheap
signals the API gives you for free.

Changes:
- `top` / `tag` / `user`: add `id`, `reading_time`, `published_at` columns
  alongside existing rank/title/etc. `user` keeps its no-author shape since
  it's already user-scoped.
- New `devto read <id>`: hits `dev.to/api/articles/<id>` and returns one
  row with the article body (truncated by `--max-length`, default 20000,
  min 100). DEV.to's public API does not expose comments yet, so this is
  intentionally a single-row reader rather than a HN/lobsters-style
  threaded tree — if/when comments become public we can extend to
  POST + L0/L1.
- Typed fail-fast: `ArgumentError` for non-numeric id and for `--max-length`
  below 100; `EmptyResultError` on 404; `CommandExecutionError` for other
  non-2xx HTTP statuses. No silent clamps.
- Defensive tag normalization: the `/api/articles/<id>` endpoint returns
  `tag_list` as a comma-string and `tags` as an array (the opposite shape
  from listing endpoints). Caught this on live verification — both shapes
  now collapse to a comma-joined string.

Tests: 12 vitest assertions covering listing column shape (all 3) +
register/args/strategy + typed-error fail-fast paths + happy-path body
extraction + truncation marker + alternate tag_list shape.

Live verification: `devto top --limit 3` and `devto read 3602287` both
return the expected agent-native shape.

* fix(devto): harden article read contract
2026-05-04 18:55:14 +08:00

35 lines
1.3 KiB
JavaScript

import { cli, Strategy } from '@jackwener/opencli/registry';
cli({
site: 'devto',
name: 'user',
description: 'Recent DEV.to articles from a specific user',
domain: 'dev.to',
strategy: Strategy.PUBLIC,
browser: false,
args: [
{
name: 'username',
required: true,
positional: true,
help: 'DEV.to username (e.g. ben, thepracticaldev)',
},
{ name: 'limit', type: 'int', default: 20, help: 'Number of articles' },
],
columns: ['rank', 'id', 'title', 'reactions', 'comments', 'reading_time', 'published_at', 'tags', 'url'],
pipeline: [
{ fetch: { url: 'https://dev.to/api/articles?username=${{ args.username }}&per_page=${{ args.limit }}' } },
{ map: {
rank: '${{ index + 1 }}',
id: '${{ item.id }}',
title: '${{ item.title }}',
reactions: '${{ item.public_reactions_count }}',
comments: '${{ item.comments_count }}',
reading_time: '${{ item.reading_time_minutes }}',
published_at: '${{ item.published_at }}',
tags: `\${{ item.tag_list | join(', ') }}`,
url: '${{ item.url }}',
} },
{ limit: '${{ args.limit }}' },
],
});