# Plan: gcalcli Plugin + Dashboard Plugin Settings Hook

**Status:** Planning  
**Scope:** Two parallel workstreams — (1) a new plugin manifest + API + dashboard capability for per-plugin settings, and (2) a production-quality gcalcli integration plugin that exercises every major hook type.

---

## Overview

### Workstream A — Dashboard Plugin Settings Hook
Plugins currently have no way to expose configuration to the operator without editing raw YAML. This work adds a first-class `settings_schema` block to `PLUGIN.md` that the dashboard renders as a live settings form. Changes are persisted to a per-plugin `settings.json` file that the plugin reads at runtime via the SDK.

### Workstream B — gcalcli Plugin
A multi-hook plugin that gives Furoshiki real-time calendar awareness:
- **`pre_conversation`** — inject today's agenda into every conversation's context
- **`pre_reflection`** — inject a broader weekly view when reflecting
- **`callable`** — LLM-invokable tools (get agenda, add event, search events, list calendars)
- **`cron`** — periodic agenda snapshot written to session-context for passive awareness

---

## Workstream A: Dashboard Plugin Settings Hook

### A1 — Manifest extension: `settings_schema`

Add an optional `settings_schema` list to `PLUGIN.md`. Each entry describes one setting field:

```yaml
settings_schema:
  - key: agenda_days
    type: integer
    label: "Days of agenda to fetch"
    description: "How many calendar days to pull for pre-conversation context"
    default: 2
    min: 1
    max: 30

  - key: calendars
    type: string_list
    label: "Calendars to include"
    description: "Leave empty to include all calendars. One name per line."
    default: []

  - key: pre_conversation_enabled
    type: boolean
    label: "Inject agenda before conversations"
    default: true

  - key: reflection_days
    type: integer
    label: "Days to summarise for reflection"
    default: 7
    min: 1
    max: 90

  - key: max_context_events
    type: integer
    label: "Max events in context block"
    default: 20
    min: 1
    max: 100
```

**Supported field types:**

| type | Dashboard widget | Python type |
|------|-----------------|-------------|
| `string` | Single-line text input | `str` |
| `string_list` | Multi-line textarea (one entry per line) | `list[str]` |
| `integer` | Number input (min/max respected) | `int` |
| `boolean` | Checkbox | `bool` |
| `select` | Dropdown — requires `options: [...]` | `str` |
| `multi_select` | Multi-checkbox — requires `options: [...]` | `list[str]` |
| `text` | Multi-line textarea | `str` |

**Storage:** Settings are written to `~/.furoshiki/plugins/<name>/settings.json` (the plugin's own data directory). Core never writes to this file; the plugin owns it. Operators may also hand-edit the file.

**Effective value resolution (plugin perspective):**
```
settings.json value  →  overrides  →  schema default
```

### A2 — Plugin manifest parser update

**File:** `scripts/plugin_loader.py`

- `load_plugin_manifest(plugin_dir)` already parses PLUGIN.md YAML. Extend it to extract `settings_schema` as a typed list.
- Add `get_plugin_settings(plugin_dir)` — loads `settings.json` (or `{}` if absent), fills missing keys from schema defaults, returns dict. Used by dashboard API.

### A3 — New API endpoints

**File:** `scripts/api_v1_routes.py` (admin auth, not plugin token — only operator can view/change)

```
GET  /api/v1/plugins                          # list all installed plugins + active status
GET  /api/v1/plugins/{name}/settings-schema   # return parsed settings_schema list
GET  /api/v1/plugins/{name}/settings          # return current effective settings dict
PUT  /api/v1/plugins/{name}/settings          # write settings.json (body: {key: value})
```

`PUT` validates each value against its schema entry (type coercion, min/max, options membership) before writing.

### A4 — Plugin SDK: `get_settings()`

**File:** `scripts/furoshiki_plugin_sdk.py`

Add:
```python
def get_settings(defaults: dict | None = None) -> dict:
    """Read this plugin's settings.json, merged over defaults.

    Reads FUROSHIKI_PLUGIN_NAME from env (set by plugin_loader at invocation)
    and FUROSHIKI_DATA to locate the plugin data dir.
    Falls back to `defaults` dict when keys are absent.
    """
```

Plugins call:
```python
from furoshiki_plugin_sdk import get_settings
cfg = get_settings()
agenda_days = cfg.get('agenda_days', 2)
```

### A5 — Dashboard: dynamic plugin settings panel

**File:** `scripts/dashboard/index.html`

Add a **"Plugins"** sub-tab under the existing Settings tab (or as its own top-level nav entry). For each installed plugin that declares a `settings_schema`:

1. Render the plugin name, description, and active status
2. For each schema entry render the appropriate widget (input, checkbox, select, textarea)
3. **Save** button calls `PUT /api/v1/plugins/{name}/settings` with the form values
4. Show current effective values on load via `GET /api/v1/plugins/{name}/settings`
5. Status indicator: unsaved changes highlighted

The panel also shows plugins without `settings_schema` (just name, status, hooks declared) so operators have a full plugin inventory.

### A6 — Spec and docs update

- `docs/PLUGIN-SPEC.md` — add `settings_schema` to the manifest schema section; document all field types, SDK helper, storage path
- `docs/PLUGIN-HOW-TO.md` — add "Exposing settings in the dashboard" section with working example
- `https://furoshiki.ai/plugins-extensions.html` — update architecture visuals and permission/hook tables

---

## Workstream B: gcalcli Plugin

### B1 — Directory layout

```
~/.furoshiki/plugins/gcalcli/
  PLUGIN.md               # manifest
  scripts/
    agenda.py             # pre_conversation hook
    reflection_agenda.py  # pre_reflection hook
    snapshot.py           # cron hook — periodic agenda to session-context
    callable/
      get_agenda.py       # callable: get agenda for date range
      add_event.py        # callable: add event via gcalcli quick
      search_events.py    # callable: search by keyword
      list_calendars.py   # callable: list available calendars
    _gcal_lib.py          # shared: run gcalcli, parse output, apply settings
  settings.json           # written by dashboard; read by _gcal_lib.py
```

### B2 — PLUGIN.md manifest

```yaml
---
name: gcalcli
active: true
description: >
  Google Calendar integration via gcalcli. Injects agenda context before
  conversations and reflections. Exposes callable tools for the model to
  query, search, and add calendar events.
version: "1.0"
author: operator

max_priority: high        # mind_queue notifications from this plugin

permissions:
  output_mind_queue: false  # no proactive notifications for v1
  network: false            # gcalcli manages its own OAuth; no outbound needed
  llm: false

hooks:
  - type: pre_conversation
    script: scripts/agenda.py
    timeout_sec: 10
    priority: 40

  - type: pre_reflection
    script: scripts/reflection_agenda.py
    timeout_sec: 10
    priority: 40

  - type: cron
    script: scripts/snapshot.py
    schedule: "*/30 * * * *"     # refresh every 30 minutes
    timeout_sec: 15
    description: "Refresh calendar snapshot in session-context"

  - type: callable
    name: calendar_get_agenda
    script: scripts/callable/get_agenda.py
    timeout_sec: 10
    description: "Get calendar events for a date or date range"
    when_to_use: >
      When the user asks what they have scheduled, what's on their calendar,
      what's happening today/tomorrow/this week, or mentions a specific date.
    parameters:
      - name: start_date
        type: string
        description: "Start date in YYYY-MM-DD format (default: today)"
        required: false
      - name: end_date
        type: string
        description: "End date in YYYY-MM-DD format (default: start_date + 2 days)"
        required: false

  - type: callable
    name: calendar_add_event
    script: scripts/callable/add_event.py
    timeout_sec: 10
    description: "Add a calendar event using natural language"
    when_to_use: >
      When the user asks to schedule something, add an event, block time,
      or create a calendar entry. Extract the details from context.
    parameters:
      - name: event_text
        type: string
        description: "Natural language event description, e.g. 'Lunch with Alex tomorrow at noon'"
        required: true
      - name: calendar
        type: string
        description: "Calendar name to add to (omit to use default)"
        required: false

  - type: callable
    name: calendar_search
    script: scripts/callable/search_events.py
    timeout_sec: 10
    description: "Search calendar events by keyword"
    when_to_use: >
      When the user asks to find a specific past or future event, or asks
      if something is on the calendar.
    parameters:
      - name: query
        type: string
        description: "Search term"
        required: true
      - name: days_back
        type: integer
        description: "How many days back to search (default: 30)"
        required: false

  - type: callable
    name: calendar_list_calendars
    script: scripts/callable/list_calendars.py
    timeout_sec: 5
    description: "List available Google Calendars"
    when_to_use: >
      When the user asks which calendars are available, or to confirm
      calendar names before adding an event.

settings_schema:
  - key: agenda_days
    type: integer
    label: "Days of agenda (pre-conversation)"
    description: "How many days forward to fetch for conversation context"
    default: 2
    min: 1
    max: 14

  - key: reflection_days_back
    type: integer
    label: "Reflection look-back (days)"
    description: "How many past days to include in reflection context"
    default: 7
    min: 1
    max: 30

  - key: reflection_days_forward
    type: integer
    label: "Reflection look-ahead (days)"
    description: "How many future days to include in reflection context"
    default: 7
    min: 1
    max: 30

  - key: calendars
    type: string_list
    label: "Calendars to include"
    description: "Leave empty to include all. One calendar name per line."
    default: []

  - key: max_context_events
    type: integer
    label: "Max events in context block"
    default: 20
    min: 1
    max: 100

  - key: pre_conversation_enabled
    type: boolean
    label: "Inject agenda before conversations"
    default: true

  - key: pre_reflection_enabled
    type: boolean
    label: "Inject calendar context before reflection"
    default: true

  - key: snapshot_enabled
    type: boolean
    label: "Enable periodic agenda snapshot (every 30 min)"
    default: true
---
```

### B3 — Shared library: `_gcal_lib.py`

Handles all gcalcli interaction:

```python
def run_gcalcli(*args, timeout=10) -> str:
    """Run gcalcli with given args, return stdout. Raises on error."""

def get_agenda(start_date, end_date, calendars=None, max_events=20) -> list[dict]:
    """Return parsed list of events: {summary, start, end, calendar, location}"""

def format_agenda_block(events, label="Agenda") -> str:
    """Format events as a readable context block for injection."""

def get_settings() -> dict:
    """Load settings.json merged with schema defaults."""
```

### B4 — Hook implementations

**`agenda.py` (pre_conversation):**
- Read settings: `agenda_days`, `calendars`, `max_context_events`, `pre_conversation_enabled`
- If disabled, output `{"label": "calendar", "content": "", "priority": 40}` (empty = skipped)
- Run `gcalcli agenda <today> <today+N>` for configured calendars
- Parse output into events list
- Output JSON: `{"label": "today's calendar", "content": "<formatted block>", "priority": 40}`
- If gcalcli unavailable or returns error → output empty content with a `suggested_questions` hint

**`reflection_agenda.py` (pre_reflection):**
- Same pattern but wider window (`reflection_days_back` back, `reflection_days_forward` forward)
- Label: `"calendar context for reflection"`

**`snapshot.py` (cron):**
- Fetch next 24h of events
- Write a short summary to a well-known file (`~/.furoshiki/plugins/gcalcli/snapshot.json`)
- The pre_conversation hook reads this file if fresh (< 35 min old) instead of calling gcalcli again, avoiding redundant API calls

**`callable/get_agenda.py`:**
- Receives `--mode callable --args '{"start_date":"...","end_date":"..."}'`
- Runs gcalcli for the requested range
- Returns JSON: `{"events": [...], "count": N, "range": "Mon Apr 14 – Wed Apr 16"}`

**`callable/add_event.py`:**
- Receives `event_text` (natural language)
- Runs `gcalcli quick "<event_text>"` (optionally with `--calendar`)
- Returns `{"ok": true, "event_text": "..."}` or `{"ok": false, "error": "..."}`

**`callable/search_events.py`:**
- Runs `gcalcli search "<query>" <start> <end>`
- Returns matching events list

**`callable/list_calendars.py`:**
- Runs `gcalcli list`
- Returns parsed list: `{"calendars": ["Work", "Personal", ...]}`

### B5 — Setup documentation: `docs/GCALCLI-PLUGIN-SETUP.md`

Covers:
1. Prerequisites: `gcalcli` installed and authenticated (`gcalcli agenda` works in terminal)
2. Installing the plugin: `furoshiki plugins apply` or copy directory
3. Dashboard: navigate to Settings → Plugins → gcalcli, configure calendars and day range
4. Testing: `furoshiki plugins test gcalcli --hook pre_conversation`
5. Callable tools: how the model invokes them (no user action needed — auto-discovered)
6. Troubleshooting: gcalcli PATH issues, OAuth token refresh, calendar name case sensitivity

---

## Implementation Order

```
Phase 1 — Settings infrastructure (no UI yet)
  A1  manifest schema field (plugin_loader.py parser extension)
  A2  get_plugin_settings() in plugin_loader.py
  A4  get_settings() SDK helper in furoshiki_plugin_sdk.py
  A3  API endpoints (api_v1_routes.py)

Phase 2 — Dashboard UI
  A5  Plugin settings panel in dashboard/index.html
  A6  Spec and HTML docs updated

Phase 3 — gcalcli plugin (shared lib + hooks)
  B3  _gcal_lib.py
  B4a PLUGIN.md manifest
  B4b agenda.py (pre_conversation)
  B4c reflection_agenda.py (pre_reflection)
  B4d snapshot.py (cron)
  B4e callable tools (get_agenda, add_event, search, list_calendars)

Phase 4 — Polish and docs
  B5  GCALCLI-PLUGIN-SETUP.md
  A6  PLUGIN-SPEC.md, PLUGIN-HOW-TO.md, https://furoshiki.ai/plugins-extensions.html
  furoshiki design update
```

---

## Open Questions / Design Decisions Needed

1. **Dashboard tab placement** — plugin settings as a sub-section under "Settings → Operator", or a dedicated top-level "Plugins" nav tab?
2. **Settings schema in YAML body vs separate file** — current plan keeps it in PLUGIN.md YAML frontmatter. Alternative: `settings-schema.json` sidecar.
3. **Snapshot vs live call for pre_conversation** — plan uses a snapshot file to avoid blocking conversation start on a gcalcli API call. Tradeoff: up to 30 min stale. Acceptable?
4. **gcalcli PATH** — on some systems gcalcli may not be on the subprocess PATH. Should the plugin let the operator configure the path via settings (`gcalcli_path`)? Probably yes — add to settings_schema.
5. **Calendar filtering by gcalcli flags** — gcalcli `agenda` accepts `--cal` flag. Confirm exact flag syntax for installed version before hardcoding.
6. **Error surfacing** — if gcalcli fails (token expired), should the plugin emit a `mind_queue` notification? Probably yes for cron hook; silent skip for pre_conversation/pre_reflection.

---

## Files Created / Modified

| File | Action |
|------|--------|
| `scripts/plugin_loader.py` | Extend manifest parser + add `get_plugin_settings()` |
| `scripts/furoshiki_plugin_sdk.py` | Add `get_settings()` |
| `scripts/api_v1_routes.py` | Add 4 new plugin settings endpoints |
| `scripts/dashboard/index.html` | Add Plugins settings panel |
| `docs/PLUGIN-SPEC.md` | Document `settings_schema` |
| `docs/PLUGIN-HOW-TO.md` | "Exposing settings" section |
| `docs/GCALCLI-PLUGIN-SETUP.md` | New: gcalcli setup guide |
| `https://furoshiki.ai/plugins-extensions.html` | Update hooks table, settings hook section |
| `~/.furoshiki/plugins/gcalcli/` | New plugin (not in source tree) |
