@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.
- package/.claude/hooks/gh-setup.sh +49 -0
- package/.claude/settings.json +15 -0
- package/.claude/skills/release-notes/SKILL.md +331 -0
- package/.editorconfig +10 -0
- package/.github/FUNDING.yml +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +59 -0
- package/.github/copilot-instructions.md +45 -0
- package/.github/pull_request_template.md +32 -0
- package/.github/workflows/ci.yaml +25 -0
- package/.github/workflows/docs.yml +58 -0
- package/.github/workflows/relay_to_openclaw.yml +59 -0
- package/.github/workflows/release.yaml +45 -0
- package/.prettierignore +10 -0
- package/.prettierrc +13 -0
- package/.vscode/extensions.json +7 -0
- package/.vscode/settings.json +37 -0
- package/.zed/settings.json +42 -0
- package/AGENTS.md +330 -0
- package/ARCHITECTURE.md +390 -0
- package/CONTRIBUTING.md +216 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.ja.md +121 -0
- package/README.md +125 -0
- package/docs/.vitepress/config.mts +124 -0
- package/docs/.vitepress/theme/custom.css +111 -0
- package/docs/.vitepress/theme/index.ts +4 -0
- package/docs/agent-setup/claude-code.md +84 -0
- package/docs/agent-setup/codex.md +76 -0
- package/docs/agent-setup/custom-agents.md +67 -0
- package/docs/agent-setup/gemini-cli.md +99 -0
- package/docs/agent-setup/index.md +34 -0
- package/docs/announcements/gemini-cli-deprecation.md +73 -0
- package/docs/getting-started/index.md +78 -0
- package/docs/getting-started/quick-start.md +38 -0
- package/docs/help/faq.md +181 -0
- package/docs/help/troubleshooting.md +221 -0
- package/docs/index.md +63 -0
- package/docs/public/apple-touch-icon.png +0 -0
- package/docs/public/demo.mp4 +0 -0
- package/docs/public/favicon-16x16.png +0 -0
- package/docs/public/favicon-32x32.png +0 -0
- package/docs/public/favicon.ico +0 -0
- package/docs/public/images/editing.webp +0 -0
- package/docs/public/images/export.webp +0 -0
- package/docs/public/images/floating-chat-button.webp +0 -0
- package/docs/public/images/floating-chat-instance-menu.webp +0 -0
- package/docs/public/images/floating-chat-view.webp +0 -0
- package/docs/public/images/mode-selection.webp +0 -0
- package/docs/public/images/model-selection.webp +0 -0
- package/docs/public/images/multi-session.webp +0 -0
- package/docs/public/images/remove-image.webp +0 -0
- package/docs/public/images/ribbon-icon.webp +0 -0
- package/docs/public/images/selection-context.gif +0 -0
- package/docs/public/images/sending-images.webp +0 -0
- package/docs/public/images/sending-messages.webp +0 -0
- package/docs/public/images/session-history-button.webp +0 -0
- package/docs/public/images/slash-commands-1.webp +0 -0
- package/docs/public/images/slash-commands-2.webp +0 -0
- package/docs/public/images/switch-agent.webp +0 -0
- package/docs/public/images/switch-default-agent.webp +0 -0
- package/docs/public/images/temporary-disable.gif +0 -0
- package/docs/reference/acp-support.md +110 -0
- package/docs/usage/chat-export.md +80 -0
- package/docs/usage/commands.md +51 -0
- package/docs/usage/context-files.md +57 -0
- package/docs/usage/editing.md +69 -0
- package/docs/usage/floating-chat.md +84 -0
- package/docs/usage/index.md +97 -0
- package/docs/usage/mcp-tools.md +33 -0
- package/docs/usage/mentions.md +70 -0
- package/docs/usage/mode-selection.md +28 -0
- package/docs/usage/model-selection.md +32 -0
- package/docs/usage/multi-session.md +68 -0
- package/docs/usage/sending-images.md +64 -0
- package/docs/usage/session-history.md +91 -0
- package/docs/usage/slash-commands.md +44 -0
- package/esbuild.config.mjs +49 -0
- package/eslint.config.mjs +25 -0
- package/main.js +228 -0
- package/manifest.json +11 -0
- package/package.json +52 -0
- package/src/acp/acp-client.ts +921 -0
- package/src/acp/acp-handler.ts +252 -0
- package/src/acp/permission-handler.ts +282 -0
- package/src/acp/terminal-handler.ts +264 -0
- package/src/acp/type-converter.ts +272 -0
- package/src/hooks/useAgent.ts +250 -0
- package/src/hooks/useAgentMessages.ts +470 -0
- package/src/hooks/useAgentSession.ts +544 -0
- package/src/hooks/useChatActions.ts +400 -0
- package/src/hooks/useHistoryModal.ts +219 -0
- package/src/hooks/useSessionHistory.ts +863 -0
- package/src/hooks/useSettings.ts +19 -0
- package/src/hooks/useSuggestions.ts +342 -0
- package/src/main.ts +9 -0
- package/src/plugin.ts +1126 -0
- package/src/services/chat-exporter.ts +552 -0
- package/src/services/message-sender.ts +755 -0
- package/src/services/message-state.ts +375 -0
- package/src/services/session-helpers.ts +211 -0
- package/src/services/session-state.ts +130 -0
- package/src/services/session-storage.ts +267 -0
- package/src/services/settings-normalizer.ts +255 -0
- package/src/services/settings-service.ts +285 -0
- package/src/services/update-checker.ts +128 -0
- package/src/services/vault-service.ts +558 -0
- package/src/services/view-registry.ts +345 -0
- package/src/types/agent.ts +92 -0
- package/src/types/chat.ts +351 -0
- package/src/types/errors.ts +136 -0
- package/src/types/obsidian-internals.d.ts +14 -0
- package/src/types/session.ts +731 -0
- package/src/ui/ChangeDirectoryModal.ts +137 -0
- package/src/ui/ChatContext.ts +25 -0
- package/src/ui/ChatHeader.tsx +295 -0
- package/src/ui/ChatPanel.tsx +1162 -0
- package/src/ui/ChatView.tsx +348 -0
- package/src/ui/ErrorBanner.tsx +104 -0
- package/src/ui/FloatingButton.tsx +351 -0
- package/src/ui/FloatingChatView.tsx +531 -0
- package/src/ui/InputArea.tsx +1107 -0
- package/src/ui/InputToolbar.tsx +371 -0
- package/src/ui/MessageBubble.tsx +442 -0
- package/src/ui/MessageList.tsx +265 -0
- package/src/ui/PermissionBanner.tsx +61 -0
- package/src/ui/SessionHistoryModal.tsx +821 -0
- package/src/ui/SettingsTab.ts +1337 -0
- package/src/ui/SuggestionPopup.tsx +138 -0
- package/src/ui/TerminalBlock.tsx +107 -0
- package/src/ui/ToolCallBlock.tsx +456 -0
- package/src/ui/shared/AttachmentStrip.tsx +57 -0
- package/src/ui/shared/IconButton.tsx +55 -0
- package/src/ui/shared/MarkdownRenderer.tsx +103 -0
- package/src/ui/view-host.ts +56 -0
- package/src/utils/error-utils.ts +274 -0
- package/src/utils/logger.ts +44 -0
- package/src/utils/mention-parser.ts +129 -0
- package/src/utils/paths.ts +246 -0
- package/src/utils/platform.ts +425 -0
- package/styles.css +2322 -0
- package/tsconfig.json +18 -0
- package/version-bump.mjs +18 -0
- 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
|
+
}
|