@mseep/obsidian-agent-client 0.10.6

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.
Files changed (146) hide show
  1. package/.claude/hooks/gh-setup.sh +49 -0
  2. package/.claude/settings.json +15 -0
  3. package/.claude/skills/release-notes/SKILL.md +331 -0
  4. package/.editorconfig +10 -0
  5. package/.github/FUNDING.yml +2 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -0
  7. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.yml +59 -0
  9. package/.github/copilot-instructions.md +45 -0
  10. package/.github/pull_request_template.md +32 -0
  11. package/.github/workflows/ci.yaml +25 -0
  12. package/.github/workflows/docs.yml +58 -0
  13. package/.github/workflows/relay_to_openclaw.yml +59 -0
  14. package/.github/workflows/release.yaml +45 -0
  15. package/.prettierignore +10 -0
  16. package/.prettierrc +13 -0
  17. package/.vscode/extensions.json +7 -0
  18. package/.vscode/settings.json +37 -0
  19. package/.zed/settings.json +42 -0
  20. package/AGENTS.md +330 -0
  21. package/ARCHITECTURE.md +390 -0
  22. package/CONTRIBUTING.md +216 -0
  23. package/LICENSE +202 -0
  24. package/NOTICE +2 -0
  25. package/README.ja.md +121 -0
  26. package/README.md +125 -0
  27. package/docs/.vitepress/config.mts +124 -0
  28. package/docs/.vitepress/theme/custom.css +111 -0
  29. package/docs/.vitepress/theme/index.ts +4 -0
  30. package/docs/agent-setup/claude-code.md +84 -0
  31. package/docs/agent-setup/codex.md +76 -0
  32. package/docs/agent-setup/custom-agents.md +67 -0
  33. package/docs/agent-setup/gemini-cli.md +99 -0
  34. package/docs/agent-setup/index.md +34 -0
  35. package/docs/announcements/gemini-cli-deprecation.md +73 -0
  36. package/docs/getting-started/index.md +78 -0
  37. package/docs/getting-started/quick-start.md +38 -0
  38. package/docs/help/faq.md +181 -0
  39. package/docs/help/troubleshooting.md +221 -0
  40. package/docs/index.md +63 -0
  41. package/docs/public/apple-touch-icon.png +0 -0
  42. package/docs/public/demo.mp4 +0 -0
  43. package/docs/public/favicon-16x16.png +0 -0
  44. package/docs/public/favicon-32x32.png +0 -0
  45. package/docs/public/favicon.ico +0 -0
  46. package/docs/public/images/editing.webp +0 -0
  47. package/docs/public/images/export.webp +0 -0
  48. package/docs/public/images/floating-chat-button.webp +0 -0
  49. package/docs/public/images/floating-chat-instance-menu.webp +0 -0
  50. package/docs/public/images/floating-chat-view.webp +0 -0
  51. package/docs/public/images/mode-selection.webp +0 -0
  52. package/docs/public/images/model-selection.webp +0 -0
  53. package/docs/public/images/multi-session.webp +0 -0
  54. package/docs/public/images/remove-image.webp +0 -0
  55. package/docs/public/images/ribbon-icon.webp +0 -0
  56. package/docs/public/images/selection-context.gif +0 -0
  57. package/docs/public/images/sending-images.webp +0 -0
  58. package/docs/public/images/sending-messages.webp +0 -0
  59. package/docs/public/images/session-history-button.webp +0 -0
  60. package/docs/public/images/slash-commands-1.webp +0 -0
  61. package/docs/public/images/slash-commands-2.webp +0 -0
  62. package/docs/public/images/switch-agent.webp +0 -0
  63. package/docs/public/images/switch-default-agent.webp +0 -0
  64. package/docs/public/images/temporary-disable.gif +0 -0
  65. package/docs/reference/acp-support.md +110 -0
  66. package/docs/usage/chat-export.md +80 -0
  67. package/docs/usage/commands.md +51 -0
  68. package/docs/usage/context-files.md +57 -0
  69. package/docs/usage/editing.md +69 -0
  70. package/docs/usage/floating-chat.md +84 -0
  71. package/docs/usage/index.md +97 -0
  72. package/docs/usage/mcp-tools.md +33 -0
  73. package/docs/usage/mentions.md +70 -0
  74. package/docs/usage/mode-selection.md +28 -0
  75. package/docs/usage/model-selection.md +32 -0
  76. package/docs/usage/multi-session.md +68 -0
  77. package/docs/usage/sending-images.md +64 -0
  78. package/docs/usage/session-history.md +91 -0
  79. package/docs/usage/slash-commands.md +44 -0
  80. package/esbuild.config.mjs +49 -0
  81. package/eslint.config.mjs +25 -0
  82. package/main.js +228 -0
  83. package/manifest.json +11 -0
  84. package/package.json +52 -0
  85. package/src/acp/acp-client.ts +921 -0
  86. package/src/acp/acp-handler.ts +252 -0
  87. package/src/acp/permission-handler.ts +282 -0
  88. package/src/acp/terminal-handler.ts +264 -0
  89. package/src/acp/type-converter.ts +272 -0
  90. package/src/hooks/useAgent.ts +250 -0
  91. package/src/hooks/useAgentMessages.ts +470 -0
  92. package/src/hooks/useAgentSession.ts +544 -0
  93. package/src/hooks/useChatActions.ts +400 -0
  94. package/src/hooks/useHistoryModal.ts +219 -0
  95. package/src/hooks/useSessionHistory.ts +863 -0
  96. package/src/hooks/useSettings.ts +19 -0
  97. package/src/hooks/useSuggestions.ts +342 -0
  98. package/src/main.ts +9 -0
  99. package/src/plugin.ts +1126 -0
  100. package/src/services/chat-exporter.ts +552 -0
  101. package/src/services/message-sender.ts +755 -0
  102. package/src/services/message-state.ts +375 -0
  103. package/src/services/session-helpers.ts +211 -0
  104. package/src/services/session-state.ts +130 -0
  105. package/src/services/session-storage.ts +267 -0
  106. package/src/services/settings-normalizer.ts +255 -0
  107. package/src/services/settings-service.ts +285 -0
  108. package/src/services/update-checker.ts +128 -0
  109. package/src/services/vault-service.ts +558 -0
  110. package/src/services/view-registry.ts +345 -0
  111. package/src/types/agent.ts +92 -0
  112. package/src/types/chat.ts +351 -0
  113. package/src/types/errors.ts +136 -0
  114. package/src/types/obsidian-internals.d.ts +14 -0
  115. package/src/types/session.ts +731 -0
  116. package/src/ui/ChangeDirectoryModal.ts +137 -0
  117. package/src/ui/ChatContext.ts +25 -0
  118. package/src/ui/ChatHeader.tsx +295 -0
  119. package/src/ui/ChatPanel.tsx +1162 -0
  120. package/src/ui/ChatView.tsx +348 -0
  121. package/src/ui/ErrorBanner.tsx +104 -0
  122. package/src/ui/FloatingButton.tsx +351 -0
  123. package/src/ui/FloatingChatView.tsx +531 -0
  124. package/src/ui/InputArea.tsx +1107 -0
  125. package/src/ui/InputToolbar.tsx +371 -0
  126. package/src/ui/MessageBubble.tsx +442 -0
  127. package/src/ui/MessageList.tsx +265 -0
  128. package/src/ui/PermissionBanner.tsx +61 -0
  129. package/src/ui/SessionHistoryModal.tsx +821 -0
  130. package/src/ui/SettingsTab.ts +1337 -0
  131. package/src/ui/SuggestionPopup.tsx +138 -0
  132. package/src/ui/TerminalBlock.tsx +107 -0
  133. package/src/ui/ToolCallBlock.tsx +456 -0
  134. package/src/ui/shared/AttachmentStrip.tsx +57 -0
  135. package/src/ui/shared/IconButton.tsx +55 -0
  136. package/src/ui/shared/MarkdownRenderer.tsx +103 -0
  137. package/src/ui/view-host.ts +56 -0
  138. package/src/utils/error-utils.ts +274 -0
  139. package/src/utils/logger.ts +44 -0
  140. package/src/utils/mention-parser.ts +129 -0
  141. package/src/utils/paths.ts +246 -0
  142. package/src/utils/platform.ts +425 -0
  143. package/styles.css +2322 -0
  144. package/tsconfig.json +18 -0
  145. package/version-bump.mjs +18 -0
  146. package/versions.json +3 -0
@@ -0,0 +1,129 @@
1
+ import { TFile } from "obsidian";
2
+ import { getLogger } from "./logger";
3
+
4
+ // Interface for mention service to avoid circular dependency
5
+ export interface IMentionService {
6
+ getAllFiles(): TFile[];
7
+ }
8
+
9
+ // Mention detection utilities
10
+ export interface MentionContext {
11
+ start: number; // Start index of the @ symbol
12
+ end: number; // Current cursor position
13
+ query: string; // Text after @ symbol
14
+ }
15
+
16
+ // Detect @-mention at current cursor position
17
+ export function detectMention(
18
+ text: string,
19
+ cursorPosition: number,
20
+ ): MentionContext | null {
21
+ const logger = getLogger();
22
+
23
+ if (cursorPosition < 0 || cursorPosition > text.length) {
24
+ logger.log("[detectMention] Invalid cursor position:", cursorPosition);
25
+ return null;
26
+ }
27
+
28
+ // Get text up to cursor position
29
+ const textUpToCursor = text.slice(0, cursorPosition);
30
+
31
+ // Find the last @ symbol
32
+ const atIndex = textUpToCursor.lastIndexOf("@");
33
+ if (atIndex === -1) {
34
+ return null;
35
+ }
36
+
37
+ // Get the token after @
38
+ const afterAt = textUpToCursor.slice(atIndex + 1);
39
+
40
+ // Trigger on @ and allow typing query directly
41
+ let query = "";
42
+ let endPos = cursorPosition;
43
+
44
+ // If already in @[[...]] format, handle it (allow spaces inside brackets)
45
+ if (afterAt.startsWith("[[")) {
46
+ const closingBrackets = afterAt.indexOf("]]");
47
+ if (closingBrackets === -1) {
48
+ // Still typing inside brackets
49
+ query = afterAt.slice(2); // Remove opening [[
50
+ endPos = cursorPosition;
51
+ } else {
52
+ // Found closing brackets - check if cursor is after them
53
+ const closingBracketsPos = atIndex + 1 + closingBrackets + 1; // +1 for second ]
54
+ if (cursorPosition > closingBracketsPos) {
55
+ // Cursor is after ]], no longer a mention
56
+ return null;
57
+ }
58
+ // Complete bracket format
59
+ query = afterAt.slice(2, closingBrackets); // Between [[ and ]]
60
+ endPos = closingBracketsPos + 1; // Include closing ]]
61
+ }
62
+ } else {
63
+ // Simple @query format - use everything after @
64
+ // But end at whitespace (space, tab, newline)
65
+ if (
66
+ afterAt.includes(" ") ||
67
+ afterAt.includes("\t") ||
68
+ afterAt.includes("\n")
69
+ ) {
70
+ return null;
71
+ }
72
+ query = afterAt;
73
+ endPos = cursorPosition;
74
+ }
75
+
76
+ const mentionContext = {
77
+ start: atIndex,
78
+ end: endPos,
79
+ query: query,
80
+ };
81
+ logger.log("[detectMention] Mention context:", mentionContext);
82
+ return mentionContext;
83
+ }
84
+
85
+ // Replace mention in text with the selected note
86
+ export function replaceMention(
87
+ text: string,
88
+ mentionContext: MentionContext,
89
+ noteTitle: string,
90
+ ): { newText: string; newCursorPos: number } {
91
+ const before = text.slice(0, mentionContext.start);
92
+ const after = text.slice(mentionContext.end);
93
+
94
+ // Always use @[[filename]] format
95
+ const replacement = ` @[[${noteTitle}]] `;
96
+
97
+ const newText = before + replacement + after;
98
+ const newCursorPos = mentionContext.start + replacement.length;
99
+
100
+ return { newText, newCursorPos };
101
+ }
102
+
103
+ // Extract all @mentions from text
104
+ export function extractMentionedNotes(
105
+ text: string,
106
+ noteMentionService: IMentionService,
107
+ ): Array<{ noteTitle: string; file: TFile | undefined }> {
108
+ const mentionRegex = /@\[\[([^\]]+)\]\]/g;
109
+ const matches = Array.from(text.matchAll(mentionRegex));
110
+ const result: Array<{ noteTitle: string; file: TFile | undefined }> = [];
111
+ const seen = new Set<string>(); // Avoid duplicates
112
+
113
+ for (const match of matches) {
114
+ const noteTitle = match[1];
115
+ if (seen.has(noteTitle)) {
116
+ continue;
117
+ }
118
+ seen.add(noteTitle);
119
+
120
+ // Find the file by basename
121
+ const file = noteMentionService
122
+ .getAllFiles()
123
+ .find((f: TFile) => f.basename === noteTitle);
124
+
125
+ result.push({ noteTitle, file });
126
+ }
127
+
128
+ return result;
129
+ }
@@ -0,0 +1,246 @@
1
+ import { execFile } from "child_process";
2
+ import { Platform } from "obsidian";
3
+ import { access, stat } from "fs/promises";
4
+ import { constants } from "fs";
5
+ import { join } from "path";
6
+ import { buildWslShellWrapper, getLoginShell } from "./platform";
7
+
8
+ /**
9
+ * Check whether a path string is an absolute path (Unix or Windows).
10
+ */
11
+ export function isAbsolutePath(path: string): boolean {
12
+ return path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path);
13
+ }
14
+
15
+ /**
16
+ * Best-effort fallback for when `which` returns nothing — e.g. macOS GUI-launched
17
+ * apps (Finder/Dock) inherit a reduced PATH that excludes /opt/homebrew/bin, so
18
+ * `which` can fail even when the command is installed. Probes common install
19
+ * directories directly (PATH-independent), returning only an executable regular
20
+ * file so the result matches what `which` would have returned.
21
+ *
22
+ * Best-effort and intentionally narrow: version-manager installs
23
+ * (nvm/fnm/asdf/volta) and ~/.local/bin live in per-version directories a static
24
+ * list cannot enumerate and are out of scope — those users get an honest
25
+ * "Not found". (Windows solves the same reduced-PATH problem authoritatively via
26
+ * the registry; see getFullWindowsPath in platform.ts. macOS has no such
27
+ * side-channel, so this static last-resort list is a deliberate trade-off.)
28
+ *
29
+ * @param command - Bare command name (e.g. "node", "codex-acp")
30
+ * @returns Absolute path to an executable file, or null if not found
31
+ */
32
+ async function findInKnownPaths(command: string): Promise<string | null> {
33
+ // Only resolve bare names within the listed dirs; never let a separator
34
+ // escape via join() (defensive — current callers pass hardcoded names).
35
+ if (command.includes("/") || command.includes("\\")) return null;
36
+
37
+ const dirs = Platform.isMacOS
38
+ ? ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"]
39
+ : ["/usr/local/bin", "/usr/bin", "/bin"];
40
+
41
+ for (const dir of dirs) {
42
+ const candidate = join(dir, command);
43
+ try {
44
+ const st = await stat(candidate); // follows symlinks
45
+ if (!st.isFile()) continue; // reject directories/sockets
46
+ await access(candidate, constants.X_OK); // must be executable
47
+ return candidate;
48
+ } catch {
49
+ // missing / not a runnable file / dangling symlink → keep scanning
50
+ }
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * Resolve the absolute path of a command using `which` (macOS/Linux) or `where` (Windows).
58
+ * If the command is already an absolute path, returns it as-is.
59
+ * Runs asynchronously to avoid blocking the Electron main thread.
60
+ *
61
+ * @param command - Command name (e.g. "node", "claude") or absolute path
62
+ * @returns Absolute path string, or null if not found
63
+ */
64
+ export function resolveCommandPath(command: string): Promise<string | null> {
65
+ if (!command || command.trim().length === 0) return Promise.resolve(null);
66
+
67
+ const trimmed = command.trim();
68
+
69
+ if (isAbsolutePath(trimmed)) {
70
+ return Promise.resolve(trimmed);
71
+ }
72
+
73
+ return new Promise((resolve) => {
74
+ if (Platform.isWin) {
75
+ execFile(
76
+ "where",
77
+ [trimmed],
78
+ { timeout: 5000, windowsHide: true },
79
+ (err, stdout) => {
80
+ if (err) {
81
+ resolve(null);
82
+ return;
83
+ }
84
+ const resolved = stdout.split("\n")[0].trim();
85
+ resolve(resolved.length > 0 ? resolved : null);
86
+ },
87
+ );
88
+ } else {
89
+ const shell = getLoginShell();
90
+ const escaped = trimmed.replace(/'/g, "'\\''");
91
+ execFile(
92
+ shell,
93
+ ["-l", "-c", `which '${escaped}'`],
94
+ { timeout: 5000 },
95
+ (err, stdout) => {
96
+ const fallback = () => {
97
+ findInKnownPaths(trimmed).then(resolve, () =>
98
+ resolve(null),
99
+ );
100
+ };
101
+ if (err) {
102
+ fallback();
103
+ return;
104
+ }
105
+ const resolved = stdout.split("\n")[0].trim();
106
+ if (resolved.length > 0) {
107
+ resolve(resolved);
108
+ } else {
109
+ fallback();
110
+ }
111
+ },
112
+ );
113
+ }
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Resolve the absolute path of a command inside WSL.
119
+ * Uses the WSL shell wrapper (buildWslShellWrapper) to resolve within the Linux environment.
120
+ *
121
+ * @param command - Command name (e.g. "node", "claude")
122
+ * @param distribution - Optional WSL distribution name
123
+ * @returns Linux absolute path string, or null if not found
124
+ */
125
+ export function resolveCommandPathInWsl(
126
+ command: string,
127
+ distribution?: string,
128
+ ): Promise<string | null> {
129
+ if (!command || command.trim().length === 0) return Promise.resolve(null);
130
+
131
+ const trimmed = command.trim();
132
+
133
+ if (isAbsolutePath(trimmed)) {
134
+ return Promise.resolve(trimmed);
135
+ }
136
+
137
+ return new Promise((resolve) => {
138
+ const escaped = trimmed.replace(/'/g, "'\\''");
139
+ const args: string[] = [];
140
+ if (distribution) {
141
+ args.push("-d", distribution);
142
+ }
143
+ const innerCommand = `which '${escaped}'`;
144
+ args.push("sh", "-c", buildWslShellWrapper(innerCommand));
145
+ execFile(
146
+ "C:\\Windows\\System32\\wsl.exe",
147
+ args,
148
+ { timeout: 5000 },
149
+ (err, stdout) => {
150
+ if (err) {
151
+ // No known-paths fallback here on purpose: a host-side
152
+ // existsSync would check the Windows filesystem, not the
153
+ // Linux FS inside WSL. The wrapper already runs a login
154
+ // shell (-l, sources ~/.profile), so the reduced-PATH
155
+ // problem is milder here than on a GUI-launched macOS app.
156
+ resolve(null);
157
+ return;
158
+ }
159
+ const resolved = stdout.split("\n")[0].trim();
160
+ resolve(resolved.length > 0 ? resolved : null);
161
+ },
162
+ );
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Extract the directory containing a command (for PATH adjustments).
168
+ * Example: /usr/local/bin/node → /usr/local/bin
169
+ *
170
+ * @param command - Full path to a command
171
+ * @returns Directory path, or null if cannot be determined
172
+ */
173
+ export function resolveCommandDirectory(command: string): string | null {
174
+ if (!command) {
175
+ return null;
176
+ }
177
+ const lastSlash = Math.max(
178
+ command.lastIndexOf("/"),
179
+ command.lastIndexOf("\\"),
180
+ );
181
+ if (lastSlash <= 0) {
182
+ return null;
183
+ }
184
+ return command.slice(0, lastSlash);
185
+ }
186
+
187
+ /**
188
+ * Resolve the Node.js directory from the plugin's nodePath setting.
189
+ * Returns the directory only when nodePath is an absolute path.
190
+ * When nodePath is empty or a bare command name, returns undefined
191
+ * (the login shell handles PATH resolution).
192
+ *
193
+ * @param nodePathSetting - The raw nodePath setting value
194
+ * @returns Directory path, or undefined
195
+ */
196
+ export function resolveNodeDirectory(
197
+ nodePathSetting: string | undefined,
198
+ ): string | undefined {
199
+ if (!nodePathSetting) return undefined;
200
+ const trimmed = nodePathSetting.trim();
201
+ if (!isAbsolutePath(trimmed)) return undefined;
202
+ return resolveCommandDirectory(trimmed) || undefined;
203
+ }
204
+
205
+ /**
206
+ * Convert absolute path to relative path if it's under basePath.
207
+ * Otherwise return the absolute path as-is.
208
+ *
209
+ * @param absolutePath - The absolute path to convert
210
+ * @param basePath - The base path (e.g., vault path)
211
+ * @returns Relative path if under basePath, otherwise absolute path
212
+ */
213
+ export function toRelativePath(absolutePath: string, basePath: string): string {
214
+ // Normalize paths (remove trailing slashes)
215
+ const normalizedBase = basePath.replace(/\/+$/, "");
216
+ const normalizedPath = absolutePath.replace(/\/+$/, "");
217
+
218
+ if (normalizedPath.startsWith(normalizedBase + "/")) {
219
+ return normalizedPath.slice(normalizedBase.length + 1);
220
+ }
221
+ return absolutePath;
222
+ }
223
+
224
+ /**
225
+ * Build a file URI from an absolute path.
226
+ * Handles both Windows and Unix paths.
227
+ *
228
+ * @param absolutePath - Absolute file path
229
+ * @returns file:// URI
230
+ *
231
+ * @example
232
+ * buildFileUri("/Users/user/note.md") // "file:///Users/user/note.md"
233
+ * buildFileUri("C:\\Users\\user\\note.md") // "file:///C:/Users/user/note.md"
234
+ */
235
+ export function buildFileUri(absolutePath: string): string {
236
+ // Normalize backslashes to forward slashes
237
+ const normalizedPath = absolutePath.replace(/\\/g, "/");
238
+
239
+ // Windows path (e.g., C:/Users/...)
240
+ if (/^[A-Za-z]:/.test(normalizedPath)) {
241
+ return `file:///${normalizedPath}`;
242
+ }
243
+
244
+ // Unix path (e.g., /Users/...)
245
+ return `file://${normalizedPath}`;
246
+ }