@stackbilt/aegis-core 0.1.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.
- package/package.json +96 -0
- package/schema.sql +586 -0
- package/src/adapters/voice/cloudflare-agent.ts +34 -0
- package/src/auth.ts +124 -0
- package/src/bluesky.ts +464 -0
- package/src/claude-tools/content.ts +188 -0
- package/src/claude-tools/email.ts +69 -0
- package/src/claude-tools/github.ts +440 -0
- package/src/claude-tools/goals.ts +116 -0
- package/src/claude-tools/index.ts +353 -0
- package/src/claude-tools/web.ts +59 -0
- package/src/claude.ts +406 -0
- package/src/codebeast.ts +200 -0
- package/src/composite.ts +715 -0
- package/src/content/column.ts +80 -0
- package/src/content/hero-image.ts +47 -0
- package/src/content/index.ts +27 -0
- package/src/content/journal.ts +91 -0
- package/src/content/roundtable.ts +163 -0
- package/src/core.ts +309 -0
- package/src/dashboard.ts +620 -0
- package/src/decision-docs.ts +284 -0
- package/src/dispatch.ts +13 -0
- package/src/edge-env.ts +58 -0
- package/src/email.ts +850 -0
- package/src/exports.ts +156 -0
- package/src/github-projects.ts +312 -0
- package/src/github.ts +670 -0
- package/src/groq.ts +247 -0
- package/src/health-page.ts +578 -0
- package/src/index.ts +89 -0
- package/src/kernel/argus-actions.ts +397 -0
- package/src/kernel/argus-correlation.ts +639 -0
- package/src/kernel/board.ts +91 -0
- package/src/kernel/briefing.ts +177 -0
- package/src/kernel/classify-memory-topic.ts +166 -0
- package/src/kernel/cognition.ts +377 -0
- package/src/kernel/court-cards.ts +163 -0
- package/src/kernel/dispatch.ts +587 -0
- package/src/kernel/domain.ts +50 -0
- package/src/kernel/dynamic-tools.ts +322 -0
- package/src/kernel/executor-port.ts +45 -0
- package/src/kernel/executors/claude.ts +73 -0
- package/src/kernel/executors/direct.ts +237 -0
- package/src/kernel/executors/groq.ts +18 -0
- package/src/kernel/executors/index.ts +87 -0
- package/src/kernel/executors/tarotscript.ts +104 -0
- package/src/kernel/executors/workers-ai.ts +54 -0
- package/src/kernel/insight-cache.ts +76 -0
- package/src/kernel/memory/agenda.ts +200 -0
- package/src/kernel/memory/blocks.ts +188 -0
- package/src/kernel/memory/consolidation.ts +194 -0
- package/src/kernel/memory/episodic.ts +241 -0
- package/src/kernel/memory/goals.ts +156 -0
- package/src/kernel/memory/graph.ts +290 -0
- package/src/kernel/memory/index.ts +11 -0
- package/src/kernel/memory/insights.ts +316 -0
- package/src/kernel/memory/procedural.ts +467 -0
- package/src/kernel/memory/pruning.ts +67 -0
- package/src/kernel/memory/recall.ts +367 -0
- package/src/kernel/memory/semantic.ts +315 -0
- package/src/kernel/memory/synthesis.ts +161 -0
- package/src/kernel/memory-adapter.ts +369 -0
- package/src/kernel/memory-guardrails.ts +76 -0
- package/src/kernel/port.ts +23 -0
- package/src/kernel/resilience.ts +322 -0
- package/src/kernel/router.ts +471 -0
- package/src/kernel/scheduled/agent-dispatch.ts +252 -0
- package/src/kernel/scheduled/argus-analytics.ts +247 -0
- package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
- package/src/kernel/scheduled/argus-notify.ts +348 -0
- package/src/kernel/scheduled/board-sync.ts +110 -0
- package/src/kernel/scheduled/ci-watcher.ts +125 -0
- package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
- package/src/kernel/scheduled/consolidation.ts +229 -0
- package/src/kernel/scheduled/content-drip.ts +47 -0
- package/src/kernel/scheduled/content.ts +6 -0
- package/src/kernel/scheduled/conversation-facts.ts +204 -0
- package/src/kernel/scheduled/cost-report.ts +84 -0
- package/src/kernel/scheduled/curiosity.ts +219 -0
- package/src/kernel/scheduled/dev-activity.ts +44 -0
- package/src/kernel/scheduled/digest.ts +317 -0
- package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
- package/src/kernel/scheduled/dreaming/facts.ts +239 -0
- package/src/kernel/scheduled/dreaming/index.ts +8 -0
- package/src/kernel/scheduled/dreaming/llm.ts +33 -0
- package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
- package/src/kernel/scheduled/dreaming/persona.ts +75 -0
- package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
- package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
- package/src/kernel/scheduled/dreaming.ts +66 -0
- package/src/kernel/scheduled/entropy.ts +149 -0
- package/src/kernel/scheduled/escalation.ts +192 -0
- package/src/kernel/scheduled/feed-watcher.ts +206 -0
- package/src/kernel/scheduled/goals.ts +214 -0
- package/src/kernel/scheduled/governance.ts +41 -0
- package/src/kernel/scheduled/heartbeat.ts +220 -0
- package/src/kernel/scheduled/inbox-processor.ts +174 -0
- package/src/kernel/scheduled/index.ts +245 -0
- package/src/kernel/scheduled/issue-proposer.ts +478 -0
- package/src/kernel/scheduled/issue-watcher.ts +128 -0
- package/src/kernel/scheduled/pr-automerge.ts +213 -0
- package/src/kernel/scheduled/product-health.ts +107 -0
- package/src/kernel/scheduled/reflection.ts +373 -0
- package/src/kernel/scheduled/self-improvement.ts +114 -0
- package/src/kernel/scheduled/social-engage.ts +175 -0
- package/src/kernel/scheduled/task-audit.ts +60 -0
- package/src/kernel/symbolic.ts +156 -0
- package/src/kernel/types.ts +145 -0
- package/src/landing.ts +1190 -0
- package/src/lib/audit-chain/chain.ts +28 -0
- package/src/lib/audit-chain/types.ts +12 -0
- package/src/lib/observability/errors.ts +55 -0
- package/src/markdown.ts +164 -0
- package/src/mcp/handlers.ts +647 -0
- package/src/mcp/server.ts +184 -0
- package/src/mcp/tools.ts +316 -0
- package/src/mcp-client.ts +275 -0
- package/src/mcp-server.ts +2 -0
- package/src/operator/config.example.ts +60 -0
- package/src/operator/config.ts +60 -0
- package/src/operator/index.ts +46 -0
- package/src/operator/persona.example.ts +34 -0
- package/src/operator/persona.ts +34 -0
- package/src/operator/prompt-builder.ts +190 -0
- package/src/operator/types.ts +43 -0
- package/src/pulse.ts +1179 -0
- package/src/routes/bluesky.ts +116 -0
- package/src/routes/cc-tasks.ts +328 -0
- package/src/routes/codebeast.ts +1 -0
- package/src/routes/content.ts +194 -0
- package/src/routes/conversations.ts +25 -0
- package/src/routes/dynamic-tools.ts +111 -0
- package/src/routes/feedback.ts +192 -0
- package/src/routes/health.ts +147 -0
- package/src/routes/messages.ts +228 -0
- package/src/routes/observability.ts +82 -0
- package/src/routes/operator-logs.ts +42 -0
- package/src/routes/pages.ts +96 -0
- package/src/routes/sessions.ts +54 -0
- package/src/sanitize.ts +73 -0
- package/src/schema-enums.ts +155 -0
- package/src/search.ts +112 -0
- package/src/task-intelligence.ts +497 -0
- package/src/types.ts +194 -0
- package/src/ui.ts +5 -0
- package/src/version.ts +3 -0
- package/src/workers-ai-chat.ts +333 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Agent } from 'agents';
|
|
2
|
+
import { withVoice, WorkersAIFluxSTT, WorkersAITTS } from '@cloudflare/voice';
|
|
3
|
+
import type { VoiceTurnContext } from '@cloudflare/voice';
|
|
4
|
+
import { buildEdgeEnv } from '../../edge-env.js';
|
|
5
|
+
import { KernelExecutorPort } from '../../kernel/executor-port.js';
|
|
6
|
+
import type { AegisTurnInput } from '../../kernel/port.js';
|
|
7
|
+
import type { Env } from '../../types.js';
|
|
8
|
+
|
|
9
|
+
// CF-specific mixin lives here and nowhere else — the kernel is never imported by voice code.
|
|
10
|
+
const VoiceAgent = withVoice(Agent);
|
|
11
|
+
|
|
12
|
+
export class AegisVoiceAdapter extends VoiceAgent<Env> {
|
|
13
|
+
transcriber = new WorkersAIFluxSTT(this.env.AI);
|
|
14
|
+
tts = new WorkersAITTS(this.env.AI);
|
|
15
|
+
|
|
16
|
+
async onTurn(transcript: string, ctx: VoiceTurnContext): Promise<AsyncIterable<string>> {
|
|
17
|
+
const input: AegisTurnInput = {
|
|
18
|
+
sessionId: ctx.connection.id,
|
|
19
|
+
userId: 'default-operator',
|
|
20
|
+
text: transcript,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const port = new KernelExecutorPort(buildEdgeEnv(this.env));
|
|
24
|
+
const eventStream = port.dispatch(input);
|
|
25
|
+
|
|
26
|
+
return (async function* () {
|
|
27
|
+
for await (const event of eventStream) {
|
|
28
|
+
if (event.type === 'text.delta') {
|
|
29
|
+
yield event.text;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Context, Next } from 'hono';
|
|
2
|
+
import type { Env } from './types.js';
|
|
3
|
+
|
|
4
|
+
export async function bearerAuth(c: Context<{ Bindings: Env }>, next: Next): Promise<Response | void> {
|
|
5
|
+
// Public routes — no auth required
|
|
6
|
+
if (c.req.path === '/health' || c.req.path === '/pulse' || (c.req.path === '/' && c.req.method === 'GET') || c.req.path.startsWith('/tech') || c.req.path === '/api/feedback' || c.req.path === '/observe' || c.req.path.startsWith('/api/overworld/public')) {
|
|
7
|
+
return next();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Webhook routes — auth handled by per-route HMAC verification
|
|
11
|
+
if (c.req.path.startsWith('/webhooks/') || c.req.path === '/api/webhook') {
|
|
12
|
+
return next();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// OAuth discovery endpoints — must be public so MCP clients can probe them
|
|
16
|
+
// without a token. We return 404 for the auth server and resource metadata
|
|
17
|
+
// so clients learn there is no OAuth server here and fall back to configured
|
|
18
|
+
// bearer token headers.
|
|
19
|
+
if (
|
|
20
|
+
c.req.path === '/.well-known/oauth-protected-resource' ||
|
|
21
|
+
c.req.path === '/.well-known/oauth-authorization-server' ||
|
|
22
|
+
c.req.path === '/.well-known/openid-configuration'
|
|
23
|
+
) {
|
|
24
|
+
return next();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const authHeader = c.req.header('Authorization');
|
|
28
|
+
const cookieToken = getCookie(c.req.header('Cookie') ?? '', 'aegis_token');
|
|
29
|
+
const queryToken = c.req.query('token');
|
|
30
|
+
|
|
31
|
+
const token = extractBearer(authHeader) ?? cookieToken ?? queryToken;
|
|
32
|
+
|
|
33
|
+
if (!token || token !== c.env.AEGIS_TOKEN) {
|
|
34
|
+
// UI pages — show login page
|
|
35
|
+
if ((c.req.path === '/chat' || c.req.path === '/overworld' || c.req.path === '/console') && c.req.method === 'GET') {
|
|
36
|
+
return c.html(loginPage(), 401);
|
|
37
|
+
}
|
|
38
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return next();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function extractBearer(header: string | undefined): string | null {
|
|
45
|
+
if (!header?.startsWith('Bearer ')) return null;
|
|
46
|
+
return header.slice(7);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getCookie(cookieHeader: string, name: string): string | null {
|
|
50
|
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
|
51
|
+
return match?.[1] ?? null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loginPage(): string {
|
|
55
|
+
return `<!DOCTYPE html>
|
|
56
|
+
<html lang="en">
|
|
57
|
+
<head>
|
|
58
|
+
<meta charset="utf-8">
|
|
59
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
60
|
+
<title>AEGIS</title>
|
|
61
|
+
<style>
|
|
62
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
63
|
+
body {
|
|
64
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
65
|
+
background: #0a0a0f;
|
|
66
|
+
color: #e0e0e0;
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
min-height: 100vh;
|
|
71
|
+
}
|
|
72
|
+
.login {
|
|
73
|
+
background: #141420;
|
|
74
|
+
border: 1px solid #2a2a3a;
|
|
75
|
+
border-radius: 12px;
|
|
76
|
+
padding: 2rem;
|
|
77
|
+
width: 90%;
|
|
78
|
+
max-width: 360px;
|
|
79
|
+
}
|
|
80
|
+
h1 { font-size: 1.25rem; margin-bottom: 1.5rem; color: #8b8bff; }
|
|
81
|
+
input {
|
|
82
|
+
width: 100%;
|
|
83
|
+
padding: 0.75rem;
|
|
84
|
+
background: #0a0a0f;
|
|
85
|
+
border: 1px solid #2a2a3a;
|
|
86
|
+
border-radius: 8px;
|
|
87
|
+
color: #e0e0e0;
|
|
88
|
+
font-size: 1rem;
|
|
89
|
+
margin-bottom: 1rem;
|
|
90
|
+
}
|
|
91
|
+
input:focus { outline: none; border-color: #8b8bff; }
|
|
92
|
+
button {
|
|
93
|
+
width: 100%;
|
|
94
|
+
padding: 0.75rem;
|
|
95
|
+
background: #8b8bff;
|
|
96
|
+
color: #0a0a0f;
|
|
97
|
+
border: none;
|
|
98
|
+
border-radius: 8px;
|
|
99
|
+
font-size: 1rem;
|
|
100
|
+
font-weight: 600;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
}
|
|
103
|
+
button:hover { background: #7a7aee; }
|
|
104
|
+
</style>
|
|
105
|
+
</head>
|
|
106
|
+
<body>
|
|
107
|
+
<div class="login">
|
|
108
|
+
<h1>AEGIS</h1>
|
|
109
|
+
<form id="f">
|
|
110
|
+
<input type="password" id="t" placeholder="Access token" autocomplete="off" autofocus>
|
|
111
|
+
<button type="submit">Enter</button>
|
|
112
|
+
</form>
|
|
113
|
+
</div>
|
|
114
|
+
<script>
|
|
115
|
+
document.getElementById('f').onsubmit = (e) => {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
const token = document.getElementById('t').value;
|
|
118
|
+
document.cookie = 'aegis_token=' + encodeURIComponent(token) + ';path=/;max-age=31536000;SameSite=Strict;Secure';
|
|
119
|
+
window.location.href = window.location.pathname;
|
|
120
|
+
};
|
|
121
|
+
</script>
|
|
122
|
+
</body>
|
|
123
|
+
</html>`;
|
|
124
|
+
}
|
package/src/bluesky.ts
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
// ─── Bluesky AT Protocol Client ──────────────────────────────
|
|
2
|
+
// Posts to Bluesky via the AT Protocol API.
|
|
3
|
+
// Auth: app password → createSession → accessJwt.
|
|
4
|
+
// Supports: text posts, links (facets), images (blob upload).
|
|
5
|
+
|
|
6
|
+
const BSKY_API = 'https://bsky.social/xrpc';
|
|
7
|
+
|
|
8
|
+
interface BlueskySession {
|
|
9
|
+
accessJwt: string;
|
|
10
|
+
refreshJwt: string;
|
|
11
|
+
did: string;
|
|
12
|
+
handle: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface BlueskyPostResult {
|
|
16
|
+
uri: string;
|
|
17
|
+
cid: string;
|
|
18
|
+
url: string; // human-readable URL
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─── Auth ────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
let cachedSession: { session: BlueskySession; expiresAt: number } | null = null;
|
|
24
|
+
|
|
25
|
+
async function createSession(handle: string, appPassword: string): Promise<BlueskySession> {
|
|
26
|
+
// Reuse cached session if not expired (tokens last ~2 hours, refresh at 90 min)
|
|
27
|
+
if (cachedSession && Date.now() < cachedSession.expiresAt) {
|
|
28
|
+
return cachedSession.session;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const res = await fetch(`${BSKY_API}/com.atproto.server.createSession`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ identifier: handle, password: appPassword }),
|
|
35
|
+
signal: AbortSignal.timeout(10_000),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const err = await res.text();
|
|
40
|
+
throw new Error(`Bluesky auth failed: ${res.status} ${err}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const session = await res.json() as BlueskySession;
|
|
44
|
+
cachedSession = { session, expiresAt: Date.now() + 90 * 60 * 1000 };
|
|
45
|
+
return session;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Facets (links in post text) ─────────────────────────────
|
|
49
|
+
|
|
50
|
+
interface Facet {
|
|
51
|
+
index: { byteStart: number; byteEnd: number };
|
|
52
|
+
features: Array<{ $type: string; uri?: string; did?: string }>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractLinkFacets(text: string): Facet[] {
|
|
56
|
+
const facets: Facet[] = [];
|
|
57
|
+
const urlRegex = /https?:\/\/[^\s)]+/g;
|
|
58
|
+
let match;
|
|
59
|
+
|
|
60
|
+
while ((match = urlRegex.exec(text)) !== null) {
|
|
61
|
+
const url = match[0];
|
|
62
|
+
// Byte offsets (AT Protocol uses UTF-8 byte positions)
|
|
63
|
+
const encoder = new TextEncoder();
|
|
64
|
+
const byteStart = encoder.encode(text.slice(0, match.index)).length;
|
|
65
|
+
const byteEnd = byteStart + encoder.encode(url).length;
|
|
66
|
+
|
|
67
|
+
facets.push({
|
|
68
|
+
index: { byteStart, byteEnd },
|
|
69
|
+
features: [{ $type: 'app.bsky.richtext.facet#link', uri: url }],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return facets;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Image Upload ────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
interface BlobRef {
|
|
79
|
+
$type: 'blob';
|
|
80
|
+
ref: { $link: string };
|
|
81
|
+
mimeType: string;
|
|
82
|
+
size: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function uploadImage(
|
|
86
|
+
session: BlueskySession,
|
|
87
|
+
imageUrl: string,
|
|
88
|
+
): Promise<BlobRef> {
|
|
89
|
+
// Fetch the image
|
|
90
|
+
const imgRes = await fetch(imageUrl, { signal: AbortSignal.timeout(30_000) });
|
|
91
|
+
if (!imgRes.ok) throw new Error(`Failed to fetch image: ${imgRes.status}`);
|
|
92
|
+
|
|
93
|
+
const contentType = imgRes.headers.get('content-type') ?? 'image/png';
|
|
94
|
+
const imageData = await imgRes.arrayBuffer();
|
|
95
|
+
|
|
96
|
+
// Upload blob to Bluesky
|
|
97
|
+
const uploadRes = await fetch(`${BSKY_API}/com.atproto.repo.uploadBlob`, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'Authorization': `Bearer ${session.accessJwt}`,
|
|
101
|
+
'Content-Type': contentType,
|
|
102
|
+
},
|
|
103
|
+
body: imageData,
|
|
104
|
+
signal: AbortSignal.timeout(30_000),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!uploadRes.ok) {
|
|
108
|
+
const err = await uploadRes.text();
|
|
109
|
+
throw new Error(`Bluesky blob upload failed: ${uploadRes.status} ${err}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const result = await uploadRes.json() as { blob: BlobRef };
|
|
113
|
+
return result.blob;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Post ────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export interface BlueskyPostOptions {
|
|
119
|
+
text: string;
|
|
120
|
+
imageUrl?: string;
|
|
121
|
+
imageAlt?: string;
|
|
122
|
+
langs?: string[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Public API: re-export session creator for routes ────────
|
|
126
|
+
|
|
127
|
+
export { createSession, extractLinkFacets, uploadImage, BSKY_API };
|
|
128
|
+
export type { BlueskySession, BlobRef, Facet };
|
|
129
|
+
|
|
130
|
+
export async function postToBluesky(
|
|
131
|
+
handle: string,
|
|
132
|
+
appPassword: string,
|
|
133
|
+
options: BlueskyPostOptions,
|
|
134
|
+
): Promise<BlueskyPostResult> {
|
|
135
|
+
const session = await createSession(handle, appPassword);
|
|
136
|
+
|
|
137
|
+
// Truncate to 300 chars (Bluesky limit)
|
|
138
|
+
const text = options.text.length > 300
|
|
139
|
+
? options.text.slice(0, 297) + '...'
|
|
140
|
+
: options.text;
|
|
141
|
+
|
|
142
|
+
const facets = extractLinkFacets(text);
|
|
143
|
+
|
|
144
|
+
const record: Record<string, unknown> = {
|
|
145
|
+
$type: 'app.bsky.feed.post',
|
|
146
|
+
text,
|
|
147
|
+
langs: options.langs ?? ['en'],
|
|
148
|
+
createdAt: new Date().toISOString(),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (facets.length > 0) {
|
|
152
|
+
record.facets = facets;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Upload and embed image if provided
|
|
156
|
+
if (options.imageUrl) {
|
|
157
|
+
try {
|
|
158
|
+
const blob = await uploadImage(session, options.imageUrl);
|
|
159
|
+
record.embed = {
|
|
160
|
+
$type: 'app.bsky.embed.images',
|
|
161
|
+
images: [{
|
|
162
|
+
alt: options.imageAlt ?? '',
|
|
163
|
+
image: blob,
|
|
164
|
+
}],
|
|
165
|
+
};
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.warn('[bluesky] Image upload failed (posting without image):', err instanceof Error ? err.message : String(err));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const res = await fetch(`${BSKY_API}/com.atproto.repo.createRecord`, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: {
|
|
174
|
+
'Authorization': `Bearer ${session.accessJwt}`,
|
|
175
|
+
'Content-Type': 'application/json',
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
repo: session.did,
|
|
179
|
+
collection: 'app.bsky.feed.post',
|
|
180
|
+
record,
|
|
181
|
+
}),
|
|
182
|
+
signal: AbortSignal.timeout(10_000),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!res.ok) {
|
|
186
|
+
const err = await res.text();
|
|
187
|
+
throw new Error(`Bluesky post failed: ${res.status} ${err}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const result = await res.json() as { uri: string; cid: string };
|
|
191
|
+
|
|
192
|
+
// Convert AT URI to web URL
|
|
193
|
+
// at://did:plc:xxx/app.bsky.feed.post/rkey → https://bsky.app/profile/handle/post/rkey
|
|
194
|
+
const rkey = result.uri.split('/').pop() ?? '';
|
|
195
|
+
const url = `https://bsky.app/profile/${session.handle}/post/${rkey}`;
|
|
196
|
+
|
|
197
|
+
console.log(`[bluesky] Posted: ${url}`);
|
|
198
|
+
|
|
199
|
+
return { uri: result.uri, cid: result.cid, url };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── Feed ───────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
export interface BlueskyFeedItem {
|
|
205
|
+
uri: string;
|
|
206
|
+
cid: string;
|
|
207
|
+
text: string;
|
|
208
|
+
createdAt: string;
|
|
209
|
+
likeCount: number;
|
|
210
|
+
repostCount: number;
|
|
211
|
+
replyCount: number;
|
|
212
|
+
url: string;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function getAuthorFeed(
|
|
216
|
+
actor: string,
|
|
217
|
+
limit = 20,
|
|
218
|
+
): Promise<BlueskyFeedItem[]> {
|
|
219
|
+
const url = `https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${encodeURIComponent(actor)}&limit=${limit}&filter=posts_no_replies`;
|
|
220
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
|
|
221
|
+
if (!res.ok) {
|
|
222
|
+
const err = await res.text();
|
|
223
|
+
throw new Error(`Bluesky feed fetch failed: ${res.status} ${err}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const data = await res.json() as { feed: Array<{ post: Record<string, unknown> }> };
|
|
227
|
+
|
|
228
|
+
return data.feed.map((item) => {
|
|
229
|
+
const p = item.post as Record<string, unknown>;
|
|
230
|
+
const rec = p.record as Record<string, unknown>;
|
|
231
|
+
const author = p.author as Record<string, unknown>;
|
|
232
|
+
const handle = (author.handle as string) ?? actor;
|
|
233
|
+
const rkey = (p.uri as string).split('/').pop() ?? '';
|
|
234
|
+
return {
|
|
235
|
+
uri: p.uri as string,
|
|
236
|
+
cid: p.cid as string,
|
|
237
|
+
text: (rec.text as string) ?? '',
|
|
238
|
+
createdAt: (rec.createdAt as string) ?? '',
|
|
239
|
+
likeCount: (p.likeCount as number) ?? 0,
|
|
240
|
+
repostCount: (p.repostCount as number) ?? 0,
|
|
241
|
+
replyCount: (p.replyCount as number) ?? 0,
|
|
242
|
+
url: `https://bsky.app/profile/${handle}/post/${rkey}`,
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── Like ───────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
export async function likePost(
|
|
250
|
+
handle: string,
|
|
251
|
+
appPassword: string,
|
|
252
|
+
targetUri: string,
|
|
253
|
+
targetCid: string,
|
|
254
|
+
): Promise<{ uri: string }> {
|
|
255
|
+
const session = await createSession(handle, appPassword);
|
|
256
|
+
|
|
257
|
+
const res = await fetch(`${BSKY_API}/com.atproto.repo.createRecord`, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: {
|
|
260
|
+
'Authorization': `Bearer ${session.accessJwt}`,
|
|
261
|
+
'Content-Type': 'application/json',
|
|
262
|
+
},
|
|
263
|
+
body: JSON.stringify({
|
|
264
|
+
repo: session.did,
|
|
265
|
+
collection: 'app.bsky.feed.like',
|
|
266
|
+
record: {
|
|
267
|
+
$type: 'app.bsky.feed.like',
|
|
268
|
+
subject: { uri: targetUri, cid: targetCid },
|
|
269
|
+
createdAt: new Date().toISOString(),
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
signal: AbortSignal.timeout(10_000),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (!res.ok) {
|
|
276
|
+
const err = await res.text();
|
|
277
|
+
throw new Error(`Bluesky like failed: ${res.status} ${err}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const result = await res.json() as { uri: string };
|
|
281
|
+
return { uri: result.uri };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─── Repost ─────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
export async function repostPost(
|
|
287
|
+
handle: string,
|
|
288
|
+
appPassword: string,
|
|
289
|
+
targetUri: string,
|
|
290
|
+
targetCid: string,
|
|
291
|
+
): Promise<{ uri: string }> {
|
|
292
|
+
const session = await createSession(handle, appPassword);
|
|
293
|
+
|
|
294
|
+
const res = await fetch(`${BSKY_API}/com.atproto.repo.createRecord`, {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
headers: {
|
|
297
|
+
'Authorization': `Bearer ${session.accessJwt}`,
|
|
298
|
+
'Content-Type': 'application/json',
|
|
299
|
+
},
|
|
300
|
+
body: JSON.stringify({
|
|
301
|
+
repo: session.did,
|
|
302
|
+
collection: 'app.bsky.feed.repost',
|
|
303
|
+
record: {
|
|
304
|
+
$type: 'app.bsky.feed.repost',
|
|
305
|
+
subject: { uri: targetUri, cid: targetCid },
|
|
306
|
+
createdAt: new Date().toISOString(),
|
|
307
|
+
},
|
|
308
|
+
}),
|
|
309
|
+
signal: AbortSignal.timeout(10_000),
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (!res.ok) {
|
|
313
|
+
const err = await res.text();
|
|
314
|
+
throw new Error(`Bluesky repost failed: ${res.status} ${err}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const result = await res.json() as { uri: string };
|
|
318
|
+
return { uri: result.uri };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ─── Delete Post ────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
export async function deleteBlueskyPost(
|
|
324
|
+
handle: string,
|
|
325
|
+
appPassword: string,
|
|
326
|
+
postUri: string,
|
|
327
|
+
): Promise<void> {
|
|
328
|
+
const session = await createSession(handle, appPassword);
|
|
329
|
+
const rkey = postUri.split('/').pop() ?? '';
|
|
330
|
+
|
|
331
|
+
const res = await fetch(`${BSKY_API}/com.atproto.repo.deleteRecord`, {
|
|
332
|
+
method: 'POST',
|
|
333
|
+
headers: {
|
|
334
|
+
'Authorization': `Bearer ${session.accessJwt}`,
|
|
335
|
+
'Content-Type': 'application/json',
|
|
336
|
+
},
|
|
337
|
+
body: JSON.stringify({
|
|
338
|
+
repo: session.did,
|
|
339
|
+
collection: 'app.bsky.feed.post',
|
|
340
|
+
rkey,
|
|
341
|
+
}),
|
|
342
|
+
signal: AbortSignal.timeout(10_000),
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
if (!res.ok) {
|
|
346
|
+
const err = await res.text();
|
|
347
|
+
throw new Error(`Bluesky delete failed: ${res.status} ${err}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ─── Notifications ──────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
export interface BlueskyNotification {
|
|
354
|
+
uri: string;
|
|
355
|
+
cid: string;
|
|
356
|
+
reason: string; // 'like' | 'repost' | 'follow' | 'mention' | 'reply' | 'quote'
|
|
357
|
+
author: { handle: string; displayName?: string; did: string };
|
|
358
|
+
indexedAt: string;
|
|
359
|
+
text?: string;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function getNotifications(
|
|
363
|
+
handle: string,
|
|
364
|
+
appPassword: string,
|
|
365
|
+
limit = 30,
|
|
366
|
+
): Promise<BlueskyNotification[]> {
|
|
367
|
+
const session = await createSession(handle, appPassword);
|
|
368
|
+
|
|
369
|
+
const res = await fetch(`${BSKY_API}/app.bsky.notification.listNotifications?limit=${limit}`, {
|
|
370
|
+
headers: { 'Authorization': `Bearer ${session.accessJwt}` },
|
|
371
|
+
signal: AbortSignal.timeout(10_000),
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (!res.ok) {
|
|
375
|
+
const err = await res.text();
|
|
376
|
+
throw new Error(`Bluesky notifications failed: ${res.status} ${err}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const data = await res.json() as { notifications: Array<Record<string, unknown>> };
|
|
380
|
+
|
|
381
|
+
return data.notifications.map((n) => {
|
|
382
|
+
const author = n.author as Record<string, unknown>;
|
|
383
|
+
const rec = n.record as Record<string, unknown> | undefined;
|
|
384
|
+
return {
|
|
385
|
+
uri: n.uri as string,
|
|
386
|
+
cid: n.cid as string,
|
|
387
|
+
reason: n.reason as string,
|
|
388
|
+
author: {
|
|
389
|
+
handle: author.handle as string,
|
|
390
|
+
displayName: author.displayName as string | undefined,
|
|
391
|
+
did: author.did as string,
|
|
392
|
+
},
|
|
393
|
+
indexedAt: n.indexedAt as string,
|
|
394
|
+
text: rec?.text as string | undefined,
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ─── Follow ─────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
export async function followAccount(
|
|
402
|
+
handle: string,
|
|
403
|
+
appPassword: string,
|
|
404
|
+
targetDid: string,
|
|
405
|
+
): Promise<{ uri: string }> {
|
|
406
|
+
const session = await createSession(handle, appPassword);
|
|
407
|
+
|
|
408
|
+
const res = await fetch(`${BSKY_API}/com.atproto.repo.createRecord`, {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
headers: {
|
|
411
|
+
'Authorization': `Bearer ${session.accessJwt}`,
|
|
412
|
+
'Content-Type': 'application/json',
|
|
413
|
+
},
|
|
414
|
+
body: JSON.stringify({
|
|
415
|
+
repo: session.did,
|
|
416
|
+
collection: 'app.bsky.graph.follow',
|
|
417
|
+
record: {
|
|
418
|
+
$type: 'app.bsky.graph.follow',
|
|
419
|
+
subject: targetDid,
|
|
420
|
+
createdAt: new Date().toISOString(),
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
signal: AbortSignal.timeout(10_000),
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
if (!res.ok) {
|
|
427
|
+
const err = await res.text();
|
|
428
|
+
throw new Error(`Bluesky follow failed: ${res.status} ${err}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const result = await res.json() as { uri: string };
|
|
432
|
+
return { uri: result.uri };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ─── Profile ────────────────────────────────────────────────
|
|
436
|
+
|
|
437
|
+
export interface BlueskyProfile {
|
|
438
|
+
did: string;
|
|
439
|
+
handle: string;
|
|
440
|
+
displayName?: string;
|
|
441
|
+
description?: string;
|
|
442
|
+
followersCount: number;
|
|
443
|
+
followsCount: number;
|
|
444
|
+
postsCount: number;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export async function getProfile(actor: string): Promise<BlueskyProfile> {
|
|
448
|
+
const res = await fetch(
|
|
449
|
+
`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(actor)}`,
|
|
450
|
+
{ signal: AbortSignal.timeout(10_000) },
|
|
451
|
+
);
|
|
452
|
+
if (!res.ok) throw new Error(`Profile fetch failed: ${res.status}`);
|
|
453
|
+
|
|
454
|
+
const data = await res.json() as Record<string, unknown>;
|
|
455
|
+
return {
|
|
456
|
+
did: data.did as string,
|
|
457
|
+
handle: data.handle as string,
|
|
458
|
+
displayName: data.displayName as string | undefined,
|
|
459
|
+
description: data.description as string | undefined,
|
|
460
|
+
followersCount: (data.followersCount as number) ?? 0,
|
|
461
|
+
followsCount: (data.followsCount as number) ?? 0,
|
|
462
|
+
postsCount: (data.postsCount as number) ?? 0,
|
|
463
|
+
};
|
|
464
|
+
}
|