@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/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: ["state", "tools", "unguardedAgents", "exportDirectory", "pluginVersion", "hostVersion", "next"],
11
+ required: ["version", "host", "state", "tools", "unguardedAgents", "exportDirectory", "nextStep"],
11
12
  properties: {
12
- state: { type: "string", enum: ["connected", "pending", "disabled", "failed", "needs_auth", "absent"] },
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", "next"],
18
- properties: { code: { type: "string" }, message: { type: "string" }, next: { 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
- next: { type: "string" }
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
- pending: "connecting",
57
- needs_auth: "sign-in required",
68
+ connecting: "connecting",
69
+ sign_in_required: "sign-in required",
58
70
  failed: "failed",
59
71
  disabled: "disabled",
60
- absent: "not configured"
72
+ not_configured: "not configured",
73
+ conflict: "conflict"
61
74
  };
62
75
  var TONES = {
63
76
  connected: "ok",
64
- pending: "info",
65
- needs_auth: "warning",
77
+ connecting: "info",
78
+ sign_in_required: "warning",
66
79
  failed: "error",
67
80
  disabled: "info",
68
- absent: "warning"
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: status.conflict ? "warning" : TONES[status.state] },
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
- ...agents.length ? [{
106
+ agents.length ? {
87
107
  label: "Agents without default rules",
88
- text: `${agents.join(", ")}: Granola writes run there without asking`,
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
- status.hostWarning ? { label: "OpenCode", text: `${status.hostVersion} (host untested: ${status.hostWarning}), plugin ${status.pluginVersion}`, tone: "warning" } : { label: "OpenCode", text: `${status.hostVersion} (tested), plugin ${status.pluginVersion}` }
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.next };
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 reason(error) {
299
+ function failure(error) {
279
300
  const type = typeof error === "object" && error !== null && "type" in error ? error.type : void 0;
280
- return type === "rpc.unavailable" || type === "rpc.method_not_found" ? "the Granola plugin is not running on this OpenCode server" : "the status request failed";
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
- }), (error) => ({
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joe-broadhead/opencode-granola",
3
- "version": "0.0.0",
3
+ "version": "0.0.2",
4
4
  "description": "Unofficial OpenCode plugin for Granola meeting notes, through Granola's official MCP server",
5
5
  "keywords": [
6
6
  "opencode",
@@ -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 folder filter of both tools above.
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.