1.0.2: address community-directory review findings

- Settings: migrate to declarative getSettingDefinitions API (1.13+),
  drop deprecated display()/setDynamicTooltip; minAppVersion -> 1.13.0
- Types: generic apiGet<T>, typed list response, no unsafe any assignment
- Promises: void-mark fire-and-forget sync() call sites
- sanitizeFilename: replace control-char regex range with charCode filter
- Build: replace builtin-modules dep with node:module builtinModules
- Release: GitHub Actions workflow with artifact attestations;
  release assets now only main.js + manifest.json (versions.json stays in repo)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Arshia Navabi 2026-07-16 23:58:37 -07:00
parent 85d6a4f95e
commit cc1daf13a5
No known key found for this signature in database
12 changed files with 312 additions and 260 deletions

51
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,51 @@
name: Release
on:
push:
tags:
- "*"
permissions:
contents: write
id-token: write
attestations: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Build
run: |
npm ci
npm run build
npm test
- name: Verify tag matches manifest version
run: |
MANIFEST_VERSION=$(node -p "require('./manifest.json').version")
if [ "$MANIFEST_VERSION" != "${GITHUB_REF_NAME}" ]; then
echo "Tag ${GITHUB_REF_NAME} does not match manifest version ${MANIFEST_VERSION}" >&2
exit 1
fi
- name: Attest build provenance
uses: actions/attest-build-provenance@v2
with:
subject-path: |
main.js
manifest.json
- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${GITHUB_REF_NAME}" \
--title "${GITHUB_REF_NAME}" \
--generate-notes \
main.js manifest.json

View file

@ -1,6 +1,6 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import { builtinModules } from "node:module";
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
@ -28,7 +28,7 @@ const context = await esbuild.context({
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
...builtinModules,
],
format: "cjs",
target: "es2018",

View file

@ -1,8 +1,8 @@
{
"id": "granola-api-sync",
"name": "Granola API Sync",
"version": "1.0.1",
"minAppVersion": "1.5.0",
"version": "1.0.2",
"minAppVersion": "1.13.0",
"description": "Sync meeting notes and full transcripts from Granola into your vault using the official Granola API. Just paste an API key, no credential scraping.",
"author": "Arshia Navabi",
"authorUrl": "https://github.com/ArshiaEcho",

18
package-lock.json generated
View file

@ -1,16 +1,15 @@
{
"name": "granola-api-sync",
"version": "1.0.0",
"version": "1.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "granola-api-sync",
"version": "1.0.0",
"version": "1.0.2",
"license": "MIT",
"devDependencies": {
"@types/node": "^22.0.0",
"builtin-modules": "^4.0.0",
"esbuild": "^0.24.0",
"obsidian": "latest",
"tslib": "^2.6.0",
@ -512,19 +511,6 @@
"@types/estree": "*"
}
},
"node_modules/builtin-modules": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz",
"integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/crelt": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "granola-api-sync",
"version": "1.0.1",
"version": "1.0.2",
"description": "Sync meeting notes and full transcripts from Granola into Obsidian using the official Granola API.",
"main": "main.js",
"scripts": {
@ -18,7 +18,6 @@
"license": "MIT",
"devDependencies": {
"@types/node": "^22.0.0",
"builtin-modules": "^4.0.0",
"esbuild": "^0.24.0",
"obsidian": "latest",
"tslib": "^2.6.0",

View file

@ -3,7 +3,7 @@ import {
Notice,
Plugin,
PluginSettingTab,
Setting,
SettingDefinitionItem,
TFile,
TFolder,
normalizePath,
@ -43,6 +43,12 @@ interface GranolaNote extends GranolaNoteStub {
summary_text?: string;
}
interface GranolaListResponse {
notes?: GranolaNoteStub[];
hasMore?: boolean;
cursor?: string | null;
}
interface GranolaSyncSettings {
apiKey: string;
notesFolder: string;
@ -78,7 +84,10 @@ interface PluginData {
export function sanitizeFilename(title: string): string {
let name = title.replace(/\//g, "__");
name = name.replace(/[\\:*?"<>|\u0000-\u001f]/g, "");
name = name.replace(/[\\:*?"<>|]/g, "");
name = Array.from(name)
.filter((c) => c.charCodeAt(0) > 0x1f)
.join("");
name = name.replace(/\s+/g, " ").trim().replace(/^\.+|\.+$/g, "");
return name || "Untitled Granola Note";
}
@ -233,11 +242,11 @@ export default class GranolaApiSyncPlugin extends Plugin {
this.addCommand({
id: "sync-now",
name: "Sync now",
callback: () => this.sync(true),
callback: () => void this.sync(true),
});
this.app.workspace.onLayoutReady(() => {
this.sync(false);
void this.sync(false);
this.scheduleInterval();
});
@ -245,7 +254,7 @@ export default class GranolaApiSyncPlugin extends Plugin {
// backgrounded Obsidian can miss every interval tick. Catch up the
// moment the window is used again (throttled to once a minute).
this.registerDomEvent(window, "focus", () => {
if (Date.now() - this.lastSyncAt > 60_000) this.sync(false);
if (Date.now() - this.lastSyncAt > 60_000) void this.sync(false);
});
}
@ -256,7 +265,7 @@ export default class GranolaApiSyncPlugin extends Plugin {
}
const minutes = Math.max(1, this.settings.intervalMinutes);
this.intervalId = window.setInterval(
() => this.sync(false),
() => void this.sync(false),
minutes * 60 * 1000
);
this.registerInterval(this.intervalId);
@ -280,10 +289,10 @@ export default class GranolaApiSyncPlugin extends Plugin {
// ---------- API ----------
private async apiGet(
private async apiGet<T>(
path: string,
params: Record<string, string> = {}
): Promise<Record<string, unknown>> {
): Promise<T> {
const qs = new URLSearchParams(params).toString();
const resp = await requestUrl({
url: `${API_BASE}${path}${qs ? "?" + qs : ""}`,
@ -295,16 +304,18 @@ export default class GranolaApiSyncPlugin extends Plugin {
});
if (resp.status === 401) {
throw new Error(
"API key rejected (401). Check the key in settings — note that Granola API keys require a Business plan."
"API key rejected (401). Check the key in settings. Note that Granola API keys require a Business plan."
);
}
if (resp.status === 429) {
throw new Error("Rate limited by Granola (429) — will retry on next sync.");
throw new Error(
"Rate limited by Granola (429). Will retry on next sync."
);
}
if (resp.status >= 400) {
throw new Error(`Granola API error ${resp.status} on ${path}`);
}
return resp.json as Record<string, unknown>;
return resp.json as T;
}
private async listRecentNotes(): Promise<GranolaNoteStub[]> {
@ -318,10 +329,13 @@ export default class GranolaApiSyncPlugin extends Plugin {
created_after: createdAfter,
};
if (cursor) params.cursor = cursor;
const data = await this.apiGet("/notes", params);
notes.push(...((data.notes as GranolaNoteStub[]) ?? []));
if (!data.hasMore) return notes;
cursor = data.cursor as string;
const data = await this.apiGet<GranolaListResponse>(
"/notes",
params
);
notes.push(...(data.notes ?? []));
if (!data.hasMore || !data.cursor) return notes;
cursor = data.cursor;
}
}
@ -330,7 +344,9 @@ export default class GranolaApiSyncPlugin extends Plugin {
private async ensureFolder(path: string) {
const p = normalizePath(path);
if (!this.app.vault.getAbstractFileByPath(p)) {
await this.app.vault.createFolder(p).catch(() => {});
await this.app.vault.createFolder(p).catch(() => {
// folder may have been created concurrently; harmless
});
}
}
@ -343,9 +359,8 @@ export default class GranolaApiSyncPlugin extends Plugin {
if (!(folder instanceof TFolder)) return idx;
for (const child of folder.children) {
if (!(child instanceof TFile) || child.extension !== "md") continue;
const gid =
this.app.metadataCache.getFileCache(child)?.frontmatter
?.granola_id;
const fm = this.app.metadataCache.getFileCache(child)?.frontmatter;
const gid: unknown = fm?.granola_id;
if (typeof gid === "string" && gid) idx.set(gid, child);
}
return idx;
@ -398,11 +413,11 @@ export default class GranolaApiSyncPlugin extends Plugin {
let errors = 0;
for (const stub of pending) {
try {
const note = (await this.apiGet(`/notes/${stub.id}`, {
include: "transcript",
})) as unknown as GranolaNote;
const uuid =
note.web_url?.match(UUID_RE)?.[0] ?? note.id;
const note = await this.apiGet<GranolaNote>(
`/notes/${stub.id}`,
{ include: "transcript" }
);
const uuid = note.web_url?.match(UUID_RE)?.[0] ?? note.id;
// adopt existing file by granola_id, else create
// (date-suffix on title collision with a different id)
@ -430,7 +445,10 @@ export default class GranolaApiSyncPlugin extends Plugin {
notePath,
buildNoteMd(note, uuid, fname, this.settings)
);
if (this.settings.syncTranscripts && note.transcript?.length) {
if (
this.settings.syncTranscripts &&
note.transcript?.length
) {
await this.writeFile(
`${this.settings.transcriptsFolder}/${fname}-transcript.md`,
buildTranscriptMd(note, uuid, fname, this.settings)
@ -453,7 +471,7 @@ export default class GranolaApiSyncPlugin extends Plugin {
await this.savePluginData();
if (errors) {
this.notice(
`${synced} synced, ${errors} failed — see developer console.`,
`${synced} synced, ${errors} failed. See developer console.`,
true
);
} else {
@ -469,7 +487,7 @@ export default class GranolaApiSyncPlugin extends Plugin {
}
}
// ---------- settings UI ----------
// ---------- settings UI (declarative API, Obsidian 1.13+) ----------
class GranolaSyncSettingTab extends PluginSettingTab {
plugin: GranolaApiSyncPlugin;
@ -479,132 +497,99 @@ class GranolaSyncSettingTab extends PluginSettingTab {
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
getSettingDefinitions(): SettingDefinitionItem[] {
return [
{
name: "Granola API key",
desc: "Create one in the Granola app under Settings → API → Create new key (requires a Granola Business plan). Stored locally in this vault.",
aliases: ["token", "authentication"],
control: { type: "text", key: "apiKey", placeholder: "grn_..." },
},
{
name: "Notes folder",
desc: "Vault folder where meeting notes are created.",
control: { type: "text", key: "notesFolder" },
},
{
name: "Sync transcripts",
desc: "Also save the full meeting transcript as a linked note.",
control: { type: "toggle", key: "syncTranscripts" },
},
{
name: "Transcripts folder",
visible: () => this.plugin.settings.syncTranscripts,
control: { type: "text", key: "transcriptsFolder" },
},
{
name: "Sync interval (minutes)",
desc: "How often to check for new or updated meetings.",
control: {
type: "slider",
key: "intervalMinutes",
min: 1,
max: 60,
step: 1,
},
},
{
name: "Days to look back",
desc: "Meetings created within this window are kept in sync (including late summary updates).",
control: {
type: "slider",
key: "daysBack",
min: 1,
max: 90,
step: 1,
},
},
{
name: "Your email",
desc: "Optional. Excludes you from attendee lists and labels your transcript lines with the name below.",
control: {
type: "text",
key: "myEmail",
placeholder: "you@company.com",
},
},
{
name: "Your name in transcripts",
control: { type: "text", key: "myLabel" },
},
{
name: "Show sync notifications",
desc: "Errors are always shown.",
control: { type: "toggle", key: "showNotices" },
},
];
}
new Setting(containerEl)
.setName("Granola API key")
.setDesc(
"Create one in the Granola app under Settings → API → Create new key (requires a Granola Business plan). Stored locally in this vault."
)
.addText((text) => {
text.setPlaceholder("grn_...")
.setValue(this.plugin.settings.apiKey)
.onChange(async (v) => {
this.plugin.settings.apiKey = v.trim();
await this.plugin.savePluginData();
});
text.inputEl.type = "password";
});
getControlValue(key: string): unknown {
return this.plugin.settings[key as keyof GranolaSyncSettings];
}
new Setting(containerEl)
.setName("Notes folder")
.setDesc("Vault folder where meeting notes are created.")
.addText((t) =>
t
.setValue(this.plugin.settings.notesFolder)
.onChange(async (v) => {
this.plugin.settings.notesFolder =
normalizePath(v || "Granola");
await this.plugin.savePluginData();
})
);
new Setting(containerEl)
.setName("Sync transcripts")
.setDesc("Also save the full meeting transcript as a linked note.")
.addToggle((t) =>
t
.setValue(this.plugin.settings.syncTranscripts)
.onChange(async (v) => {
this.plugin.settings.syncTranscripts = v;
await this.plugin.savePluginData();
this.display();
})
);
if (this.plugin.settings.syncTranscripts) {
new Setting(containerEl)
.setName("Transcripts folder")
.addText((t) =>
t
.setValue(this.plugin.settings.transcriptsFolder)
.onChange(async (v) => {
this.plugin.settings.transcriptsFolder =
normalizePath(v || "Granola/transcripts");
await this.plugin.savePluginData();
})
async setControlValue(key: string, value: unknown): Promise<void> {
const s = this.plugin.settings as unknown as Record<string, unknown>;
switch (key) {
case "apiKey":
case "myEmail":
s[key] = String(value ?? "").trim();
break;
case "myLabel":
s[key] = String(value ?? "").trim() || "Me";
break;
case "notesFolder":
s[key] = normalizePath(String(value ?? "") || "Granola");
break;
case "transcriptsFolder":
s[key] = normalizePath(
String(value ?? "") || "Granola/transcripts"
);
break;
default:
s[key] = value;
}
new Setting(containerEl)
.setName("Sync interval (minutes)")
.setDesc("How often to check for new or updated meetings.")
.addSlider((s) =>
s
.setLimits(1, 60, 1)
.setValue(this.plugin.settings.intervalMinutes)
.setDynamicTooltip()
.onChange(async (v) => {
this.plugin.settings.intervalMinutes = v;
await this.plugin.savePluginData();
this.plugin.scheduleInterval();
})
);
new Setting(containerEl)
.setName("Days to look back")
.setDesc(
"Meetings created within this window are kept in sync (including late summary updates)."
)
.addSlider((s) =>
s
.setLimits(1, 90, 1)
.setValue(this.plugin.settings.daysBack)
.setDynamicTooltip()
.onChange(async (v) => {
this.plugin.settings.daysBack = v;
await this.plugin.savePluginData();
})
);
new Setting(containerEl)
.setName("Your email")
.setDesc(
"Optional. Excludes you from attendee lists and labels your transcript lines with the name below."
)
.addText((t) =>
t
.setPlaceholder("you@company.com")
.setValue(this.plugin.settings.myEmail)
.onChange(async (v) => {
this.plugin.settings.myEmail = v.trim();
await this.plugin.savePluginData();
})
);
new Setting(containerEl)
.setName("Your name in transcripts")
.addText((t) =>
t
.setValue(this.plugin.settings.myLabel)
.onChange(async (v) => {
this.plugin.settings.myLabel = v.trim() || "Me";
await this.plugin.savePluginData();
})
);
new Setting(containerEl)
.setName("Show sync notifications")
.setDesc("Errors are always shown.")
.addToggle((t) =>
t
.setValue(this.plugin.settings.showNotices)
.onChange(async (v) => {
this.plugin.settings.showNotices = v;
await this.plugin.savePluginData();
})
);
await this.plugin.savePluginData();
if (key === "intervalMinutes") this.plugin.scheduleInterval();
if (key === "syncTranscripts") this.update();
}
}

View file

@ -5,8 +5,6 @@ var PluginSettingTab = class {
};
var Notice = class {
};
var Setting = class {
};
var TFile = class {
};
var TFolder = class {
@ -31,7 +29,8 @@ var DEFAULT_SETTINGS = {
};
function sanitizeFilename(title) {
let name = title.replace(/\//g, "__");
name = name.replace(/[\\:*?"<>|\u0000-\u001f]/g, "");
name = name.replace(/[\\:*?"<>|]/g, "");
name = Array.from(name).filter((c) => c.charCodeAt(0) > 31).join("");
name = name.replace(/\s+/g, " ").trim().replace(/^\.+|\.+$/g, "");
return name || "Untitled Granola Note";
}
@ -150,14 +149,14 @@ var GranolaApiSyncPlugin = class extends Plugin {
this.addCommand({
id: "sync-now",
name: "Sync now",
callback: () => this.sync(true)
callback: () => void this.sync(true)
});
this.app.workspace.onLayoutReady(() => {
this.sync(false);
void this.sync(false);
this.scheduleInterval();
});
this.registerDomEvent(window, "focus", () => {
if (Date.now() - this.lastSyncAt > 6e4) this.sync(false);
if (Date.now() - this.lastSyncAt > 6e4) void this.sync(false);
});
}
scheduleInterval() {
@ -167,7 +166,7 @@ var GranolaApiSyncPlugin = class extends Plugin {
}
const minutes = Math.max(1, this.settings.intervalMinutes);
this.intervalId = window.setInterval(
() => this.sync(false),
() => void this.sync(false),
minutes * 60 * 1e3
);
this.registerInterval(this.intervalId);
@ -198,11 +197,13 @@ var GranolaApiSyncPlugin = class extends Plugin {
});
if (resp.status === 401) {
throw new Error(
"API key rejected (401). Check the key in settings \u2014 note that Granola API keys require a Business plan."
"API key rejected (401). Check the key in settings. Note that Granola API keys require a Business plan."
);
}
if (resp.status === 429) {
throw new Error("Rate limited by Granola (429) \u2014 will retry on next sync.");
throw new Error(
"Rate limited by Granola (429). Will retry on next sync."
);
}
if (resp.status >= 400) {
throw new Error(`Granola API error ${resp.status} on ${path}`);
@ -220,9 +221,12 @@ var GranolaApiSyncPlugin = class extends Plugin {
created_after: createdAfter
};
if (cursor) params.cursor = cursor;
const data = await this.apiGet("/notes", params);
const data = await this.apiGet(
"/notes",
params
);
notes.push(...data.notes ?? []);
if (!data.hasMore) return notes;
if (!data.hasMore || !data.cursor) return notes;
cursor = data.cursor;
}
}
@ -243,7 +247,8 @@ var GranolaApiSyncPlugin = class extends Plugin {
if (!(folder instanceof TFolder)) return idx;
for (const child of folder.children) {
if (!(child instanceof TFile) || child.extension !== "md") continue;
const gid = this.app.metadataCache.getFileCache(child)?.frontmatter?.granola_id;
const fm = this.app.metadataCache.getFileCache(child)?.frontmatter;
const gid = fm?.granola_id;
if (typeof gid === "string" && gid) idx.set(gid, child);
}
return idx;
@ -291,9 +296,10 @@ var GranolaApiSyncPlugin = class extends Plugin {
let errors = 0;
for (const stub of pending) {
try {
const note = await this.apiGet(`/notes/${stub.id}`, {
include: "transcript"
});
const note = await this.apiGet(
`/notes/${stub.id}`,
{ include: "transcript" }
);
const uuid = note.web_url?.match(UUID_RE)?.[0] ?? note.id;
let fname;
const adopted = idx.get(uuid);
@ -340,7 +346,7 @@ var GranolaApiSyncPlugin = class extends Plugin {
await this.savePluginData();
if (errors) {
this.notice(
`${synced} synced, ${errors} failed \u2014 see developer console.`,
`${synced} synced, ${errors} failed. See developer console.`,
true
);
} else {
@ -360,74 +366,98 @@ var GranolaSyncSettingTab = class extends PluginSettingTab {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl).setName("Granola API key").setDesc(
"Create one in the Granola app under Settings \u2192 API \u2192 Create new key (requires a Granola Business plan). Stored locally in this vault."
).addText((text) => {
text.setPlaceholder("grn_...").setValue(this.plugin.settings.apiKey).onChange(async (v) => {
this.plugin.settings.apiKey = v.trim();
await this.plugin.savePluginData();
});
text.inputEl.type = "password";
});
new Setting(containerEl).setName("Notes folder").setDesc("Vault folder where meeting notes are created.").addText(
(t) => t.setValue(this.plugin.settings.notesFolder).onChange(async (v) => {
this.plugin.settings.notesFolder = normalizePath(v || "Granola");
await this.plugin.savePluginData();
})
);
new Setting(containerEl).setName("Sync transcripts").setDesc("Also save the full meeting transcript as a linked note.").addToggle(
(t) => t.setValue(this.plugin.settings.syncTranscripts).onChange(async (v) => {
this.plugin.settings.syncTranscripts = v;
await this.plugin.savePluginData();
this.display();
})
);
if (this.plugin.settings.syncTranscripts) {
new Setting(containerEl).setName("Transcripts folder").addText(
(t) => t.setValue(this.plugin.settings.transcriptsFolder).onChange(async (v) => {
this.plugin.settings.transcriptsFolder = normalizePath(v || "Granola/transcripts");
await this.plugin.savePluginData();
})
);
getSettingDefinitions() {
return [
{
name: "Granola API key",
desc: "Create one in the Granola app under Settings \u2192 API \u2192 Create new key (requires a Granola Business plan). Stored locally in this vault.",
aliases: ["token", "authentication"],
control: { type: "text", key: "apiKey", placeholder: "grn_..." }
},
{
name: "Notes folder",
desc: "Vault folder where meeting notes are created.",
control: { type: "text", key: "notesFolder" }
},
{
name: "Sync transcripts",
desc: "Also save the full meeting transcript as a linked note.",
control: { type: "toggle", key: "syncTranscripts" }
},
{
name: "Transcripts folder",
visible: () => this.plugin.settings.syncTranscripts,
control: { type: "text", key: "transcriptsFolder" }
},
{
name: "Sync interval (minutes)",
desc: "How often to check for new or updated meetings.",
control: {
type: "slider",
key: "intervalMinutes",
min: 1,
max: 60,
step: 1
}
},
{
name: "Days to look back",
desc: "Meetings created within this window are kept in sync (including late summary updates).",
control: {
type: "slider",
key: "daysBack",
min: 1,
max: 90,
step: 1
}
},
{
name: "Your email",
desc: "Optional. Excludes you from attendee lists and labels your transcript lines with the name below.",
control: {
type: "text",
key: "myEmail",
placeholder: "you@company.com"
}
},
{
name: "Your name in transcripts",
control: { type: "text", key: "myLabel" }
},
{
name: "Show sync notifications",
desc: "Errors are always shown.",
control: { type: "toggle", key: "showNotices" }
}
];
}
getControlValue(key) {
return this.plugin.settings[key];
}
async setControlValue(key, value) {
const s = this.plugin.settings;
switch (key) {
case "apiKey":
case "myEmail":
s[key] = String(value ?? "").trim();
break;
case "myLabel":
s[key] = String(value ?? "").trim() || "Me";
break;
case "notesFolder":
s[key] = normalizePath(String(value ?? "") || "Granola");
break;
case "transcriptsFolder":
s[key] = normalizePath(
String(value ?? "") || "Granola/transcripts"
);
break;
default:
s[key] = value;
}
new Setting(containerEl).setName("Sync interval (minutes)").setDesc("How often to check for new or updated meetings.").addSlider(
(s) => s.setLimits(1, 60, 1).setValue(this.plugin.settings.intervalMinutes).setDynamicTooltip().onChange(async (v) => {
this.plugin.settings.intervalMinutes = v;
await this.plugin.savePluginData();
this.plugin.scheduleInterval();
})
);
new Setting(containerEl).setName("Days to look back").setDesc(
"Meetings created within this window are kept in sync (including late summary updates)."
).addSlider(
(s) => s.setLimits(1, 90, 1).setValue(this.plugin.settings.daysBack).setDynamicTooltip().onChange(async (v) => {
this.plugin.settings.daysBack = v;
await this.plugin.savePluginData();
})
);
new Setting(containerEl).setName("Your email").setDesc(
"Optional. Excludes you from attendee lists and labels your transcript lines with the name below."
).addText(
(t) => t.setPlaceholder("you@company.com").setValue(this.plugin.settings.myEmail).onChange(async (v) => {
this.plugin.settings.myEmail = v.trim();
await this.plugin.savePluginData();
})
);
new Setting(containerEl).setName("Your name in transcripts").addText(
(t) => t.setValue(this.plugin.settings.myLabel).onChange(async (v) => {
this.plugin.settings.myLabel = v.trim() || "Me";
await this.plugin.savePluginData();
})
);
new Setting(containerEl).setName("Show sync notifications").setDesc("Errors are always shown.").addToggle(
(t) => t.setValue(this.plugin.settings.showNotices).onChange(async (v) => {
this.plugin.settings.showNotices = v;
await this.plugin.savePluginData();
})
);
await this.plugin.savePluginData();
if (key === "intervalMinutes") this.plugin.scheduleInterval();
if (key === "syncTranscripts") this.update();
}
};
export {

View file

@ -3,7 +3,7 @@ granola_id: 874b7bfd-0aa2-43a4-94c2-247d0edba2ef
title: Finn Grahe and Arshia Navabi
type: note
created: 2026-07-16T18:59:50.339Z
updated: 2026-07-16T20:06:42.499Z
updated: 2026-07-16T23:53:10.393Z
attendees:
- finn@resonumai.com
transcript: '[[inbox/granola/transcripts/Finn Grahe and Arshia Navabi-transcript.md]]'

View file

@ -3,7 +3,7 @@ granola_id: 874b7bfd-0aa2-43a4-94c2-247d0edba2ef
title: Finn Grahe and Arshia Navabi
type: note
created: 2026-07-16T18:59:50.339Z
updated: 2026-07-16T20:06:42.499Z
updated: 2026-07-16T23:53:10.393Z
attendees:
- finn@resonumai.com
transcript: '[[inbox/granola/transcripts/Finn Grahe and Arshia Navabi-transcript.md]]'

View file

@ -3,7 +3,7 @@ granola_id: 874b7bfd-0aa2-43a4-94c2-247d0edba2ef
title: Finn Grahe and Arshia Navabi - Transcript
type: transcript
created: 2026-07-16T18:59:50.339Z
updated: 2026-07-16T20:06:42.499Z
updated: 2026-07-16T23:53:10.393Z
attendees:
- finn@resonumai.com
note: "[[inbox/granola/Finn Grahe and Arshia Navabi.md]]"

View file

@ -3,7 +3,7 @@ granola_id: 874b7bfd-0aa2-43a4-94c2-247d0edba2ef
title: Finn Grahe and Arshia Navabi - Transcript
type: transcript
created: 2026-07-16T18:59:50.339Z
updated: 2026-07-16T20:06:42.499Z
updated: 2026-07-16T23:53:10.393Z
attendees:
- finn@resonumai.com
note: "[[inbox/granola/Finn Grahe and Arshia Navabi.md]]"

View file

@ -1,4 +1,5 @@
{
"1.0.0": "1.5.0",
"1.0.1": "1.5.0"
"1.0.1": "1.5.0",
"1.0.2": "1.13.0"
}