@cueloop/integration-obsidian 0.1.0-alpha.4 → 0.1.0-alpha.41

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/package.json CHANGED
@@ -1,28 +1,33 @@
1
1
  {
2
2
  "name": "@cueloop/integration-obsidian",
3
- "version": "0.1.0-alpha.4",
3
+ "version": "0.1.0-alpha.41",
4
+ "description": "Export reviewed cueloop plans into an Obsidian vault",
5
+ "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/mmurakaru/cueloop/issues"
8
+ },
4
9
  "license": "Apache-2.0",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/mmurakaru/cueloop.git",
13
+ "directory": "packages/integrations/obsidian"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "README.md"
18
+ ],
5
19
  "type": "module",
6
20
  "exports": {
7
21
  ".": "./src/index.ts"
8
22
  },
9
- "dependencies": {
10
- "@cueloop/extension-api": "0.1.0-alpha.4",
11
- "@cueloop/schema": "0.1.0-alpha.4"
12
- },
13
23
  "publishConfig": {
14
24
  "access": "public"
15
25
  },
16
- "files": [
17
- "src",
18
- "README.md"
19
- ],
26
+ "dependencies": {
27
+ "@cueloop/extension-api": "0.1.0-alpha.41",
28
+ "@cueloop/schema": "0.1.0-alpha.41"
29
+ },
20
30
  "engines": {
21
31
  "bun": ">=1.3.0"
22
- },
23
- "repository": {
24
- "type": "git",
25
- "url": "git+https://github.com/mmurakaru/cueloop.git",
26
- "directory": "packages/integrations/obsidian"
27
32
  }
28
33
  }
@@ -15,6 +15,7 @@ afterEach(() => {
15
15
 
16
16
  describe("detectVaults", () => {
17
17
  test("returns registered vault paths that exist on disk", () => {
18
+ // Arrange
18
19
  const alive = join(dir, "notes");
19
20
  mkdirSync(alive);
20
21
  const gone = join(dir, "deleted-vault");
@@ -28,6 +29,8 @@ describe("detectVaults", () => {
28
29
  },
29
30
  }),
30
31
  );
32
+
33
+ // Assert
31
34
  expect(detectVaults(configPath)).toEqual([alive]);
32
35
  });
33
36
 
@@ -44,7 +47,9 @@ describe("detectVaults", () => {
44
47
 
45
48
  describe("obsidianConfigPath", () => {
46
49
  test("resolves per platform", () => {
47
- expect(obsidianConfigPath("darwin")).toContain(join("Library", "Application Support", "obsidian", "obsidian.json"));
50
+ expect(obsidianConfigPath("darwin")).toContain(
51
+ join("Library", "Application Support", "obsidian", "obsidian.json"),
52
+ );
48
53
  expect(obsidianConfigPath("linux")).toContain(join("obsidian", "obsidian.json"));
49
54
  expect(obsidianConfigPath("win32")).toContain(join("obsidian", "obsidian.json"));
50
55
  });
package/src/detect.ts CHANGED
@@ -14,9 +14,17 @@ export function obsidianConfigPath(platform: NodeJS.Platform = process.platform)
14
14
  return join(homedir(), "Library", "Application Support", "obsidian", "obsidian.json");
15
15
  }
16
16
  if (platform === "win32") {
17
- return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "obsidian", "obsidian.json");
17
+ return join(
18
+ process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"),
19
+ "obsidian",
20
+ "obsidian.json",
21
+ );
18
22
  }
19
- return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "obsidian", "obsidian.json");
23
+ return join(
24
+ process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"),
25
+ "obsidian",
26
+ "obsidian.json",
27
+ );
20
28
  }
21
29
 
22
30
  /** Vault paths registered with Obsidian that exist on disk. */
@@ -26,8 +34,8 @@ export function detectVaults(configPath: string = obsidianConfigPath()): string[
26
34
  vaults?: Record<string, { path?: string }>;
27
35
  };
28
36
  return Object.values(raw.vaults ?? {})
29
- .map((v) => v.path)
30
- .filter((p): p is string => typeof p === "string" && existsSync(p));
37
+ .map((vault) => vault.path)
38
+ .filter((path): path is string => typeof path === "string" && existsSync(path));
31
39
  } catch {
32
40
  // no Obsidian install, or an unreadable registry: no vaults
33
41
  return [];
@@ -20,7 +20,13 @@ function resolvedSession(overrides: Partial<ReviewSession> = {}): ReviewSession
20
20
  content: "# Migration Plan\n\nMove the store.\n",
21
21
  meta: { title: "Migration Plan" },
22
22
  },
23
- revisions: [{ revision: 1, content: "# Migration Plan\n\nMove the store.\n", submittedAt: NOW.toISOString() }],
23
+ revisions: [
24
+ {
25
+ revision: 1,
26
+ content: "# Migration Plan\n\nMove the store.\n",
27
+ submittedAt: NOW.toISOString(),
28
+ },
29
+ ],
24
30
  annotations: [],
25
31
  verdict: { kind: "approve", summary: "ship it", feedback: "", resolvedAt: NOW.toISOString() },
26
32
  status: "resolved",
@@ -44,8 +50,13 @@ function config(overrides: Partial<ObsidianConfig> = {}): ObsidianConfig {
44
50
 
45
51
  describe("exportSession", () => {
46
52
  test("writes the final plan with frontmatter into vault/folder", () => {
53
+ // Arrange
47
54
  const session = resolvedSession();
55
+
56
+ // Act
48
57
  const result = exportSession(session, config(), NOW);
58
+
59
+ // Assert
49
60
  expect(result.success).toBe(true);
50
61
  expect(result.path).toBe(join(vault, "cueloop", "2026-08-07 - Migration Plan.md"));
51
62
  const written = readFileSync(result.path!, "utf8");
@@ -57,40 +68,73 @@ describe("exportSession", () => {
57
68
  });
58
69
 
59
70
  test("the working copy wins over the submitted content", () => {
60
- const session = resolvedSession({ workingCopy: "# Migration Plan\n\nMove the store carefully.\n" });
71
+ // Arrange
72
+ const session = resolvedSession({
73
+ workingCopy: "# Migration Plan\n\nMove the store carefully.\n",
74
+ });
75
+
76
+ // Act
61
77
  const result = exportSession(session, config(), NOW);
78
+
79
+ // Assert
62
80
  expect(readFileSync(result.path!, "utf8")).toContain("Move the store carefully.");
63
81
  });
64
82
 
65
83
  test("never overwrites: collisions get counting suffixes", () => {
84
+ // Arrange
66
85
  const session = resolvedSession();
86
+
87
+ // Act
67
88
  const first = exportSession(session, config(), NOW);
68
89
  const second = exportSession(session, config(), NOW);
69
90
  const third = exportSession(session, config(), NOW);
91
+
92
+ // Assert
70
93
  expect(second.path).toBe(join(vault, "cueloop", "2026-08-07 - Migration Plan 2.md"));
71
94
  expect(third.path).toBe(join(vault, "cueloop", "2026-08-07 - Migration Plan 3.md"));
72
95
  expect(existsSync(first.path!)).toBe(true);
73
96
  });
74
97
 
75
98
  test("no vault configured and none detected fails without writing", () => {
99
+ // Arrange
76
100
  const emptyConfigPath = join(vault, "obsidian.json");
77
- const result = exportSession(resolvedSession(), config({ vault: undefined, obsidianConfigPath: emptyConfigPath }), NOW);
101
+
102
+ // Act
103
+ const result = exportSession(
104
+ resolvedSession(),
105
+ config({ vault: undefined, obsidianConfigPath: emptyConfigPath }),
106
+ NOW,
107
+ );
108
+
109
+ // Assert
78
110
  expect(result.success).toBe(false);
79
111
  expect(result.error).toContain("no Obsidian vault");
80
112
  });
81
113
 
82
114
  test("a configured vault path that does not exist fails", () => {
115
+ // Act
83
116
  const result = exportSession(resolvedSession(), config({ vault: join(vault, "missing") }), NOW);
117
+
118
+ // Assert
84
119
  expect(result.success).toBe(false);
85
120
  expect(result.error).toContain("vault not found");
86
121
  });
87
122
 
88
123
  test("falls back to the first auto-detected vault", () => {
124
+ // Arrange
89
125
  const detected = join(vault, "detected-vault");
90
126
  mkdirSync(detected);
91
127
  const obsidianJson = join(vault, "obsidian.json");
92
128
  writeFileSync(obsidianJson, JSON.stringify({ vaults: { v1: { path: detected } } }));
93
- const result = exportSession(resolvedSession(), config({ vault: undefined, obsidianConfigPath: obsidianJson }), NOW);
129
+
130
+ // Act
131
+ const result = exportSession(
132
+ resolvedSession(),
133
+ config({ vault: undefined, obsidianConfigPath: obsidianJson }),
134
+ NOW,
135
+ );
136
+
137
+ // Assert
94
138
  expect(result.success).toBe(true);
95
139
  expect(result.path).toBe(join(detected, "cueloop", "2026-08-07 - Migration Plan.md"));
96
140
  });
@@ -98,12 +142,21 @@ describe("exportSession", () => {
98
142
 
99
143
  describe("obsidian extension", () => {
100
144
  test("registers the exporter through the extension API and exports a resolved session", async () => {
145
+ // Arrange
101
146
  const registry = new Registry();
147
+
148
+ // Act
102
149
  const record = await registry.load("obsidian", createObsidianExtension(config()));
150
+
151
+ // Assert
103
152
  expect(record.errors).toEqual([]);
104
153
  const exporter = record.exporters.get("obsidian");
105
154
  expect(exporter).toBeDefined();
155
+
156
+ // Act
106
157
  const result = await exporter!(resolvedSession());
158
+
159
+ // Assert
107
160
  expect(result.success).toBe(true);
108
161
  expect(existsSync(result.path!)).toBe(true);
109
162
  });
package/src/export.ts CHANGED
@@ -43,7 +43,11 @@ export interface ExportResult {
43
43
  error?: string;
44
44
  }
45
45
 
46
- export function exportSession(session: ReviewSession, config: ObsidianConfig, now: Date = new Date()): ExportResult {
46
+ export function exportSession(
47
+ session: ReviewSession,
48
+ config: ObsidianConfig,
49
+ now: Date = new Date(),
50
+ ): ExportResult {
47
51
  const vault = config.vault ?? detectVaults(config.obsidianConfigPath)[0];
48
52
  if (!vault) return { success: false, error: "no Obsidian vault configured or detected" };
49
53
  if (!existsSync(vault)) return { success: false, error: `vault not found: ${vault}` };
@@ -23,7 +23,10 @@ describe("titleFrom", () => {
23
23
  });
24
24
 
25
25
  test("caps the title at 50 characters", () => {
26
+ // Arrange
26
27
  const long = "# " + "word ".repeat(20);
28
+
29
+ // Assert
27
30
  expect(titleFrom(long).length).toBeLessThanOrEqual(50);
28
31
  expect(titleFrom(long).endsWith(" ")).toBe(false);
29
32
  });
@@ -35,11 +38,15 @@ describe("titleFrom", () => {
35
38
 
36
39
  describe("formatFilename", () => {
37
40
  test("renders date tokens and the title with the space separator", () => {
38
- expect(formatFilename("{YYYY}-{MM}-{DD} - {title}", "My Plan", DATE, "space")).toBe("2026-08-07 - My Plan");
41
+ expect(formatFilename("{YYYY}-{MM}-{DD} - {title}", "My Plan", DATE, "space")).toBe(
42
+ "2026-08-07 - My Plan",
43
+ );
39
44
  });
40
45
 
41
46
  test("dash and underscore separators replace whitespace runs", () => {
42
- expect(formatFilename("{YYYY}-{MM}-{DD} - {title}", "My Plan", DATE, "dash")).toBe("2026-08-07---My-Plan");
47
+ expect(formatFilename("{YYYY}-{MM}-{DD} - {title}", "My Plan", DATE, "dash")).toBe(
48
+ "2026-08-07---My-Plan",
49
+ );
43
50
  expect(formatFilename("{title}", "My Plan", DATE, "underscore")).toBe("My_Plan");
44
51
  });
45
52
  });
@@ -54,10 +61,19 @@ describe("uniquePath", () => {
54
61
  });
55
62
 
56
63
  test("appends counting suffixes instead of overwriting", () => {
64
+ // Assert
57
65
  expect(uniquePath(dir, "plan")).toBe(join(dir, "plan.md"));
66
+
67
+ // Act
58
68
  writeFileSync(join(dir, "plan.md"), "");
69
+
70
+ // Assert
59
71
  expect(uniquePath(dir, "plan")).toBe(join(dir, "plan 2.md"));
72
+
73
+ // Act
60
74
  writeFileSync(join(dir, "plan 2.md"), "");
75
+
76
+ // Assert
61
77
  expect(uniquePath(dir, "plan")).toBe(join(dir, "plan 3.md"));
62
78
  });
63
79
  });
package/src/filename.ts CHANGED
@@ -31,8 +31,13 @@ export function titleFrom(content: string, fallback?: string): string {
31
31
  * Render the filename pattern ({YYYY}, {MM}, {DD}, {title}); each whitespace
32
32
  * run in the result becomes the configured separator.
33
33
  */
34
- export function formatFilename(format: string, title: string, date: Date, separator: Separator = "space"): string {
35
- const pad = (n: number) => String(n).padStart(2, "0");
34
+ export function formatFilename(
35
+ format: string,
36
+ title: string,
37
+ date: Date,
38
+ separator: Separator = "space",
39
+ ): string {
40
+ const pad = (value: number) => String(value).padStart(2, "0");
36
41
  const name = format
37
42
  .replaceAll("{YYYY}", String(date.getFullYear()))
38
43
  .replaceAll("{MM}", pad(date.getMonth() + 1))