From da4f334a0c23ef2b8c0196ab6b76af1bb683e56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kope=C4=87?= Date: Wed, 15 Jul 2026 15:19:22 +0200 Subject: [PATCH] Drop invoice-prep-test, standardize on .env for credentials userConfig never prompted the user at install time in testing, so all credential-needing skills stay on .env.example + .env. Adds the missing .env.example/.gitignore to check-my-timesheet and invoice-prep to match invoice-checker and timesheet-checker. --- .claude-plugin/marketplace.json | 6 - README.md | 25 +- .../.claude-plugin/plugin.json | 13 - .../skills/invoice-prep-test/.env.example | 17 - .../skills/invoice-prep-test/SKILL.md | 94 ----- .../skills/invoice-prep-test/scripts/run.py | 366 ------------------ .../skills/check-my-timesheet/.env.example | 21 + .../skills/check-my-timesheet}/.gitignore | 0 .../skills/invoice-prep/.env.example | 16 + .../timesheet/skills/invoice-prep/.gitignore | 1 + 10 files changed, 44 insertions(+), 515 deletions(-) delete mode 100644 plugins/invoice-prep-test/.claude-plugin/plugin.json delete mode 100644 plugins/invoice-prep-test/skills/invoice-prep-test/.env.example delete mode 100644 plugins/invoice-prep-test/skills/invoice-prep-test/SKILL.md delete mode 100644 plugins/invoice-prep-test/skills/invoice-prep-test/scripts/run.py create mode 100644 plugins/timesheet/skills/check-my-timesheet/.env.example rename plugins/{invoice-prep-test/skills/invoice-prep-test => timesheet/skills/check-my-timesheet}/.gitignore (100%) create mode 100644 plugins/timesheet/skills/invoice-prep/.env.example create mode 100644 plugins/timesheet/skills/invoice-prep/.gitignore diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 19c77e4..b41738a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,12 +11,6 @@ "description": "Rewrite and humanize text to remove AI writing patterns and match your own voice.", "version": "1.0.0" }, - { - "name": "invoice-prep-test", - "source": "./plugins/invoice-prep-test", - "description": "TEST: copy of invoice-prep that asks for the Tempo API token via plugin userConfig at install time.", - "version": "1.0.0" - }, { "name": "ivona", "source": "./plugins/ivona", diff --git a/README.md b/README.md index f2b4f59..d0c5aca 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ writing, and invoice/timesheet checks. Each skill is packaged as its own install | `ghost-writer` | Rewrite and humanize text to remove AI writing patterns and match your own voice. | | `hr-manager` | Run mock job interviews and produce a scored assessment with a prioritised learning plan. | | `interview` | Run a structured interview and save the captured knowledge to a markdown file. | -| `invoice-prep-test` | **Test plugin.** Copy of `invoice-prep` that asks for the Tempo API token via plugin `userConfig` at install time instead of `.env`. | | `ivona` | Check KSeF invoices against contractors/white-list and audit team timesheet completion via Tempo. | | `meeting-notes` | Interview a participant about a meeting and produce structured meeting notes. | | `meeting-prep` | Generate a prioritised list of questions for an upcoming meeting based on wiki knowledge gaps. | @@ -34,25 +33,13 @@ bare `owner/repo` shorthand won't resolve — use the full clone URL instead: /plugin marketplace add https://git.wierzbowa.cloud/Cloud_Drift/skills-marketplace.git ``` -## Credential setup — under test +## Credential setup -Most plugins that call an external API (Tempo, Jira, KSeF) currently read credentials -from a `.env` file the user creates from `.env.example`. `invoice-prep-test` is a -one-off experiment to see whether Claude Code's plugin `userConfig` mechanism — which -prompts for a value at install/enable time and can mark it `sensitive` — actually -reaches a skill's script as an environment variable -(`CLAUDE_PLUGIN_OPTION_TEMPO_API_TOKEN`) when that script runs via the Bash tool. - -To test: -```bash -/plugin install invoice-prep-test@skills-marketplace -``` -You should be prompted for a "Tempo API Token" during install. Then ask Claude -something like "what should I put on my invoice for last month" and check the -script's printed "Tempo token source" line — it reports whether it used the -plugin-provided value or fell back to `.env`/plain env. Once we know which -mechanism works, we'll standardize the rest of the credential-needing plugins -(`ivona`, `timesheet`) on it. +Plugins that call an external API (Tempo, Jira, KSeF) read credentials from a `.env` +file the user creates from each skill's `.env.example`. We tested Claude Code's plugin +`userConfig` mechanism (prompting for a credential at install time) as an alternative — +in practice it did not prompt the user at all, so we're sticking with `.env` files +across all plugins. ## Licensing diff --git a/plugins/invoice-prep-test/.claude-plugin/plugin.json b/plugins/invoice-prep-test/.claude-plugin/plugin.json deleted file mode 100644 index 20dd413..0000000 --- a/plugins/invoice-prep-test/.claude-plugin/plugin.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "invoice-prep-test", - "version": "1.0.0", - "description": "TEST plugin: copy of invoice-prep that asks for the Tempo API token via plugin userConfig at install time, instead of a .env file.", - "userConfig": { - "tempo_api_token": { - "type": "string", - "title": "Tempo API Token", - "description": "Tempo read token, used to fetch your worklogs. Generate at Tempo → Settings → API Integration.", - "sensitive": true - } - } -} diff --git a/plugins/invoice-prep-test/skills/invoice-prep-test/.env.example b/plugins/invoice-prep-test/skills/invoice-prep-test/.env.example deleted file mode 100644 index d13b00d..0000000 --- a/plugins/invoice-prep-test/skills/invoice-prep-test/.env.example +++ /dev/null @@ -1,17 +0,0 @@ -# Invoice Prep (test) — credentials template -# Copy this file to .env and fill in the values. -# .env is gitignored and never committed. -# -# NOTE: the Tempo API token is intentionally NOT here for this test — it's -# meant to be supplied via the plugin's install-time configuration prompt -# (userConfig.tempo_api_token in plugin.json), so we can verify whether that -# reaches this skill's script as CLAUDE_PLUGIN_OPTION_TEMPO_API_TOKEN. - -# Your Atlassian / Jira Cloud URL -JIRA_BASE_URL=https://your-company.atlassian.net - -# The email address you use to log into Jira -JIRA_EMAIL=your.email@company.com - -# Jira personal API token — generate at: https://id.atlassian.com → Security → API tokens -JIRA_API_TOKEN= diff --git a/plugins/invoice-prep-test/skills/invoice-prep-test/SKILL.md b/plugins/invoice-prep-test/skills/invoice-prep-test/SKILL.md deleted file mode 100644 index b51a4d2..0000000 --- a/plugins/invoice-prep-test/skills/invoice-prep-test/SKILL.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -name: invoice-prep-test -description: > - TEST VERSION of invoice-prep — show what to put on an invoice for a selected - month by checking the user's own Tempo timesheets. Lists every Jira project - code and the total hours logged against it. Warns if there are missing hours - or days with no logged time. This variant requests the Tempo API token via - the plugin's install-time configuration instead of a .env file. - TRIGGER when user asks what to put on an invoice, what to invoice, invoice - preparation, timesheet summary for invoicing, or similar phrases. - Trigger phrases: "what should I put on an invoice", "prepare my invoice", - "invoice for [month]", "what to invoice for", "check my TS for invoice". ---- - -# Invoice Preparation (test — plugin userConfig for Tempo token) - -This is a test copy of the `invoice-prep` skill. It exists to check whether a -credential declared in the plugin's `userConfig` (prompted for when the plugin -is installed/enabled) actually reaches this skill's script as an environment -variable — `CLAUDE_PLUGIN_OPTION_TEMPO_API_TOKEN` — when the script is run via -the Bash tool. Everything else about the skill is unchanged from `invoice-prep`. - -Use this skill when someone wants to know what project codes and hours to put on their invoice for a given month. It reads their own Tempo worklogs, groups hours by Jira project, and warns about any missing time. - -**In Claude Code, you run the pipeline automatically.** The user provides a month — you handle everything else. - -## Prerequisites - -| Variable | Purpose | Where it comes from | -|---|---|---| -| Tempo API Token | Fetch worklogs from Tempo | Prompted at plugin install/enable time (`userConfig.tempo_api_token`). Falls back to `TEMPO_API_TOKEN` in `.env` if the plugin-provided value isn't available. | -| `JIRA_BASE_URL` | Resolve the user's account ID | `.env` file in the project root | -| `JIRA_EMAIL` | Identifies whose timesheets to read | `.env` file in the project root | -| `JIRA_API_TOKEN` | Authenticate with Jira | `.env` file in the project root | - -Copy `.env.example` to `.env` in this skill folder (or the project root) and fill in the three Jira values. The Tempo token should NOT go in `.env` for this test — it's meant to come from the plugin's install-time prompt, so we can see whether that mechanism actually works. - -The script prints which source it used for the Tempo token (`CLAUDE_PLUGIN_OPTION_TEMPO_API_TOKEN` vs `.env` fallback) — check that line in the output to see the test result. - -## Extracting the month - -Convert natural language to `YYYY-MM` before running: - -| User says | CLI value | -|---|---| -| "last month" | Previous calendar month | -| "this month" | Current calendar month | -| "June", "June 2026" | `2026-06` | -| "May" | `2026-05` (current year) | - -If no month is specified, ask: "Which month should I check? (e.g. last month, June 2026)" - -## Workflow - -**Step 1 — Run the script:** - -```bash -python3 {SKILL_DIR}/scripts/run.py \ - --month {YYYY-MM} \ - --output-dir ./timesheet-output -``` - -This resolves the user's Jira account, fetches their Tempo worklogs for the full calendar month, groups hours by Jira project code, checks completeness against the Polish working calendar, and writes `invoice_prep_{YYYY-MM}.json` to `./timesheet-output/`. - -Report the printed "Tempo token source" line back to the user along with the results — that's the point of this test. - -**Step 2 — Present results.** Read the output JSON and show the user: - -1. A table of project codes and hours — formatted ready to copy onto an invoice. - Include **every project in the `projects` array**. Do NOT label any project as "non-billable" or omit it from the table — the script already excludes ignored issues; everything remaining is billable: - - ``` - | Project | Hours | - | IAA | 45.5 | - | PROJ | 22.0 | - ``` - - Below the table, note ignored issues and their hours as a parenthetical: `(INTERNAL-1: 40h ignored per config.)` — do not mix them into the billable table. - -2. The **total billable hours** from `total_billable_hours` (not `total_logged_hours`). - -3. If `missing_hours > 0`: a clear warning showing: - - How many hours are missing vs the expected total for the month - - Which specific dates have no logged time (up to 10; if more, show count) - - Reminder to complete the timesheet before issuing the invoice - -4. If `total_logged_hours == 0`: warn that no hours were found at all for the month. - -## Guardrails - -- Never fabricate or estimate hours. All data comes from Tempo. -- If `JIRA_EMAIL` is not set, stop and tell the user. -- If any script step fails, show the error and stop. -- Always remind the user that incomplete timesheets will cause Jira compliance failures when the invoice is checked. diff --git a/plugins/invoice-prep-test/skills/invoice-prep-test/scripts/run.py b/plugins/invoice-prep-test/skills/invoice-prep-test/scripts/run.py deleted file mode 100644 index f5c83f6..0000000 --- a/plugins/invoice-prep-test/skills/invoice-prep-test/scripts/run.py +++ /dev/null @@ -1,366 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate invoice line items from the user's own Tempo worklogs for a given month. - -Groups hours by Jira project code and reports missing time vs the Polish -working calendar. -""" - -from __future__ import annotations - -import argparse -import base64 -import calendar -import json -import os -import re -import sys -import urllib.error -import urllib.request -from datetime import date, timedelta -from pathlib import Path -from typing import Any - - -# ── .env loading ────────────────────────────────────────────────────────────── - -def load_dotenv(*paths: Path) -> Path | None: - for path in paths: - if not path.exists(): - continue - for raw in path.read_text(encoding="utf-8").splitlines(): - line = raw.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, _, value = line.partition("=") - key = key.strip() - value = value.strip().strip('"').strip("'") - if key and key not in os.environ: - os.environ[key] = value - return path - return None - - -# ── Period helpers ──────────────────────────────────────────────────────────── - -def parse_month(month_str: str) -> tuple[date, date]: - """Parse 'YYYY-MM' into (first_day, last_day) of that month.""" - s = month_str.strip() - if not re.fullmatch(r"\d{4}-\d{2}", s): - raise ValueError(f"Invalid month '{month_str}'. Use YYYY-MM.") - year, month = int(s[:4]), int(s[5:7]) - first = date(year, month, 1) - last = date(year, month, calendar.monthrange(year, month)[1]) - return first, last - - -# ── Public holidays ─────────────────────────────────────────────────────────── - -def get_public_holidays(country: str, year: int) -> set[date]: - url = f"https://date.nager.at/api/v3/PublicHolidays/{year}/{country.upper()}" - req = urllib.request.Request(url, headers={"User-Agent": "invoice-prep/1.0"}) - try: - with urllib.request.urlopen(req, timeout=10) as resp: - return {date.fromisoformat(item["date"]) for item in json.loads(resp.read())} - except Exception as exc: - print(f"Warning: could not fetch holidays for {country} {year}: {exc}", file=sys.stderr) - return set() - - -def working_days(start: date, end: date, holidays: set[date]) -> list[date]: - result, cur = [], start - while cur <= end: - if cur.weekday() < 5 and cur not in holidays: - result.append(cur) - cur += timedelta(days=1) - return result - - -# ── Jira account lookup ─────────────────────────────────────────────────────── - -def resolve_account_id(email: str, jira_base_url: str, - jira_email: str, jira_token: str) -> str: - creds = base64.b64encode(f"{jira_email}:{jira_token}".encode()).decode() - headers = {"Authorization": f"Basic {creds}", "Accept": "application/json"} - url = f"{jira_base_url.rstrip('/')}/rest/api/3/user/search?query={email}&maxResults=5" - req = urllib.request.Request(url, headers=headers) - try: - with urllib.request.urlopen(req, timeout=10) as resp: - users = json.loads(resp.read()) - except urllib.error.HTTPError as exc: - if exc.code == 401: - print("Error: Jira API returned 401. Check JIRA_EMAIL and JIRA_API_TOKEN.", - file=sys.stderr) - sys.exit(1) - raise RuntimeError(f"Jira API {exc.code}") from exc - match = next( - (u for u in users if u.get("emailAddress", "").lower() == email.lower()), - None, - ) - if not match: - print(f"Error: Jira account not found for {email}.", file=sys.stderr) - sys.exit(1) - return match["accountId"] - - -# ── Tempo worklogs ──────────────────────────────────────────────────────────── - -_PROJECT_CODE_RE = re.compile(r"\b([A-Z][A-Z0-9]{1,9})-\d+\b") - - -def load_jira_ignore(path: Path) -> set[str]: - """Load issue keys to ignore from a markdown table in jira_ignore.md.""" - if not path.exists(): - return set() - ignored: set[str] = set() - in_table = False - header_seen = False - for line in path.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not (line.startswith("|") and line.endswith("|")): - in_table = False - header_seen = False - continue - cells = [c.strip() for c in line.strip("|").split("|")] - if not header_seen: - # First row is header or separator - if all(re.fullmatch(r":?-{3,}:?", c) for c in cells if c): - continue # separator row - header_seen = True - in_table = True - continue - if all(re.fullmatch(r":?-{3,}:?", c) for c in cells if c): - continue # separator row - if in_table and cells and cells[0]: - ignored.add(cells[0].strip().upper()) - return ignored - - -def extract_project_code(issue_key: str) -> str | None: - """Extract project code from a Jira issue key, e.g. 'IAA' from 'IAA-42'.""" - m = _PROJECT_CODE_RE.match(issue_key.strip()) - return m.group(1) if m else None - - -def fetch_worklogs(account_id: str, start: date, end: date, - token: str, base_url: str = "https://api.tempo.io/4") -> list[dict]: - """Return all Tempo worklog entries for the account in [start, end].""" - results: list[dict] = [] - headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"} - next_url: str | None = ( - f"{base_url}/worklogs/user/{account_id}" - f"?from={start}&to={end}&limit=1000" - ) - while next_url: - req = urllib.request.Request(next_url, headers=headers) - try: - with urllib.request.urlopen(req, timeout=30) as resp: - data = json.loads(resp.read()) - except urllib.error.HTTPError as exc: - body = exc.read().decode("utf-8", errors="replace") - if exc.code == 401: - print("Error: Tempo API returned 401. Check TEMPO_API_TOKEN.", file=sys.stderr) - sys.exit(1) - raise RuntimeError(f"Tempo API {exc.code}: {body[:200]}") from exc - results.extend(data.get("results", [])) - next_url = data.get("metadata", {}).get("next") - return results - - -def resolve_issue_keys(worklogs: list[dict], jira_email: str, - jira_token: str) -> dict[int, str]: - """ - Resolve Jira issue IDs → keys by calling each issue's self URL directly. - Uses the self URL already present in each Tempo worklog entry. - """ - creds = base64.b64encode(f"{jira_email}:{jira_token}".encode()).decode() - headers = {"Authorization": f"Basic {creds}", "Accept": "application/json"} - seen: dict[int, str] = {} - for wl in worklogs: - issue = wl.get("issue", {}) - issue_id = issue.get("id") - if not issue_id or int(issue_id) in seen: - continue - self_url = issue.get("self", "") - if not self_url: - continue - # Swap api/2 → api/3 if needed; append fields filter - url = re.sub(r"/rest/api/\d+/", "/rest/api/3/", self_url) + "?fields=key" - req = urllib.request.Request(url, headers=headers) - try: - with urllib.request.urlopen(req, timeout=10) as resp: - data = json.loads(resp.read()) - seen[int(issue_id)] = data.get("key", "") - except Exception as exc: - print(f"Warning: could not resolve issue {issue_id}: {exc}", file=sys.stderr) - return seen - - -# ── Main ────────────────────────────────────────────────────────────────────── - -def run(args: argparse.Namespace) -> int: - skill_dir = Path(__file__).parent.parent - loaded = load_dotenv(Path.cwd() / ".env", skill_dir / ".env") - if loaded: - print(f"Loaded credentials from {loaded}") - - # TEST: tempo_api_token is requested via plugin.json userConfig at install - # time. Claude Code is expected to inject it as CLAUDE_PLUGIN_OPTION_TEMPO_API_TOKEN. - # Fall back to a plain TEMPO_API_TOKEN (env/.env) so this still works if that - # injection doesn't reach this script. - plugin_option_token = os.environ.get("CLAUDE_PLUGIN_OPTION_TEMPO_API_TOKEN", "") - env_token = os.environ.get("TEMPO_API_TOKEN", "") - tempo_token = plugin_option_token or env_token - if plugin_option_token: - print("Tempo token source: CLAUDE_PLUGIN_OPTION_TEMPO_API_TOKEN (plugin userConfig)") - elif env_token: - print("Tempo token source: TEMPO_API_TOKEN (env/.env) — plugin userConfig var was not set") - else: - print("Tempo token source: none found") - - jira_base_url = os.environ.get("JIRA_BASE_URL", "") - jira_email = os.environ.get("JIRA_EMAIL", "") - jira_token = os.environ.get("JIRA_API_TOKEN", "") - - missing = [n for n, v in [ - ("TEMPO_API_TOKEN (via userConfig or env)", tempo_token), ("JIRA_BASE_URL", jira_base_url), - ("JIRA_EMAIL", jira_email), ("JIRA_API_TOKEN", jira_token), - ] if not v] - if missing: - print(f"Error: missing credentials: {', '.join(missing)}", file=sys.stderr) - return 1 - - try: - start, end = parse_month(args.month) - except ValueError as exc: - print(f"Error: {exc}", file=sys.stderr) - return 1 - - # Load ignore list - ignore_path = Path.cwd() / "jira_ignore.md" - ignored_keys = load_jira_ignore(ignore_path) - if ignored_keys: - print(f"Ignoring issues: {', '.join(sorted(ignored_keys))}") - - print(f"Month: {args.month} ({start} – {end})") - print(f"User: {jira_email}") - - # Resolve account ID - print("Resolving Jira account...") - account_id = resolve_account_id(jira_email, jira_base_url, jira_email, jira_token) - print(f" {jira_email} → {account_id}") - - # Working days (Polish calendar) - print("Fetching public holidays...") - holidays = get_public_holidays("PL", start.year) - wdays = working_days(start, end, holidays) - expected_hours = len(wdays) * 8.0 - print(f" Working days: {len(wdays)} Expected hours: {expected_hours:.0f}h") - - # Fetch Tempo worklogs - print("Fetching Tempo worklogs...") - worklogs = fetch_worklogs(account_id, start, end, tempo_token) - print(f" Worklogs fetched: {len(worklogs)}") - - # Resolve issue IDs → keys via each issue's self URL (Tempo v4 omits the key) - unique_ids = {int(wl["issue"]["id"]) for wl in worklogs if wl.get("issue", {}).get("id")} - issue_key_map: dict[int, str] = {} - if unique_ids: - print(f" Resolving {len(unique_ids)} unique issue key(s) from Jira...") - issue_key_map = resolve_issue_keys(worklogs, jira_email, jira_token) - print(f" Resolved: {len(issue_key_map)}") - - # Group hours by project code and by date, skipping ignored issues - project_hours: dict[str, float] = {} - hours_by_date: dict[str, float] = {} - unmatched_hours = 0.0 - ignored_hours = 0.0 - - for wl in worklogs: - issue_id = int(wl.get("issue", {}).get("id", 0)) - issue_key = issue_key_map.get(issue_id, "") - hours = wl.get("timeSpentSeconds", 0) / 3600 - wl_date = wl.get("startDate", "") - - if issue_key.upper() in ignored_keys: - ignored_hours += hours - # Still count toward date totals so missing-day detection stays accurate - if wl_date: - hours_by_date[wl_date] = hours_by_date.get(wl_date, 0.0) + hours - continue - - code = extract_project_code(issue_key) - if code: - project_hours[code] = project_hours.get(code, 0.0) + hours - else: - unmatched_hours += hours - if wl_date: - hours_by_date[wl_date] = hours_by_date.get(wl_date, 0.0) + hours - - # Sort projects by hours descending - projects = sorted( - [{"code": k, "hours": round(v, 2)} for k, v in project_hours.items()], - key=lambda x: x["hours"], - reverse=True, - ) - - total_logged = round(sum(project_hours.values()) + unmatched_hours + ignored_hours, 2) - total_billable = round(sum(project_hours.values()) + unmatched_hours, 2) - missing_hours = round(max(0.0, expected_hours - total_logged), 2) - - # Find working days with no logged time - days_missing = [ - d.isoformat() for d in wdays - if hours_by_date.get(d.isoformat(), 0.0) == 0.0 - ] - - output = { - "month": args.month, - "user_email": jira_email, - "account_id": account_id, - "working_days": len(wdays), - "expected_hours": expected_hours, - "total_logged_hours": total_logged, - "total_billable_hours": total_billable, - "ignored_hours": round(ignored_hours, 2), - "ignored_keys": sorted(ignored_keys), - "missing_hours": missing_hours, - "unmatched_hours": round(unmatched_hours, 2), - "days_with_no_hours": days_missing, - "projects": projects, - } - - output_dir = Path(args.output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - out_path = output_dir / f"invoice_prep_{args.month}.json" - out_path.write_text(json.dumps(output, indent=2, ensure_ascii=False), encoding="utf-8") - print(f"Wrote {out_path}") - - # Print quick summary - print(f"\nProjects:") - for p in projects: - print(f" {p['code']:12s} {p['hours']:.2f}h") - if unmatched_hours: - print(f" (no project) {unmatched_hours:.2f}h") - if ignored_hours: - print(f" (ignored) {ignored_hours:.2f}h ({', '.join(sorted(ignored_keys))})") - print(f"\nBillable total: {total_billable:.2f}h") - print(f"Total logged: {total_logged:.2f}h / {expected_hours:.0f}h expected") - if missing_hours > 0: - print(f"⚠ Missing: {missing_hours:.2f}h ({len(days_missing)} day(s) with no time logged)") - - return 0 - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - description="Generate invoice line items from Tempo worklogs." - ) - parser.add_argument("--month", required=True, help="Month to check: YYYY-MM") - parser.add_argument("--output-dir", required=True, - help="Directory for invoice_prep_YYYY-MM.json") - return parser - - -if __name__ == "__main__": - sys.exit(run(build_parser().parse_args())) diff --git a/plugins/timesheet/skills/check-my-timesheet/.env.example b/plugins/timesheet/skills/check-my-timesheet/.env.example new file mode 100644 index 0000000..0d7882c --- /dev/null +++ b/plugins/timesheet/skills/check-my-timesheet/.env.example @@ -0,0 +1,21 @@ +# Check My Timesheet — credentials template +# Copy this file to .env (in the project root) and fill in the values. +# .env is gitignored and never committed. + +# Tempo API token — generate at: Tempo → Settings → API Integration +TEMPO_API_TOKEN= + +# Your Atlassian / Jira Cloud URL +JIRA_BASE_URL=https://your-company.atlassian.net + +# The email address you use to log into Jira +JIRA_EMAIL=your.email@company.com + +# Jira personal API token — generate at: https://id.atlassian.com → Security → API tokens +JIRA_API_TOKEN= + +# Country code for the public holiday calendar (optional, defaults to PL) +MY_TIMESHEET_COUNTRY=PL + +# Expected working hours per day (optional, defaults to 8) +MY_TIMESHEET_HOURS_PER_DAY=8 diff --git a/plugins/invoice-prep-test/skills/invoice-prep-test/.gitignore b/plugins/timesheet/skills/check-my-timesheet/.gitignore similarity index 100% rename from plugins/invoice-prep-test/skills/invoice-prep-test/.gitignore rename to plugins/timesheet/skills/check-my-timesheet/.gitignore diff --git a/plugins/timesheet/skills/invoice-prep/.env.example b/plugins/timesheet/skills/invoice-prep/.env.example new file mode 100644 index 0000000..8e587ea --- /dev/null +++ b/plugins/timesheet/skills/invoice-prep/.env.example @@ -0,0 +1,16 @@ +# Invoice Prep — credentials template +# Copy this file to .env (in the project root) and fill in the values. +# .env is gitignored and never committed. +# Same credentials as check-my-timesheet — one .env can be shared by both. + +# Tempo API token — generate at: Tempo → Settings → API Integration +TEMPO_API_TOKEN= + +# Your Atlassian / Jira Cloud URL +JIRA_BASE_URL=https://your-company.atlassian.net + +# The email address you use to log into Jira +JIRA_EMAIL=your.email@company.com + +# Jira personal API token — generate at: https://id.atlassian.com → Security → API tokens +JIRA_API_TOKEN= diff --git a/plugins/timesheet/skills/invoice-prep/.gitignore b/plugins/timesheet/skills/invoice-prep/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/plugins/timesheet/skills/invoice-prep/.gitignore @@ -0,0 +1 @@ +.env