Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
{
"name": "claudian",
"description": "Obsidian integration for Claude Code. Auto-saves conversation notes and decisions to your Obsidian vault (田中雄一郎OS保管庫).",
"version": "1.3.0",
"version": "1.4.0",
"author": {
"name": "YUICHIRO TANAKA"
},
Expand Down
2 changes: 1 addition & 1 deletion plugins/claudian/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "claudian",
"version": "1.3.0",
"version": "1.4.0",
"description": "Obsidian integration for Claude Code. Auto-saves conversation notes and decisions to your Obsidian vault (田中雄一郎OS保管庫).",
"author": {
"name": "YUICHIRO TANAKA"
Expand Down
10 changes: 8 additions & 2 deletions plugins/claudian/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,18 @@ order:
1. `--vault <path>` on the command line
2. `CLAUDIAN_VAULT_ROOT` environment variable
3. `vaultRoot` in `~/.claudian/config.json` (path overridable with `CLAUDIAN_CONFIG`)
4. Auto-detection of a directory named `田中雄一郎OS保管庫` under:
`~/TANAKA-BRAIN`, `~`, `~/Documents`,
4. Auto-detection of a directory named `田中雄一郎OS保管庫` under, in order:
`~` (the current layout — `/Users/nesty/田中雄一郎OS保管庫`),
`~/TANAKA-BRAIN` (the old layout), `~/Documents`,
`~/Library/Mobile Documents/iCloud~md~obsidian/Documents`,
`~/Library/Mobile Documents/com~apple~CloudDocs`, `~/Dropbox`,
`~/Google Drive`, `~/obsidian`

The home folder is checked first on purpose: a backup or an old copy restored
from the Trash must not outrank the live vault. When more than one candidate
exists the script uses the first and prints the rest on stderr, so a duplicate
never swallows notes unnoticed.

If the vault lives in one of those places, nothing to configure. Otherwise pin
it once per machine:

Expand Down
47 changes: 39 additions & 8 deletions plugins/claudian/scripts/obsidian-save.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ export function configFilePath(env = process.env, home = homedir()) {

export function vaultCandidates(vaultName = DEFAULT_VAULT_NAME, home = homedir()) {
return [
join(home, "TANAKA-BRAIN", vaultName),
join(home, vaultName),
// Legacy layout, kept last-resort on purpose: an old copy restored from the
// Trash must never outrank the vault that actually sits in the home folder.
join(home, "TANAKA-BRAIN", vaultName),
join(home, "Documents", vaultName),
join(home, "Library", "Mobile Documents", "iCloud~md~obsidian", "Documents", vaultName),
join(home, "Library", "Mobile Documents", "com~apple~CloudDocs", vaultName),
Expand Down Expand Up @@ -121,9 +123,11 @@ export function resolveVaultRoot({ explicit = null, env = process.env, home = ho
}

const candidates = vaultCandidates(vaultName, home);
const found = candidates.find(isDirectory);
if (found) {
return { root: found, source: "自動検出", vaultName };
const matches = candidates.filter(isDirectory);
if (matches.length > 0) {
// Duplicates and restored backups are common while a vault is being
// reorganised, so report the ones that lost rather than picking silently.
return { root: matches[0], source: "自動検出", vaultName, alternatives: matches.slice(1) };
}

return { root: null, source: null, vaultName, candidates };
Expand Down Expand Up @@ -304,7 +308,13 @@ export function listFolders(argv, { env = process.env, home = homedir() } = {})
const resolved = requireVault({ explicit: vault, createVault, env, home });
const { folders, sources } = resolveFolders({ vaultRoot: resolved.root, env, home });

return { vaultRoot: resolved.root, source: resolved.source, folders, sources };
return {
vaultRoot: resolved.root,
source: resolved.source,
alternatives: resolved.alternatives ?? [],
folders,
sources
};
}

/**
Expand Down Expand Up @@ -369,6 +379,7 @@ export function scanFolders(argv, { env = process.env, home = homedir() } = {})
return {
vaultRoot: resolved.root,
source: resolved.source,
alternatives: resolved.alternatives ?? [],
configPath,
directories,
proposed,
Expand Down Expand Up @@ -411,7 +422,13 @@ export function saveNote(argv, { env = process.env, home = homedir(), now = new
const filePath = uniqueFilePath(dirPath, `${date}_${sanitizeTitle(title)}`);
writeFileSync(filePath, renderNote({ title, content, tags, date }), "utf8");

return { filePath, vaultRoot: resolved.root, source: resolved.source, folder: relativeDir };
return {
filePath,
vaultRoot: resolved.root,
source: resolved.source,
alternatives: resolved.alternatives ?? [],
folder: relativeDir
};
}

const WIDE_CHARACTER = /[ᄀ-ᅟ⺀-꓏가-힣豈-﫿︰-﹯＀-⦆¢-₩]/;
Expand All @@ -424,13 +441,25 @@ function displayWidth(text) {
return width;
}

function warnAboutAlternatives(alternatives) {
if (!alternatives || alternatives.length === 0) {
return;
}
console.error(`注意: 保管庫の候補が ${alternatives.length + 1} 件見つかりました。使わなかったもの:`);
for (const alternative of alternatives) {
console.error(` - ${alternative}`);
}
console.error("こちらが正しい場合は CLAUDIAN_VAULT_ROOT で固定してください。");
}

function main() {
try {
const argv = process.argv.slice(2);

if (argv.includes("--scan-folders")) {
const scan = scanFolders(argv);
console.log(`保管庫: ${scan.vaultRoot} (${scan.source})`);
warnAboutAlternatives(scan.alternatives);
console.log(`検出したフォルダ (${scan.directories.length}):`);
for (const directory of scan.directories) {
console.log(` ${directory}`);
Expand All @@ -451,8 +480,9 @@ function main() {
}

if (argv.includes("--list-folders")) {
const { vaultRoot, source, folders, sources } = listFolders(argv);
const { vaultRoot, source, alternatives, folders, sources } = listFolders(argv);
console.log(`保管庫: ${vaultRoot} (${source})`);
warnAboutAlternatives(alternatives);
console.log(`フォルダ定義: ${sources.join(" → ")}`);
const width = Math.max(0, ...Object.keys(folders).map(displayWidth));
for (const [alias, directory] of Object.entries(folders)) {
Expand All @@ -461,8 +491,9 @@ function main() {
return;
}

const { filePath, vaultRoot, source, folder } = saveNote(argv);
const { filePath, vaultRoot, source, alternatives, folder } = saveNote(argv);
console.error(`保管庫: ${vaultRoot}/${folder} (${source})`);
warnAboutAlternatives(alternatives);
console.log(basename(filePath));
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
Expand Down
7 changes: 6 additions & 1 deletion plugins/claudian/skills/obsidian-save/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,14 @@ place. Resolution order:
1. `--vault <path>`
2. `CLAUDIAN_VAULT_ROOT`
3. `vaultRoot` in `~/.claudian/config.json`
4. Auto-detection of `田中雄一郎OS保管庫` under `~/TANAKA-BRAIN`, `~`,
4. Auto-detection of `田中雄一郎OS保管庫` under `~` (the real layout:
`/Users/nesty/田中雄一郎OS保管庫`), then `~/TANAKA-BRAIN` (old layout),
`~/Documents`, the Obsidian/iCloud Drive folders, `~/Dropbox`, `~/Google Drive`

If several candidates exist, the script uses the first and prints the others on
stderr. Pass that warning on to the user — it usually means a backup or a copy
restored from the Trash is sitting next to the real vault.

Default folder mapping (overridden by `<vault>/.claudian.json`):

| folder value | directory |
Expand Down
47 changes: 47 additions & 0 deletions tests/obsidian-save.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,53 @@ test("resolveVaultRoot honours --vault, the env var, the config file, then auto-
assert.equal(detected.source, "自動検出");
});

test("resolveVaultRoot finds the vault directly under the home folder", () => {
const home = makeTempDir("claudian-home-");
const vault = path.join(home, DEFAULT_VAULT_NAME);
fs.mkdirSync(vault, { recursive: true });

const resolved = resolveVaultRoot({ env: {}, home });

assert.equal(resolved.root, vault);
assert.equal(resolved.source, "自動検出");
assert.deepEqual(resolved.alternatives, []);
});

test("the home-folder vault outranks an old copy restored under TANAKA-BRAIN", () => {
const home = makeTempDir("claudian-home-");
const vault = path.join(home, DEFAULT_VAULT_NAME);
const legacy = path.join(home, "TANAKA-BRAIN", DEFAULT_VAULT_NAME);
fs.mkdirSync(vault, { recursive: true });
fs.mkdirSync(legacy, { recursive: true });

const resolved = resolveVaultRoot({ env: {}, home });

assert.equal(resolved.root, vault);
assert.deepEqual(resolved.alternatives, [legacy]);

// The losing candidate is reported so a duplicate cannot swallow notes unnoticed.
const saved = saveNote(["--title", "メモ", "--content", "本文"], { env: {}, home });
assert.equal(saved.vaultRoot, vault);
assert.deepEqual(saved.alternatives, [legacy]);
});

test("the CLI warns when more than one vault candidate exists", () => {
const home = makeTempDir("claudian-home-");
const vault = path.join(home, DEFAULT_VAULT_NAME);
const legacy = path.join(home, "TANAKA-BRAIN", DEFAULT_VAULT_NAME);
fs.mkdirSync(vault, { recursive: true });
fs.mkdirSync(legacy, { recursive: true });

const result = run(process.execPath, [SCRIPT, "--title", "メモ", "--content", "本文"], {
env: { ...process.env, HOME: home, CLAUDIAN_VAULT_ROOT: "", CLAUDIAN_CONFIG: path.join(home, "absent.json") }
});

assert.equal(result.status, 0);
assert.match(result.stderr, /保管庫の候補が 2 件見つかりました/);
assert.match(result.stderr, new RegExp(legacy.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.equal(fs.existsSync(path.join(vault, "00_INBOX", result.stdout.trim())), true);
});

test("resolveVaultRoot expands ~ and honours CLAUDIAN_VAULT_NAME", () => {
const home = makeTempDir("claudian-home-");
const named = path.join(home, "Documents", "別の保管庫");
Expand Down