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.
- package/README.md +5 -7
- package/dist/cli.js +301 -18
- 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(
|
|
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"}"
|