Metadata-Version: 2.5
Name: freshservice-cli
Version: 0.2.0
Summary: Instance-agnostic Freshservice CLI: tickets, service catalog, MSP routing and Project Management, with live ID discovery.
Project-URL: Homepage, https://github.com/KamorionLabs/freshservice-cli
Project-URL: Issues, https://github.com/KamorionLabs/freshservice-cli/issues
Author: Kamorion
License: MIT
License-File: LICENSE
Keywords: cli,freshservice,itsm,msp,service-desk
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Utilities
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# freshservice-cli (`fresh`)

An instance-agnostic [Freshservice](https://freshservice.com) command-line client:
tickets, service catalog, MSP routing and Project Management — with **live ID
discovery** so you never hardcode group / department / catalog / project ids.

Standard library only (no runtime dependencies). Python ≥ 3.11.

## Install

```bash
pipx install freshservice-cli      # recommended
# or
pip install freshservice-cli
```

## Configure

```bash
mkdir -p ~/.config/freshservice-cli
cp freshservice.example.toml ~/.config/freshservice-cli/config.toml
$EDITOR ~/.config/freshservice-cli/config.toml
```

```toml
domain = "yourcompany"            # https://yourcompany.freshservice.com
api_key = "abcd..."               # or: api_key_command = "op item get ... --reveal"
```

Env overrides: `FRESHSERVICE_DOMAIN`, `FRESHSERVICE_BASE_URL`, `FRESHSERVICE_API_KEY`,
`FRESHSERVICE_CONFIG`. Multiple instances via `[profile.<name>]` + `fresh --profile <name>`.

Check it:

```bash
fresh config        # shows resolved base_url + masked key
fresh whoami
```

## Usage

```bash
# Tickets
fresh tickets --limit 30
fresh ticket 2800
fresh conv 2800
fresh activities 2800                  # chronological activity log (workflow trace)
fresh filter "group_id:149671 AND status:2"
fresh views                            # list configured saved views ([view.*])
fresh view front-end                   # run a saved view (replicates a UI view)
fresh mine
fresh reply 2800 "looking into it" --public
fresh close 2800                       # handles required responder/criticality/severity

# Discovery (resolve names, not ids)
fresh groups
fresh departments
fresh agents

# Changes
fresh changes --limit 10
fresh change 973
fresh create-change --subject "..." --description "..." --planned-start "2026-09-25T22:00:00Z" --planned-end "2026-09-26T22:00:00Z" --group "MSP" --set change_category="Firewall Rule Change"

# Service catalog (the catalog API is opaque to agent keys, so we discover it)
fresh catalog-discover --scan 300      # map item-id -> name by scanning tickets
fresh place-request "Firewall & VPN" --summary "..." --description-file req.txt

# Run a config-defined workflow (e.g. "route to our MSP") — see [workflow.*] in config
fresh workflows                        # list configured workflows
fresh run msp-request --summary "Whitelist X" --description-file req.txt
fresh -i run msp-request               # prompt for item / summary / description / category

# Project Management (/pm) — refs accept id | key | name
fresh projects
fresh ptasks PROJ
fresh ptask PROJ PROJ-43
fresh sprints PROJ
fresh setstatus PROJ PROJ-43 2000221122
fresh setsprint PROJ PROJ-43 W25
fresh note PROJ PROJ-43 "diagnostic ..."
fresh linkticket PROJ PROJ-43 2800
fresh newtask PROJ '{"title":"...","type_id":...,"assignee_id":...}'

# Escape hatch
fresh raw GET "tickets?per_page=5"
fresh raw POST "tickets" '{"subject":"x","description":"y","email":"a@b.com","status":2,"priority":2}'
```

## Interactive mode

When a required field is missing, `fresh` prompts for it and **consults the live API
to propose the options** — instead of you having to know ids:

* `--group` → numbered list of groups
* catalog `item` → list of items **discovered** from recent tickets
* `--category` (and other non-enumerable fields) → the values **actually seen on
  recent tickets**, or type a fresh one
* `--summary`, `--description` → free text

Interactive prompting is **on automatically when stdin is a TTY**. Force it with
`-i/--interactive`, or disable it (for scripts/CI) with `--no-interactive` (missing
required fields then raise a clear error).

```bash
fresh -i run msp-request      # asks: item? summary? description? category?
fresh --no-interactive create # errors listing what's missing
```

## What the API does not expose (handled, not guessed)

| Data | Limit | How `fresh` copes |
|------|-------|-------------------|
| `ticket_fields` (category / criticality / severity option lists) | `403` for agent keys | `close` surfaces the API's own validation error, which lists allowed values |
| `service_catalog/items` | opaque for agent keys | **discovered** by scanning tickets' `requested_items` + inferring the name from `Request for <user> : <item>` subjects |
| `pm/statuses`, `pm/task_types` | UI-config only, no GET | ids shown raw; optional labels via `pm_labels_file` sidecar JSON |

## Workflows (the dynamic, tenant-defined part)

Nothing tenant-specific lives in the code — no notion of "MSP", no fixed custom
fields. Instead you declare **workflows** in config and run them with
`fresh run <name>`. A workflow describes:

- `action` — `catalog_request`, `create_ticket`, or `create_change`;
- `item` — default catalog item (for `catalog_request`);
- `[workflow.<name>.set]` — fields applied (PUT) after creation. Values may be a
  literal, `@prompt` (ask free text), or `@suggest:<field>` (propose values seen on
  recent tickets). The key `group` is resolved by name to a `group_id`;
- `[workflow.<name>.wait]` — poll a `custom_field` until it is populated = success
  (with `report_fields`, `polls`, `interval`).

Example — the same "route a request to our MSP so a ServiceNow case is created"
behaviour, expressed entirely in config:

```toml
[workflow.msp-request]
action = "catalog_request"
item = "PIM"
[workflow.msp-request.set]
group = "MSP"
category = "@suggest:category"
[workflow.msp-request.wait]
custom_field = "msp_case_reference"
report_fields = ["msp_id", "msp_case_reference"]
```

```bash
fresh run msp-request --summary "…" --description-file req.txt
```

Why this matters: on MSP-integrated tenants an external case is created only when
the ticket is a **catalog request** (a raw `POST /tickets` never routes), its
**group** is right, and a **category** is set — the workflow encodes exactly that,
without the CLI knowing anything about your MSP.

## Publishing (maintainers)

```bash
pip install build twine
python -m build
twine upload dist/*
```

## License

MIT
