···1+# vscode
2+.vscode
3+4+# Intellij
5+*.iml
6+.idea
7+8+# npm
9+node_modules
10+11+# Don't include the compiled main.js file in the repo.
12+# They should be uploaded to GitHub releases instead.
13+main.js
14+15+# Exclude sourcemaps
16+*.map
17+18+# obsidian
19+data.json
20+21+# Exclude macOS Finder (System Explorer) View States
22+.DS_Store
···1+# Obsidian community plugin
2+3+## Project overview
4+5+- Target: Obsidian Community Plugin (TypeScript โ bundled JavaScript).
6+- Entry point: `main.ts` compiled to `main.js` and loaded by Obsidian.
7+- Required release artifacts: `main.js`, `manifest.json`, and optional `styles.css`.
8+9+## Environment & tooling
10+11+- Node.js: use current LTS (Node 18+ recommended).
12+- **Package manager: npm** (required for this sample - `package.json` defines npm scripts and dependencies).
13+- **Bundler: esbuild** (required for this sample - `esbuild.config.mjs` and build scripts depend on it). Alternative bundlers like Rollup or webpack are acceptable for other projects if they bundle all external dependencies into `main.js`.
14+- Types: `obsidian` type definitions.
15+16+**Note**: This sample project has specific technical dependencies on npm and esbuild. If you're creating a plugin from scratch, you can choose different tools, but you'll need to replace the build configuration accordingly.
17+18+### Install
19+20+```bash
21+npm install
22+```
23+24+### Dev (watch)
25+26+```bash
27+npm run dev
28+```
29+30+### Production build
31+32+```bash
33+npm run build
34+```
35+36+## Linting
37+38+- To use eslint install eslint from terminal: `npm install -g eslint`
39+- To use eslint to analyze this project use this command: `eslint main.ts`
40+- eslint will then create a report with suggestions for code improvement by file and line number.
41+- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder: `eslint ./src/`
42+43+## File & folder conventions
44+45+- **Organize code into multiple files**: Split functionality across separate modules rather than putting everything in `main.ts`.
46+- Source lives in `src/`. Keep `main.ts` small and focused on plugin lifecycle (loading, unloading, registering commands).
47+- **Example file structure**:
48+ ```
49+ src/
50+ main.ts # Plugin entry point, lifecycle management
51+ settings.ts # Settings interface and defaults
52+ commands/ # Command implementations
53+ command1.ts
54+ command2.ts
55+ ui/ # UI components, modals, views
56+ modal.ts
57+ view.ts
58+ utils/ # Utility functions, helpers
59+ helpers.ts
60+ constants.ts
61+ types.ts # TypeScript interfaces and types
62+ ```
63+- **Do not commit build artifacts**: Never commit `node_modules/`, `main.js`, or other generated files to version control.
64+- Keep the plugin small. Avoid large dependencies. Prefer browser-compatible packages.
65+- Generated output should be placed at the plugin root or `dist/` depending on your build setup. Release artifacts must end up at the top level of the plugin folder in the vault (`main.js`, `manifest.json`, `styles.css`).
66+67+## Manifest rules (`manifest.json`)
68+69+- Must include (non-exhaustive):
70+ - `id` (plugin ID; for local dev it should match the folder name)
71+ - `name`
72+ - `version` (Semantic Versioning `x.y.z`)
73+ - `minAppVersion`
74+ - `description`
75+ - `isDesktopOnly` (boolean)
76+ - Optional: `author`, `authorUrl`, `fundingUrl` (string or map)
77+- Never change `id` after release. Treat it as stable API.
78+- Keep `minAppVersion` accurate when using newer APIs.
79+- Canonical requirements are coded here: https://github.com/obsidianmd/obsidian-releases/blob/master/.github/workflows/validate-plugin-entry.yml
80+81+## Testing
82+83+- Manual install for testing: copy `main.js`, `manifest.json`, `styles.css` (if any) to:
84+ ```
85+ <Vault>/.obsidian/plugins/<plugin-id>/
86+ ```
87+- Reload Obsidian and enable the plugin in **Settings โ Community plugins**.
88+89+## Commands & settings
90+91+- Any user-facing commands should be added via `this.addCommand(...)`.
92+- If the plugin has configuration, provide a settings tab and sensible defaults.
93+- Persist settings using `this.loadData()` / `this.saveData()`.
94+- Use stable command IDs; avoid renaming once released.
95+96+## Versioning & releases
97+98+- Bump `version` in `manifest.json` (SemVer) and update `versions.json` to map plugin version โ minimum app version.
99+- Create a GitHub release whose tag exactly matches `manifest.json`'s `version`. Do not use a leading `v`.
100+- Attach `manifest.json`, `main.js`, and `styles.css` (if present) to the release as individual assets.
101+- After the initial release, follow the process to add/update your plugin in the community catalog as required.
102+103+## Security, privacy, and compliance
104+105+Follow Obsidian's **Developer Policies** and **Plugin Guidelines**. In particular:
106+107+- Default to local/offline operation. Only make network requests when essential to the feature.
108+- No hidden telemetry. If you collect optional analytics or call third-party services, require explicit opt-in and document clearly in `README.md` and in settings.
109+- Never execute remote code, fetch and eval scripts, or auto-update plugin code outside of normal releases.
110+- Minimize scope: read/write only what's necessary inside the vault. Do not access files outside the vault.
111+- Clearly disclose any external services used, data sent, and risks.
112+- Respect user privacy. Do not collect vault contents, filenames, or personal information unless absolutely necessary and explicitly consented.
113+- Avoid deceptive patterns, ads, or spammy notifications.
114+- Register and clean up all DOM, app, and interval listeners using the provided `register*` helpers so the plugin unloads safely.
115+116+## UX & copy guidelines (for UI text, commands, settings)
117+118+- Prefer sentence case for headings, buttons, and titles.
119+- Use clear, action-oriented imperatives in step-by-step copy.
120+- Use **bold** to indicate literal UI labels. Prefer "select" for interactions.
121+- Use arrow notation for navigation: **Settings โ Community plugins**.
122+- Keep in-app strings short, consistent, and free of jargon.
123+124+## Performance
125+126+- Keep startup light. Defer heavy work until needed.
127+- Avoid long-running tasks during `onload`; use lazy initialization.
128+- Batch disk access and avoid excessive vault scans.
129+- Debounce/throttle expensive operations in response to file system events.
130+131+## Coding conventions
132+133+- TypeScript with `"strict": true` preferred.
134+- **Keep `main.ts` minimal**: Focus only on plugin lifecycle (onload, onunload, addCommand calls). Delegate all feature logic to separate modules.
135+- **Split large files**: If any file exceeds ~200-300 lines, consider breaking it into smaller, focused modules.
136+- **Use clear module boundaries**: Each file should have a single, well-defined responsibility.
137+- Bundle everything into `main.js` (no unbundled runtime deps).
138+- Avoid Node/Electron APIs if you want mobile compatibility; set `isDesktopOnly` accordingly.
139+- Prefer `async/await` over promise chains; handle errors gracefully.
140+141+## Mobile
142+143+- Where feasible, test on iOS and Android.
144+- Don't assume desktop-only behavior unless `isDesktopOnly` is `true`.
145+- Avoid large in-memory structures; be mindful of memory and storage constraints.
146+147+## Agent do/don't
148+149+**Do**
150+- Add commands with stable IDs (don't rename once released).
151+- Provide defaults and validation in settings.
152+- Write idempotent code paths so reload/unload doesn't leak listeners or intervals.
153+- Use `this.register*` helpers for everything that needs cleanup.
154+155+**Don't**
156+- Introduce network calls without an obvious user-facing reason and documentation.
157+- Ship features that require cloud services without clear disclosure and explicit opt-in.
158+- Store or transmit vault contents unless essential and consented.
159+160+## Common tasks
161+162+### Organize code across multiple files
163+164+**main.ts** (minimal, lifecycle only):
165+```ts
166+import { Plugin } from "obsidian";
167+import { MySettings, DEFAULT_SETTINGS } from "./settings";
168+import { registerCommands } from "./commands";
169+170+export default class MyPlugin extends Plugin {
171+ settings: MySettings;
172+173+ async onload() {
174+ this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
175+ registerCommands(this);
176+ }
177+}
178+```
179+180+**settings.ts**:
181+```ts
182+export interface MySettings {
183+ enabled: boolean;
184+ apiKey: string;
185+}
186+187+export const DEFAULT_SETTINGS: MySettings = {
188+ enabled: true,
189+ apiKey: "",
190+};
191+```
192+193+**commands/index.ts**:
194+```ts
195+import { Plugin } from "obsidian";
196+import { doSomething } from "./my-command";
197+198+export function registerCommands(plugin: Plugin) {
199+ plugin.addCommand({
200+ id: "do-something",
201+ name: "Do something",
202+ callback: () => doSomething(plugin),
203+ });
204+}
205+```
206+207+### Add a command
208+209+```ts
210+this.addCommand({
211+ id: "your-command-id",
212+ name: "Do the thing",
213+ callback: () => this.doTheThing(),
214+});
215+```
216+217+### Persist settings
218+219+```ts
220+interface MySettings { enabled: boolean }
221+const DEFAULT_SETTINGS: MySettings = { enabled: true };
222+223+async onload() {
224+ this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
225+ await this.saveData(this.settings);
226+}
227+```
228+229+### Register listeners safely
230+231+```ts
232+this.registerEvent(this.app.workspace.on("file-open", f => { /* ... */ }));
233+this.registerDomEvent(window, "resize", () => { /* ... */ });
234+this.registerInterval(window.setInterval(() => { /* ... */ }, 1000));
235+```
236+237+## Troubleshooting
238+239+- Plugin doesn't load after build: ensure `main.js` and `manifest.json` are at the top level of the plugin folder under `<Vault>/.obsidian/plugins/<plugin-id>/`.
240+- Build issues: if `main.js` is missing, run `npm run build` or `npm run dev` to compile your TypeScript source code.
241+- Commands not appearing: verify `addCommand` runs after `onload` and IDs are unique.
242+- Settings not persisting: ensure `loadData`/`saveData` are awaited and you re-render the UI after changes.
243+- Mobile-only issues: confirm you're not using desktop-only APIs; check `isDesktopOnly` and adjust.
244+245+## References
246+247+- Obsidian sample plugin: https://github.com/obsidianmd/obsidian-sample-plugin
248+- API documentation: https://docs.obsidian.md
249+- Developer policies: https://docs.obsidian.md/Developer+policies
250+- Plugin guidelines: https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines
251+- Style guide: https://help.obsidian.md/style-guide
+21
LICENSE
···000000000000000000000
···1+MIT License
2+3+Copyright (c) 2025 treethought
4+5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
14+15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.
+40-2
README.md
···1-# ATmark (archived)
23Obsidian plugin for AT Protocol bookmarking platforms.
45-This project has been renamed and moved to [obsidian-atmosphere](https://tangled.org/treethought.xyz/obsidian-atmosphere)
00000000000000000000000000000000000000
···1+# ATmark
23Obsidian plugin for AT Protocol bookmarking platforms.
45+6+## Supported platforms
7+8+- **Semble** (`network.cosmik.*`) - Collections and cards
9+- **Bookmarks** (`community.lexicon.bookmarks.*`) - Community bookmarks lexicon with tag filtering (supports kipclip tags)
10+- **margin.at** (`at.margin.*`) - Bookmarks with collections and tags support
11+12+
13+
14+15+## Installation
16+17+Install via [BRAT](https://github.com/TfTHacker/obsidian42-brat):
18+19+1. Install the BRAT plugin from Community Plugins
20+2. Open BRAT settings
21+3. Click "Add Beta plugin"
22+4. Enter the GitHub URL: `https://github.com/treethought/obsidian-atmark`
23+5. Enable the plugin in Community Plugins
24+25+## Getting Started
26+27+### Authentication
28+29+1. Open Settings > ATmark
30+2. Enter your AT Protocol handle or DID
31+3. Create an app password in your AT Protocol client (Bluesky: Settings > Privacy and security > App passwords)
32+4. Enter the app password in the plugin settings
33+5. Save settings
34+35+The plugin will automatically connect using your credentials.
36+37+### Opening the View
38+39+Open the command palette (Ctrl/Cmd + P) and search for "ATmark: Open view". The view will show your bookmarks from all supported platforms.
40+41+## Network Use
42+43+This plugin connects to AT Protocol services to fetch and manage your bookmarks.
···1+{
2+ "lexicon": 1,
3+ "id": "network.cosmik.collectionLink",
4+ "description": "A record that links a card to a collection.",
5+ "defs": {
6+ "main": {
7+ "type": "record",
8+ "description": "A record representing the relationship between a card and a collection.",
9+ "key": "tid",
10+ "record": {
11+ "type": "object",
12+ "required": ["collection", "card", "addedBy", "addedAt"],
13+ "properties": {
14+ "collection": {
15+ "type": "ref",
16+ "description": "Strong reference to the collection record.",
17+ "ref": "com.atproto.repo.strongRef"
18+ },
19+ "card": {
20+ "type": "ref",
21+ "description": "Strong reference to the card record in the users library.",
22+ "ref": "com.atproto.repo.strongRef"
23+ },
24+ "originalCard": {
25+ "type": "ref",
26+ "description": "Strong reference to the original card record (may be in another library).",
27+ "ref": "com.atproto.repo.strongRef"
28+ },
29+ "addedBy": {
30+ "type": "string",
31+ "description": "DID of the user who added the card to the collection"
32+ },
33+ "addedAt": {
34+ "type": "string",
35+ "format": "datetime",
36+ "description": "Timestamp when the card was added to the collection."
37+ },
38+ "createdAt": {
39+ "type": "string",
40+ "format": "datetime",
41+ "description": "Timestamp when this link record was created (usually set by PDS)."
42+ },
43+ "provenance": {
44+ "type": "ref",
45+ "description": "Optional provenance information for this link.",
46+ "ref": "network.cosmik.defs#provenance"
47+ }
48+ }
49+ }
50+ }
51+ }
52+}
+18
lexicons/network/cosmik/defs.json
···000000000000000000
···1+{
2+ "lexicon": 1,
3+ "id": "network.cosmik.defs",
4+ "description": "Common definitions for annotation types and references",
5+ "defs": {
6+ "provenance": {
7+ "type": "object",
8+ "description": "Represents the provenance or source of a record.",
9+ "properties": {
10+ "via": {
11+ "type": "ref",
12+ "description": "Strong reference to the card that led to this record.",
13+ "ref": "com.atproto.repo.strongRef"
14+ }
15+ }
16+ }
17+ }
18+}
···1+export * as AtMarginAnnotation from "./types/at/margin/annotation.js";
2+export * as AtMarginBookmark from "./types/at/margin/bookmark.js";
3+export * as AtMarginCollection from "./types/at/margin/collection.js";
4+export * as AtMarginCollectionItem from "./types/at/margin/collectionItem.js";
5+export * as AtMarginHighlight from "./types/at/margin/highlight.js";
6+export * as AtMarginLike from "./types/at/margin/like.js";
7+export * as AtMarginProfile from "./types/at/margin/profile.js";
8+export * as AtMarginReply from "./types/at/margin/reply.js";
9+export * as ComAtprotoRepoStrongRef from "./types/com/atproto/repo/strongRef.js";
10+export * as NetworkCosmikCard from "./types/network/cosmik/card.js";
11+export * as NetworkCosmikCollection from "./types/network/cosmik/collection.js";
12+export * as NetworkCosmikCollectionLink from "./types/network/cosmik/collectionLink.js";
13+export * as NetworkCosmikDefs from "./types/network/cosmik/defs.js";
···1+import type {} from "@atcute/lexicons";
2+import * as v from "@atcute/lexicons/validations";
3+import type {} from "@atcute/lexicons/ambient";
4+import * as ComAtprotoRepoStrongRef from "../../com/atproto/repo/strongRef.js";
5+import * as NetworkCosmikDefs from "./defs.js";
6+7+const _mainSchema = /*#__PURE__*/ v.record(
8+ /*#__PURE__*/ v.tidString(),
9+ /*#__PURE__*/ v.object({
10+ $type: /*#__PURE__*/ v.literal("network.cosmik.collectionLink"),
11+ /**
12+ * Timestamp when the card was added to the collection.
13+ */
14+ addedAt: /*#__PURE__*/ v.datetimeString(),
15+ /**
16+ * DID of the user who added the card to the collection
17+ */
18+ addedBy: /*#__PURE__*/ v.string(),
19+ /**
20+ * Strong reference to the card record in the users library.
21+ */
22+ get card() {
23+ return ComAtprotoRepoStrongRef.mainSchema;
24+ },
25+ /**
26+ * Strong reference to the collection record.
27+ */
28+ get collection() {
29+ return ComAtprotoRepoStrongRef.mainSchema;
30+ },
31+ /**
32+ * Timestamp when this link record was created (usually set by PDS).
33+ */
34+ createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()),
35+ /**
36+ * Strong reference to the original card record (may be in another library).
37+ */
38+ get originalCard() {
39+ return /*#__PURE__*/ v.optional(ComAtprotoRepoStrongRef.mainSchema);
40+ },
41+ /**
42+ * Optional provenance information for this link.
43+ */
44+ get provenance() {
45+ return /*#__PURE__*/ v.optional(NetworkCosmikDefs.provenanceSchema);
46+ },
47+ }),
48+);
49+50+type main$schematype = typeof _mainSchema;
51+52+export interface mainSchema extends main$schematype {}
53+54+export const mainSchema = _mainSchema as mainSchema;
55+56+export interface Main extends v.InferInput<typeof mainSchema> {}
57+58+declare module "@atcute/lexicons/ambient" {
59+ interface Records {
60+ "network.cosmik.collectionLink": mainSchema;
61+ }
62+}
+23
src/lexicons/types/network/cosmik/defs.ts
···00000000000000000000000
···1+import type {} from "@atcute/lexicons";
2+import * as v from "@atcute/lexicons/validations";
3+import * as ComAtprotoRepoStrongRef from "../../com/atproto/repo/strongRef.js";
4+5+const _provenanceSchema = /*#__PURE__*/ v.object({
6+ $type: /*#__PURE__*/ v.optional(
7+ /*#__PURE__*/ v.literal("network.cosmik.defs#provenance"),
8+ ),
9+ /**
10+ * Strong reference to the card that led to this record.
11+ */
12+ get via() {
13+ return /*#__PURE__*/ v.optional(ComAtprotoRepoStrongRef.mainSchema);
14+ },
15+});
16+17+type provenance$schematype = typeof _provenanceSchema;
18+19+export interface provenanceSchema extends provenance$schematype {}
20+21+export const provenanceSchema = _provenanceSchema as provenanceSchema;
22+23+export interface Provenance extends v.InferInput<typeof provenanceSchema> {}
+39
src/lib/atproto.ts
···000000000000000000000000000000000000000
···1+import type { Client } from "@atcute/client";
2+import type { ActorIdentifier, Nsid } from "@atcute/lexicons";
3+4+export async function getRecord(client: Client, repo: string, collection: string, rkey: string) {
5+ return await client.get("com.atproto.repo.getRecord", {
6+ params: {
7+ repo: repo as ActorIdentifier,
8+ collection: collection as Nsid,
9+ rkey,
10+ },
11+ });
12+}
13+14+export async function deleteRecord(client: Client, repo: string, collection: string, rkey: string) {
15+ return await client.post("com.atproto.repo.deleteRecord", {
16+ input: {
17+ repo: repo as ActorIdentifier,
18+ collection: collection as Nsid,
19+ rkey,
20+ },
21+ });
22+}
23+24+export async function putRecord<T = unknown>(client: Client, repo: string, collection: string, rkey: string, record: T) {
25+ return await client.post("com.atproto.repo.putRecord", {
26+ input: {
27+ repo: repo as ActorIdentifier,
28+ collection: collection as Nsid,
29+ rkey,
30+ record: record as unknown as { [key: string]: unknown },
31+ },
32+ });
33+}
34+35+export async function getProfile(client: Client, actor: string) {
36+ return await client.get("app.bsky.actor.getProfile", {
37+ params: { actor: actor as ActorIdentifier },
38+ });
39+}
···1+import { readFileSync, writeFileSync } from "fs";
2+3+const targetVersion = process.env.npm_package_version;
4+5+// read minAppVersion from manifest.json and bump version to target version
6+const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
7+const { minAppVersion } = manifest;
8+manifest.version = targetVersion;
9+writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
10+11+// update versions.json with target version and minAppVersion from manifest.json
12+// but only if the target version is not already in versions.json
13+const versions = JSON.parse(readFileSync('versions.json', 'utf8'));
14+if (!Object.values(versions).includes(minAppVersion)) {
15+ versions[targetVersion] = minAppVersion;
16+ writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));
17+}