├── .eslintignore
├── .eslintrc.js
├── .github
├── FUNDING.yml
└── workflows
│ └── main.yml
├── .gitignore
├── .prettierrc.js
├── README.md
├── manifest.json
├── package.json
├── rollup.config.js
├── screenshot.gif
├── src
├── OpenRandomTaggedNoteModalView.svelte
├── main.ts
├── openRandomTaggedNoteModal.ts
├── settingTab.ts
├── smartRandomNoteNotice.ts
├── types.ts
└── utilities.ts
├── styles.css
├── tsconfig.json
└── versions.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser
3 | parserOptions: {
4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
5 | sourceType: 'module', // Allows for the use of imports
6 | },
7 | extends: [
8 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
9 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
10 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
11 | ],
12 | rules: {
13 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
14 | // e.g. "@typescript-eslint/explicit-function-return-type": "off",
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: ["erichalldev"]
2 | custom: ["https://www.buymeacoffee.com/erichall"]
3 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Build obsidian plugin
2 |
3 | on:
4 | push:
5 | # Sequence of patterns matched against refs/tags
6 | tags:
7 | - '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10
8 |
9 | env:
10 | PLUGIN_NAME: smart-random-note # Change this to the name of your plugin-id folder
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Use Node.js
20 | uses: actions/setup-node@v1
21 | with:
22 | node-version: '14.x' # You might need to adjust this value to your own version
23 | - name: Build
24 | id: build
25 | run: |
26 | npm install
27 | npm run build --if-present
28 | mkdir ${{ env.PLUGIN_NAME }}
29 | cp dist/main.js manifest.json ${{ env.PLUGIN_NAME }}
30 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
31 | ls
32 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)"
33 | - name: Create Release
34 | id: create_release
35 | uses: actions/create-release@v1
36 | env:
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38 | VERSION: ${{ github.ref }}
39 | with:
40 | tag_name: ${{ github.ref }}
41 | release_name: ${{ github.ref }}
42 | draft: false
43 | prerelease: false
44 | - name: Upload zip file
45 | id: upload-zip
46 | uses: actions/upload-release-asset@v1
47 | env:
48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49 | with:
50 | upload_url: ${{ steps.create_release.outputs.upload_url }}
51 | asset_path: ./${{ env.PLUGIN_NAME }}.zip
52 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
53 | asset_content_type: application/zip
54 | - name: Upload main.js
55 | id: upload-main
56 | uses: actions/upload-release-asset@v1
57 | env:
58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 | with:
60 | upload_url: ${{ steps.create_release.outputs.upload_url }}
61 | asset_path: ./dist/main.js
62 | asset_name: main.js
63 | asset_content_type: text/javascript
64 | - name: Upload manifest.json
65 | id: upload-manifest
66 | uses: actions/upload-release-asset@v1
67 | env:
68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69 | with:
70 | upload_url: ${{ steps.create_release.outputs.upload_url }}
71 | asset_path: ./manifest.json
72 | asset_name: manifest.json
73 | asset_content_type: application/json
74 | # - name: Upload styles.css
75 | # id: upload-css
76 | # uses: actions/upload-release-asset@v1
77 | # env:
78 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
79 | # with:
80 | # upload_url: ${{ steps.create_release.outputs.upload_url }}
81 | # asset_path: ./styles.css
82 | # asset_name: styles.css
83 | # asset_content_type: text/css
84 |
85 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | package-lock.json
4 | copy-to-vault.bat
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | semi: true,
3 | trailingComma: "all",
4 | singleQuote: true,
5 | printWidth: 120,
6 | tabWidth: 4,
7 | endOfLine: 'auto',
8 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Smart Random Note Obsidian Plugin
4 |
5 | This plugin enhances opening random notes.
6 |
7 | Three commands are available:
8 |
9 | - Open Random Note from Search: opens a random note from the list of search results.
10 | - Insert Link at Cursor to Random Note from Search: inserts a link where the cursor is positioned to a raondom note from the list of search results.
11 | - Open Tagged Random Note: opens a random note that has a selected tag.
12 | - Open Random Note: behaves similarly to the core random note plugin.
13 |
14 | 
15 |
16 | ## Future Plans
17 |
18 | - Originally I had plans to implement spaced repetition capabilities, but other plugins have been developed that handle that domain well. They are:
19 | - [Flashcards](https://github.com/reuseman/flashcards-obsidian)
20 | - [Spaced Repetition](https://github.com/st3v3nmw/obsidian-spaced-repetition)
21 | - [Recall](https://github.com/martin-jw/obsidian-recall)
22 | - I'd like to stay as close as possible to the Unix adage "do one thing, and do it well" with this plugin. Therefore any features and improvements must stay close to its core function: opening random notes with greater control.
23 |
24 | ## Installation
25 |
26 | ### From within Obsidian
27 |
28 | From Obsidian 0.9.8, you can activate this plugin within Obsidian by doing the following:
29 |
30 | - Open Settings > Third-party plugin
31 | - Make sure Safe mode is **off**
32 | - Click Browse community plugins
33 | - Search for "Smart Random Note"
34 | - Click Install
35 | - Once installed, close the community plugins window and activate the plugin
36 |
37 | ## Compatibility
38 |
39 | Custom plugins are officially supported in Obsidian version 0.9.7. This plugin currently targets API version 0.9.15 but should be compatible with version 0.9.7 or higher.
40 |
41 | ## Version History
42 |
43 | ### 0.2.1
44 |
45 | - Add command to insert a link at the cursor to a random note from search
46 | - Fix opening a new markdown note when an image was selected to open. Opening any files except markdown is not supported.
47 |
48 | ### 0.1.3
49 |
50 | - Fix broken open random note from search command in Obsidian 0.9.18
51 |
52 | ### 0.1.2
53 |
54 | - Add support for frontmatter tags introduced in Obsidian 0.9.16
55 |
56 | ### 0.1.1
57 |
58 | - Add command for opening a random note from the current search results
59 | - Add setting to add a button to the ribbon for opening a random note from the current search results
60 | - Add setting to open the random note in the active leaf or a new leaf
61 |
62 | ### 0.0.5
63 |
64 | - Initial Release
65 | - Add command for opening a random note from all notes
66 | - Add command for opening a random note given a tag
67 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "smart-random-note",
3 | "name": "Smart Random Note",
4 | "version": "0.2.1",
5 | "minAppVersion": "0.9.18",
6 | "description": "A smart random note plugin",
7 | "author": "Eric Hall",
8 | "authorUrl": "https://erichall.io",
9 | "isDesktopOnly": false
10 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "smart-random-note",
3 | "version": "0.2.0",
4 | "description": "A smart random note plugin for Obsidian (https://obsidian.md)",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "rollup --config rollup.config.js -w",
8 | "build": "rollup --config rollup.config.js",
9 | "lint": "svelte-check && eslint ./**/*.{js,ts} --quiet --fix"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "devDependencies": {
14 | "@rollup/plugin-commonjs": "^15.1.0",
15 | "@rollup/plugin-node-resolve": "^10.0.0",
16 | "@rollup/plugin-typescript": "^6.0.0",
17 | "@tsconfig/svelte": "^1.0.10",
18 | "@types/node": "^14.14.10",
19 | "@typescript-eslint/eslint-plugin": "^4.8.2",
20 | "@typescript-eslint/parser": "^4.8.2",
21 | "eslint": "^7.13.0",
22 | "eslint-config-prettier": "^6.15.0",
23 | "eslint-plugin-prettier": "^3.1.4",
24 | "obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master",
25 | "prettier": "^2.1.2",
26 | "rollup": "^2.33.2",
27 | "rollup-plugin-svelte": "^7.0.0",
28 | "svelte": "^3.30.0",
29 | "svelte-check": "^1.1.17",
30 | "svelte-preprocess": "^4.6.1",
31 | "tslib": "^2.0.3",
32 | "typescript": "^4.0.3"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte';
2 | import typescript from '@rollup/plugin-typescript';
3 | import resolve from '@rollup/plugin-node-resolve';
4 | import commonjs from '@rollup/plugin-commonjs';
5 | import autoPreprocess from 'svelte-preprocess';
6 |
7 | export default {
8 | input: 'src/main.ts',
9 | output: {
10 | dir: 'dist',
11 | sourcemap: 'inline',
12 | format: 'cjs',
13 | exports: 'default',
14 | },
15 | external: ['obsidian'],
16 | plugins: [
17 | svelte({ preprocess: autoPreprocess() }),
18 | typescript({ sourceMap: true }),
19 | resolve({ browser: true, dedupe: ['svelte'] }),
20 | commonjs({ include: 'node_modules/**' }),
21 | ],
22 | };
23 |
--------------------------------------------------------------------------------
/screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erichalldev/obsidian-smart-random-note/e17078681af5659c0400c4924cf8424dfa6d3b2a/screenshot.gif
--------------------------------------------------------------------------------
/src/OpenRandomTaggedNoteModalView.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
Select Tag
11 |
12 |
20 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { MarkdownView, Plugin, TFile } from 'obsidian';
2 | import { getTagFilesMap, randomElement } from './utilities';
3 | import { SmartRandomNoteSettingTab } from './settingTab';
4 | import { SearchView, SmartRandomNoteSettings } from './types';
5 | import { SmartRandomNoteNotice } from './smartRandomNoteNotice';
6 | import { OpenRandomTaggedNoteModal } from './openRandomTaggedNoteModal';
7 |
8 | export default class SmartRandomNotePlugin extends Plugin {
9 | settings: SmartRandomNoteSettings = { openInNewLeaf: true, enableRibbonIcon: true };
10 | ribbonIconEl: HTMLElement | undefined = undefined;
11 |
12 | async onload(): Promise {
13 | console.log('loading smart-random-note');
14 |
15 | await this.loadSettings();
16 |
17 | this.addSettingTab(new SmartRandomNoteSettingTab(this));
18 |
19 | this.addCommand({
20 | id: 'open-random-note',
21 | name: 'Open Random Note',
22 | callback: this.handleOpenRandomNote,
23 | });
24 |
25 | this.addCommand({
26 | id: 'open-tagged-random-note',
27 | name: 'Open Tagged Random Note',
28 | callback: this.handleOpenTaggedRandomNote,
29 | });
30 |
31 | this.addCommand({
32 | id: 'open-random-note-from-search',
33 | name: 'Open Random Note from Search',
34 | callback: this.handleOpenRandomNoteFromSearch,
35 | });
36 |
37 | this.addCommand({
38 | id: 'insert-link-to-random-note-at-cursor',
39 | name: 'Insert Link at Cursor to Random Note from Search',
40 | callback: this.handleInsertLinkFromSearch,
41 | });
42 | }
43 |
44 | onunload = (): void => {
45 | console.log('unloading smart-random-note');
46 | };
47 |
48 | handleOpenRandomNote = async (): Promise => {
49 | const markdownFiles = this.app.vault.getMarkdownFiles();
50 |
51 | this.openRandomNote(markdownFiles);
52 | };
53 |
54 | handleOpenTaggedRandomNote = (): void => {
55 | const tagFilesMap = getTagFilesMap(this.app);
56 |
57 | const tags = Object.keys(tagFilesMap);
58 | const modal = new OpenRandomTaggedNoteModal(this.app, tags);
59 |
60 | modal.submitCallback = async (selectedTag: string): Promise => {
61 | const taggedFiles = tagFilesMap[selectedTag];
62 | await this.openRandomNote(taggedFiles);
63 | };
64 |
65 | modal.open();
66 | };
67 |
68 | handleOpenRandomNoteFromSearch = async (): Promise => {
69 | const searchView = this.app.workspace.getLeavesOfType('search')[0]?.view as SearchView;
70 |
71 | if (!searchView) {
72 | new SmartRandomNoteNotice('The core search plugin is not enabled', 5000);
73 | return;
74 | }
75 |
76 | const searchResults = searchView.dom.getFiles();
77 |
78 | if (!searchResults.length) {
79 | new SmartRandomNoteNotice('No search results available', 5000);
80 | return;
81 | }
82 |
83 | await this.openRandomNote(searchResults);
84 | };
85 |
86 | handleInsertLinkFromSearch = async (): Promise => {
87 | const searchView = this.app.workspace.getLeavesOfType('search')[0]?.view as SearchView;
88 |
89 | if (!searchView) {
90 | new SmartRandomNoteNotice('The core search plugin is not enabled', 5000);
91 | return;
92 | }
93 |
94 | const searchResults = searchView.dom.getFiles();
95 |
96 | if (!searchResults.length) {
97 | new SmartRandomNoteNotice('No search results available', 5000);
98 | return;
99 | }
100 |
101 | await this.insertRandomLinkAtCursor(searchResults);
102 | };
103 |
104 | openRandomNote = async (files: TFile[]): Promise => {
105 | const markdownFiles = files.filter((file) => file.extension === 'md');
106 |
107 | if (!markdownFiles.length) {
108 | new SmartRandomNoteNotice("Can't open note. No markdown files available to open.", 5000);
109 | return;
110 | }
111 |
112 | const fileToOpen = randomElement(markdownFiles);
113 | await this.app.workspace.openLinkText(fileToOpen.basename, '', this.settings.openInNewLeaf, {
114 | active: true,
115 | });
116 | };
117 |
118 | insertRandomLinkAtCursor = async (files: TFile[]): Promise => {
119 | const fileToLink = randomElement(files);
120 | const activeLeaf = this.app.workspace.activeLeaf;
121 | if (!activeLeaf) {
122 | new SmartRandomNoteNotice("Can't insert link. No active note to insert link into", 5000);
123 | return;
124 | }
125 | const viewState = activeLeaf.getViewState();
126 | const canEdit = viewState.type === 'markdown' && viewState.state && viewState.state.mode == 'source';
127 |
128 | if (!canEdit) {
129 | new SmartRandomNoteNotice("Can't insert link. The active file is not a markdown file in edit mode.", 5000);
130 | return;
131 | }
132 |
133 | const markdownView = activeLeaf.view as MarkdownView;
134 | const cursorPos = markdownView.editor.getCursor();
135 | const textToInsert = `[[${fileToLink.name}]]`;
136 | markdownView.editor.replaceRange(textToInsert, cursorPos);
137 | };
138 |
139 | loadSettings = async (): Promise => {
140 | const loadedSettings = (await this.loadData()) as SmartRandomNoteSettings;
141 | if (loadedSettings) {
142 | this.setOpenInNewLeaf(loadedSettings.openInNewLeaf);
143 | this.setEnableRibbonIcon(loadedSettings.enableRibbonIcon);
144 | } else {
145 | this.refreshRibbonIcon();
146 | }
147 | };
148 |
149 | setOpenInNewLeaf = (value: boolean): void => {
150 | this.settings.openInNewLeaf = value;
151 | this.saveData(this.settings);
152 | };
153 |
154 | setEnableRibbonIcon = (value: boolean): void => {
155 | this.settings.enableRibbonIcon = value;
156 | this.refreshRibbonIcon();
157 | this.saveData(this.settings);
158 | };
159 |
160 | refreshRibbonIcon = (): void => {
161 | this.ribbonIconEl?.remove();
162 | if (this.settings.enableRibbonIcon) {
163 | this.ribbonIconEl = this.addRibbonIcon(
164 | 'dice',
165 | 'Open Random Note from Search',
166 | this.handleOpenRandomNoteFromSearch,
167 | );
168 | }
169 | };
170 | }
171 |
--------------------------------------------------------------------------------
/src/openRandomTaggedNoteModal.ts:
--------------------------------------------------------------------------------
1 | import { App, Modal } from 'obsidian';
2 | import OpenRandomTaggedNoteModalView from './OpenRandomTaggedNoteModalView.svelte';
3 |
4 | export class OpenRandomTaggedNoteModal extends Modal {
5 | view: OpenRandomTaggedNoteModalView;
6 | tags: string[];
7 | submitCallback: ((selectedTag: string) => Promise) | undefined = undefined;
8 |
9 | constructor(app: App, tags: string[]) {
10 | super(app);
11 | this.tags = tags;
12 | this.view = new OpenRandomTaggedNoteModalView({
13 | target: this.contentEl,
14 | props: { tags, handleSubmit: this.handleSubmit },
15 | });
16 | }
17 |
18 | handleSubmit = (tag: string): void => {
19 | if (this.submitCallback) {
20 | this.submitCallback(tag);
21 | }
22 | this.close();
23 | };
24 | }
25 |
--------------------------------------------------------------------------------
/src/settingTab.ts:
--------------------------------------------------------------------------------
1 | import SmartRandomNotePlugin from './main';
2 | import { PluginSettingTab, Setting } from 'obsidian';
3 |
4 | export class SmartRandomNoteSettingTab extends PluginSettingTab {
5 | plugin: SmartRandomNotePlugin;
6 |
7 | constructor(plugin: SmartRandomNotePlugin) {
8 | super(plugin.app, plugin);
9 | this.plugin = plugin;
10 | }
11 |
12 | display(): void {
13 | const { containerEl } = this;
14 |
15 | containerEl.empty();
16 |
17 | containerEl.createEl('h2', { text: 'Smart Random Note Settings ' });
18 |
19 | new Setting(containerEl)
20 | .setName('Open in New Leaf')
21 | .setDesc('Default setting for opening random notes')
22 | .addToggle((toggle) => {
23 | toggle.setValue(this.plugin.settings.openInNewLeaf);
24 | toggle.onChange(this.plugin.setOpenInNewLeaf);
25 | });
26 |
27 | new Setting(containerEl)
28 | .setName('Enable Ribbon Icon')
29 | .setDesc('Place an icon on the ribbon to open a random note from search')
30 | .addToggle((toggle) => {
31 | toggle.setValue(this.plugin.settings.enableRibbonIcon);
32 | toggle.onChange(this.plugin.setEnableRibbonIcon);
33 | });
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/smartRandomNoteNotice.ts:
--------------------------------------------------------------------------------
1 | import { Notice } from 'obsidian';
2 |
3 | export class SmartRandomNoteNotice extends Notice {
4 | constructor(message: string, timeout?: number) {
5 | super('Smart Random Note: ' + message, timeout);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { TFile, View } from 'obsidian';
2 |
3 | export type TagFilesMap = { [tag: string]: TFile[] };
4 |
5 | export interface SearchDOM {
6 | getFiles(): TFile[];
7 | }
8 |
9 | export interface SearchView extends View {
10 | dom: SearchDOM;
11 | }
12 |
13 | export interface SmartRandomNoteSettings {
14 | openInNewLeaf: boolean;
15 | enableRibbonIcon: boolean;
16 | }
17 |
--------------------------------------------------------------------------------
/src/utilities.ts:
--------------------------------------------------------------------------------
1 | import { App, CachedMetadata } from 'obsidian';
2 | import { TagFilesMap } from './types';
3 |
4 | export function getTagFilesMap(app: App): TagFilesMap {
5 | const metadataCache = app.metadataCache;
6 | const markdownFiles = app.vault.getMarkdownFiles();
7 |
8 | const tagFilesMap: TagFilesMap = {};
9 |
10 | for (const markdownFile of markdownFiles) {
11 | const cachedMetadata = metadataCache.getFileCache(markdownFile);
12 |
13 | if (cachedMetadata) {
14 | const cachedTags = getCachedTags(cachedMetadata);
15 | if (cachedTags.length) {
16 | for (const cachedTag of cachedTags) {
17 | if (tagFilesMap[cachedTag]) {
18 | tagFilesMap[cachedTag].push(markdownFile);
19 | } else {
20 | tagFilesMap[cachedTag] = [markdownFile];
21 | }
22 | }
23 | }
24 | }
25 | }
26 |
27 | return tagFilesMap;
28 | }
29 |
30 | function getCachedTags(cachedMetadata: CachedMetadata): string[] {
31 | const bodyTags: string[] = cachedMetadata.tags?.map((x) => x.tag) || [];
32 | const frontMatterTags: string[] = cachedMetadata.frontmatter?.tags || [];
33 |
34 | // frontmatter tags might not have a hashtag in front of them
35 | const cachedTags = bodyTags.concat(frontMatterTags).map((x) => (x.startsWith('#') ? x : '#' + x));
36 |
37 | return cachedTags;
38 | }
39 |
40 | export function randomElement(array: T[]): T {
41 | return array[(array.length * Math.random()) << 0];
42 | }
43 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erichalldev/obsidian-smart-random-note/e17078681af5659c0400c4924cf8424dfa6d3b2a/styles.css
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "types": ["node", "svelte"],
4 | "baseUrl": ".",
5 | "inlineSourceMap": true,
6 | "inlineSources": true,
7 | "module": "ESNext",
8 | "target": "es6",
9 | "allowJs": false,
10 | "strict": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "noImplicitReturns": true,
14 | "noFallthroughCasesInSwitch": true,
15 | "moduleResolution": "node",
16 | "importHelpers": true,
17 | "lib": [
18 | "dom",
19 | "es5",
20 | "scripthost",
21 | "es2015"
22 | ]
23 | },
24 | "include": [
25 | "**/*.ts"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "0.2.0": "0.9.18",
3 | "0.1.3": "0.9.18",
4 | "0.1.2": "0.9.16",
5 | "0.1.1": "0.9.7",
6 | "0.0.5": "0.9.7"
7 | }
--------------------------------------------------------------------------------