diff --git a/Tools/TranscriptedCLI/Sources/TranscriptedCLI/ContextStore.swift b/Tools/TranscriptedCLI/Sources/TranscriptedCLI/ContextStore.swift index ed260d5e..27d0dfa4 100644 --- a/Tools/TranscriptedCLI/Sources/TranscriptedCLI/ContextStore.swift +++ b/Tools/TranscriptedCLI/Sources/TranscriptedCLI/ContextStore.swift @@ -202,8 +202,11 @@ struct CLIContextDirectories { let data = try? Data(contentsOf: manifestURL), let manifest = try? JSONDecoder().decode(CLIAppDirectoryManifest.self, from: data), manifest.version >= 1, + let captureLibrary = validatedConfiguredDirectory(manifest.captureLibraryDirectory, homeDirectory: home), let meetings = validatedConfiguredDirectory(manifest.meetingsDirectory, homeDirectory: home), - let dictations = validatedConfiguredDirectory(manifest.dictationsDirectory, homeDirectory: home) else { + let dictations = validatedConfiguredDirectory(manifest.dictationsDirectory, homeDirectory: home), + isManifestDirectory(meetings, named: "meetings", under: captureLibrary), + isManifestDirectory(dictations, named: "dictations", under: captureLibrary) else { return nil } @@ -256,6 +259,19 @@ struct CLIContextDirectories { return configuredURL } + + private static func isManifestDirectory(_ directory: URL, named name: String, under captureLibrary: URL) -> Bool { + let expected = captureLibrary + .appendingPathComponent(name, isDirectory: true) + .standardizedFileURL + .resolvingSymlinksInPath() + .standardizedFileURL + let actual = directory + .standardizedFileURL + .resolvingSymlinksInPath() + .standardizedFileURL + return actual.path == expected.path + } } struct CLIContextPathOptions: ParsableArguments { diff --git a/Tools/TranscriptedCLI/Tests/TranscriptedCLITests/ContextDirectoriesTests.swift b/Tools/TranscriptedCLI/Tests/TranscriptedCLITests/ContextDirectoriesTests.swift index abbf2767..279a5e53 100644 --- a/Tools/TranscriptedCLI/Tests/TranscriptedCLITests/ContextDirectoriesTests.swift +++ b/Tools/TranscriptedCLI/Tests/TranscriptedCLITests/ContextDirectoriesTests.swift @@ -232,6 +232,49 @@ final class ContextDirectoriesTests: XCTestCase { ]) } + func testResolveIgnoresManifestWhenDirectoriesDoNotMatchCaptureLibrary() throws { + let tempHome = makeTempDir() + defer { removeTempDir(tempHome) } + + let transcriptedRoot = tempHome + .appendingPathComponent("Library", isDirectory: true) + .appendingPathComponent("Application Support", isDirectory: true) + .appendingPathComponent("Transcripted", isDirectory: true) + let manifestRoot = tempHome.appendingPathComponent("manifest-captures", isDirectory: true) + let unrelatedMeetings = tempHome.appendingPathComponent("unrelated-meetings", isDirectory: true) + let unrelatedDictations = tempHome.appendingPathComponent("unrelated-dictations", isDirectory: true) + let defaultCaptures = transcriptedRoot.appendingPathComponent("captures", isDirectory: true) + let defaultMeetings = defaultCaptures.appendingPathComponent("meetings", isDirectory: true) + let defaultDictations = defaultCaptures.appendingPathComponent("dictations", isDirectory: true) + try FileManager.default.createDirectory(at: transcriptedRoot, withIntermediateDirectories: true) + + let manifestURL = transcriptedRoot.appendingPathComponent("mcp-directories.json", isDirectory: false) + try """ + { + "version": 1, + "captureLibraryDirectory": "\(manifestRoot.path)", + "meetingsDirectory": "\(unrelatedMeetings.path)", + "dictationsDirectory": "\(unrelatedDictations.path)" + } + """.write(to: manifestURL, atomically: true, encoding: .utf8) + + let directories = CLIContextDirectories.resolve( + dataDir: nil, + meetingsDir: nil, + dictationsDir: nil, + environment: [:], + fileManager: .default, + homeDirectory: tempHome + ) + + XCTAssertEqual(directories.meetingDirs.map(\.standardizedFileURL.path), [ + defaultMeetings.standardizedFileURL.path, + ]) + XCTAssertEqual(directories.dictationDirs.map(\.standardizedFileURL.path), [ + defaultDictations.standardizedFileURL.path, + ]) + } + func testResolveUsesAppCaptureLibraryPreferenceWhenManifestIsMissing() throws { let tempHome = makeTempDir() defer { removeTempDir(tempHome) } diff --git a/Tools/TranscriptedMCP/CLAUDE.md b/Tools/TranscriptedMCP/CLAUDE.md index c42792c3..a37fd62d 100644 --- a/Tools/TranscriptedMCP/CLAUDE.md +++ b/Tools/TranscriptedMCP/CLAUDE.md @@ -117,6 +117,7 @@ This lets the server answer both meeting-specific queries (`who_is`, `read_meeti cd Tools/TranscriptedMCP swift build -c release swift test +./.build/release/transcripted-mcp --help ./.build/release/transcripted-mcp --self-test ``` diff --git a/Tools/TranscriptedMCP/Sources/TranscriptedMCP/Main.swift b/Tools/TranscriptedMCP/Sources/TranscriptedMCP/Main.swift index f77ee832..864fcd57 100644 --- a/Tools/TranscriptedMCP/Sources/TranscriptedMCP/Main.swift +++ b/Tools/TranscriptedMCP/Sources/TranscriptedMCP/Main.swift @@ -4,6 +4,11 @@ import MCP @main struct TranscriptedMCP { static func main() async throws { + if CommandLine.arguments.contains("--help") || CommandLine.arguments.contains("-h") { + print(Self.helpText) + return + } + if CommandLine.arguments.contains("--version") { print("transcripted-mcp 1.0.0") return @@ -69,6 +74,23 @@ struct TranscriptedMCP { log("MCP server stopped") } + private static let helpText = """ + OVERVIEW: Read-only MCP server for Transcripted meetings and dictations. + + USAGE: transcripted-mcp [--self-test] [--version] [--help] + + OPTIONS: + --self-test Verify directory resolution and SQLite indexing, then exit. + --version Show the version. + -h, --help Show help information. + + ENVIRONMENT: + TRANSCRIPTED_DATA_DIR Shared root with meetings/ and dictations/. + TRANSCRIPTED_MEETINGS_DIR Meeting directory override. + TRANSCRIPTED_DICTATIONS_DIR Dictation directory override. + TRANSCRIPTED_INDEX_DIR SQLite index directory override. + """ + private static func runSelfTest() throws { let directories = TranscriptedDataDirectories.resolve()