granola-toolkit 0.57.0 → 0.58.0

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.
Files changed (3) hide show
  1. package/README.md +5 -7
  2. package/dist/cli.js +301 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -24,19 +24,17 @@ macOS arm64, Linux x64, and Windows x64. Extract the archive and run `granola` (
24
24
  ## Quick Start
25
25
 
26
26
  ```bash
27
+ granola init --provider openrouter
27
28
  granola auth login --api-key grn_...
28
29
  granola sync
29
30
  granola sync --watch
30
- granola automation rules
31
- granola automation runs
32
- granola search customer onboarding
33
- granola folder list
34
- granola meeting list --limit 10
35
- granola notes --folder Team
36
31
  granola web
37
- granola tui
38
32
  ```
39
33
 
34
+ `granola init` creates a local `.granola.toml`, starter harnesses, starter automation rules, and
35
+ prompt files under `./.granola/` so the first-run setup is not just “read docs and assemble JSON by
36
+ hand”.
37
+
40
38
  If you prefer to reuse the desktop app session instead, `granola auth login` still imports it from
41
39
  `supabase.json`.
42
40
 
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { Input, ProcessTerminal, TUI, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
3
3
  import { existsSync } from "node:fs";
4
- import { appendFile, mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
4
+ import { access, appendFile, mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
5
5
  import { homedir, platform, tmpdir } from "node:os";
6
- import { basename, dirname, extname, join, resolve } from "node:path";
6
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
7
7
  import { NodeHtmlMarkdown } from "node-html-markdown";
8
8
  import { createHash, randomUUID } from "node:crypto";
9
9
  import { execFile, spawn } from "node:child_process";
@@ -577,7 +577,7 @@ function latestDocumentTimestamp(document) {
577
577
  });
578
578
  return candidates[0] ?? document.updatedAt;
579
579
  }
580
- async function writeTextFile(filePath, content) {
580
+ async function writeTextFile$1(filePath, content) {
581
581
  await mkdir(dirname(filePath), { recursive: true });
582
582
  await writeFile(filePath, content, "utf8");
583
583
  }
@@ -866,7 +866,7 @@ async function syncManagedExports({ items, kind, onProgress, outputDir }) {
866
866
  const filePath = join(outputDir, plan.fileName);
867
867
  const shouldWrite = !plan.existing || plan.existing.contentHash !== plan.contentHash || plan.existing.fileName !== plan.fileName || !await fileExists(filePath);
868
868
  if (shouldWrite) {
869
- await writeTextFile(filePath, plan.content);
869
+ await writeTextFile$1(filePath, plan.content);
870
870
  written += 1;
871
871
  }
872
872
  const nextEntry = {
@@ -904,7 +904,7 @@ async function syncManagedExports({ items, kind, onProgress, outputDir }) {
904
904
  }, null, 2)}\n`;
905
905
  const statePath = exportStatePath(outputDir, kind);
906
906
  const existingState = await fileExists(statePath) ? await readUtf8(statePath) : void 0;
907
- if (stateChanged || existingState !== serialisedState) await writeTextFile(statePath, serialisedState);
907
+ if (stateChanged || existingState !== serialisedState) await writeTextFile$1(statePath, serialisedState);
908
908
  return written;
909
909
  }
910
910
  //#endregion
@@ -3071,6 +3071,18 @@ function cloneHarness(harness) {
3071
3071
  } : void 0
3072
3072
  };
3073
3073
  }
3074
+ function resolveRelativeHarnessPath(baseDirectory, value) {
3075
+ if (!value?.trim()) return value;
3076
+ return isAbsolute(value) ? value : resolve(baseDirectory, value);
3077
+ }
3078
+ function normaliseFileHarness(baseDirectory, harness) {
3079
+ const cloned = cloneHarness(harness);
3080
+ const cwd = resolveRelativeHarnessPath(baseDirectory, cloned.cwd) ?? baseDirectory;
3081
+ return {
3082
+ ...cloned,
3083
+ cwd
3084
+ };
3085
+ }
3074
3086
  function includesIgnoreCase$1(candidate, values) {
3075
3087
  const lowerCandidate = candidate.toLowerCase();
3076
3088
  return values.some((value) => lowerCandidate.includes(value.toLowerCase()));
@@ -3210,7 +3222,7 @@ var FileAgentHarnessStore = class {
3210
3222
  async readHarnesses() {
3211
3223
  try {
3212
3224
  const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
3213
- return (Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.harnesses) ? parsed.harnesses : []).map((harness) => parseHarness(harness)).filter((harness) => Boolean(harness)).map(cloneHarness);
3225
+ return (Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.harnesses) ? parsed.harnesses : []).map((harness) => parseHarness(harness)).filter((harness) => Boolean(harness)).map((harness) => normaliseFileHarness(dirname(this.filePath), harness));
3214
3226
  } catch {
3215
3227
  return [];
3216
3228
  }
@@ -7761,7 +7773,7 @@ var GranolaApp = class {
7761
7773
  const filePath = resolveWriteFilePath(action, payload);
7762
7774
  if (existsSync(filePath) && action.overwrite === false) throw new Error(`automation write-file target already exists: ${filePath}`);
7763
7775
  const content = renderWriteFileContent(action, payload);
7764
- await writeTextFile(filePath, content);
7776
+ await writeTextFile$1(filePath, content);
7765
7777
  return {
7766
7778
  bytes: Buffer.byteLength(content, "utf8"),
7767
7779
  filePath,
@@ -7802,7 +7814,7 @@ var GranolaApp = class {
7802
7814
  const meetingTitle = match.title || context.artefact.structured.title;
7803
7815
  const folderName = match.folders[0]?.name;
7804
7816
  const filePath = join(target.folderSubdirectories && folderName ? join(target.outputDir, sanitiseFilename(folderName, "folder")) : target.outputDir, sanitiseFilename(target.filenameTemplate?.trim() ? target.filenameTemplate.replaceAll("{{meeting.title}}", meetingTitle).replaceAll("{{artefact.kind}}", context.artefact.kind).replaceAll("{{artefact.title}}", context.artefact.structured.title) : `${sanitiseFilename(`${meetingTitle}-${context.artefact.kind}`)}.md`, "meeting.md"));
7805
- await writeTextFile(filePath, `${this.buildPkmFrontmatter(target, context.artefact, match)}${context.artefact.structured.markdown.trim()}\n`);
7817
+ await writeTextFile$1(filePath, `${this.buildPkmFrontmatter(target, context.artefact, match)}${context.artefact.structured.markdown.trim()}\n`);
7806
7818
  return {
7807
7819
  filePath,
7808
7820
  targetId: target.id
@@ -8465,9 +8477,15 @@ function envFlag(value) {
8465
8477
  if (/^(1|true|yes|on)$/i.test(value)) return true;
8466
8478
  if (/^(0|false|no|off)$/i.test(value)) return false;
8467
8479
  }
8480
+ function resolveConfigPath(configPath, value) {
8481
+ if (!value?.trim()) return value;
8482
+ if (!configPath || isAbsolute(value)) return value;
8483
+ return resolve(dirname(configPath), value);
8484
+ }
8468
8485
  async function loadConfig(options) {
8469
8486
  const env = options.env ?? process.env;
8470
8487
  const config = await loadTomlConfig(pickString(options.globalFlags.config));
8488
+ const configPath = config.path;
8471
8489
  const configValues = config.values;
8472
8490
  const defaultSupabase = firstExistingPath(granolaSupabaseCandidates());
8473
8491
  const defaultCache = firstExistingPath(granolaCacheCandidates());
@@ -8475,9 +8493,9 @@ async function loadConfig(options) {
8475
8493
  const timeoutValue = pickString(options.subcommandFlags.timeout) ?? pickString(env.TIMEOUT) ?? pickString(configValues.timeout) ?? "2m";
8476
8494
  return {
8477
8495
  automation: {
8478
- artefactsFile: pickString(env.GRANOLA_AUTOMATION_ARTEFACTS_FILE) ?? pickString(configValues["automation-artefacts-file"]) ?? pickString(configValues.automationArtefactsFile) ?? defaultGranolaToolkitPersistenceLayout().automationArtefactsFile,
8479
- pkmTargetsFile: pickString(env.GRANOLA_PKM_TARGETS_FILE) ?? pickString(configValues["pkm-targets-file"]) ?? pickString(configValues.pkmTargetsFile) ?? defaultGranolaToolkitPersistenceLayout().pkmTargetsFile,
8480
- rulesFile: pickString(options.globalFlags.rules) ?? pickString(env.GRANOLA_AUTOMATION_RULES_FILE) ?? pickString(configValues["automation-rules-file"]) ?? pickString(configValues.automationRulesFile) ?? defaultGranolaToolkitPersistenceLayout().automationRulesFile
8496
+ artefactsFile: pickString(env.GRANOLA_AUTOMATION_ARTEFACTS_FILE) ?? resolveConfigPath(configPath, pickString(configValues["automation-artefacts-file"]) ?? pickString(configValues.automationArtefactsFile)) ?? defaultGranolaToolkitPersistenceLayout().automationArtefactsFile,
8497
+ pkmTargetsFile: pickString(env.GRANOLA_PKM_TARGETS_FILE) ?? resolveConfigPath(configPath, pickString(configValues["pkm-targets-file"]) ?? pickString(configValues.pkmTargetsFile)) ?? defaultGranolaToolkitPersistenceLayout().pkmTargetsFile,
8498
+ rulesFile: pickString(options.globalFlags.rules) ?? pickString(env.GRANOLA_AUTOMATION_RULES_FILE) ?? resolveConfigPath(configPath, pickString(configValues["automation-rules-file"]) ?? pickString(configValues.automationRulesFile)) ?? defaultGranolaToolkitPersistenceLayout().automationRulesFile
8481
8499
  },
8482
8500
  agents: {
8483
8501
  codexCommand: pickString(env.GRANOLA_CODEX_COMMAND) ?? pickString(configValues["codex-command"]) ?? pickString(configValues.codexCommand) ?? "codex",
@@ -8487,7 +8505,7 @@ async function loadConfig(options) {
8487
8505
  return value === "codex" || value === "openai" || value === "openrouter" ? value : void 0;
8488
8506
  })(),
8489
8507
  dryRun: envFlag(env.GRANOLA_AGENT_DRY_RUN) ?? pickBoolean(configValues["agent-dry-run"]) ?? pickBoolean(configValues.agentDryRun) ?? false,
8490
- harnessesFile: pickString(env.GRANOLA_AGENT_HARNESSES_FILE) ?? pickString(configValues["agent-harnesses-file"]) ?? pickString(configValues.agentHarnessesFile) ?? defaultGranolaToolkitPersistenceLayout().agentHarnessesFile,
8508
+ harnessesFile: pickString(env.GRANOLA_AGENT_HARNESSES_FILE) ?? resolveConfigPath(configPath, pickString(configValues["agent-harnesses-file"]) ?? pickString(configValues.agentHarnessesFile)) ?? defaultGranolaToolkitPersistenceLayout().agentHarnessesFile,
8491
8509
  maxRetries: pickNumber(env.GRANOLA_AGENT_MAX_RETRIES) ?? pickNumber(configValues["agent-max-retries"]) ?? pickNumber(configValues.agentMaxRetries) ?? 2,
8492
8510
  openaiBaseUrl: pickString(env.GRANOLA_OPENAI_BASE_URL) ?? pickString(env.OPENAI_BASE_URL) ?? pickString(configValues["openai-base-url"]) ?? pickString(configValues.openaiBaseUrl) ?? "https://api.openai.com/v1",
8493
8511
  openrouterBaseUrl: pickString(env.GRANOLA_OPENROUTER_BASE_URL) ?? pickString(env.OPENROUTER_BASE_URL) ?? pickString(configValues["openrouter-base-url"]) ?? pickString(configValues.openrouterBaseUrl) ?? "https://openrouter.ai/api/v1",
@@ -8497,13 +8515,13 @@ async function loadConfig(options) {
8497
8515
  configFileUsed: config.path,
8498
8516
  debug: pickBoolean(options.globalFlags.debug) ?? envFlag(env.DEBUG_MODE) ?? pickBoolean(configValues.debug) ?? false,
8499
8517
  notes: {
8500
- output: pickString(options.subcommandFlags.output) ?? pickString(env.OUTPUT) ?? pickString(configValues.output) ?? "./notes",
8518
+ output: pickString(options.subcommandFlags.output) ?? pickString(env.OUTPUT) ?? resolveConfigPath(configPath, pickString(configValues.output)) ?? "./notes",
8501
8519
  timeoutMs: parseDuration(timeoutValue)
8502
8520
  },
8503
- supabase: pickString(options.globalFlags.supabase) ?? pickString(env.SUPABASE_FILE) ?? pickString(configValues.supabase) ?? defaultSupabase,
8521
+ supabase: pickString(options.globalFlags.supabase) ?? pickString(env.SUPABASE_FILE) ?? resolveConfigPath(configPath, pickString(configValues.supabase)) ?? defaultSupabase,
8504
8522
  transcripts: {
8505
- cacheFile: pickString(options.subcommandFlags.cache) ?? pickString(env.CACHE_FILE) ?? pickString(configValues["cache-file"]) ?? pickString(configValues.cacheFile) ?? defaultCache ?? "",
8506
- output: pickString(options.subcommandFlags.output) ?? pickString(env.TRANSCRIPT_OUTPUT) ?? pickString(configValues["transcript-output"]) ?? pickString(configValues.transcriptOutput) ?? "./transcripts"
8523
+ cacheFile: pickString(options.subcommandFlags.cache) ?? pickString(env.CACHE_FILE) ?? resolveConfigPath(configPath, pickString(configValues["cache-file"]) ?? pickString(configValues.cacheFile)) ?? defaultCache ?? "",
8524
+ output: pickString(options.subcommandFlags.output) ?? pickString(env.TRANSCRIPT_OUTPUT) ?? resolveConfigPath(configPath, pickString(configValues["transcript-output"]) ?? pickString(configValues.transcriptOutput)) ?? "./transcripts"
8507
8525
  }
8508
8526
  };
8509
8527
  }
@@ -8724,7 +8742,7 @@ function parseHarnessIds(value) {
8724
8742
  const ids = value.split(",").map((item) => item.trim()).filter(Boolean);
8725
8743
  return ids.length > 0 ? ids : void 0;
8726
8744
  }
8727
- function parseProvider(value) {
8745
+ function parseProvider$1(value) {
8728
8746
  switch (value) {
8729
8747
  case void 0: return;
8730
8748
  case "codex":
@@ -8873,7 +8891,7 @@ const automationCommand = {
8873
8891
  harnessIds: parseHarnessIds(commandFlags.harness),
8874
8892
  kind: parseArtefactKind(commandFlags.kind) ?? "notes",
8875
8893
  model: typeof commandFlags.model === "string" ? commandFlags.model.trim() : void 0,
8876
- provider: parseProvider(commandFlags.provider)
8894
+ provider: parseProvider$1(commandFlags.provider)
8877
8895
  });
8878
8896
  console.log(renderEvaluations(result, format).trimEnd());
8879
8897
  return 0;
@@ -9229,6 +9247,269 @@ async function view$1(query, commandFlags, globalFlags) {
9229
9247
  return 0;
9230
9248
  }
9231
9249
  //#endregion
9250
+ //#region src/init.ts
9251
+ const DEFAULT_MODELS = {
9252
+ codex: "gpt-5-codex",
9253
+ openai: "gpt-5-mini",
9254
+ openrouter: "openai/gpt-5-mini"
9255
+ };
9256
+ const TEAM_PROMPT = `# Team Notes Harness
9257
+
9258
+ Turn this meeting into concise internal notes for the team.
9259
+
9260
+ Requirements:
9261
+
9262
+ - lead with the most important outcome, not a meeting recap
9263
+ - capture decisions, blockers, risks, and follow-ups
9264
+ - keep action items concrete and assign them to named people when possible
9265
+ - prefer the canonical participant names from the meeting context over vague owners like "you"
9266
+ - call out anything that needs manual follow-up if the transcript is ambiguous
9267
+ - keep the tone direct and useful for people who did not attend
9268
+ `;
9269
+ const CUSTOMER_PROMPT = `# Customer Follow-Up Harness
9270
+
9271
+ Turn this meeting into customer-facing follow-up notes for the internal team.
9272
+
9273
+ Requirements:
9274
+
9275
+ - summarise the customer's goals, requests, and concerns
9276
+ - capture commitments we made and anything we still owe them
9277
+ - highlight product feedback, risks, blockers, and dates
9278
+ - prefer named owners for follow-up actions
9279
+ - separate confirmed facts from assumptions when the transcript is unclear
9280
+ - optimise for a quick post-call handoff to sales, success, or product
9281
+ `;
9282
+ function quoteTomlString(value) {
9283
+ return JSON.stringify(value);
9284
+ }
9285
+ function defaultModel(provider, explicitModel) {
9286
+ return explicitModel?.trim() || DEFAULT_MODELS[provider];
9287
+ }
9288
+ function configTemplate(options) {
9289
+ return [
9290
+ "# Generated by `granola init`.",
9291
+ "# Relative paths in this file resolve from the directory that contains this config file.",
9292
+ "# Store Granola auth once with: granola auth login --api-key grn_...",
9293
+ "",
9294
+ `agent-provider = ${quoteTomlString(options.provider)}`,
9295
+ `agent-model = ${quoteTomlString(options.model)}`,
9296
+ "agent-timeout = \"5m\"",
9297
+ "agent-max-retries = 2",
9298
+ "",
9299
+ `agent-harnesses-file = ${quoteTomlString(options.harnessesFile)}`,
9300
+ `automation-rules-file = ${quoteTomlString(options.rulesFile)}`,
9301
+ `pkm-targets-file = ${quoteTomlString(options.pkmTargetsFile)}`,
9302
+ "output = \"./exports/notes\"",
9303
+ "transcript-output = \"./exports/transcripts\"",
9304
+ "",
9305
+ "# For provider credentials:",
9306
+ "# - OpenRouter: export OPENROUTER_API_KEY=...",
9307
+ "# - OpenAI: export OPENAI_API_KEY=...",
9308
+ "# - Codex: make sure `codex exec` works locally",
9309
+ ""
9310
+ ].join("\n");
9311
+ }
9312
+ function harnessesTemplate(options) {
9313
+ return { harnesses: [{
9314
+ id: "team-notes",
9315
+ match: {
9316
+ folderNames: ["Team"],
9317
+ transcriptLoaded: true
9318
+ },
9319
+ model: options.model,
9320
+ name: "Team Notes",
9321
+ priority: 50,
9322
+ promptFile: options.teamPromptFile,
9323
+ provider: options.provider
9324
+ }, {
9325
+ id: "customer-follow-up",
9326
+ match: {
9327
+ folderNames: ["Customers"],
9328
+ transcriptLoaded: true
9329
+ },
9330
+ model: options.model,
9331
+ name: "Customer Follow-Up",
9332
+ priority: 60,
9333
+ promptFile: options.customerPromptFile,
9334
+ provider: options.provider
9335
+ }] };
9336
+ }
9337
+ function rulesTemplate() {
9338
+ return { rules: [{
9339
+ actions: [{
9340
+ approvalMode: "manual",
9341
+ harnessId: "team-notes",
9342
+ id: "team-notes-pipeline",
9343
+ kind: "agent",
9344
+ name: "Generate team notes",
9345
+ pipeline: { kind: "notes" }
9346
+ }],
9347
+ id: "team-notes-on-transcript",
9348
+ name: "Review team notes when a transcript is ready",
9349
+ when: {
9350
+ eventKinds: ["transcript.ready"],
9351
+ folderNames: ["Team"],
9352
+ transcriptLoaded: true
9353
+ }
9354
+ }, {
9355
+ actions: [{
9356
+ approvalMode: "manual",
9357
+ harnessId: "customer-follow-up",
9358
+ id: "customer-follow-up-pipeline",
9359
+ kind: "agent",
9360
+ name: "Generate customer follow-up",
9361
+ pipeline: { kind: "notes" }
9362
+ }],
9363
+ id: "customer-follow-up-on-transcript",
9364
+ name: "Review customer follow-up notes when a transcript is ready",
9365
+ when: {
9366
+ eventKinds: ["transcript.ready"],
9367
+ folderNames: ["Customers"],
9368
+ transcriptLoaded: true
9369
+ }
9370
+ }] };
9371
+ }
9372
+ async function pathExists(filePath) {
9373
+ try {
9374
+ await access(filePath);
9375
+ return true;
9376
+ } catch {
9377
+ return false;
9378
+ }
9379
+ }
9380
+ async function ensureWritable(filePaths, force) {
9381
+ if (force) return;
9382
+ const existing = [];
9383
+ for (const filePath of filePaths) if (await pathExists(filePath)) existing.push(filePath);
9384
+ if (existing.length > 0) throw new Error(`init would overwrite existing files:\n${existing.map((filePath) => `- ${filePath}`).join("\n")}\nRe-run with --force to replace them.`);
9385
+ }
9386
+ async function writeTextFile(filePath, contents) {
9387
+ await mkdir(dirname(filePath), { recursive: true });
9388
+ await writeFile(filePath, contents, "utf8");
9389
+ }
9390
+ async function writeJsonFile(filePath, value) {
9391
+ await writeTextFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
9392
+ }
9393
+ async function initialiseGranolaToolkitProject(options) {
9394
+ const directory = resolve(options.directory);
9395
+ const configPath = join(directory, ".granola.toml");
9396
+ const projectDirectory = join(directory, ".granola");
9397
+ const promptsDirectory = join(projectDirectory, "prompts");
9398
+ const teamPromptPath = join(promptsDirectory, "team-notes.md");
9399
+ const customerPromptPath = join(promptsDirectory, "customer-follow-up.md");
9400
+ const harnessesPath = join(projectDirectory, "agent-harnesses.json");
9401
+ const rulesPath = join(projectDirectory, "automation-rules.json");
9402
+ const pkmTargetsPath = join(projectDirectory, "pkm-targets.json");
9403
+ const files = [
9404
+ configPath,
9405
+ harnessesPath,
9406
+ rulesPath,
9407
+ pkmTargetsPath,
9408
+ teamPromptPath,
9409
+ customerPromptPath
9410
+ ];
9411
+ const provider = options.provider;
9412
+ const model = defaultModel(provider, options.model);
9413
+ await mkdir(directory, { recursive: true });
9414
+ await ensureWritable(files, options.force === true);
9415
+ await writeTextFile(configPath, configTemplate({
9416
+ harnessesFile: `./${relative(directory, harnessesPath)}`,
9417
+ model,
9418
+ pkmTargetsFile: `./${relative(directory, pkmTargetsPath)}`,
9419
+ provider,
9420
+ rulesFile: `./${relative(directory, rulesPath)}`
9421
+ }));
9422
+ await writeJsonFile(harnessesPath, harnessesTemplate({
9423
+ customerPromptFile: `./${relative(directory, customerPromptPath)}`,
9424
+ model,
9425
+ provider,
9426
+ teamPromptFile: `./${relative(directory, teamPromptPath)}`
9427
+ }));
9428
+ await writeJsonFile(rulesPath, rulesTemplate());
9429
+ await writeJsonFile(pkmTargetsPath, { targets: [] });
9430
+ await writeTextFile(teamPromptPath, TEAM_PROMPT);
9431
+ await writeTextFile(customerPromptPath, CUSTOMER_PROMPT);
9432
+ return {
9433
+ configPath,
9434
+ createdFiles: files,
9435
+ directory
9436
+ };
9437
+ }
9438
+ //#endregion
9439
+ //#region src/commands/init.ts
9440
+ function initHelp() {
9441
+ return `Granola init
9442
+
9443
+ Usage:
9444
+ granola init [options]
9445
+
9446
+ Create a local project bootstrap with:
9447
+ - .granola.toml
9448
+ - starter automation rules
9449
+ - starter harness definitions
9450
+ - prompt files for common meeting types
9451
+
9452
+ Options:
9453
+ --dir <path> Target directory (default: current directory)
9454
+ --force Overwrite existing generated files
9455
+ --model <value> Override the starter model for generated harnesses
9456
+ --provider <value> codex, openai, openrouter (default: codex)
9457
+ -h, --help Show help
9458
+ `;
9459
+ }
9460
+ function parseProvider(value) {
9461
+ switch (value) {
9462
+ case void 0: return "codex";
9463
+ case "codex":
9464
+ case "openai":
9465
+ case "openrouter": return value;
9466
+ default: throw new Error("invalid init provider: expected codex, openai, or openrouter");
9467
+ }
9468
+ }
9469
+ function providerNextStep(provider) {
9470
+ switch (provider) {
9471
+ case "openai": return "2. Export OPENAI_API_KEY in the shell or service that runs your sync loop.";
9472
+ case "openrouter": return "2. Export OPENROUTER_API_KEY in the shell or service that runs your sync loop.";
9473
+ default: return "2. Make sure `codex exec` works locally before you enable agent-driven automation.";
9474
+ }
9475
+ }
9476
+ const initCommand = {
9477
+ description: "Create a local Granola Toolkit project bootstrap",
9478
+ flags: {
9479
+ dir: { type: "string" },
9480
+ force: { type: "boolean" },
9481
+ help: { type: "boolean" },
9482
+ model: { type: "string" },
9483
+ provider: { type: "string" }
9484
+ },
9485
+ help: initHelp,
9486
+ name: "init",
9487
+ async run({ commandArgs, commandFlags }) {
9488
+ if (commandArgs.length > 0) throw new Error("granola init does not accept positional arguments");
9489
+ const directory = typeof commandFlags.dir === "string" && commandFlags.dir.trim() ? commandFlags.dir.trim() : process.cwd();
9490
+ const provider = parseProvider(commandFlags.provider);
9491
+ const result = await initialiseGranolaToolkitProject({
9492
+ directory,
9493
+ force: commandFlags.force === true,
9494
+ model: typeof commandFlags.model === "string" ? commandFlags.model.trim() : void 0,
9495
+ provider
9496
+ });
9497
+ const root = resolve(result.directory);
9498
+ console.log(`Initialised Granola Toolkit in ${root}`);
9499
+ console.log("");
9500
+ console.log("Created:");
9501
+ for (const filePath of result.createdFiles) console.log(` - ./${relative(root, filePath)}`);
9502
+ console.log("");
9503
+ console.log("Next:");
9504
+ console.log("1. Store Granola auth once with `granola auth login --api-key grn_...`.");
9505
+ console.log(providerNextStep(provider));
9506
+ console.log("3. Edit the prompt files under ./.granola/prompts/ to match your real meeting types.");
9507
+ console.log("4. Run `granola sync --config ./.granola.toml` and `granola web --config ./.granola.toml`.");
9508
+ console.log("5. If your folders are not named Team or Customers, adjust ./.granola/automation-rules.json and ./.granola/agent-harnesses.json before enabling a watch loop.");
9509
+ return 0;
9510
+ }
9511
+ };
9512
+ //#endregion
9232
9513
  //#region src/browser.ts
9233
9514
  const execFileAsync = promisify(execFile);
9234
9515
  function getBrowserOpenCommand(url, platform = process.platform) {
@@ -10824,6 +11105,7 @@ const commands = [
10824
11105
  authCommand,
10825
11106
  exportsCommand,
10826
11107
  folderCommand,
11108
+ initCommand,
10827
11109
  meetingCommand,
10828
11110
  notesCommand,
10829
11111
  searchCommand,
@@ -10959,6 +11241,7 @@ Global options:
10959
11241
  Examples:
10960
11242
  granola attach http://127.0.0.1:4123
10961
11243
  granola folder list
11244
+ granola init --provider openrouter
10962
11245
  granola sync
10963
11246
  granola notes --supabase "${granolaSupabaseCandidates()[0] ?? "/path/to/supabase.json"}"
10964
11247
  granola transcripts --cache "${granolaCacheCandidates()[0] ?? "/path/to/cache-v3.json"}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granola-toolkit",
3
- "version": "0.57.0",
3
+ "version": "0.58.0",
4
4
  "description": "Toolkit for exporting and working with Granola meetings, notes, and transcripts",
5
5
  "keywords": [
6
6
  "cli",