grok-digest 0.3.1 → 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 CHANGED
@@ -54,7 +54,7 @@ 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."
@@ -62,7 +62,11 @@ bun run validate
62
62
  "action": "Have an agent run /no-comments on one repo and sort each comment into refactor or delete.",
63
63
  "links": [{ "title": "pstack: the /no-comments skill", "url": "https://github.com/example/pstack" }],
64
64
  "media": [
65
- { "url": "https://pbs.twimg.com/media/G3xAmPLeAAAbCdE.jpg", "alt": "A diff that deletes a workaround comment" }
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
+ }
66
70
  ]
67
71
  }
68
72
  ```
@@ -156,10 +160,11 @@ Every link in the post and its thread, followed to where it really lands. Expand
156
160
 
157
161
  ### media
158
162
 
159
- The images in the post and its thread, in thread order, so the card can show them. One object per photo:
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:
160
164
 
161
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.
162
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.
163
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.
164
169
  - Don't add link-preview cards or the author's profile picture.
165
170
  - Write `[]` when the post has no images.
@@ -5,7 +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 { imagesWithAlt } from "../lib/media";
8
+ import { postParts } from "../lib/media";
9
9
  import { TOPIC_LABELS } from "../lib/topics";
10
10
 
11
11
  type Props = {
@@ -13,14 +13,13 @@ type Props = {
13
13
  };
14
14
 
15
15
  const { bookmark } = Astro.props;
16
- const { id, url, author, postedAt, savedAt, topic, shelfLife, tldr, summary, text, keyPoints, action, links } =
17
- bookmark;
16
+ const { id, url, author, postedAt, savedAt, topic, shelfLife, tldr, summary, keyPoints, action, links } = bookmark;
18
17
 
19
18
  const dateFormat = new Intl.DateTimeFormat("en", { dateStyle: "medium", timeZone: "UTC" });
20
19
  const isoDay = (date: Date) => date.toISOString().slice(0, 10);
21
20
  const postedOtherDay = isoDay(postedAt) !== isoDay(savedAt);
22
21
  const hostOf = (link: string) => new URL(link).hostname.replace(/^www\./, "");
23
- const images = imagesWithAlt(bookmark);
22
+ const post = postParts(bookmark);
24
23
  ---
25
24
 
26
25
  <li id={`b-${id}`} data-bookmark-id={id} data-url={url} data-topic={topic} class="group/bookmark scroll-mt-6">
@@ -90,39 +89,44 @@ const images = imagesWithAlt(bookmark);
90
89
  ))}
91
90
  </ul>
92
91
  )}
93
- {(text !== "" || images.length > 0) && (
92
+ {post.length > 0 && (
94
93
  <figure class="border-t border-border pt-4">
95
94
  <figcaption class="mb-2 text-xs font-medium text-muted-foreground">Original post</figcaption>
96
- {text !== "" && (
97
- <blockquote class="text-sm leading-relaxed whitespace-pre-wrap text-pretty text-foreground/80">
98
- {text}
99
- </blockquote>
100
- )}
101
- {images.length > 0 && (
102
- <ul
103
- class:list={["grid max-w-md gap-2", { "grid-cols-2": images.length > 1, "mt-3": text !== "" }]}
104
- aria-label="Images in the post"
105
- >
106
- {images.map((image, index) => (
107
- <li class:list={{ "col-span-2": index === 0 && images.length > 1 && images.length % 2 === 1 }}>
108
- <a
109
- href={image.url}
110
- target="_blank"
111
- rel="noopener noreferrer"
112
- class="block overflow-hidden rounded-lg border border-border bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none"
113
- >
114
- <img
115
- src={image.url}
116
- alt={image.alt}
117
- loading="lazy"
118
- decoding="async"
119
- class="aspect-video w-full object-cover"
120
- />
121
- </a>
122
- </li>
123
- ))}
124
- </ul>
125
- )}
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
+ )}
129
+ </blockquote>
126
130
  </figure>
127
131
  )}
128
132
  </div>
@@ -34,6 +34,7 @@ export declare const bookmarkSchema: z.ZodPipe<z.ZodObject<{
34
34
  media: z.ZodDefault<z.ZodArray<z.ZodObject<{
35
35
  url: z.ZodURL;
36
36
  alt: z.ZodOptional<z.ZodString>;
37
+ link: z.ZodOptional<z.ZodString>;
37
38
  }, z.core.$strict>>>;
38
39
  }, z.core.$strict>, z.ZodTransform<{
39
40
  shelfLife: "news" | "evergreen";
@@ -58,6 +59,7 @@ export declare const bookmarkSchema: z.ZodPipe<z.ZodObject<{
58
59
  media: {
59
60
  url: string;
60
61
  alt?: string | undefined;
62
+ link?: string | undefined;
61
63
  }[];
62
64
  }, {
63
65
  id: string;
@@ -82,6 +84,7 @@ export declare const bookmarkSchema: z.ZodPipe<z.ZodObject<{
82
84
  media: {
83
85
  url: string;
84
86
  alt?: string | undefined;
87
+ link?: string | undefined;
85
88
  }[];
86
89
  }>>;
87
90
  export declare const bookmarksFileSchema: z.ZodArray<z.ZodPipe<z.ZodObject<{
@@ -118,6 +121,7 @@ export declare const bookmarksFileSchema: z.ZodArray<z.ZodPipe<z.ZodObject<{
118
121
  media: z.ZodDefault<z.ZodArray<z.ZodObject<{
119
122
  url: z.ZodURL;
120
123
  alt: z.ZodOptional<z.ZodString>;
124
+ link: z.ZodOptional<z.ZodString>;
121
125
  }, z.core.$strict>>>;
122
126
  }, z.core.$strict>, z.ZodTransform<{
123
127
  shelfLife: "news" | "evergreen";
@@ -142,6 +146,7 @@ export declare const bookmarksFileSchema: z.ZodArray<z.ZodPipe<z.ZodObject<{
142
146
  media: {
143
147
  url: string;
144
148
  alt?: string | undefined;
149
+ link?: string | undefined;
145
150
  }[];
146
151
  }, {
147
152
  id: string;
@@ -166,6 +171,7 @@ export declare const bookmarksFileSchema: z.ZodArray<z.ZodPipe<z.ZodObject<{
166
171
  media: {
167
172
  url: string;
168
173
  alt?: string | undefined;
174
+ link?: string | undefined;
169
175
  }[];
170
176
  }>>>;
171
177
  export declare const stateFileSchema: z.ZodArray<z.ZodString>;
@@ -30,8 +30,19 @@ export const bookmarkSchema = z
30
30
  action: z.string().trim(),
31
31
  links: z.array(z.strictObject({ title: nonEmpty, url: z.url({ protocol: /^https$/ }) })),
32
32
  media: z
33
- .array(z.strictObject({ url: z.url({ protocol: /^https$/, hostname: X_MEDIA_HOSTNAME }), alt: nonEmpty.optional() }))
33
+ .array(z.strictObject({
34
+ url: z.url({ protocol: /^https$/, hostname: X_MEDIA_HOSTNAME }),
35
+ alt: nonEmpty.optional(),
36
+ link: nonEmpty.optional(),
37
+ }))
34
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
+ });
35
46
  })
36
47
  .transform(({ posted_at, saved_at, shelf_life, key_points, ...rest }) => ({
37
48
  ...rest,
package/dist/lib/feed.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isTopic } from "./filter.js";
2
- import { imagesWithAlt } from "./media.js";
2
+ import { postParts } from "./media.js";
3
3
  import { TOPIC_LABELS } from "./topics.js";
4
4
  const LOCAL_ORIGIN = "http://localhost:4321";
5
5
  export function feedOrigin(site) {
@@ -30,9 +30,10 @@ function feedItem(bookmark, page) {
30
30
  const link = new URL(page.href);
31
31
  link.hash = `b-${id}`;
32
32
  const points = keyPoints.map((point) => `<li>${escapeHtml(point)}</li>`).join("");
33
- const quote = text === "" ? "" : `<blockquote><p>${escapeHtml(text)}</p></blockquote>`;
34
- const images = imagesWithAlt(bookmark)
35
- .map((image) => `<p><img src="${escapeHtml(image.url)}" alt="${escapeHtml(image.alt)}"></p>`)
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>`))
36
37
  .join("");
37
38
  const linkItems = links
38
39
  .map((outbound) => `<li><a href="${escapeHtml(outbound.url)}">${escapeHtml(outbound.title)}</a></li>`)
@@ -47,8 +48,7 @@ function feedItem(bookmark, page) {
47
48
  action === "" ? "" : `<p>Try: ${escapeHtml(action)}</p>`,
48
49
  points === "" ? "" : `<ul>${points}</ul>`,
49
50
  linkItems === "" ? "" : `<ul>${linkItems}</ul>`,
50
- quote,
51
- images,
51
+ quoted === "" ? "" : `<blockquote>${quoted}</blockquote>`,
52
52
  `<p><a href="${escapeHtml(url)}">Open on X</a></p>`,
53
53
  ]
54
54
  .filter((part) => part !== "")
@@ -21,5 +21,6 @@ export declare const bookmarks: {
21
21
  media: {
22
22
  url: string;
23
23
  alt?: string | undefined;
24
+ link?: string | undefined;
24
25
  }[];
25
26
  }[];
@@ -1,5 +1,14 @@
1
1
  import type { Bookmark } from "./bookmarks.ts";
2
- export declare function imagesWithAlt({ author, media }: Bookmark): {
2
+ type Image = {
3
3
  url: string;
4
4
  alt: string;
5
- }[];
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 {};
package/dist/lib/media.js CHANGED
@@ -1,7 +1,22 @@
1
- export function imagesWithAlt({ author, media }) {
1
+ export function postParts({ author, text, media }) {
2
2
  const position = (index) => (media.length === 1 ? "" : ` ${index + 1} of ${media.length}`);
3
- return media.map(({ url, alt }, index) => ({
3
+ const images = media.map(({ url, alt, link }, index) => ({
4
4
  url,
5
5
  alt: alt ?? `Image${position(index)} from the post by @${author.handle}`,
6
+ link,
6
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));
7
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grok-digest",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Astro integration that renders a Grok Digest site: X bookmarks summarized by Grok, searchable, sorted by topic, with RSS feeds.",
5
5
  "keywords": [
6
6
  "astro",