From b00b35111788c337b4717ef8f60ac0ad9bb6e33e Mon Sep 17 00:00:00 2001 From: Julien <162975217+shiftcontrol-julien@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:48:16 +0200 Subject: [PATCH 1/4] refresh-subscription-info v0.2.0: cost structure mandatory, PDF invoices, manual entry, free/archive, multi-mailbox, opt-in proration, clearer labels --- skills/refresh-subscription-info/SKILL.md | 84 +++++++++++++++++------ 1 file changed, 62 insertions(+), 22 deletions(-) diff --git a/skills/refresh-subscription-info/SKILL.md b/skills/refresh-subscription-info/SKILL.md index b6bf0b8..67dbeac 100644 --- a/skills/refresh-subscription-info/SKILL.md +++ b/skills/refresh-subscription-info/SKILL.md @@ -16,7 +16,7 @@ This skill reconciles the user's ShiftControl subscription records with their ac ## When NOT to use - The user wants to **add a new app** they don't already track in ShiftControl. (Use the ShiftControl UI to add the app first; this skill only updates existing apps.) -- The user wants to **delete or disable** an app. +- The user wants to **delete or disable** an app. (This skill can flag an app as an *archive candidate* — see Step 6b — but never archives or deletes anything itself.) - The invoices in question are for **non-SaaS purchases** (hardware, professional services, hosting unrelated to a tracked SaaS). - The user wants to look at invoices they **sent** (outbound invoices to their own customers). This skill only inspects invoices the user **received**. @@ -25,13 +25,16 @@ This skill reconciles the user's ShiftControl subscription records with their ac This skill requires two MCP servers to be installed and authenticated in the user's AI assistant: 1. **ShiftControl MCP** — at `https://mcp.shiftcontrol.io/mcp`. If not installed, direct the user to https://github.com/ShiftControl-io/skills/blob/main/INSTALL.md for their platform. -2. **An email-search MCP** — typically Gmail (Anthropic publishes one), but any MCP that exposes tools to list and read recent emails works. Common names: `gmail`, `outlook`, `imap`. +2. **An email-search MCP** — typically Gmail (Anthropic publishes one), but any MCP that exposes tools to list and read recent emails works. Common names: `gmail`, `outlook`, `imap`, `superhuman`. Verify availability before starting the workflow: - Confirm `list_apps` is callable (proves ShiftControl MCP is wired). - Confirm an email-search tool is callable (proves the email MCP is wired). -- If either is missing, **stop** and tell the user which one to install. +- **Check attachment capability.** Many invoices carry the actual figures in a PDF attachment, not the email body (GitHub, JumpCloud, Zoom, Salesforce-billed Slack, etc. — see references/invoice-detection.md). Whether you can read those depends on the email MCP: + - If the email MCP exposes an attachment tool that returns file **content or a download URL** (e.g. Superhuman's `get_attachment`), you can read PDF invoices — see Step 4. + - If the email MCP returns attachment **filenames only** (the default Anthropic Gmail connector does this), you cannot read PDF invoices. Note this up front and tell the user which invoices you'll have to skip or ask them to provide manually. Do not silently miss them. +- If either MCP is missing, **stop** and tell the user which one to install. ## Workflow @@ -45,11 +48,13 @@ Call `list_apps` (paginated — keep calling until you've enumerated all pages) This is your authoritative "what's currently recorded" baseline. +**Data-integrity audit (do this now, before searching email).** As you enumerate apps, flag any app that has a **cost set but a blank `costStructure`**. In ShiftControl a cost with no cost structure is **not counted in spend totals** — the app silently drops out of the numbers even though a cost is recorded. Collect these into a "needs cost structure" list and surface them in the proposal (Step 7) so the user can fix them, even for apps you find no new invoice for. If you can infer the structure with high confidence from an invoice, propose it; if not, ask the user (see Step 4, cost structure). Never leave a costed app with a blank structure once you've touched it. + ### Step 2 — Define the search window By default, search the user's email for invoices received in the last **18 months**. Annual contracts are common in SaaS, and a one-year window risks missing the most recent renewal invoice for any app that bills annually — 18 months gives you the current annual invoice plus a buffer to confirm you have the latest one. If the user asks for a narrower window ("just the last quarter", "this month only"), honor it. -### Step 3 — Search the email inbox +### Step 3 — Search the email inbox(es) Use the email-search MCP to find candidate invoice emails. See [references/invoice-detection.md](references/invoice-detection.md) for the full heuristics — at minimum: @@ -59,7 +64,11 @@ Use the email-search MCP to find candidate invoice emails. See [references/invoi - Filter OUT **inbound-payments-received** — emails saying things like *", Inc. has sent you a payment"* or *"Coupa Pay has remitted X to your account"*. These represent money coming TO the user (referrals, vendor-side payments) and are not SaaS invoices. - Filter OUT clearly non-SaaS receipts (Amazon shopping, ride-share, restaurants, hardware, SSL certs, professional services, contractor invoices, telecom bills). -**Shared billing inboxes:** most organizations set up a shared `invoices@.io`, `billing@.io`, or `finance@.io` address (a Google Group or distribution list) as the billing contact on every SaaS account. Invoices arrive at the *receiver* side at that address — the vendor is still the actual From-address. A search like `to:invoices@.io newer_than:18m` is the highest-precision starting point. Ask the user which shared inbox they use if you're unsure; if there isn't one, fall back to searching the personal inbox. +**Search across every mailbox you can reach, not just one.** Invoices for different apps often land in different places: + +- **Shared billing inboxes** — most organizations route SaaS billing to a shared `invoices@`, `billing@`, or `finance@` alias (a Google Group or distribution list). A search like `to:invoices@ newer_than:18m` is the highest-precision starting point. Ask the user which shared inbox(es) they use if you're unsure. +- **Individual owners** — some invoices are addressed to a specific person (`jordan@`, `chichen@`, the app's owner), not the shared alias. If ShiftControl records an owner for an app, or the user names likely recipients, search those addresses too. +- **Name the gap when you can't reach a mailbox.** The email MCP only sees the authenticated user's own and shared mailboxes — it cannot read a colleague's private inbox. When an app has no invoice in the mailboxes you *can* see and its invoice likely lives in one you *can't* (e.g. billed to a specific person), say so explicitly rather than reporting "not found": *"Equals may be billed to chichen@ — I can't see that mailbox. Forward the invoice or tell me the cost and I'll record it."* ### Step 4 — Classify, then extract structured data @@ -85,18 +94,28 @@ For **full invoices**, every field below is fair game. For **incrementals**, tre - **Invoice date** — when the invoice was issued. - **Service period** — what the charge covers (e.g. "Jan 1 – Jan 31, 2026"). - **Per-unit cost** — per-user or per-seat cost, as a decimal string (e.g. `"5.00"`). **Full invoices only** — skip on incrementals. -- **Cost structure** — `user` (per-seat), `flat` (fixed), or `tiered`. Skip if ambiguous on an incremental. +- **Cost structure** — `user` (per user / per-seat) or `flat` (a **flat fee** for the whole account — i.e. **per contract**, not per user); `tiered` for usage brackets. Always describe these to the user as **"per user"** vs **"flat fee (per contract)"** — the bare word "flat" is unclear. The value stored in ShiftControl is still `user` / `flat` / `tiered`. **This field is MANDATORY whenever you propose a cost — see the rule below.** - **Currency** — ISO 4217 code (USD, EUR, etc.). -- **Billing frequency** — `month`, `quarter`, or `year`, inferred from the service period. **Full invoices only** — the service period on an incremental is partial. +- **Billing frequency** — `month` or `year`, inferred from the service period. **Full invoices only.** Do NOT emit `quarter` — see the cadence rule below. - **Total seats after the change** — useful for both types when stated ("Your subscription now includes 50 seats"). - **Contract renewal/end date** — if mentioned ("renews on…", "auto-renews", "contract through…"). Incrementals often confirm or update this. - **Plan/tier** — the plan name on the invoice ("Pro", "Business", "Enterprise"). If a recent incremental announces a plan change, trust it over an older full invoice. -**PDF attachments:** if the email body says "Your invoice is attached" but lacks the cost details, the data is in the PDF. Most email MCPs return attachment contents — fetch and parse the PDF the same way as an inline-bodied invoice. If your email MCP can't return attachments, mark the invoice uncertain. +**Cost structure is a MUST-fill field (do not leave it blank).** ShiftControl does not count a cost that has no cost structure — the app drops out of spend totals. So the cost record is **all-or-nothing**: whenever you propose a `cost`, you MUST also set `costStructure` and `costCurrency`. Never write a bare cost. +- If the invoice makes the structure clear (per-seat line items → `user`; a single fixed charge → `flat`, i.e. a flat fee per contract; usage brackets → `tiered`), set it. +- If it's genuinely ambiguous, **ask the user** — do not skip the field and do not guess silently: *"Is Notion charged per user, or a flat fee (per contract)? ShiftControl needs this to count the spend."* + +**Billing cadence — `month` or `year` only.** ShiftControl's product currently supports monthly and annual cadences. Even though the API technically accepts `quarter`, a quarterly value doesn't render correctly in the product, so **do not emit `quarter`**. When an invoice is billed quarterly, convert it to the annual equivalent (quarterly amount × 4), set `billingFrequency = "year"`, and record the real cadence in the note: *"Billed quarterly; stored as annual equivalent."* -**Web-hosted invoices:** if the invoice details are behind a "Click here to view your invoice" link rather than in the body or an attachment, **ask the user** before following the link: *"I found invoices from where the details are behind a 'view invoice' link — should I follow those links to extract the cost details?"* If the user agrees AND your assistant can fetch URLs, fetch and parse. Otherwise mark uncertain. +**Multi-plan apps.** Some apps run more than one plan at once — e.g. Figma with an annual base plan plus monthly seat overflow. ShiftControl holds a single cost + structure + cadence per app, so you cannot represent both lines faithfully. When you detect this, do NOT silently pick one: surface it in the proposal — *"Figma has two billing lines (annual base + monthly overflow). I can record one; which should be the tracked cost, or should I record the combined effective monthly?"* — and let the user decide. -If a candidate has an unclear vendor, no reliable extractable data, or is only an incremental for a vendor with no full invoice in the window, mark it **uncertain** and exclude that vendor's cost fields from the proposal. Surface it to the user separately: "I only found mid-period adjustment invoices for — I can update the plan tier and contract date, but the per-user cost is too noisy to propose." +**PDF attachments — read them when you can.** If the email body says "Your invoice is attached" (or simply lacks the figures) but a PDF is present, the data is in the PDF. Handle it per the attachment capability you checked in Prerequisites: +1. If the email MCP exposes an attachment tool that returns content or a download URL (e.g. `get_attachment`), fetch it, read the PDF, and extract the same fields as an inline invoice. (Most assistants read PDFs natively; if you get a URL, fetch and parse it.) +2. If the email MCP returns filenames only, you cannot read the PDF. Tell the user plainly: *"GitHub's amount is in a PDF attachment this email connector won't hand me. Paste the figure, forward the invoice, or connect an attachment-capable email tool."* Then treat the field as user-provided (Step 6a) or leave the app unchanged — never invent a number. + +**Web-hosted invoices:** if the details are behind a "Click here to view your invoice" link, **ask the user** before following it: *"I found invoices from where the details are behind a 'view invoice' link — should I follow those links to extract the cost details?"* If the user agrees AND your assistant can fetch URLs, fetch and parse. Otherwise mark uncertain. + +If a candidate has an unclear vendor, no reliable extractable data, or is only an incremental for a vendor with no full invoice in the window, mark it **uncertain** and exclude that vendor's cost fields from the proposal. Surface it to the user separately. ### Step 5 — Match invoices to ShiftControl apps @@ -111,17 +130,30 @@ If no app matches, **skip the invoice** and surface it in a "found but not track ### Step 6 — Build the diff -For each matched (invoice, app) pair, compute field-by-field which values differ between what's currently in ShiftControl and what the invoice shows. Only changed fields become candidate updates. +For each matched (invoice, app) pair, compute field-by-field which values differ between what's currently in ShiftControl and what the invoice shows. Only changed fields become candidate updates. Remember the cost-structure rule: if you're changing `cost`, ensure `costStructure` and `costCurrency` are part of the same proposed update. + +### Step 6a — Apps with no invoice: offer manual entry + +Some tracked apps won't have a findable email invoice at all — the vendor bills by card or portal only (e.g. HReasily), the invoice went to a mailbox you can't see, or the figure was in a PDF you couldn't read. For these, do NOT propose a change and do NOT zero the cost. Instead, offer the user a direct path: *"I couldn't find an invoice for HReasily. Tell me the cost, whether it's per user or a flat fee (per contract), the currency, and the cadence, and I'll record it in ShiftControl."* If the user provides the values, treat them exactly like extracted invoice values — show the before→after diff and write them back via `update_app_subscription` behind the same approval gate (Step 8/9). Note them as `"Updated from user-provided figures on "`. + +### Step 6b — Free, stopped-paying, and archive candidates + +Classify each tracked app into one of: *still paying* / *now free* / *stopped paying* / *unknown*. + +- **Now free** (e.g. the plan is genuinely free, or included at no cost in another plan): do NOT infer "free" merely from the absence of an invoice — that's how a real cost gets wrongly zeroed. **Confirm with the user** first: *"Attio looks like it's on a free plan now — set its cost to zero?"* When ShiftControl exposes a dedicated "mark as free" capability, use it; until then, on confirmation set `cost = "0.00"` with `costStructure` still set and a note explaining the free plan. +- **Stopped paying** (the user no longer uses/pays for the app, e.g. a cancelled ChatGPT plan): surface as an **archive candidate** in a dedicated section of the proposal — *"You indicated you stopped paying for ChatGPT — consider archiving it in ShiftControl."* **Never archive or delete anything yourself** (archiving isn't in this skill's MCP surface); just flag it for the user to action in the UI. +- **Free-then-paid transition** (e.g. a first-year-free plan that has now started charging, like Granola): detect the first paid invoice after the free period and propose the new cost + a note recording the transition. ### Step 7 — Present the proposal -Show the user a structured proposal. See [references/proposal-format.md](references/proposal-format.md) for the exact format. The high-level shape: +Show the user a structured proposal. See [references/proposal-format.md](references/proposal-format.md) for the exact format, including the sections for **cost-structure fixes** (from the Step 1 audit), **archive candidates** (Step 6b), and **no-invoice / manual-entry** apps (Step 6a). The high-level shape: ``` Found invoices for 10 of your 23 ShiftControl apps. Proposed updates: 1. Slack cost: $8.00/user/month → $7.00/user/month + costStructure: user (unchanged — kept so the cost still counts) billingFrequency: month → year contractEndDate: (not set) → 2027-03-15 notes update: "Updated from Slack invoice dated 2026-03-15 (billed via Salesforce)" @@ -130,9 +162,12 @@ Found invoices for 10 of your 23 ShiftControl apps. Proposed updates: cost: $12.00/user/month → $10.00/user/month notes update: "Updated from Notion invoice dated 2026-03-08" -[... more ...] +Needs cost structure (won't count in spend until fixed) (2): + - Documenso: cost $9.00 recorded but structure is blank — per user or flat fee (per contract)? + - Aspire: cost $5.00 recorded but structure is blank — per user or flat fee (per contract)? -Apps with no invoice in the last 18 months (13): [list] +Archive candidates (1): ChatGPT — you said you stopped paying. +Apps with no invoice found (3): [list] — reply with a cost to record any of them. Invoices found for apps not in ShiftControl (2): [list of vendor names] Reply "approve N" (e.g. "approve 1, 3, 5") to apply specific changes, @@ -154,6 +189,7 @@ Acceptable refinements that loop back to Step 7: - `show details for 3` - `drop the contract change on Slack but apply the cost change` - `exclude the GitHub one` +- `for Framer, estimate the annual cost from the proration` — the **opt-in proration estimate**. By default the skill does NOT derive a price from a prorated invoice. But if the user explicitly asks, compute the annualized figure from the prorated amount and billing period, propose it clearly **labelled as an estimate** (`"~S$652/year, estimated from a prorated invoice"`), and only write it on approval. NEVER treat any of the following as approval: @@ -170,8 +206,8 @@ For each approved (app, changes) pair, call `update_app_subscription` with: - `appId`: the UUID from Step 1. - `confirm: true` — set this only because you just obtained the user's explicit approval. -- **Only the fields that actually changed** (omit unchanged ones — they keep their current value on the backend). -- `notes`: write a single line in the form `"Updated from invoice dated "`. If the invoice came via a reseller (e.g. Slack billed by Salesforce, Google Workspace billed by ShiftControl, JumpCloud billed by a partner), append `(billed via )` — e.g. `"Updated from Slack invoice dated 2026-03-15 (billed via Salesforce)"`. **Replace** any previous skill-written line of the same form (this skill runs repeatedly; we don't want notes to accumulate one line per run). Match for replacement using the pattern: line begins with `Updated from ` and contains `invoice dated `. **Preserve every other line** the user (or any other source) put in the notes field — only the skill's own previous "Updated from..." line gets replaced. If no such previous line exists, add the new one at the end. +- **Only the fields that actually changed** (omit unchanged ones — they keep their current value on the backend), **except** that whenever `cost` is in the change set you MUST also send `costStructure` and `costCurrency` so the cost is counted. +- `notes`: write a single line in the form `"Updated from invoice dated "`. If the invoice came via a reseller (e.g. Slack billed by Salesforce, Google Workspace billed by ShiftControl), append `(billed via )` — e.g. `"Updated from Slack invoice dated 2026-03-15 (billed via Salesforce)"`. For user-provided figures use `"Updated from user-provided figures on "`. **Replace** any previous skill-written line of the same form (this skill runs repeatedly; we don't want notes to accumulate one line per run). Match for replacement using the pattern: line begins with `Updated from ` and contains `invoice dated ` or `user-provided figures on`. **Preserve every other line** the user (or any other source) put in the notes field — only the skill's own previous "Updated from..." line gets replaced. If no such previous line exists, add the new one at the end. Process each app **sequentially** (not parallel) so errors are clearly attributable. After all writes, report back: @@ -191,15 +227,19 @@ If any write fails, report which one and why, but keep going with the rest. **Do - ❌ Calling `update_app_subscription` with `confirm: true` because "the user is asking for updates". They're asking for a **proposal**, not blanket approval. Always present the diff first. - ❌ Constructing an `appId` from a name. The UUID must come from `list_apps`. -- ❌ Inferring `costStructure` when the invoice is ambiguous. If you can't tell whether it's per-seat or flat, leave that field out of the proposal and let the user decide. -- ❌ Wiping out user-written notes. The skill replaces ONLY its own previous line (matching `Updated from invoice dated ...`). Any other content in the notes — vendor contact, negotiation history, owner email, manual annotations — must be preserved. -- ❌ Accumulating one new note line every time the skill runs. The skill is designed to be re-run regularly; replace the prior skill line, don't pile on. -- ❌ Proposing changes for apps where no invoice was found, based on "you probably renewed at the same rate". This skill is **invoice-driven**: no invoice → no change. -- ❌ Creating new apps. If an invoice doesn't match a tracked app, surface it as "not tracked" and stop there. +- ❌ **Writing a `cost` without a `costStructure`.** A blank cost structure means ShiftControl ignores the cost entirely — the app silently drops out of spend totals. If you can't tell whether it's charged per user or as a flat fee (per contract), **ask the user**; never leave the field blank and never write a bare cost. (This reverses earlier guidance — cost structure is now mandatory, not optional.) +- ❌ Emitting `billingFrequency: "quarter"`. The product doesn't render it; convert quarterly invoices to the annual equivalent and note the real cadence. +- ❌ Inferring "free" from a missing invoice. Absence of an invoice is NOT evidence of a free plan. Confirm with the user before zeroing any cost. +- ❌ Wiping out user-written notes. The skill replaces ONLY its own previous "Updated from..." line. Any other content — vendor contact, negotiation history, owner email, manual annotations — must be preserved. +- ❌ Accumulating one new note line every time the skill runs. Replace the prior skill line, don't pile on. +- ❌ Proposing changes for apps where no invoice was found, based on "you probably renewed at the same rate". This skill is **invoice-driven**: no invoice → offer manual entry (Step 6a), don't guess. +- ❌ Creating new apps, or archiving/deleting apps. If an invoice doesn't match a tracked app, surface it as "not tracked". If an app is no longer paid for, surface it as an archive candidate — never act on either yourself. ## Version -**v0.1.0** — email-based invoice discovery only. Future versions will add Xero, QuickBooks, Brex, Ramp, and other finance-system sources. +**v0.2.0** — cost structure is now a mandatory field (blank structure = cost ignored by ShiftControl) with a data-integrity audit of existing apps; PDF invoices read via attachment-capable email tools with a graceful Gmail fallback; manual cost entry for apps with no email invoice; free / stopped-paying / archive-candidate handling; multi-mailbox search with explicit gap-naming; opt-in proration estimate; multi-plan-app surfacing; `quarter` cadence mapped to annual until the product supports it. + +**v0.1.0** — email-based invoice discovery only. ## See also From 3d50d7162e358540d2c056b100df275744461296 Mon Sep 17 00:00:00 2001 From: Julien <162975217+shiftcontrol-julien@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:48:17 +0200 Subject: [PATCH 2/4] refresh-subscription-info: PDF capability ladder, no-quarter and mandatory cost-structure rules --- .../references/invoice-detection.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/skills/refresh-subscription-info/references/invoice-detection.md b/skills/refresh-subscription-info/references/invoice-detection.md index 8868aca..bf0ae14 100644 --- a/skills/refresh-subscription-info/references/invoice-detection.md +++ b/skills/refresh-subscription-info/references/invoice-detection.md @@ -106,6 +106,10 @@ A separate but adjacent gotcha: full invoices can be quoted at different cadence ShiftControl stores per-unit cost AND billing frequency separately, so the normalization always preserves both pieces. Don't multiply / divide cost without also updating billingFrequency. +**Cadence is `month` or `year` only — no `quarter`.** ShiftControl's product currently renders monthly and annual cadences; a `quarter` value (which the API technically accepts) does not display correctly. When an invoice is billed quarterly, convert it to the annual equivalent (quarterly amount × 4), store `billingFrequency = "year"`, and record the true cadence in the note (e.g. *"Billed quarterly; stored as annual equivalent"*). Revisit this once the product adds more cadences. + +**Cost structure is mandatory whenever a cost is written.** In ShiftControl, a cost with a blank `costStructure` is **not counted in spend totals** — the app silently drops out of the numbers. So treat `cost` + `costStructure` + `costCurrency` as an all-or-nothing set: never propose a bare cost. Full invoices reliably reveal the structure (per-seat line items → `user`, presented as "per user"; a single fixed charge → `flat`, presented as "flat fee (per contract)"; usage brackets → `tiered`). When it's genuinely ambiguous, ASK the user rather than leaving it blank. Always use the plain labels "per user" and "flat fee (per contract)" when talking to the user — the bare word "flat" is unclear — while still storing the `flat` value. This also means a useful standalone audit: any existing app that already has a cost but a blank structure should be flagged for the user to fix, because its spend isn't being counted today. + ## Vendor identity beyond the "From" address The vendor billing the user isn't always the vendor named on the invoice. Two big patterns: @@ -207,7 +211,10 @@ Credit Memos and payment-received notifications, even for direct consumption: do ## Other failure modes worth flagging - **Currency conversion** — invoice in USD but user's org defaults to EUR/SGD. Surface the invoice currency; don't auto-convert. Let the user decide whether to store in invoice currency or org default. -- **PDF invoices** — many SaaS vendors send the invoice details as a PDF attachment with only a short summary in the email body ("Your invoice is attached"). **Parse the PDF**: most email MCPs return attachment contents, and most AI assistants can read PDFs natively. Extract the same fields (vendor, amounts, service period, seats, plan tier) from the PDF as you would from an inline body. If your email MCP can't return attachments OR your assistant can't read PDFs, mark the invoice uncertain and surface to the user. +- **PDF invoices** — many SaaS vendors send the invoice details as a PDF attachment with only a short summary in the email body ("Your invoice is attached"). Whether you can read the PDF depends on the email MCP's attachment capability, which varies a lot — **check it before assuming**: + - **Returns attachment content or a download URL** (e.g. Superhuman's `get_attachment`, which returns a short-lived download URL for PDFs): fetch it, read the PDF (most assistants read PDFs natively; if you get a URL, fetch and parse it), and extract the same fields (vendor, amounts, service period, seats, plan tier) as you would from an inline body. This is the path that lets you read GitHub/JumpCloud/Salesforce-billed PDF invoices. + - **Returns attachment filenames only** — the default Anthropic Gmail connector does this: it gives you the filename but not the bytes or a link. In this case you genuinely **cannot** read the PDF. Do not guess the amount. Tell the user which invoices are affected and offer the manual-entry path, or suggest connecting an attachment-capable email tool (Superhuman is verified to work). This is not a skill bug — it's a connector limitation. + - Note: often the figure is *also* in the email body (GitHub receipts list the amount in the body text), so always parse the body thoroughly first; fall to the PDF only when the body lacks the numbers. - **Web-hosted invoices** — "Click here to view your invoice" with the actual numbers behind a link rather than in the body or attachment. **Ask the user before following the link** — most will say yes, some prefer not to follow links from their inbox. Phrasing: *"I found invoices from where the details are behind a 'view invoice' link — should I follow those links to extract the cost?"* If the user agrees and your assistant has a web-fetch capability, fetch and parse. Otherwise mark uncertain. ## What this skill does NOT do (yet) From f9df08272e672e099845bd985aa9dcf161131d1c Mon Sep 17 00:00:00 2001 From: Julien <162975217+shiftcontrol-julien@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:48:19 +0200 Subject: [PATCH 3/4] refresh-subscription-info: add needs-cost-structure, archive-candidate and no-invoice proposal sections --- .../references/proposal-format.md | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/skills/refresh-subscription-info/references/proposal-format.md b/skills/refresh-subscription-info/references/proposal-format.md index 914ac75..b75e888 100644 --- a/skills/refresh-subscription-info/references/proposal-format.md +++ b/skills/refresh-subscription-info/references/proposal-format.md @@ -32,7 +32,7 @@ Rules: - Use a blank line between apps for scannability. - Right-align the arrow column when reasonable; not required if it complicates rendering. - Show `(not set)` for currently-null fields rather than blanks. -- Format costs with **currency symbol AND unit**: `$8.00/user/month`, `$300.00/year flat`, `€50.00/user/year`. +- Format costs with **currency symbol AND unit**, using "per user" or "flat fee" (never bare "flat"): `$8.00/user/month`, `$300.00/year flat fee`, `€50.00/user/year`. - Format dates as ISO 8601 date only: `2026-03-15`. - Include the `notes update:` line on every block — showing the user that the change will also write a short context note to the app is part of "no surprises". The phrasing `notes update` (not `notes added`) is intentional: the skill replaces its own previous note line rather than accumulating new ones every time it runs. - For reseller / parent-billed invoices, include the source in the note string: `"Updated from Slack invoice dated 2026-03-15 (billed via Salesforce)"`. @@ -59,6 +59,38 @@ Skipped or uncertain (3): - "Generic Vendor LLC" invoice from 2026-03-10 — couldn't extract per-unit cost from the body. ``` +## Needs-cost-structure blocks (data-integrity audit) + +From the Step 1 audit: apps that already have a cost recorded but a **blank `costStructure`**. These don't count toward spend in ShiftControl until fixed, so surface them even when you found no new invoice: + +``` +Needs cost structure — not counted in spend until fixed (2): + - Documenso: cost $9.00 recorded, structure blank — per user or flat fee (per contract)? + - Aspire: cost $5.00 recorded, structure blank — per user or flat fee (per contract)? +``` + +Where an invoice or the user tells you the structure, fold the fix into that app's normal proposal block instead of listing it here. + +## Archive-candidate blocks + +Apps the user has indicated they no longer pay for. The skill NEVER archives — it only flags: + +``` +Archive candidates (1): + - ChatGPT — you mentioned you stopped paying. Archive it in ShiftControl → Apps if so. (I won't change it.) +``` + +## No-invoice / manual-entry blocks + +Apps with no findable email invoice (portal/card billing, mailbox you can't see, or a PDF you couldn't read). Offer manual entry rather than guessing or zeroing: + +``` +No invoice found — reply with the cost and I'll record it (3): + - HReasily — likely billed via portal, not email. + - logo.dev — may be billed to a mailbox I can't access. + - GitHub — amount is in a PDF this email connector won't open; paste it or forward the invoice. +``` + ## Truncation If there are MANY proposed changes (>20), show the first 20 with a summary footer: From c2dfd3f62b7a8a82b7c163cd19fa62a5241f5025 Mon Sep 17 00:00:00 2001 From: Julien <162975217+shiftcontrol-julien@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:48:20 +0200 Subject: [PATCH 4/4] INSTALL: fix Claude Desktop skill-install path, add PDF-capability note and recommended permissions --- INSTALL.md | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 617d0bf..f70b9fe 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -115,10 +115,21 @@ Any MCP client that supports remote OAuth 2.1 servers with Dynamic Client Regist - **Endpoint:** `https://mcp.shiftcontrol.io/mcp` - **OAuth discovery:** `https://mcp.shiftcontrol.io/.well-known/oauth-authorization-server` +### Recommended permissions + +When your client asks how the ShiftControl tools may run, a good default is: + +- **Always allow** the read-only tools: `list_apps`, `get_app`, `list_groups`, `get_group`, `list_departments`, `list_locations`, `list_teams`. +- **Require approval** for `update_app_subscription` (it writes changes) and `list_my_orgs`. + +Skills can then read your data freely while every write stays behind an explicit confirmation. + --- ## Step 2 — Install a skill +> **For `refresh-subscription-info`:** this skill also needs an **email-search MCP** (to find your invoices) alongside the ShiftControl MCP. Any email MCP that lists and reads messages works. If some invoices arrive as **PDF attachments** (GitHub, JumpCloud, Salesforce-billed Slack, and others), use an email MCP that can return attachment **content or a download URL** — e.g. Superhuman. The default Anthropic Gmail connector returns attachment *filenames only*, so it cannot open PDF invoices; when that happens the skill tells you and asks you to provide the figure instead. + ### Claude Code (filesystem) ```bash @@ -135,12 +146,18 @@ cd .. && rm -rf _tmp Claude Code discovers it on the next session. -### claude.ai (web) +### Claude Desktop app (macOS / Windows) and claude.ai (web) + +**The same zip upload works for both the Claude Desktop app and the Claude website** — this is the easiest path for most people, and it's the right one if you're on the Mac or Windows desktop app. + +1. Download the skill zip from the [latest release](https://github.com/ShiftControl-io/skills/releases/latest). +2. Open the skills panel: + - **Claude Desktop app:** **Customize → Skills** + - **claude.ai (web):** **Customize → Skills** (also under **Settings → Features → Skills**) +3. Click **Upload skill**, or just **drag and drop the zip** onto the panel. +4. The skill is available immediately for new conversations. -1. Download the skill zip from the [latest release](https://github.com/ShiftControl-io/skills/releases/latest) -2. Open **claude.ai → Settings → Features → Skills** -3. Click **Upload skill** and select the zip -4. The skill is available immediately for new conversations +Note: Step 1's Claude Desktop instructions connect the MCP *server*; this step adds the *skill* itself. They're two separate installs — you need both. ### Cursor / Windsurf / Cline (rules-based tools)