obsidian_fetch 1.2.0 → 1.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +1 -1
- data/README_ja.md +1 -1
- data/lib/exe.rb +12 -4
- data/lib/obsidian_fetch/logic.rb +162 -25
- data/lib/obsidian_fetch/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6b142d17c24902e0a12e9cec6e76cd0b013847ec51f32e25ce902cd882eab987
|
|
4
|
+
data.tar.gz: 4c3783977dd9554b9bcba102a73b77a5a59d6e5915004cd3a304011e91a08588
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c550ba9a5d0c37f0a35d46f4667a1c7d4b79fedf4e10120e7f64877d9b34e5312ae1d913e74056317b5274ad90bf95bdeeec855fb3d146003b8fe656d40679ee
|
|
7
|
+
data.tar.gz: 17cd3805955d03eec7030e52ed07cb4f32c41f65cae11f1fc34a9ea07ffb0b0b4792fc4564b941b9347824a24d327a8f4ff56c3197eaf061101034e30d41820b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.3.0] - 2026-10-28
|
|
4
|
+
|
|
5
|
+
- Dependencies: mcp 1.8.2 (constraint `~> 1.8`), puma 8.0.2, rack 3.2.7, simplecov 1.3.2. Required Ruby is 3.3.0 or later.
|
|
6
|
+
- list tool: stopped shuffling. Results are ordered by note name, compared case-insensitively with an ASCII tiebreak. Backlinks, link fallback lists and file enumerations are ordered the same way. When the result is truncated by limit, the truncated priority group shows its head and tail with a line reporting the omitted count.
|
|
7
|
+
- list tool: added the `limit` argument (1 to 200, default 20) and the total number of matches in the response.
|
|
8
|
+
- list tool: folder names are searched, matches are ranked by the distance from the file, and the response reports the breakdown of matches.
|
|
9
|
+
- read tool: a case-insensitive fuzzy match is accepted when no exact match exists, and case variants are suggested when an exact match exists.
|
|
10
|
+
- The initialize response advertises only the tools capability.
|
|
11
|
+
|
|
12
|
+
## [1.2.0] - 2026-07-10
|
|
13
|
+
|
|
14
|
+
- Vault files are reloaded when they change.
|
|
15
|
+
- isError handling was added to the tools.
|
|
16
|
+
- Tests were restructured and coverage measurement was added.
|
|
17
|
+
|
|
18
|
+
## [1.1.2] - 2026-04-28
|
|
19
|
+
|
|
20
|
+
- The Puma bind address was changed from 127.0.0.1 to 0.0.0.0 to allow external access.
|
|
21
|
+
- The `enable_json_response` option added in 1.1.1 was reverted.
|
|
22
|
+
|
|
23
|
+
## [1.1.1] - 2026-04-28
|
|
24
|
+
|
|
25
|
+
- Added the `enable_json_response` option for mcp 0.14.0.
|
|
26
|
+
|
|
27
|
+
## [1.1.0] - 2026-04-28
|
|
28
|
+
|
|
29
|
+
- Added the Streamable HTTP transport.
|
|
30
|
+
- README was made multilingual.
|
|
31
|
+
- The mcp gem was upgraded to 0.14.0.
|
|
32
|
+
|
|
33
|
+
## [1.0.0] - 2025-08-20
|
|
34
|
+
|
|
35
|
+
- Switched to the official MCP Ruby SDK.
|
|
36
|
+
- Fixed the way tools are specified.
|
|
37
|
+
- read tool normalizes a path given as `name`.
|
|
38
|
+
- Display shows note names instead of paths.
|
|
39
|
+
- Refactored and extended the list tool.
|
|
40
|
+
|
|
3
41
|
## [0.1.0] - 2025-04-27
|
|
4
42
|
|
|
5
43
|
- Initial release
|
data/README.md
CHANGED
|
@@ -48,7 +48,7 @@ obsidian_fetch /path/to/your/vault --transport streamable-http --port 3000
|
|
|
48
48
|
## Tools
|
|
49
49
|
|
|
50
50
|
- **read**: Read a note from the Obsidian vault. If multiple notes with the same name are found, all will be shown.
|
|
51
|
-
- **list**: Search for
|
|
51
|
+
- **list**: Search for notes whose name or containing folder names partially match. It returns 20 notes by default. The `limit` argument accepts an integer between 1 and 200, and the response states how many notes matched in total.
|
|
52
52
|
|
|
53
53
|
## Contributing
|
|
54
54
|
|
data/README_ja.md
CHANGED
|
@@ -52,7 +52,7 @@ obsidian_fetch /path/to/your/vault --transport streamable-http --port 3000
|
|
|
52
52
|
## ツール
|
|
53
53
|
|
|
54
54
|
- **read**: Obsidian Vaultからノートを取得します。同名のノートが複数存在する場合、すべて取得します。
|
|
55
|
-
- **list**:
|
|
55
|
+
- **list**: ノート名とフォルダ名で検索します。部分一致での検索が可能です。既定では20件を返します。limit 引数には1から200の整数を受け付けます。応答にはマッチした総件数が含まれます。
|
|
56
56
|
|
|
57
57
|
## 貢献
|
|
58
58
|
|
data/lib/exe.rb
CHANGED
|
@@ -101,26 +101,34 @@ class ReadTool < MCP::Tool
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
class ListTool < MCP::Tool
|
|
104
|
-
description "Search for
|
|
104
|
+
description "Search for notes whose name or containing folder names partially match."
|
|
105
105
|
input_schema(
|
|
106
106
|
properties: {
|
|
107
|
-
name: { type: "string", description: "Note name to search" }
|
|
107
|
+
name: { type: "string", description: "Note name to search" },
|
|
108
|
+
limit: { type: "integer", description: "Number of notes to return. Between 1 and #{ObsidianFetch::MAX_LIST_LIMIT}. Default: #{ObsidianFetch::MAX_LIST_SIZE}." }
|
|
108
109
|
},
|
|
109
110
|
required: ["name"]
|
|
110
111
|
)
|
|
111
112
|
|
|
112
|
-
def self.call(name:)
|
|
113
|
+
def self.call(name:, limit: ObsidianFetch::MAX_LIST_SIZE)
|
|
113
114
|
return MCP::Tool::Response.new([{ type: "text", text: "Name must be a string" }], error: true) unless name.is_a?(String)
|
|
114
|
-
result = ObsidianFetch.list_notes($manager.vault, name)
|
|
115
|
+
result = ObsidianFetch.list_notes($manager.vault, name, limit: limit)
|
|
115
116
|
MCP::Tool::Response.new([{ type: "text", text: result.text }], error: result.error)
|
|
116
117
|
end
|
|
117
118
|
end
|
|
118
119
|
|
|
119
120
|
# MCPサーバーの初期化
|
|
121
|
+
#
|
|
122
|
+
# capabilities を明示的に指定しています。
|
|
123
|
+
# MCP::Server は capabilities を渡さない場合、default_capabilities を使います。
|
|
124
|
+
# default_capabilities には tools だけでなく prompts、resources、logging が含まれるため、
|
|
125
|
+
# リソースを持たないこのサーバーでもクライアントが resources/list を呼び出し、空の一覧が返ってしまいます。
|
|
126
|
+
# https://github.com/modelcontextprotocol/ruby-sdk/blob/v1.8.2/lib/mcp/server.rb
|
|
120
127
|
server = MCP::Server.new(
|
|
121
128
|
name: "obsidian-fetch",
|
|
122
129
|
version: ObsidianFetch::VERSION,
|
|
123
130
|
tools: [ReadTool, ListTool],
|
|
131
|
+
capabilities: { tools: { listChanged: true } },
|
|
124
132
|
)
|
|
125
133
|
|
|
126
134
|
# トランスポートを選択してサーバーを起動
|
data/lib/obsidian_fetch/logic.rb
CHANGED
|
@@ -8,7 +8,10 @@ module ObsidianFetch
|
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# list ツールの既定の返却件数
|
|
11
12
|
MAX_LIST_SIZE = 20
|
|
13
|
+
# list ツールで指定できる返却件数の上限
|
|
14
|
+
MAX_LIST_LIMIT = 200
|
|
12
15
|
|
|
13
16
|
# ノート名を正規化する
|
|
14
17
|
# ファイル名に使えない文字については除去しない。aliasで設定されている場合もあるため。
|
|
@@ -16,6 +19,10 @@ module ObsidianFetch
|
|
|
16
19
|
note_name.gsub(/[\[\]\#\^|]/, '')
|
|
17
20
|
end
|
|
18
21
|
|
|
22
|
+
PATH_CORRECTION_PREFACE = <<~EOS
|
|
23
|
+
Presumably a path was specified. The process was automatically renamed and processed.
|
|
24
|
+
EOS
|
|
25
|
+
|
|
19
26
|
# --- ツールロジック ---
|
|
20
27
|
def self.read_note(vault, name)
|
|
21
28
|
name = ObsidianFetch.normalize_note_name(name)
|
|
@@ -27,47 +34,55 @@ module ObsidianFetch
|
|
|
27
34
|
fixed_name = File.basename(name, '.md')
|
|
28
35
|
file_pathes = vault.notes[fixed_name]
|
|
29
36
|
if file_pathes.nil?
|
|
37
|
+
# 完全一致がない場合のみ、大文字小文字の差異を許容した検索をする
|
|
38
|
+
candidates = case_variant_candidates(vault, fixed_name)
|
|
39
|
+
return fuzzy_match_response(vault, fixed_name, candidates, PATH_CORRECTION_PREFACE) unless candidates.empty?
|
|
40
|
+
|
|
30
41
|
# もしも名前で見つからなければ、リンクにも存在しないか確認する
|
|
31
42
|
link_pathes = vault.links_by_file_name[fixed_name]
|
|
32
43
|
if link_pathes.nil?
|
|
33
44
|
return Result.new(text: note_not_found(name), error: true)
|
|
34
45
|
else
|
|
35
46
|
# リンク先のノートが見つかった場合は、prefaceを追加する
|
|
36
|
-
|
|
37
|
-
Presumably a path was specified. The process was automatically renamed and processed.
|
|
38
|
-
EOS
|
|
39
|
-
return Result.new(text: list_links(fixed_name, preface, vault), error: true)
|
|
47
|
+
return Result.new(text: list_links(fixed_name, PATH_CORRECTION_PREFACE, vault), error: true)
|
|
40
48
|
end
|
|
41
49
|
else
|
|
42
50
|
# ノート名が見つかった場合は、prefaceを追加する
|
|
43
|
-
|
|
44
|
-
Presumably a path was specified. The process was automatically renamed and processed.
|
|
45
|
-
EOS
|
|
46
|
-
return Result.new(text: open_file(fixed_name, file_pathes, preface, vault))
|
|
51
|
+
return Result.new(text: PATH_CORRECTION_PREFACE + case_variant_suggestion(vault, fixed_name) + open_file(fixed_name, file_pathes, "", vault))
|
|
47
52
|
end
|
|
48
53
|
end
|
|
49
54
|
|
|
50
55
|
# 名前のノートが存在しない場合
|
|
51
56
|
if file_pathes.nil?
|
|
57
|
+
# 完全一致がない場合のみ、大文字小文字の差異を許容した検索をする
|
|
58
|
+
candidates = case_variant_candidates(vault, name)
|
|
59
|
+
return fuzzy_match_response(vault, name, candidates, preface) unless candidates.empty?
|
|
60
|
+
|
|
52
61
|
return Result.new(text: note_not_found(name), error: true) if vault.links_by_file_name[name].nil?
|
|
53
62
|
return Result.new(text: list_links(name, preface, vault), error: true)
|
|
54
63
|
end
|
|
55
64
|
|
|
56
|
-
Result.new(text: open_file(name, file_pathes, preface, vault))
|
|
65
|
+
Result.new(text: case_variant_suggestion(vault, name) + open_file(name, file_pathes, preface, vault))
|
|
57
66
|
end
|
|
58
67
|
|
|
59
|
-
def self.list_notes(vault, name)
|
|
68
|
+
def self.list_notes(vault, name, limit: MAX_LIST_SIZE)
|
|
69
|
+
unless limit.is_a?(Integer) && limit >= 1 && limit <= MAX_LIST_LIMIT
|
|
70
|
+
return Result.new(text: <<~EOS, error: true)
|
|
71
|
+
The limit must be an integer between 1 and #{MAX_LIST_LIMIT}.
|
|
72
|
+
EOS
|
|
73
|
+
end
|
|
74
|
+
|
|
60
75
|
name = ObsidianFetch.normalize_note_name(name)
|
|
61
76
|
split_name = name.split(/[\t \u3000]+/)
|
|
62
77
|
# 空白文字で検索された場合は失敗した旨を返す
|
|
63
|
-
#
|
|
78
|
+
# 仮に全ノートからランダムに limit 件返してしまうと、LLMが誤って呼び出した場合に偽の関連性を持ってしまうため
|
|
64
79
|
if split_name.empty?
|
|
65
80
|
return Result.new(text: <<~EOS, error: true)
|
|
66
81
|
It cannot be listed in a blank string.
|
|
67
82
|
EOS
|
|
68
83
|
end
|
|
69
|
-
matched_notes = vault.notes.
|
|
70
|
-
|
|
84
|
+
matched_notes = vault.notes.filter_map do |note_name, file_pathes|
|
|
85
|
+
match_note(vault, note_name, file_pathes, split_name)
|
|
71
86
|
end
|
|
72
87
|
# 名前で検索したが見つからない場合
|
|
73
88
|
if matched_notes.empty?
|
|
@@ -79,18 +94,103 @@ module ObsidianFetch
|
|
|
79
94
|
return Result.new(text: <<~EOS, error: true)
|
|
80
95
|
Note not found: #{name}
|
|
81
96
|
However, I found other notes linked to this note.
|
|
82
|
-
#{vault.links_by_file_name[name].
|
|
97
|
+
#{sort_case_insensitive(vault.links_by_file_name[name].map { |file_path| File.basename(file_path, '.md') }).map { |note_name| "- #{note_name}" }.join("\n")}
|
|
83
98
|
EOS
|
|
84
99
|
end
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
# マッチしたノートの数が limit より多い場合は、優先順位の順に枠を埋める
|
|
101
|
+
# truncate されるグループは先頭と末尾を出す
|
|
102
|
+
total = matched_notes.size
|
|
103
|
+
by_name = matched_notes.count { |entry| entry[:matched_by_name] }
|
|
104
|
+
tiers = matched_notes.group_by { |entry| entry[:distance] }.keys.sort.map do |distance|
|
|
105
|
+
matched_notes.select { |entry| entry[:distance] == distance }.sort_by { |entry| [entry[:note_name].downcase, entry[:note_name]] }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
segments = []
|
|
109
|
+
remaining = limit
|
|
110
|
+
tiers.each do |tier|
|
|
111
|
+
break if remaining.zero?
|
|
112
|
+
|
|
113
|
+
if tier.size <= remaining
|
|
114
|
+
segments << {entries: tier, omitted_after: 0}
|
|
115
|
+
remaining -= tier.size
|
|
116
|
+
elsif remaining < 3
|
|
117
|
+
segments << {entries: tier.first(remaining), omitted_after: tier.size - remaining}
|
|
118
|
+
remaining = 0
|
|
119
|
+
else
|
|
120
|
+
head_size = (remaining * 2) / 3
|
|
121
|
+
tail_size = remaining - head_size
|
|
122
|
+
segments << {entries: tier.first(head_size), omitted_after: tier.size - remaining}
|
|
123
|
+
segments << {entries: tier.last(tail_size), omitted_after: 0}
|
|
124
|
+
remaining = 0
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
shown = segments.sum { |segment| segment[:entries].size }
|
|
129
|
+
preface = "#{total} notes matched '#{name}'"
|
|
130
|
+
preface += " (#{by_name} by note name, #{total - by_name} by folder name)" if total > by_name
|
|
131
|
+
preface += ".\n"
|
|
132
|
+
if shown < total
|
|
133
|
+
preface += "I will show you #{shown} of them. To see the rest, narrow the search or raise the limit.\n"
|
|
90
134
|
end
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
135
|
+
preface += "Notes matching '#{name}' are as follows.\n"
|
|
136
|
+
lines = []
|
|
137
|
+
segments.each do |segment|
|
|
138
|
+
lines += segment[:entries].map do |entry|
|
|
139
|
+
if entry[:matched_by_name]
|
|
140
|
+
"- #{entry[:note_name]}"
|
|
141
|
+
else
|
|
142
|
+
"- #{entry[:note_name]} (#{entry[:display_folder]})"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
lines << "... (#{segment[:omitted_after]} notes omitted) ..." if segment[:omitted_after].positive?
|
|
146
|
+
end
|
|
147
|
+
Result.new(text: preface + lines.join("\n") + "\n")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# ノート1件について検索語のマッチを判定し、マッチしていれば情報を返す
|
|
151
|
+
# 距離はファイルからの距離で、ノート名は距離0、直近の親フォルダは距離1、その上が距離2とする
|
|
152
|
+
# 各語の最も近いマッチ位置のうち、最も遠いものがそのノートの距離になる
|
|
153
|
+
def self.match_note(vault, note_name, file_pathes, words)
|
|
154
|
+
lowered_words = words.map(&:downcase)
|
|
155
|
+
best = nil
|
|
156
|
+
|
|
157
|
+
file_pathes.each do |file_path|
|
|
158
|
+
vault_name, rel_path = relative_path(vault, file_path)
|
|
159
|
+
folders = rel_path.split('/')[0...-1]
|
|
160
|
+
|
|
161
|
+
distances = lowered_words.map do |word|
|
|
162
|
+
distance = note_name.downcase.include?(word) ? 0 : nil
|
|
163
|
+
folders.each_with_index do |folder, index|
|
|
164
|
+
next unless folder.downcase.include?(word)
|
|
165
|
+
candidate = folders.size - index
|
|
166
|
+
distance = candidate if distance.nil? || candidate < distance
|
|
167
|
+
end
|
|
168
|
+
distance
|
|
169
|
+
end
|
|
170
|
+
next if distances.any?(&:nil?)
|
|
171
|
+
|
|
172
|
+
display_folder = folders.join('/')
|
|
173
|
+
display_folder = File.join(vault_name, display_folder) if vault.vault_pathes.size > 1 && vault_name
|
|
174
|
+
|
|
175
|
+
entry = {
|
|
176
|
+
note_name: note_name,
|
|
177
|
+
distance: distances.max,
|
|
178
|
+
matched_by_name: distances.max.zero?,
|
|
179
|
+
display_folder: display_folder,
|
|
180
|
+
}
|
|
181
|
+
best = entry if best.nil? || entry[:distance] < best[:distance]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
best
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# 保管庫ルートからの相対パスと、その保管庫名を返す
|
|
188
|
+
def self.relative_path(vault, file_path)
|
|
189
|
+
vault.vault_pathes.each do |vault_path|
|
|
190
|
+
pattern = /\A#{Regexp.escape(vault_path)}\/+/
|
|
191
|
+
return [File.basename(vault_path), file_path.sub(pattern, '')] if file_path.match?(pattern)
|
|
192
|
+
end
|
|
193
|
+
[nil, File.basename(file_path)]
|
|
94
194
|
end
|
|
95
195
|
|
|
96
196
|
# --- ヘルパーメソッド ---
|
|
@@ -100,25 +200,62 @@ module ObsidianFetch
|
|
|
100
200
|
EOS
|
|
101
201
|
end
|
|
102
202
|
|
|
203
|
+
# 大文字小文字は揃えて比較し、同値は原名の ASCII 順でタイブレークする
|
|
204
|
+
def self.sort_case_insensitive(values)
|
|
205
|
+
values.sort_by { |value| [value.downcase, value] }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# 完全一致を除いた、大文字小文字の差異だけのキーをノート名順で返す
|
|
209
|
+
def self.case_variant_candidates(vault, target)
|
|
210
|
+
sort_case_insensitive(vault.notes.keys.reject { |note_name| note_name == target }.select { |note_name| note_name.downcase == target.downcase })
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# 完全一致を読むときに、揺らぎマッチしたノート名のサジェストを組み立てる
|
|
214
|
+
def self.case_variant_suggestion(vault, target)
|
|
215
|
+
candidates = case_variant_candidates(vault, target)
|
|
216
|
+
return "" if candidates.empty?
|
|
217
|
+
|
|
218
|
+
if candidates.size == 1
|
|
219
|
+
"There is also a note with the same name in different capitalization:\n- #{candidates.first}\n\n"
|
|
220
|
+
else
|
|
221
|
+
"There are also notes with the same name in different capitalization:\n" +
|
|
222
|
+
candidates.map { |note_name| "- #{note_name}" }.join("\n") + "\n\n"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# 揺らぎマッチの応答を組み立てる
|
|
227
|
+
# 候補が1件のときはその内容を読み、複数のときは候補名の一覧を返す
|
|
228
|
+
def self.fuzzy_match_response(vault, target, candidates, preface)
|
|
229
|
+
if candidates.size == 1
|
|
230
|
+
note_name = candidates.first
|
|
231
|
+
message = "There is no note named '#{target}'. The following note has the same name with different capitalization:\n- #{note_name}\n\n"
|
|
232
|
+
Result.new(text: preface + message + open_file(note_name, vault.notes[note_name], "", vault), error: false)
|
|
233
|
+
else
|
|
234
|
+
message = "There is no note named '#{target}'. The following notes have the same name with different capitalization:\n" +
|
|
235
|
+
candidates.map { |note_name| "- #{note_name}" }.join("\n") + "\n"
|
|
236
|
+
Result.new(text: preface + message, error: false)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
103
240
|
def self.list_links(name, preface, vault)
|
|
104
241
|
<<~EOS
|
|
105
242
|
#{preface}
|
|
106
243
|
Note not found: #{name}
|
|
107
244
|
However, I found other notes linked to this note.
|
|
108
|
-
#{vault.links_by_file_name[name].
|
|
245
|
+
#{sort_case_insensitive(vault.links_by_file_name[name].map { |file_path| File.basename(file_path, '.md') }).map { |note_name| "- #{note_name}" }.join("\n")}
|
|
109
246
|
EOS
|
|
110
247
|
end
|
|
111
248
|
|
|
112
249
|
def self.open_file(name, file_pathes, preface, vault)
|
|
113
250
|
# 複数のファイルがある場合は、---とファイル名で区切って返す
|
|
114
|
-
result = file_pathes.map do |file_path|
|
|
251
|
+
result = sort_case_insensitive(file_pathes).map do |file_path|
|
|
115
252
|
content = open(file_path) { |f| f.read.force_encoding('UTF-8') }
|
|
116
253
|
link_notes = if vault.links_by_file_path[file_path].nil?
|
|
117
254
|
""
|
|
118
255
|
else
|
|
119
256
|
<<~EOS
|
|
120
257
|
This note is linked by the following notes:
|
|
121
|
-
#{(vault.links_by_file_path[file_path]
|
|
258
|
+
#{sort_case_insensitive(vault.links_by_file_path[file_path].map { |linked_path| File.basename(linked_path, '.md') }).map { |note_name| "- #{note_name}" }.join("\n")}
|
|
122
259
|
EOS
|
|
123
260
|
end
|
|
124
261
|
metadata = <<~EOS
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: obsidian_fetch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- sou7
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: '1.8'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
25
|
+
version: '1.8'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: rack
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -86,7 +86,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
86
86
|
requirements:
|
|
87
87
|
- - ">="
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: 3.
|
|
89
|
+
version: 3.3.0
|
|
90
90
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
91
91
|
requirements:
|
|
92
92
|
- - ">="
|