tangled
alpha
login
or
join now
stevedylan.dev
/
sequoia
35
fork
atom
A CLI for publishing standard.site documents to ATProto
sequoia.pub
standard
site
lexicon
cli
publishing
35
fork
atom
overview
issues
5
pulls
1
pipelines
chore: version bump and clean up
stevedylan.dev
3 weeks ago
8e848b41
43be9f09
1/1
lint.yml
success
5s
+17
-96
6 changed files
expand all
collapse all
unified
split
packages
cli
biome.json
package.json
src
commands
add.ts
components
sequoia-subscribe.js
index.ts
lib
atproto.ts
+1
-1
packages/cli/biome.json
reviewed
···
2
2
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
3
3
"extends": ["../../biome.json"],
4
4
"files": {
5
5
-
"includes": ["**", "!!**/dist"]
5
5
+
"includes": ["!!**/dist"]
6
6
}
7
7
}
+1
-1
packages/cli/package.json
reviewed
···
1
1
{
2
2
"name": "sequoia-cli",
3
3
-
"version": "0.4.0",
3
3
+
"version": "0.5.0",
4
4
"type": "module",
5
5
"bin": {
6
6
"sequoia": "dist/index.js"
+3
-1
packages/cli/src/commands/add.ts
reviewed
···
40
40
intro("Add Sequoia Component");
41
41
42
42
// Validate component name
43
43
-
const component = AVAILABLE_COMPONENTS.find((c) => c.name === componentName);
43
43
+
const component = AVAILABLE_COMPONENTS.find(
44
44
+
(c) => c.name === componentName,
45
45
+
);
44
46
if (!component) {
45
47
log.error(`Component '${componentName}' not found`);
46
48
log.info("Available components:");
+1
-86
packages/cli/src/components/sequoia-subscribe.js
reviewed
···
127
127
// ============================================================================
128
128
129
129
/**
130
130
-
* Resolve a DID to its PDS URL.
131
131
-
* Supports did:plc and did:web methods.
132
132
-
* @param {string} did - Decentralized Identifier
133
133
-
* @returns {Promise<string>} PDS URL
134
134
-
*/
135
135
-
async function resolvePDS(did) {
136
136
-
let pdsUrl;
137
137
-
138
138
-
if (did.startsWith("did:plc:")) {
139
139
-
const didDocUrl = `https://plc.directory/${did}`;
140
140
-
const didDocResponse = await fetch(didDocUrl);
141
141
-
if (!didDocResponse.ok) {
142
142
-
throw new Error(`Could not fetch DID document: ${didDocResponse.status}`);
143
143
-
}
144
144
-
const didDoc = await didDocResponse.json();
145
145
-
146
146
-
const pdsService = didDoc.service?.find(
147
147
-
(s) => s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer",
148
148
-
);
149
149
-
pdsUrl = pdsService?.serviceEndpoint;
150
150
-
} else if (did.startsWith("did:web:")) {
151
151
-
const domain = did.replace("did:web:", "");
152
152
-
const didDocUrl = `https://${domain}/.well-known/did.json`;
153
153
-
const didDocResponse = await fetch(didDocUrl);
154
154
-
if (!didDocResponse.ok) {
155
155
-
throw new Error(`Could not fetch DID document: ${didDocResponse.status}`);
156
156
-
}
157
157
-
const didDoc = await didDocResponse.json();
158
158
-
159
159
-
const pdsService = didDoc.service?.find(
160
160
-
(s) => s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer",
161
161
-
);
162
162
-
pdsUrl = pdsService?.serviceEndpoint;
163
163
-
} else {
164
164
-
throw new Error(`Unsupported DID method: ${did}`);
165
165
-
}
166
166
-
167
167
-
if (!pdsUrl) {
168
168
-
throw new Error("Could not find PDS URL for user");
169
169
-
}
170
170
-
171
171
-
return pdsUrl;
172
172
-
}
173
173
-
174
174
-
/**
175
175
-
* Create a site.standard.graph.subscription record in the subscriber's PDS.
176
176
-
* @param {string} did - DID of the subscriber
177
177
-
* @param {string} accessToken - AT Protocol access token
178
178
-
* @param {string} publicationUri - AT URI of the publication to subscribe to
179
179
-
* @returns {Promise<{uri: string, cid: string}>} The created record's URI and CID
180
180
-
*/
181
181
-
async function createRecord(did, accessToken, publicationUri) {
182
182
-
const pdsUrl = await resolvePDS(did);
183
183
-
184
184
-
const collection = "site.standard.graph.subscription";
185
185
-
const url = `${pdsUrl}/xrpc/com.atproto.repo.createRecord`;
186
186
-
const response = await fetch(url, {
187
187
-
method: "POST",
188
188
-
headers: {
189
189
-
"Content-Type": "application/json",
190
190
-
Authorization: `Bearer ${accessToken}`,
191
191
-
},
192
192
-
body: JSON.stringify({
193
193
-
repo: did,
194
194
-
collection,
195
195
-
record: {
196
196
-
$type: "site.standard.graph.subscription",
197
197
-
publication: publicationUri,
198
198
-
},
199
199
-
}),
200
200
-
});
201
201
-
202
202
-
if (!response.ok) {
203
203
-
const body = await response.json().catch(() => ({}));
204
204
-
const message = body?.message ?? body?.error ?? `HTTP ${response.status}`;
205
205
-
throw new Error(`Failed to create record: ${message}`);
206
206
-
}
207
207
-
208
208
-
const data = await response.json();
209
209
-
return { uri: data.uri, cid: data.cid };
210
210
-
}
211
211
-
212
212
-
/**
213
130
* Fetch the publication AT URI from the host site's well-known endpoint.
214
131
* @param {string} [origin] - Origin to fetch from (defaults to current page origin)
215
132
* @returns {Promise<string>} Publication AT URI
···
219
136
const url = `${base}/.well-known/site.standard.publication`;
220
137
const response = await fetch(url);
221
138
if (!response.ok) {
222
222
-
throw new Error(
223
223
-
`Could not fetch publication URI: ${response.status}`,
224
224
-
);
139
139
+
throw new Error(`Could not fetch publication URI: ${response.status}`);
225
140
}
226
141
227
142
// Accept either plain text (the AT URI itself) or JSON with a `uri` field.
+1
-1
packages/cli/src/index.ts
reviewed
···
36
36
37
37
> https://tangled.org/stevedylan.dev/sequoia
38
38
`,
39
39
-
version: "0.4.0",
39
39
+
version: "0.5.0",
40
40
cmds: {
41
41
add: addCommand,
42
42
auth: authCommand,
+10
-6
packages/cli/src/lib/atproto.ts
reviewed
···
622
622
agent: Agent,
623
623
options: CreateBlueskyPostOptions,
624
624
): Promise<StrongRef> {
625
625
-
const { title, description, bskyPost, canonicalUrl, coverImage, publishedAt } = options;
625
625
+
const {
626
626
+
title,
627
627
+
description,
628
628
+
bskyPost,
629
629
+
canonicalUrl,
630
630
+
coverImage,
631
631
+
publishedAt,
632
632
+
} = options;
626
633
627
634
// Build post text: title + description
628
635
// Max 300 graphemes for Bluesky posts
···
633
640
if (bskyPost) {
634
641
// Custom bsky post overrides any default behavior
635
642
postText = bskyPost;
636
636
-
}
637
637
-
else if (description) {
643
643
+
} else if (description) {
638
644
// Try: title + description
639
645
const fullText = `${title}\n\n${description}`;
640
646
if (countGraphemes(fullText) <= MAX_GRAPHEMES) {
···
642
648
} else {
643
649
// Truncate description to fit
644
650
const availableForDesc =
645
645
-
MAX_GRAPHEMES -
646
646
-
countGraphemes(title) -
647
647
-
countGraphemes("\n\n");
651
651
+
MAX_GRAPHEMES - countGraphemes(title) - countGraphemes("\n\n");
648
652
if (availableForDesc > 10) {
649
653
const truncatedDesc = truncateToGraphemes(
650
654
description,