grok-digest 0.3.0 → 0.3.2
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/BOT.md +24 -4
- package/README.md +1 -1
- package/dist/components/BookmarkCard.astro +38 -5
- package/dist/lib/bookmarks.d.ts +30 -0
- package/dist/lib/bookmarks.js +15 -0
- package/dist/lib/copy.d.ts +1 -1
- package/dist/lib/copy.js +2 -1
- package/dist/lib/feed.js +7 -2
- package/dist/lib/load-bookmarks.d.ts +5 -0
- package/dist/lib/media.d.ts +14 -0
- package/dist/lib/media.js +22 -0
- package/package.json +1 -1
package/BOT.md
CHANGED
|
@@ -54,17 +54,24 @@ bun run validate
|
|
|
54
54
|
"shelf_life": "evergreen",
|
|
55
55
|
"tldr": "Stale workaround comments spread because coding agents copy them",
|
|
56
56
|
"summary": "Argues you should delete most code comments, because coding agents copy existing workaround comments and spread hacks through the codebase.",
|
|
57
|
-
"text": "ban and delete most if not all code comments!\n\nagents love copying what already exists.",
|
|
57
|
+
"text": "ban and delete most if not all code comments!\n\nagents love copying what already exists. https://t.co/Xq7mPLe0Ab",
|
|
58
58
|
"key_points": [
|
|
59
59
|
"Agents imitate existing code, so stale workaround comments spread like weeds.",
|
|
60
60
|
"Keep only comments that must exist, such as browser-quirk explanations."
|
|
61
61
|
],
|
|
62
62
|
"action": "Have an agent run /no-comments on one repo and sort each comment into refactor or delete.",
|
|
63
|
-
"links": [{ "title": "pstack: the /no-comments skill", "url": "https://github.com/example/pstack" }]
|
|
63
|
+
"links": [{ "title": "pstack: the /no-comments skill", "url": "https://github.com/example/pstack" }],
|
|
64
|
+
"media": [
|
|
65
|
+
{
|
|
66
|
+
"url": "https://pbs.twimg.com/media/G3xAmPLeAAAbCdE.jpg",
|
|
67
|
+
"alt": "A diff that deletes a workaround comment",
|
|
68
|
+
"link": "https://t.co/Xq7mPLe0Ab"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
64
71
|
}
|
|
65
72
|
```
|
|
66
73
|
|
|
67
|
-
Write every field, in this order. The build rejects an entry with a missing field or a field not listed here. `text` and `action` may be empty strings, and `key_points` and `
|
|
74
|
+
Write every field, in this order. The build rejects an entry with a missing field or a field not listed here. `text` and `action` may be empty strings, and `key_points`, `links`, and `media` may be empty arrays. Entries saved before `media` existed have no `media` field; that is valid, and you don't need to add one.
|
|
68
75
|
|
|
69
76
|
### id
|
|
70
77
|
|
|
@@ -125,7 +132,7 @@ One or two sentences on what the post says and why it matters. For a thread, sum
|
|
|
125
132
|
|
|
126
133
|
### text
|
|
127
134
|
|
|
128
|
-
The post's own words, copied exactly as posted, with its line breaks. For a thread, the author's posts in order, separated by a blank line. Write `""` when the post has no text.
|
|
135
|
+
The post's own words, copied exactly as posted, with its line breaks. For a thread, the author's posts in order, separated by a blank line. Keep a `t.co` line that stands for an image or video as it is; the image itself goes in `media`. Write `""` when the post has no text.
|
|
129
136
|
|
|
130
137
|
Keep it complete. Most AI assistants can't open x.com, so when the owner copies a bookmark into one, this text is all it gets of the post.
|
|
131
138
|
|
|
@@ -151,6 +158,19 @@ Every link in the post and its thread, followed to where it really lands. Expand
|
|
|
151
158
|
- Leave out links to the post itself, to the author's profile, and to the post's images or videos.
|
|
152
159
|
- Write `[]` when there are none.
|
|
153
160
|
|
|
161
|
+
### media
|
|
162
|
+
|
|
163
|
+
The images in the post and its thread, in thread order, so the card can show each one with the post it came from. One object per photo:
|
|
164
|
+
|
|
165
|
+
- `url`: the image's `https://pbs.twimg.com/` address, such as `https://pbs.twimg.com/media/G3xAmPLeAAAbCdE.jpg`. A `?format=jpg&name=large` query is fine. Never a `t.co` link or an `x.com/…/photo/1` page; the build rejects any other host.
|
|
166
|
+
- `alt`: the author's alt text for the image when it has one, otherwise a few words on what it shows. Leave `alt` out rather than writing `""`.
|
|
167
|
+
- `link`: the short link that stands for the image in `text`, copied exactly as it appears there, such as `https://t.co/Xq7mPLe0Ab` or `pic.x.com/Xq7mPLe0Ab`. X gives all the photos of one post a single link, so photos from the same post share it. The card shows the images where that link is and hides the link. The build rejects a `link` that is not in `text`. Leave `link` out only when `text` has no link for the image; the card then shows it after the post.
|
|
168
|
+
- For a video or GIF, add its poster image (the `pbs.twimg.com` thumbnail X shows before it plays) when you can find it. Skip it otherwise.
|
|
169
|
+
- Don't add link-preview cards or the author's profile picture.
|
|
170
|
+
- Write `[]` when the post has no images.
|
|
171
|
+
|
|
172
|
+
Images never go in `links`, and `text` keeps the post's exact words, including any `t.co` lines that stood for the images.
|
|
173
|
+
|
|
154
174
|
## Change or remove a bookmark
|
|
155
175
|
|
|
156
176
|
To change one, edit its fields in place. Keep `id` and `saved_at`. Commit with `Update bookmark (@handle)`.
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ The site needs no Astro config. The package depends on the Astro version it was
|
|
|
63
63
|
import { SCHEMA_VERSION, bookmarkSchema, parseBookmarks, TOPICS } from "grok-digest/schema";
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
`SCHEMA_VERSION` is `1`. The JSON files have no version field. A `bookmarks.json` that already validates is version 1. `posted_at` with milliseconds and a `Z` offset, as the bot writes it, is version 1.
|
|
66
|
+
`SCHEMA_VERSION` is `1`. The JSON files have no version field. A `bookmarks.json` that already validates is version 1. `posted_at` with milliseconds and a `Z` offset, as the bot writes it, is version 1. The optional `media` field, a post's images, is part of version 1 too: a bookmark without it parses with `media` set to `[]`.
|
|
67
67
|
|
|
68
68
|
| File | Version 1 shape |
|
|
69
69
|
| --------------------- | -------------------------------------------------------------------- |
|
|
@@ -5,6 +5,7 @@ import { Button } from "./ui/button";
|
|
|
5
5
|
import { Card } from "./ui/card";
|
|
6
6
|
import type { Bookmark } from "../lib/bookmarks";
|
|
7
7
|
import { copyText } from "../lib/copy";
|
|
8
|
+
import { postParts } from "../lib/media";
|
|
8
9
|
import { TOPIC_LABELS } from "../lib/topics";
|
|
9
10
|
|
|
10
11
|
type Props = {
|
|
@@ -12,13 +13,13 @@ type Props = {
|
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
const { bookmark } = Astro.props;
|
|
15
|
-
const { id, url, author, postedAt, savedAt, topic, shelfLife, tldr, summary,
|
|
16
|
-
bookmark;
|
|
16
|
+
const { id, url, author, postedAt, savedAt, topic, shelfLife, tldr, summary, keyPoints, action, links } = bookmark;
|
|
17
17
|
|
|
18
18
|
const dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium", timeZone: "UTC" });
|
|
19
19
|
const isoDay = (date: Date) => date.toISOString().slice(0, 10);
|
|
20
20
|
const postedOtherDay = isoDay(postedAt) !== isoDay(savedAt);
|
|
21
21
|
const hostOf = (link: string) => new URL(link).hostname.replace(/^www\./, "");
|
|
22
|
+
const post = postParts(bookmark);
|
|
22
23
|
---
|
|
23
24
|
|
|
24
25
|
<li id={`b-${id}`} data-bookmark-id={id} data-url={url} data-topic={topic} class="group/bookmark scroll-mt-6">
|
|
@@ -88,11 +89,43 @@ const hostOf = (link: string) => new URL(link).hostname.replace(/^www\./, "");
|
|
|
88
89
|
))}
|
|
89
90
|
</ul>
|
|
90
91
|
)}
|
|
91
|
-
{
|
|
92
|
+
{post.length > 0 && (
|
|
92
93
|
<figure class="border-t border-border pt-4">
|
|
93
94
|
<figcaption class="mb-2 text-xs font-medium text-muted-foreground">Original post</figcaption>
|
|
94
|
-
<blockquote class="text-sm leading-relaxed
|
|
95
|
-
{
|
|
95
|
+
<blockquote class="flex flex-col gap-3 text-sm leading-relaxed text-pretty text-foreground/80">
|
|
96
|
+
{post.map((part) =>
|
|
97
|
+
part.kind === "text" ? (
|
|
98
|
+
<p class="whitespace-pre-wrap">{part.text}</p>
|
|
99
|
+
) : (
|
|
100
|
+
<ul
|
|
101
|
+
class:list={["grid max-w-md gap-2", { "grid-cols-2": part.images.length > 1 }]}
|
|
102
|
+
aria-label="Images in the post"
|
|
103
|
+
>
|
|
104
|
+
{part.images.map((image, index) => (
|
|
105
|
+
<li
|
|
106
|
+
class:list={{
|
|
107
|
+
"col-span-2": index === 0 && part.images.length > 1 && part.images.length % 2 === 1,
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
<a
|
|
111
|
+
href={image.url}
|
|
112
|
+
target="_blank"
|
|
113
|
+
rel="noopener noreferrer"
|
|
114
|
+
class="block overflow-hidden rounded-lg border border-border bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none"
|
|
115
|
+
>
|
|
116
|
+
<img
|
|
117
|
+
src={image.url}
|
|
118
|
+
alt={image.alt}
|
|
119
|
+
loading="lazy"
|
|
120
|
+
decoding="async"
|
|
121
|
+
class="aspect-video w-full object-cover"
|
|
122
|
+
/>
|
|
123
|
+
</a>
|
|
124
|
+
</li>
|
|
125
|
+
))}
|
|
126
|
+
</ul>
|
|
127
|
+
),
|
|
128
|
+
)}
|
|
96
129
|
</blockquote>
|
|
97
130
|
</figure>
|
|
98
131
|
)}
|
package/dist/lib/bookmarks.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export declare const bookmarkSchema: z.ZodPipe<z.ZodObject<{
|
|
|
31
31
|
title: z.ZodString;
|
|
32
32
|
url: z.ZodURL;
|
|
33
33
|
}, z.core.$strict>>;
|
|
34
|
+
media: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
35
|
+
url: z.ZodURL;
|
|
36
|
+
alt: z.ZodOptional<z.ZodString>;
|
|
37
|
+
link: z.ZodOptional<z.ZodString>;
|
|
38
|
+
}, z.core.$strict>>>;
|
|
34
39
|
}, z.core.$strict>, z.ZodTransform<{
|
|
35
40
|
shelfLife: "news" | "evergreen";
|
|
36
41
|
postedAt: Date;
|
|
@@ -51,6 +56,11 @@ export declare const bookmarkSchema: z.ZodPipe<z.ZodObject<{
|
|
|
51
56
|
title: string;
|
|
52
57
|
url: string;
|
|
53
58
|
}[];
|
|
59
|
+
media: {
|
|
60
|
+
url: string;
|
|
61
|
+
alt?: string | undefined;
|
|
62
|
+
link?: string | undefined;
|
|
63
|
+
}[];
|
|
54
64
|
}, {
|
|
55
65
|
id: string;
|
|
56
66
|
url: string;
|
|
@@ -71,6 +81,11 @@ export declare const bookmarkSchema: z.ZodPipe<z.ZodObject<{
|
|
|
71
81
|
title: string;
|
|
72
82
|
url: string;
|
|
73
83
|
}[];
|
|
84
|
+
media: {
|
|
85
|
+
url: string;
|
|
86
|
+
alt?: string | undefined;
|
|
87
|
+
link?: string | undefined;
|
|
88
|
+
}[];
|
|
74
89
|
}>>;
|
|
75
90
|
export declare const bookmarksFileSchema: z.ZodArray<z.ZodPipe<z.ZodObject<{
|
|
76
91
|
id: z.ZodString;
|
|
@@ -103,6 +118,11 @@ export declare const bookmarksFileSchema: z.ZodArray<z.ZodPipe<z.ZodObject<{
|
|
|
103
118
|
title: z.ZodString;
|
|
104
119
|
url: z.ZodURL;
|
|
105
120
|
}, z.core.$strict>>;
|
|
121
|
+
media: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
122
|
+
url: z.ZodURL;
|
|
123
|
+
alt: z.ZodOptional<z.ZodString>;
|
|
124
|
+
link: z.ZodOptional<z.ZodString>;
|
|
125
|
+
}, z.core.$strict>>>;
|
|
106
126
|
}, z.core.$strict>, z.ZodTransform<{
|
|
107
127
|
shelfLife: "news" | "evergreen";
|
|
108
128
|
postedAt: Date;
|
|
@@ -123,6 +143,11 @@ export declare const bookmarksFileSchema: z.ZodArray<z.ZodPipe<z.ZodObject<{
|
|
|
123
143
|
title: string;
|
|
124
144
|
url: string;
|
|
125
145
|
}[];
|
|
146
|
+
media: {
|
|
147
|
+
url: string;
|
|
148
|
+
alt?: string | undefined;
|
|
149
|
+
link?: string | undefined;
|
|
150
|
+
}[];
|
|
126
151
|
}, {
|
|
127
152
|
id: string;
|
|
128
153
|
url: string;
|
|
@@ -143,6 +168,11 @@ export declare const bookmarksFileSchema: z.ZodArray<z.ZodPipe<z.ZodObject<{
|
|
|
143
168
|
title: string;
|
|
144
169
|
url: string;
|
|
145
170
|
}[];
|
|
171
|
+
media: {
|
|
172
|
+
url: string;
|
|
173
|
+
alt?: string | undefined;
|
|
174
|
+
link?: string | undefined;
|
|
175
|
+
}[];
|
|
146
176
|
}>>>;
|
|
147
177
|
export declare const stateFileSchema: z.ZodArray<z.ZodString>;
|
|
148
178
|
export type BookmarkFileEntry = z.input<typeof bookmarkSchema>;
|
package/dist/lib/bookmarks.js
CHANGED
|
@@ -3,6 +3,7 @@ import { TOPICS } from "./topics.js";
|
|
|
3
3
|
export const SCHEMA_VERSION = 1;
|
|
4
4
|
const X_HOSTNAME = /^(www\.|mobile\.)?(x|twitter)\.com$/;
|
|
5
5
|
const X_HANDLE = /^[A-Za-z0-9_]{1,15}$/;
|
|
6
|
+
const X_MEDIA_HOSTNAME = /^pbs\.twimg\.com$/;
|
|
6
7
|
const nonEmpty = z.string().trim().min(1);
|
|
7
8
|
const instant = z.iso.datetime({ offset: true });
|
|
8
9
|
const SHELF_LIVES = ["news", "evergreen"];
|
|
@@ -28,6 +29,20 @@ export const bookmarkSchema = z
|
|
|
28
29
|
key_points: z.array(nonEmpty),
|
|
29
30
|
action: z.string().trim(),
|
|
30
31
|
links: z.array(z.strictObject({ title: nonEmpty, url: z.url({ protocol: /^https$/ }) })),
|
|
32
|
+
media: z
|
|
33
|
+
.array(z.strictObject({
|
|
34
|
+
url: z.url({ protocol: /^https$/, hostname: X_MEDIA_HOSTNAME }),
|
|
35
|
+
alt: nonEmpty.optional(),
|
|
36
|
+
link: nonEmpty.optional(),
|
|
37
|
+
}))
|
|
38
|
+
.default([]),
|
|
39
|
+
})
|
|
40
|
+
.superRefine(({ text, media }, ctx) => {
|
|
41
|
+
media.forEach(({ link }, index) => {
|
|
42
|
+
if (link !== undefined && !text.includes(link)) {
|
|
43
|
+
ctx.addIssue({ code: "custom", path: ["media", index, "link"], message: `"${link}" is not in text` });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
31
46
|
})
|
|
32
47
|
.transform(({ posted_at, saved_at, shelf_life, key_points, ...rest }) => ({
|
|
33
48
|
...rest,
|
package/dist/lib/copy.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { Bookmark } from "./bookmarks.ts";
|
|
2
|
-
export declare function copyText({ url, author, postedAt, tldr, summary, text, keyPoints, action, links }: Bookmark): string;
|
|
2
|
+
export declare function copyText({ url, author, postedAt, tldr, summary, text, media, keyPoints, action, links, }: Bookmark): string;
|
package/dist/lib/copy.js
CHANGED
|
@@ -8,11 +8,12 @@ function quote(text) {
|
|
|
8
8
|
function list(heading, items) {
|
|
9
9
|
return items.length === 0 ? "" : [`${heading}:`, ...items.map((item) => `- ${item}`)].join("\n");
|
|
10
10
|
}
|
|
11
|
-
export function copyText({ url, author, postedAt, tldr, summary, text, keyPoints, action, links }) {
|
|
11
|
+
export function copyText({ url, author, postedAt, tldr, summary, text, media, keyPoints, action, links, }) {
|
|
12
12
|
return [
|
|
13
13
|
"A post I saved from X:",
|
|
14
14
|
`**${tldr}**\n${author.name} (@${author.handle}) · posted ${dateFormat.format(postedAt)} · ${url}`,
|
|
15
15
|
text === "" ? "" : quote(text),
|
|
16
|
+
list("Images", media.map((image) => (image.alt === undefined ? image.url : `${image.alt}: ${image.url}`))),
|
|
16
17
|
`Summary: ${summary}`,
|
|
17
18
|
list("Key points", keyPoints),
|
|
18
19
|
action === "" ? "" : `Try: ${action}`,
|
package/dist/lib/feed.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isTopic } from "./filter.js";
|
|
2
|
+
import { postParts } from "./media.js";
|
|
2
3
|
import { TOPIC_LABELS } from "./topics.js";
|
|
3
4
|
const LOCAL_ORIGIN = "http://localhost:4321";
|
|
4
5
|
export function feedOrigin(site) {
|
|
@@ -29,7 +30,11 @@ function feedItem(bookmark, page) {
|
|
|
29
30
|
const link = new URL(page.href);
|
|
30
31
|
link.hash = `b-${id}`;
|
|
31
32
|
const points = keyPoints.map((point) => `<li>${escapeHtml(point)}</li>`).join("");
|
|
32
|
-
const
|
|
33
|
+
const quoted = postParts(bookmark)
|
|
34
|
+
.flatMap((part) => part.kind === "text"
|
|
35
|
+
? [`<p>${escapeHtml(part.text)}</p>`]
|
|
36
|
+
: part.images.map((image) => `<p><img src="${escapeHtml(image.url)}" alt="${escapeHtml(image.alt)}"></p>`))
|
|
37
|
+
.join("");
|
|
33
38
|
const linkItems = links
|
|
34
39
|
.map((outbound) => `<li><a href="${escapeHtml(outbound.url)}">${escapeHtml(outbound.title)}</a></li>`)
|
|
35
40
|
.join("");
|
|
@@ -43,7 +48,7 @@ function feedItem(bookmark, page) {
|
|
|
43
48
|
action === "" ? "" : `<p>Try: ${escapeHtml(action)}</p>`,
|
|
44
49
|
points === "" ? "" : `<ul>${points}</ul>`,
|
|
45
50
|
linkItems === "" ? "" : `<ul>${linkItems}</ul>`,
|
|
46
|
-
|
|
51
|
+
quoted === "" ? "" : `<blockquote>${quoted}</blockquote>`,
|
|
47
52
|
`<p><a href="${escapeHtml(url)}">Open on X</a></p>`,
|
|
48
53
|
]
|
|
49
54
|
.filter((part) => part !== "")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Bookmark } from "./bookmarks.ts";
|
|
2
|
+
type Image = {
|
|
3
|
+
url: string;
|
|
4
|
+
alt: string;
|
|
5
|
+
};
|
|
6
|
+
type PostPart = {
|
|
7
|
+
kind: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "images";
|
|
11
|
+
images: Image[];
|
|
12
|
+
};
|
|
13
|
+
export declare function postParts({ author, text, media }: Bookmark): PostPart[];
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function postParts({ author, text, media }) {
|
|
2
|
+
const position = (index) => (media.length === 1 ? "" : ` ${index + 1} of ${media.length}`);
|
|
3
|
+
const images = media.map(({ url, alt, link }, index) => ({
|
|
4
|
+
url,
|
|
5
|
+
alt: alt ?? `Image${position(index)} from the post by @${author.handle}`,
|
|
6
|
+
link,
|
|
7
|
+
}));
|
|
8
|
+
const imagesFor = (link) => ({
|
|
9
|
+
kind: "images",
|
|
10
|
+
images: images.filter((image) => image.link === link).map(({ url, alt }) => ({ url, alt })),
|
|
11
|
+
});
|
|
12
|
+
const links = [...new Set(images.flatMap(({ link }) => (link === undefined ? [] : [link])))].toSorted((a, b) => text.indexOf(a) - text.indexOf(b));
|
|
13
|
+
const parts = [];
|
|
14
|
+
let rest = text;
|
|
15
|
+
for (const link of links) {
|
|
16
|
+
const at = rest.indexOf(link);
|
|
17
|
+
parts.push({ kind: "text", text: rest.slice(0, at).trim() }, imagesFor(link));
|
|
18
|
+
rest = rest.slice(at + link.length);
|
|
19
|
+
}
|
|
20
|
+
parts.push({ kind: "text", text: rest.trim() }, imagesFor(undefined));
|
|
21
|
+
return parts.filter((part) => (part.kind === "text" ? part.text !== "" : part.images.length > 0));
|
|
22
|
+
}
|
package/package.json
CHANGED