birdclaw 0.5.1 → 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 (89) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +50 -5
  3. package/package.json +3 -2
  4. package/scripts/browser-perf.mjs +1 -0
  5. package/scripts/start-test-server.mjs +16 -3
  6. package/src/cli.ts +376 -13
  7. package/src/components/AccountSwitcher.tsx +186 -0
  8. package/src/components/AppNav.tsx +27 -7
  9. package/src/components/AvatarChip.tsx +9 -3
  10. package/src/components/DmWorkspace.tsx +18 -8
  11. package/src/components/LinkPreviewCard.tsx +40 -18
  12. package/src/components/MarkdownViewer.tsx +452 -0
  13. package/src/components/SyncNowButton.tsx +57 -25
  14. package/src/components/ThemeSlider.tsx +55 -50
  15. package/src/components/TimelineCard.tsx +225 -93
  16. package/src/components/TimelineRouteFrame.tsx +22 -8
  17. package/src/components/TweetMediaGrid.tsx +87 -38
  18. package/src/components/TweetRichText.tsx +15 -11
  19. package/src/components/account-selection.ts +64 -0
  20. package/src/components/useTimelineRouteData.ts +23 -7
  21. package/src/lib/account-sync-job.ts +654 -0
  22. package/src/lib/actions-transport.ts +216 -146
  23. package/src/lib/api-client.ts +128 -53
  24. package/src/lib/archive-finder.ts +78 -63
  25. package/src/lib/archive-import.ts +1364 -1300
  26. package/src/lib/authored-live.ts +261 -204
  27. package/src/lib/avatar-cache.ts +159 -44
  28. package/src/lib/backup.ts +1532 -951
  29. package/src/lib/bird-actions.ts +139 -57
  30. package/src/lib/bird-command.ts +101 -28
  31. package/src/lib/bird.ts +549 -194
  32. package/src/lib/blocklist.ts +40 -23
  33. package/src/lib/blocks-write.ts +129 -80
  34. package/src/lib/blocks.ts +165 -97
  35. package/src/lib/bookmark-sync-job.ts +250 -160
  36. package/src/lib/conversation-surface.ts +79 -48
  37. package/src/lib/db.ts +33 -3
  38. package/src/lib/dms-live.ts +720 -66
  39. package/src/lib/effect-runtime.ts +45 -0
  40. package/src/lib/follow-graph.ts +224 -180
  41. package/src/lib/http-effect.ts +222 -0
  42. package/src/lib/inbox.ts +74 -43
  43. package/src/lib/link-index.ts +88 -76
  44. package/src/lib/link-insights.ts +24 -0
  45. package/src/lib/link-preview-metadata.ts +472 -52
  46. package/src/lib/media-fetch.ts +286 -213
  47. package/src/lib/mention-threads-live.ts +352 -288
  48. package/src/lib/mentions-live.ts +390 -342
  49. package/src/lib/moderation-target.ts +102 -65
  50. package/src/lib/moderation-write.ts +77 -18
  51. package/src/lib/mutes-write.ts +129 -80
  52. package/src/lib/mutes.ts +8 -1
  53. package/src/lib/openai.ts +84 -53
  54. package/src/lib/period-digest.ts +953 -0
  55. package/src/lib/profile-affiliation-hydration.ts +93 -54
  56. package/src/lib/profile-hydration.ts +124 -72
  57. package/src/lib/profile-replies.ts +60 -43
  58. package/src/lib/profile-resolver.ts +402 -294
  59. package/src/lib/queries.ts +969 -199
  60. package/src/lib/research.ts +165 -120
  61. package/src/lib/sqlite.ts +1 -0
  62. package/src/lib/timeline-collections-live.ts +204 -167
  63. package/src/lib/timeline-live.ts +60 -39
  64. package/src/lib/tweet-lookup.ts +30 -19
  65. package/src/lib/types.ts +38 -1
  66. package/src/lib/ui.ts +30 -7
  67. package/src/lib/url-expansion.ts +226 -55
  68. package/src/lib/url-safety.ts +220 -0
  69. package/src/lib/web-sync.ts +216 -148
  70. package/src/lib/whois.ts +166 -147
  71. package/src/lib/x-web.ts +102 -71
  72. package/src/lib/xurl.ts +681 -411
  73. package/src/routeTree.gen.ts +42 -0
  74. package/src/routes/__root.tsx +25 -5
  75. package/src/routes/api/action.tsx +127 -78
  76. package/src/routes/api/avatar.tsx +39 -30
  77. package/src/routes/api/blocks.tsx +26 -23
  78. package/src/routes/api/conversation.tsx +25 -14
  79. package/src/routes/api/inbox.tsx +27 -21
  80. package/src/routes/api/link-insights.tsx +31 -25
  81. package/src/routes/api/link-preview.tsx +25 -21
  82. package/src/routes/api/period-digest.tsx +123 -0
  83. package/src/routes/api/profile-hydrate.tsx +31 -28
  84. package/src/routes/api/query.tsx +79 -55
  85. package/src/routes/api/status.tsx +18 -10
  86. package/src/routes/api/sync.tsx +75 -29
  87. package/src/routes/dms.tsx +95 -28
  88. package/src/routes/inbox.tsx +32 -19
  89. package/src/routes/today.tsx +441 -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
  }