@joe-broadhead/opencode-granola 0.0.0 → 0.0.2
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/CHANGELOG.md +67 -0
- package/README.md +56 -19
- package/dist/index.js +549 -151
- package/dist/tui.js +54 -28
- package/package.json +1 -1
- package/skills/granola/SKILL.md +5 -1
package/dist/tui.js
CHANGED
|
@@ -4,18 +4,26 @@ import { createSignal } from "solid-js";
|
|
|
4
4
|
|
|
5
5
|
// src/rpc/contract.ts
|
|
6
6
|
import { Rpc } from "@opencode/plugin/rpc";
|
|
7
|
+
var STATES = ["connected", "sign_in_required", "connecting", "disabled", "failed", "not_configured", "conflict"];
|
|
7
8
|
var STATUS_SCHEMA = {
|
|
8
9
|
type: "object",
|
|
9
10
|
additionalProperties: false,
|
|
10
|
-
required: ["
|
|
11
|
+
required: ["version", "host", "state", "tools", "unguardedAgents", "exportDirectory", "nextStep"],
|
|
11
12
|
properties: {
|
|
12
|
-
|
|
13
|
+
version: { type: "string" },
|
|
14
|
+
host: {
|
|
15
|
+
type: "object",
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
required: ["version", "range", "tested"],
|
|
18
|
+
properties: { version: { type: ["string", "null"] }, range: { type: "string" }, tested: { type: "boolean" } }
|
|
19
|
+
},
|
|
20
|
+
state: { type: "string", enum: STATES },
|
|
13
21
|
title: { type: "string" },
|
|
14
22
|
problem: {
|
|
15
23
|
type: "object",
|
|
16
24
|
additionalProperties: false,
|
|
17
|
-
required: ["code", "message"
|
|
18
|
-
properties: { code: { type: "string" }, message: { type: "string" }
|
|
25
|
+
required: ["code", "message"],
|
|
26
|
+
properties: { code: { type: "string" }, message: { type: "string" } }
|
|
19
27
|
},
|
|
20
28
|
conflict: { type: "string" },
|
|
21
29
|
tools: {
|
|
@@ -45,31 +53,43 @@ var STATUS_SCHEMA = {
|
|
|
45
53
|
}
|
|
46
54
|
},
|
|
47
55
|
exportDirectory: { type: "string" },
|
|
48
|
-
pluginVersion: { type: "string" },
|
|
49
|
-
hostVersion: { type: "string" },
|
|
50
56
|
hostWarning: { type: "string" },
|
|
51
|
-
|
|
57
|
+
nextStep: { type: "string" }
|
|
52
58
|
}
|
|
53
59
|
};
|
|
60
|
+
var STATUS_ERROR_SCHEMA = {
|
|
61
|
+
type: "object",
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
required: ["code", "next"],
|
|
64
|
+
properties: { code: { type: "string" }, next: { type: "string" } }
|
|
65
|
+
};
|
|
54
66
|
var STATE_LABELS = {
|
|
55
67
|
connected: "connected",
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
connecting: "connecting",
|
|
69
|
+
sign_in_required: "sign-in required",
|
|
58
70
|
failed: "failed",
|
|
59
71
|
disabled: "disabled",
|
|
60
|
-
|
|
72
|
+
not_configured: "not configured",
|
|
73
|
+
conflict: "conflict"
|
|
61
74
|
};
|
|
62
75
|
var TONES = {
|
|
63
76
|
connected: "ok",
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
connecting: "info",
|
|
78
|
+
sign_in_required: "warning",
|
|
66
79
|
failed: "error",
|
|
67
80
|
disabled: "info",
|
|
68
|
-
|
|
81
|
+
not_configured: "warning",
|
|
82
|
+
conflict: "warning"
|
|
69
83
|
};
|
|
84
|
+
function hostRow(status) {
|
|
85
|
+
const { version, range, tested } = status.host;
|
|
86
|
+
const found = version ?? "unknown";
|
|
87
|
+
if (status.hostWarning) return { label: "OpenCode", text: `${found} (host untested: ${status.hostWarning}), plugin ${status.version}`, tone: "warning" };
|
|
88
|
+
return { label: "OpenCode", text: `${found} (${tested ? "tested" : "untested"}; supported range ${range}), plugin ${status.version}`, ...tested ? {} : { tone: "info" } };
|
|
89
|
+
}
|
|
70
90
|
function statusRows(status) {
|
|
71
91
|
const connected = status.state === "connected";
|
|
72
|
-
const connection = status.conflict ? `conflict: ${status.conflict}` : `${STATE_LABELS[status.state]}${status.title ? ` (${status.title})` : ""}${status.problem ? `: ${status.problem.message} (${status.problem.code})` : ""}`;
|
|
92
|
+
const connection = status.state === "conflict" && status.conflict ? `conflict: ${status.conflict}` : `${STATE_LABELS[status.state]}${status.title ? ` (${status.title})` : ""}${status.problem ? `: ${status.problem.message} (${status.problem.code})` : ""}`;
|
|
73
93
|
const tools = [`${status.tools.visible} available`];
|
|
74
94
|
if (status.tools.hidden) tools.push(`${status.tools.hidden} hidden`);
|
|
75
95
|
if (status.tools.unknown) {
|
|
@@ -78,18 +98,18 @@ function statusRows(status) {
|
|
|
78
98
|
}
|
|
79
99
|
const agents = status.unguardedAgents;
|
|
80
100
|
return [
|
|
81
|
-
{ label: "Connection", text: connection, tone:
|
|
101
|
+
{ label: "Connection", text: connection, tone: TONES[status.state] },
|
|
82
102
|
{ label: "Account", text: !connected ? "unknown until Granola connects" : status.workspace ? `workspace ${status.workspace}` : "workspace unknown until the agent reads your Granola account" },
|
|
83
103
|
{ label: "Access", text: "reads run without asking; changes, new tools and exports ask first" },
|
|
84
104
|
{ label: "Export folder", text: status.exportDirectory },
|
|
85
105
|
{ label: "Tools", text: connected ? tools.join(", ") : "none until Granola connects" },
|
|
86
|
-
|
|
106
|
+
agents.length ? {
|
|
87
107
|
label: "Agents without default rules",
|
|
88
|
-
text: `${agents.join(", ")}: Granola
|
|
108
|
+
text: `${agents.join(", ")}: Granola changes and new tools may run there without asking`,
|
|
89
109
|
tone: "warning",
|
|
90
110
|
block: permissionsSnippet(agents, status.permissionsBlock ?? [])
|
|
91
|
-
}
|
|
92
|
-
|
|
111
|
+
} : { label: "Agents without default rules", text: "none" },
|
|
112
|
+
hostRow(status)
|
|
93
113
|
];
|
|
94
114
|
}
|
|
95
115
|
function permissionsSnippet(agents, block) {
|
|
@@ -111,7 +131,8 @@ var Granola = Rpc.define({
|
|
|
111
131
|
methods: {
|
|
112
132
|
status: {
|
|
113
133
|
input: { type: "object", properties: {}, additionalProperties: false },
|
|
114
|
-
output: STATUS_SCHEMA
|
|
134
|
+
output: STATUS_SCHEMA,
|
|
135
|
+
errors: { unavailable: STATUS_ERROR_SCHEMA }
|
|
115
136
|
}
|
|
116
137
|
},
|
|
117
138
|
events: {}
|
|
@@ -127,10 +148,10 @@ function view(details) {
|
|
|
127
148
|
if (details.type === "unavailable") {
|
|
128
149
|
return {
|
|
129
150
|
rows: [{ label: "Connection", text: `unknown: ${details.reason}`, tone: "warning" }],
|
|
130
|
-
next: "Check that granola.meetings is active in /plugins, then press r to refresh."
|
|
151
|
+
next: details.next ?? "Check that granola.meetings is active in /plugins, then press r to refresh."
|
|
131
152
|
};
|
|
132
153
|
}
|
|
133
|
-
return { rows: statusRows(details.status), next: details.status.
|
|
154
|
+
return { rows: statusRows(details.status), next: details.status.nextStep };
|
|
134
155
|
}
|
|
135
156
|
|
|
136
157
|
// src/tui/view.tsx
|
|
@@ -275,9 +296,17 @@ function StatusDialog(props) {
|
|
|
275
296
|
}
|
|
276
297
|
|
|
277
298
|
// src/tui/index.tsx
|
|
278
|
-
function
|
|
299
|
+
function failure(error) {
|
|
279
300
|
const type = typeof error === "object" && error !== null && "type" in error ? error.type : void 0;
|
|
280
|
-
|
|
301
|
+
if (type === "unavailable") return {
|
|
302
|
+
type: "unavailable",
|
|
303
|
+
reason: "Granola status could not be read (STATUS_UNAVAILABLE)",
|
|
304
|
+
next: "Press r to refresh in a few seconds."
|
|
305
|
+
};
|
|
306
|
+
return {
|
|
307
|
+
type: "unavailable",
|
|
308
|
+
reason: type === "rpc.unavailable" || type === "rpc.method_not_found" ? "the Granola plugin is not running on this OpenCode server" : "the status request failed"
|
|
309
|
+
};
|
|
281
310
|
}
|
|
282
311
|
var plugin = {
|
|
283
312
|
id: "granola.meetings.tui",
|
|
@@ -297,10 +326,7 @@ var plugin = {
|
|
|
297
326
|
} : {}).then((status) => ({
|
|
298
327
|
type: "ready",
|
|
299
328
|
status
|
|
300
|
-
}), (
|
|
301
|
-
type: "unavailable",
|
|
302
|
-
reason: reason(error)
|
|
303
|
-
})).then((next) => {
|
|
329
|
+
}), failure).then((next) => {
|
|
304
330
|
if (request === generation) setDetails(next);
|
|
305
331
|
});
|
|
306
332
|
};
|
package/package.json
CHANGED
package/skills/granola/SKILL.md
CHANGED
|
@@ -22,7 +22,7 @@ For a brief before a meeting, or a catch-up on a person, company or topic, also
|
|
|
22
22
|
- `search_meetings`: by keyword, participant or company, and date. Keywords match titles, summaries and the user's notes, not transcripts, so an empty result does not mean that a topic was never discussed.
|
|
23
23
|
- `list_meetings`: meetings in a date range, newest first (the last 7 days by default), optionally in one folder.
|
|
24
24
|
- `search_transcripts`: by keyword in what was said; it returns matching transcript passages. Not every Granola plan includes transcripts.
|
|
25
|
-
- `list_meeting_folders`: folders and their IDs, for the
|
|
25
|
+
- `list_meeting_folders`: folders and their IDs, for the `folder_id` filter of `search_meetings` and `list_meetings`.
|
|
26
26
|
- Results come in pages. While a response offers a next cursor, continue with it and the same filters before you say that a meeting does not exist.
|
|
27
27
|
|
|
28
28
|
## Read meetings
|
|
@@ -60,6 +60,10 @@ Reads run without a prompt. OpenCode asks the user before each of these, unless
|
|
|
60
60
|
|
|
61
61
|
Make a change only when the user asked for it, and say first what it will do. A folder with workspace visibility shares every note in it with the whole workspace. If the user declines or the permission is denied, do not retry or find another way. In `opencode run` with nobody present to answer, these calls are denied unless the user's OpenCode permissions allow them. If `/granola-status` warns that an agent did not get Granola's permission defaults, pass the warning and its fix to the user.
|
|
62
62
|
|
|
63
|
+
## Scripts
|
|
64
|
+
|
|
65
|
+
When the user wants a job that runs without you, such as a cron job or a shell script, it can call Granola's read tools through the OpenCode server, with no model. The RPC `granola.meetings.tools` has one method per read, such as `list_meetings`, `get_meetings` and `get_meeting_transcript`, with the tools' own input, and `describe`, which lists them with their input schemas. Each call is `opencode api post /api/rpc/granola.meetings.tools/<method>` with the body `{"input":{"sessionID":"<session>","input":<the tool's input>}}`. The script runs as that session's agent, under its permission rules, so have the script create its session once with `opencode api post /api/session` and a body such as `{"title":"weekly meetings","agent":"build"}`. An ask is refused, so the agent needs the reads allowed; the default rules allow them. Scripts cannot make changes, sign out or export files. The details are in the plugin's `docs/scripting.md`.
|
|
66
|
+
|
|
63
67
|
## Save meetings as Markdown
|
|
64
68
|
|
|
65
69
|
`export_markdown` writes the notes, the transcript, or both, for 1 to 20 meetings into one Markdown file in the user's export folder; `/granola-status` shows the folder. The content goes straight to the file without passing through the conversation, so prefer it to reading a long transcript just to save it.
|