birdclaw 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +55 -5
  3. package/bin/birdclaw.mjs +50 -11
  4. package/package.json +9 -7
  5. package/public/birdclaw-mark.png +0 -0
  6. package/scripts/browser-perf.mjs +400 -0
  7. package/scripts/build-docs-site.mjs +940 -0
  8. package/scripts/docs-site-assets.mjs +311 -0
  9. package/scripts/run-vitest.mjs +21 -0
  10. package/scripts/sanitize-node-options.mjs +23 -0
  11. package/scripts/start-test-server.mjs +42 -0
  12. package/src/cli.ts +422 -14
  13. package/src/components/AccountSwitcher.tsx +186 -0
  14. package/src/components/AppNav.tsx +29 -9
  15. package/src/components/AvatarChip.tsx +9 -3
  16. package/src/components/BrandMark.tsx +67 -0
  17. package/src/components/ConversationThread.tsx +11 -10
  18. package/src/components/DmWorkspace.tsx +39 -14
  19. package/src/components/FeedState.tsx +147 -0
  20. package/src/components/InboxCard.tsx +14 -2
  21. package/src/components/LinkPreviewCard.tsx +40 -18
  22. package/src/components/MarkdownViewer.tsx +452 -0
  23. package/src/components/SavedTimelineView.tsx +64 -56
  24. package/src/components/SyncNowButton.tsx +137 -0
  25. package/src/components/ThemeSlider.tsx +49 -93
  26. package/src/components/TimelineCard.tsx +364 -136
  27. package/src/components/TimelineRouteFrame.tsx +170 -0
  28. package/src/components/TweetMediaGrid.tsx +170 -24
  29. package/src/components/TweetRichText.tsx +28 -13
  30. package/src/components/account-selection.ts +64 -0
  31. package/src/components/useTimelineRouteData.ts +153 -0
  32. package/src/lib/account-sync-job.ts +654 -0
  33. package/src/lib/actions-transport.ts +216 -146
  34. package/src/lib/api-client.ts +304 -0
  35. package/src/lib/archive-finder.ts +72 -53
  36. package/src/lib/archive-import.ts +1377 -1298
  37. package/src/lib/authored-live.ts +261 -204
  38. package/src/lib/avatar-cache.ts +159 -44
  39. package/src/lib/backup.ts +1532 -951
  40. package/src/lib/bird-actions.ts +139 -57
  41. package/src/lib/bird-command.ts +101 -28
  42. package/src/lib/bird.ts +549 -194
  43. package/src/lib/blocklist.ts +40 -23
  44. package/src/lib/blocks-write.ts +129 -80
  45. package/src/lib/blocks.ts +165 -97
  46. package/src/lib/bookmark-sync-job.ts +250 -160
  47. package/src/lib/conversation-surface.ts +205 -0
  48. package/src/lib/db.ts +35 -3
  49. package/src/lib/dms-live.ts +720 -66
  50. package/src/lib/effect-runtime.ts +45 -0
  51. package/src/lib/follow-graph.ts +224 -180
  52. package/src/lib/http-effect.ts +222 -0
  53. package/src/lib/inbox.ts +74 -43
  54. package/src/lib/link-index.ts +88 -76
  55. package/src/lib/link-insights.ts +24 -0
  56. package/src/lib/link-preview-metadata.ts +472 -52
  57. package/src/lib/media-fetch.ts +286 -213
  58. package/src/lib/mention-threads-live.ts +352 -288
  59. package/src/lib/mentions-live.ts +390 -342
  60. package/src/lib/moderation-target.ts +102 -65
  61. package/src/lib/moderation-write.ts +77 -18
  62. package/src/lib/mutes-write.ts +129 -80
  63. package/src/lib/mutes.ts +8 -1
  64. package/src/lib/openai.ts +84 -53
  65. package/src/lib/period-digest.ts +953 -0
  66. package/src/lib/profile-affiliation-hydration.ts +93 -54
  67. package/src/lib/profile-hydration.ts +124 -72
  68. package/src/lib/profile-replies.ts +60 -43
  69. package/src/lib/profile-resolver.ts +402 -294
  70. package/src/lib/queries.ts +1024 -189
  71. package/src/lib/research.ts +165 -120
  72. package/src/lib/sqlite.ts +1 -0
  73. package/src/lib/timeline-collections-live.ts +204 -167
  74. package/src/lib/timeline-live.ts +60 -39
  75. package/src/lib/tweet-lookup.ts +30 -19
  76. package/src/lib/types.ts +38 -1
  77. package/src/lib/ui.ts +41 -10
  78. package/src/lib/url-expansion.ts +226 -55
  79. package/src/lib/url-safety.ts +220 -0
  80. package/src/lib/web-sync.ts +511 -0
  81. package/src/lib/whois.ts +166 -147
  82. package/src/lib/x-web.ts +102 -71
  83. package/src/lib/xurl.ts +681 -411
  84. package/src/routeTree.gen.ts +63 -0
  85. package/src/routes/__root.tsx +25 -5
  86. package/src/routes/api/action.tsx +127 -78
  87. package/src/routes/api/avatar.tsx +39 -30
  88. package/src/routes/api/blocks.tsx +26 -23
  89. package/src/routes/api/conversation.tsx +25 -14
  90. package/src/routes/api/inbox.tsx +27 -21
  91. package/src/routes/api/link-insights.tsx +31 -25
  92. package/src/routes/api/link-preview.tsx +25 -21
  93. package/src/routes/api/period-digest.tsx +123 -0
  94. package/src/routes/api/profile-hydrate.tsx +31 -28
  95. package/src/routes/api/query.tsx +79 -55
  96. package/src/routes/api/status.tsx +18 -10
  97. package/src/routes/api/sync.tsx +105 -0
  98. package/src/routes/dms.tsx +195 -55
  99. package/src/routes/inbox.tsx +32 -19
  100. package/src/routes/index.tsx +21 -127
  101. package/src/routes/links.tsx +50 -5
  102. package/src/routes/mentions.tsx +21 -127
  103. package/src/routes/today.tsx +441 -0
  104. package/src/styles.css +74 -11
  105. package/vite.config.ts +8 -0
package/src/lib/openai.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { Effect } from "effect";
2
+ import { runEffectPromise, tryPromise } from "./effect-runtime";
3
+
1
4
  export interface OpenAIInboxScore {
2
5
  score: number;
3
6
  summary: string;
@@ -22,65 +25,93 @@ function clampScore(value: number) {
22
25
  return Math.max(0, Math.min(100, Math.round(value)));
23
26
  }
24
27
 
25
- export async function scoreInboxItemWithOpenAI(
28
+ function toError(error: unknown) {
29
+ return error instanceof Error ? error : new Error(String(error));
30
+ }
31
+
32
+ function trySync<T>(try_: () => T) {
33
+ return Effect.try({
34
+ try: try_,
35
+ catch: toError,
36
+ });
37
+ }
38
+
39
+ export function scoreInboxItemWithOpenAIEffect(
26
40
  input: OpenAIInboxInput,
27
- ): Promise<OpenAIInboxScore> {
28
- const apiKey = process.env.OPENAI_API_KEY;
29
- if (!apiKey) {
30
- throw new Error("OPENAI_API_KEY is not set");
31
- }
41
+ ): Effect.Effect<OpenAIInboxScore, Error> {
42
+ return Effect.gen(function* () {
43
+ const apiKey = process.env.OPENAI_API_KEY;
44
+ if (!apiKey) {
45
+ return yield* Effect.fail(new Error("OPENAI_API_KEY is not set"));
46
+ }
32
47
 
33
- const model = process.env.BIRDCLAW_OPENAI_MODEL || "gpt-5.2";
34
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
35
- method: "POST",
36
- headers: {
37
- authorization: `Bearer ${apiKey}`,
38
- "content-type": "application/json",
39
- },
40
- body: JSON.stringify({
41
- model,
42
- response_format: { type: "json_object" },
43
- messages: [
44
- {
45
- role: "system",
46
- content:
47
- "You rank inbound Twitter mentions and DMs for Peter Steinberger. Return JSON only with keys score, summary, reasoning. Score 0-100. High score means worth replying soon. Prefer specific, actionable, novel, high-signal items. Penalize generic praise, low-context asks, and low-signal chatter. summary max 18 words. reasoning max 28 words.",
48
+ const model = process.env.BIRDCLAW_OPENAI_MODEL || "gpt-5.2";
49
+ const response = yield* tryPromise(() =>
50
+ fetch("https://api.openai.com/v1/chat/completions", {
51
+ method: "POST",
52
+ headers: {
53
+ authorization: `Bearer ${apiKey}`,
54
+ "content-type": "application/json",
48
55
  },
49
- {
50
- role: "user",
51
- content: JSON.stringify(input),
52
- },
53
- ],
54
- }),
55
- });
56
+ body: JSON.stringify({
57
+ model,
58
+ response_format: { type: "json_object" },
59
+ messages: [
60
+ {
61
+ role: "system",
62
+ content:
63
+ "You rank inbound Twitter mentions and DMs for Peter Steinberger. Return JSON only with keys score, summary, reasoning. Score 0-100. High score means worth replying soon. Prefer specific, actionable, novel, high-signal items. Penalize generic praise, low-context asks, and low-signal chatter. summary max 18 words. reasoning max 28 words.",
64
+ },
65
+ {
66
+ role: "user",
67
+ content: JSON.stringify(input),
68
+ },
69
+ ],
70
+ }),
71
+ }),
72
+ ).pipe(Effect.mapError(toError));
56
73
 
57
- if (!response.ok) {
58
- throw new Error(`OpenAI request failed: ${response.status}`);
59
- }
74
+ if (!response.ok) {
75
+ return yield* Effect.fail(
76
+ new Error(`OpenAI request failed: ${response.status}`),
77
+ );
78
+ }
60
79
 
61
- const payload = (await response.json()) as {
62
- choices?: Array<{
63
- message?: {
64
- content?: string;
65
- };
66
- }>;
67
- };
80
+ const payload = (yield* tryPromise(() => response.json()).pipe(
81
+ Effect.mapError(toError),
82
+ )) as {
83
+ choices?: Array<{
84
+ message?: {
85
+ content?: string;
86
+ };
87
+ }>;
88
+ };
68
89
 
69
- const content = payload.choices?.[0]?.message?.content;
70
- if (!content) {
71
- throw new Error("OpenAI returned no content");
72
- }
90
+ const content = payload.choices?.[0]?.message?.content;
91
+ if (!content) {
92
+ return yield* Effect.fail(new Error("OpenAI returned no content"));
93
+ }
73
94
 
74
- const parsed = JSON.parse(content) as {
75
- score?: number;
76
- summary?: string;
77
- reasoning?: string;
78
- };
95
+ const parsed = yield* trySync(
96
+ () =>
97
+ JSON.parse(content) as {
98
+ score?: number;
99
+ summary?: string;
100
+ reasoning?: string;
101
+ },
102
+ );
79
103
 
80
- return {
81
- model,
82
- score: clampScore(parsed.score ?? 0),
83
- summary: String(parsed.summary ?? "No summary"),
84
- reasoning: String(parsed.reasoning ?? "No reasoning"),
85
- };
104
+ return {
105
+ model,
106
+ score: clampScore(parsed.score ?? 0),
107
+ summary: String(parsed.summary ?? "No summary"),
108
+ reasoning: String(parsed.reasoning ?? "No reasoning"),
109
+ };
110
+ });
111
+ }
112
+
113
+ export function scoreInboxItemWithOpenAI(
114
+ input: OpenAIInboxInput,
115
+ ): Promise<OpenAIInboxScore> {
116
+ return runEffectPromise(scoreInboxItemWithOpenAIEffect(input));
86
117
  }