Rewrite BMS skill from bash to Python

This commit is contained in:
Steve W
2026-04-08 02:19:50 +00:00
parent 568b825e11
commit 59d6e5ba3a
17 changed files with 938 additions and 1286 deletions

590
scripts/bms-tickets.sh Normal file → Executable file
View File

@@ -1,590 +1,6 @@
#!/usr/bin/env bash
# bms-tickets.sh — Kaseya BMS ticket CRUD operations
set -euo pipefail
# Import logging
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/bms-logging.sh"
BMS_API_BASE="${BMS_API_BASE:-https://api.bms.kaseya.com}"
# ─── Helpers ─────────────────────────────────────────────────────────────────
die() { echo "ERROR: $*" >&2; exit 1; }
get_token() {
bash "${SCRIPT_DIR}/bms-auth.sh" get-token
}
bms_curl() {
local method="$1"; shift
local path="$1"; shift
local token
token=$(get_token)
curl -sf -X "$method" \
"${BMS_API_BASE}${path}" \
-H "Authorization: Bearer ${token}" \
-H "Content-Type: application/json" \
"$@"
}
# Pretty-print a ticket list
format_ticket_list() {
jq -r '
(.result // .) |
if type == "array" then .[] else empty end |
"\(.ticketNumber // .Id)\t[\(.statusName // "?")] \(.title // "?")\t| \(.accountName // "?")\t| Assignee: \(.assigneeName // "unassigned")\t| Priority: \(.priorityName // "?")"
' | sed 's/\t/ /g'
}
format_ticket_detail() {
jq -r '
.Data // . |
"Ticket: \(.TicketNumber) (ID: \(.Id))
Title: \(.Title)
Status: \(.StatusName) (ID: \(.StatusId))
Priority: \(.PriorityName) (ID: \(.PriorityId))
Account: \(.AccountName) (ID: \(.AccountId))
Location: \(.LocationName // "N/A")
Contact: \(.ContactName // "N/A")
Queue: \(.QueueName // "N/A")
Assignee: \(.AssigneeName // "unassigned")
Type: \(.TypeName // "N/A")
Created: \(.CreatedOn)
Modified: \(.ModifiedOn)
Due: \(.DueDate // "none")
---
\(.Details // "(no details)")"
'
}
# ─── Commands ────────────────────────────────────────────────────────────────
cmd_list() {
local status="" assignee="" from="" to="" priority="" queue="" account=""
local page=1 page_size=25 format="table"
while [[ $# -gt 0 ]]; do
case "$1" in
--status) status="$2"; shift 2 ;;
--assignee) assignee="$2"; shift 2 ;;
--from) from="$2"; shift 2 ;;
--to) to="$2"; shift 2 ;;
--priority) priority="$2"; shift 2 ;;
--queue) queue="$2"; shift 2 ;;
--account) account="$2"; shift 2 ;;
--page) page="$2"; shift 2 ;;
--page-size) page_size="$2"; shift 2 ;;
--format) format="$2"; shift 2 ;;
*) die "Unknown option: $1" ;;
esac
done
# Build filter JSON
local filter="{"
local sep=""
if [[ "$status" == "open" ]]; then
filter+="\"StatusNames\":\"Escalated,Open,Waiting for Customer,Waiting for Product(s),Waiting for Vendor\""
sep=","
status=""
fi
[[ -n "$status" ]] && { filter+="${sep}\"StatusNames\":\"${status}\""; sep=","; }
[[ -n "$assignee" ]] && { filter+="${sep}\"AssigneeName\":\"${assignee}\""; sep=","; }
[[ -n "$from" ]] && { filter+="${sep}\"CreatedOnFrom\":\"${from}T00:00:00\""; sep=","; }
[[ -n "$to" ]] && { filter+="${sep}\"CreatedOnTo\":\"${to}T23:59:59\""; sep=","; }
[[ -n "$priority" ]] && { filter+="${sep}\"PriorityNames\":\"${priority}\""; sep=","; }
[[ -n "$queue" ]] && { filter+="${sep}\"QueueNames\":\"${queue}\""; sep=","; }
[[ -n "$account" ]] && { filter+="${sep}\"Account\":\"${account}\""; sep=","; }
filter+="}"
local body
body=$(jq -n \
--argjson filter "$filter" \
--argjson page "$page" \
--argjson page_size "$page_size" \
'{Filter: $filter, PageNumber: $page, PageSize: $page_size}')
local response
response=$(bms_curl POST "/v2/servicedesk/tickets/search" -d "$body")
if [[ "$format" == "json" ]]; then
echo "$response" | jq .
else
local total
total=$(echo "$response" | jq -r '.TotalCount // .Total // "?"')
echo "Tickets (page ${page}, ${page_size} per page, total: ${total}):" >&2
echo "$response" | format_ticket_list
fi
}
cmd_get() {
local ticket_id="${1:-}"
local format="${2:-table}"
[[ -n "$ticket_id" ]] || die "Usage: bms tickets get <ticketId>"
local response
response=$(bms_curl GET "/v2/servicedesk/tickets/${ticket_id}")
if [[ "$format" == "json" ]] || [[ "${2:-}" == "--json" ]]; then
echo "$response" | jq .
else
echo "$response" | format_ticket_detail
fi
}
# Prompt for a value if empty; first arg is field label, second is current value (by nameref)
# Usage: prompt_if_empty "Label" varname
prompt_if_empty() {
local label="$1"
local -n _ref="$2"
if [[ -z "${_ref:-}" ]]; then
read -r -p "${label}: " _ref
fi
}
cmd_create() {
local title="" details="" account_id="" account_name="" location_id="" location_name="" contact_id=""
local status_id="" priority_id="" type_id="" source_id=""
local assignee_id="" queue_id="" due_date="" open_date=""
local template_id=""
local interactive=false
local response="" ticket_id="" ticket_number="" success=""
while [[ $# -gt 0 ]]; do
case "$1" in
--template-id) template_id="$2"; shift 2 ;;
--title) title="$2"; shift 2 ;;
--details|--description) details="$2"; shift 2 ;;
--account-id) account_id="$2"; shift 2 ;;
--account-name) account_name="$2"; shift 2 ;;
--location-id) location_id="$2"; shift 2 ;;
--location-name) location_name="$2"; shift 2 ;;
--contact-id) contact_id="$2"; shift 2 ;;
--status-id) status_id="$2"; shift 2 ;;
--priority-id) priority_id="$2"; shift 2 ;;
--type-id) type_id="$2"; shift 2 ;;
--source-id) source_id="$2"; shift 2 ;;
--assignee-id) assignee_id="$2"; shift 2 ;;
--queue-id) queue_id="$2"; shift 2 ;;
--due-date) due_date="$2"; shift 2 ;;
--interactive) interactive=true; shift ;;
*) die "Unknown option: $1" ;;
esac
done
# ── Template pre-fill ────────────────────────────────────────────────────
if [[ -n "$template_id" ]]; then
echo "Fetching template ${template_id}..." >&2
local tmpl
tmpl=$(bms_curl GET "/v2/servicedesk/templates/tickets/${template_id}" | jq '.Data // .')
# Fill only fields not already set by CLI flags
[[ -z "$title" ]] && title=$(echo "$tmpl" | jq -r '.Title // empty')
[[ -z "$details" ]] && details=$(echo "$tmpl" | jq -r '.Details // empty')
[[ -z "$status_id" ]] && status_id=$(echo "$tmpl" | jq -r '.StatusId // empty')
[[ -z "$priority_id" ]] && priority_id=$(echo "$tmpl" | jq -r '.PriorityId // empty')
[[ -z "$type_id" ]] && type_id=$(echo "$tmpl" | jq -r '(.TypeId // .IssueTypeId) // empty')
[[ -z "$source_id" ]] && source_id=$(echo "$tmpl" | jq -r '.SourceId // empty')
[[ -z "$queue_id" ]] && queue_id=$(echo "$tmpl" | jq -r '.QueueId // empty')
[[ -z "$assignee_id" ]] && assignee_id=$(echo "$tmpl" | jq -r '.AssigneeId // empty')
[[ -z "$account_id" ]] && account_id=$(echo "$tmpl" | jq -r '.AccountId // empty')
[[ -z "$location_id" ]] && location_id=$(echo "$tmpl" | jq -r '.LocationId // empty')
[[ -z "$contact_id" ]] && contact_id=$(echo "$tmpl" | jq -r '.ContactId // empty')
fi
# ── Interactive prompts ───────────────────────────────────────────────────
if $interactive; then
prompt_if_empty "Title" title
prompt_if_empty "Details" details
prompt_if_empty "Account ID" account_id
prompt_if_empty "Location ID" location_id
prompt_if_empty "Status ID" status_id
prompt_if_empty "Priority ID" priority_id
prompt_if_empty "Type ID" type_id
prompt_if_empty "Source ID" source_id
prompt_if_empty "Queue ID (optional if Assignee ID provided)" queue_id
if [[ -z "$queue_id" ]]; then
prompt_if_empty "Assignee ID (optional if Queue ID provided)" assignee_id
fi
elif [[ -n "$template_id" ]]; then
# When using a template, prompt only for fields still missing that are required
[[ -n "$title" ]] || { read -r -p "Title: " title; }
[[ -n "$details" ]] || { read -r -p "Details: " details; }
[[ -n "$account_id" ]] || { read -r -p "Account ID: " account_id; }
[[ -n "$location_id" ]] || { read -r -p "Location ID: " location_id; }
[[ -n "$status_id" ]] || { read -r -p "Status ID: " status_id; }
[[ -n "$priority_id" ]] || { read -r -p "Priority ID: " priority_id; }
[[ -n "$type_id" ]] || { read -r -p "Type ID: " type_id; }
[[ -n "$source_id" ]] || { read -r -p "Source ID: " source_id; }
if [[ -z "$queue_id" && -z "$assignee_id" ]]; then
read -r -p "Queue ID (or leave blank to provide Assignee ID): " queue_id
if [[ -z "$queue_id" ]]; then
read -r -p "Assignee ID: " assignee_id
fi
fi
fi
# ── Resolve account name to ID ─────────────────────────────────────────────
if [[ -n "$account_name" && -z "$account_id" ]]; then
account_search=$(bms_curl POST "/v2/servicedesk/accounts/search" -d "{\"Filter\":{\"AccountName\":\"$account_name\"},\"PageNumber\":1,\"PageSize\":1}")
account_id=$(echo "$account_search" | jq -r '.Data[0].Id // .result[0].id // empty')
[[ -n "$account_id" ]] || die "Account not found: $account_name"
fi
# ── Resolve location name to ID (scoped to account) ───────────────────────
if [[ -n "$location_name" && -z "$location_id" ]]; then
[[ -n "$account_id" ]] || die "Must specify --account-id or --account-name before using --location-name"
loc_search=$(bms_curl GET "/v2/crm/accounts/${account_id}/locations/lookup")
location_id=$(echo "$loc_search" | jq -r '.result[]? | select(.Name=="$location_name") | .Id // empty')
[[ -n "$location_id" ]] || die "Location not found: $location_name (account: ${account_id:-unknown})"
fi
[[ -n "$title" ]] || die "Missing required field: --title"
[[ -n "$details" ]] || die "Missing required field: --details"
[[ -n "$account_id" ]] || die "Missing required field: --account-id"
[[ -n "$location_id" ]] || die "Missing required field: --location-id"
[[ -n "$status_id" ]] || die "Missing required field: --status-id"
[[ -n "$priority_id" ]] || die "Missing required field: --priority-id"
[[ -n "$type_id" ]] || die "Missing required field: --type-id"
[[ -n "$source_id" ]] || die "Missing required field: --source-id"
[[ -n "$queue_id" || -n "$assignee_id" ]] || die "Missing required routing: provide either --queue-id or --assignee-id"
open_date="${open_date:-$(date -u +%Y-%m-%dT%H:%M:%S)}"
local body
body=$(jq -n \
--arg title "$title" \
--arg details "$details" \
--argjson account_id "$account_id" \
--argjson location_id "$location_id" \
--argjson status_id "$status_id" \
--argjson priority_id "$priority_id" \
--argjson type_id "$type_id" \
--argjson source_id "$source_id" \
--arg open_date "$open_date" \
'{
Title: $title,
Details: $details,
AccountId: $account_id,
LocationId: $location_id,
StatusId: $status_id,
PriorityId: $priority_id,
TypeId: $type_id,
SourceId: $source_id,
OpenDate: $open_date
}')
# Optionally add non-required fields
[[ -n "$contact_id" ]] && body=$(echo "$body" | jq --argjson v "$contact_id" '. + {ContactId: $v}')
[[ -n "$assignee_id" ]] && body=$(echo "$body" | jq --argjson v "$assignee_id" '. + {AssigneeId: $v}')
[[ -n "$queue_id" ]] && body=$(echo "$body" | jq --argjson v "$queue_id" '. + {QueueId: $v}')
[[ -n "$due_date" ]] && body=$(echo "$body" | jq --arg v "$due_date" '. + {DueDate: $v}')
# Single create call only. No retries here.
response=$(bms_curl POST "/v2/servicedesk/tickets" -d "$body")
success=$(echo "$response" | jq -r '.success // .Success // empty')
ticket_id=$(echo "$response" | jq -r '.Data.Id // .Id // .result.id // .result.id // empty')
ticket_number=$(echo "$response" | jq -r '.Data.TicketNumber // .TicketNumber // .result.ticketNumber // empty')
if [[ "$success" != "true" ]] || [[ -z "$ticket_id" ]] || [[ "$ticket_id" == "null" ]]; then
echo "Create ticket failed or returned ambiguous response:" >&2
echo "$response" | jq . >&2
# Log failure
local args_json result_json
args_json=$(jq -n \
--arg title "$title" \
--arg details "$details" \
--arg account_name "${account_name:-}" \
--arg location_name "${location_name:-}" \
--argjson account_id "$account_id" \
--argjson location_id "$location_id" \
--argjson status_id "$status_id" \
--argjson priority_id "$priority_id" \
--argjson type_id "$type_id" \
--argjson source_id "$source_id" \
--argjson queue_id "${queue_id:-null}" \
--argjson assignee_id "${assignee_id:-null}" \
'{title: $title, details: $details, account_name: $account_name, location_name: $location_name, account_id: $account_id, location_id: $location_id, status_id: $status_id, priority_id: $priority_id, type_id: $type_id, source_id: $source_id, queue_id: $queue_id, assignee_id: $assignee_id}')
result_json=$(jq -n '{error: "creation_failed", response: ("$response" | fromjson? // "$response")}')
log_action "tickets.create" "$args_json" "$result_json" "error"
exit 1
fi
echo "Created ticket ID: ${ticket_id}${ticket_number:-N/A}"
# Log success
local args_json result_json
args_json=$(jq -n \
--arg title "$title" \
--arg details "$details" \
--arg account_name "${account_name:-}" \
--arg location_name "${location_name:-}" \
--argjson account_id "$account_id" \
--argjson location_id "$location_id" \
--argjson status_id "$status_id" \
--argjson priority_id "$priority_id" \
--argjson type_id "$type_id" \
--argjson source_id "$source_id" \
--argjson queue_id "${queue_id:-null}" \
--argjson assignee_id "${assignee_id:-null}" \
'{title: $title, details: $details, account_name: $account_name, location_name: $location_name, account_id: $account_id, location_id: $location_id, status_id: $status_id, priority_id: $priority_id, type_id: $type_id, source_id: $source_id, queue_id: $queue_id, assignee_id: $assignee_id}')
result_json=$(jq -n --argjson tid "$ticket_id" --arg tn "${ticket_number:-}" '{ticket_id: $tid, ticket_number: $tn}')
log_action "tickets.create" "$args_json" "$result_json" "success"
}
cmd_update() {
local ticket_id="${1:-}"
[[ -n "$ticket_id" ]] || die "Usage: bms tickets update <ticketId> [options]"
shift
# Fetch current ticket first so we can do a full PUT with changes merged
local current
current=$(bms_curl GET "/v2/servicedesk/tickets/${ticket_id}" | jq '.Data // .result // .')
local patch="{}"
while [[ $# -gt 0 ]]; do
case "$1" in
--title) patch=$(echo "$patch" | jq --arg v "$2" '. + {Title: $v}'); shift 2 ;;
--details) patch=$(echo "$patch" | jq --arg v "$2" '. + {Details: $v}'); shift 2 ;;
--status-id) patch=$(echo "$patch" | jq --argjson v "$2" '. + {StatusId: $v}'); shift 2 ;;
--priority-id) patch=$(echo "$patch" | jq --argjson v "$2" '. + {PriorityId: $v}'); shift 2 ;;
--assignee-id) patch=$(echo "$patch" | jq --argjson v "$2" '. + {AssigneeId: $v}'); shift 2 ;;
--queue-id) patch=$(echo "$patch" | jq --argjson v "$2" '. + {QueueId: $v}'); shift 2 ;;
--due-date) patch=$(echo "$patch" | jq --arg v "$2" '. + {DueDate: $v}'); shift 2 ;;
*) die "Unknown option: $1" ;;
esac
done
# Merge patch onto current (keep required fields from current ticket)
local body
body=$(echo "$current" | jq \
--argjson patch "$patch" \
'{
Title: .Title,
Details: .Details,
AccountId: .AccountId,
LocationId: .LocationId,
StatusId: .StatusId,
PriorityId: .PriorityId,
TypeId: .TypeId,
SourceId: .SourceId,
OpenDate: .OpenDate,
AssigneeId: .AssigneeId,
QueueId: .QueueId,
ContactId: .ContactId
} * $patch')
local response
response=$(bms_curl PUT "/v2/servicedesk/tickets/${ticket_id}" -d "$body")
echo "$response" | jq -r '"Updated ticket \(.Data.Id // .Id // .result.id // "'"$ticket_id"'")"'
# Log success
local args_json result_json
args_json=$(jq -n --argjson ticket_id "$ticket_id" --argjson patch "$patch" '{ticket_id: $ticket_id, patch: $patch}')
result_json=$(jq -n --argjson tid "$ticket_id" '{ticket_id: $tid}')
log_action "tickets.update" "$args_json" "$result_json" "success"
}
cmd_note() {
local ticket_id="${1:-}"
[[ -n "$ticket_id" ]] || die "Usage: bms tickets note <ticketId> --message <text> [options]"
shift
local message="" is_internal=false type_id=1 status_id="" note_date=""
while [[ $# -gt 0 ]]; do
case "$1" in
--message|-m) message="$2"; shift 2 ;;
--internal) is_internal=true; shift ;;
--type-id) type_id="$2"; shift 2 ;;
--status-id) status_id="$2"; shift 2 ;;
*) die "Unknown option: $1" ;;
esac
done
[[ -n "$message" ]] || die "--message is required"
note_date=$(date -u +%Y-%m-%dT%H:%M:%S)
local body
body=$(jq -n \
--arg details "$message" \
--argjson is_internal "$is_internal" \
--argjson type_id "$type_id" \
--arg note_date "$note_date" \
'{
Details: $details,
IsInternal: $is_internal,
TypeId: $type_id,
NoteDate: $note_date
}')
[[ -n "$status_id" ]] && body=$(echo "$body" | jq --argjson v "$status_id" '. + {StatusId: $v}')
local response
response=$(bms_curl POST "/v2/servicedesk/tickets/${ticket_id}/notes" -d "$body")
local note_id
note_id=$(echo "$response" | jq -r '.Data.Id // .Id // .result.id // .result.id // empty')
if [[ -z "$note_id" || "$note_id" == "null" ]]; then
echo "Note add failed or returned ambiguous response:" >&2
echo "$response" | jq . >&2
# Log failure
local args_json result_json
args_json=$(jq -n --argjson ticket_id "$ticket_id" --arg message "$message" --argjson type_id "$type_id" '{ticket_id: $ticket_id, message: $message, type_id: $type_id}')
result_json=$(jq -n '{error: "note_add_failed", response: ("$response" | fromjson? // "$response")}')
log_action "tickets.note" "$args_json" "$result_json" "error"
exit 1
fi
echo "Note added (ID: ${note_id})"
# Log success
local args_json result_json
args_json=$(jq -n --argjson ticket_id "$ticket_id" --arg message "$message" --argjson type_id "$type_id" '{ticket_id: $ticket_id, message: $message, type_id: $type_id}')
result_json=$(jq -n --argjson note_id "$note_id" '{note_id: $note_id}')
log_action "tickets.note" "$args_json" "$result_json" "success"
}
cmd_assign() {
local ticket_id="${1:-}"
[[ -n "$ticket_id" ]] || die "Usage: bms tickets assign <ticketId> [options]"
shift
local assignee_id="" queue_id="" note="" is_internal=false type_id=1 status_id=""
while [[ $# -gt 0 ]]; do
case "$1" in
--assignee-id) assignee_id="$2"; shift 2 ;;
--queue-id) queue_id="$2"; shift 2 ;;
--note|-n) note="$2"; shift 2 ;;
--internal) is_internal=true; shift ;;
--type-id) type_id="$2"; shift 2 ;;
--status-id) status_id="$2"; shift 2 ;;
*) die "Unknown option: $1" ;;
esac
done
[[ -n "$note" ]] || note="Ticket reassigned."
local note_date
note_date=$(date -u +%Y-%m-%dT%H:%M:%S)
local body
body=$(jq -n \
--arg details "$note" \
--argjson is_internal "$is_internal" \
--argjson type_id "$type_id" \
--arg note_date "$note_date" \
'{
Details: $details,
IsInternal: $is_internal,
TypeId: $type_id,
NoteDate: $note_date
}')
[[ -n "$status_id" ]] && body=$(echo "$body" | jq --argjson v "$status_id" '. + {StatusId: $v}')
[[ -n "$assignee_id" ]] && body=$(echo "$body" | jq --argjson v "$assignee_id" '. + {AssigneeId: $v}')
[[ -n "$queue_id" ]] && body=$(echo "$body" | jq --argjson v "$queue_id" '. + {QueueId: $v}')
local response
response=$(bms_curl POST "/v2/servicedesk/tickets/${ticket_id}/assignticket" -d "$body")
echo "$response" | jq -r '"Ticket \("'"$ticket_id"'") assigned."'
# Log success
local args_json result_json
args_json=$(jq -n --argjson ticket_id "$ticket_id" --argjson assignee_id "${assignee_id:-null}" --argjson queue_id "${queue_id:-null}" '{ticket_id: $ticket_id, assignee_id: $assignee_id, queue_id: $queue_id}')
result_json=$(jq -n --argjson tid "$ticket_id" '{ticket_id: $tid}')
log_action "tickets.assign" "$args_json" "$result_json" "success"
}
cmd_resolve() {
local ticket_id="${1:-}"
[[ -n "$ticket_id" ]] || die "Usage: bms tickets resolve <ticketId> --comment <text> [options]"
shift
local comment="" status_id="" publish_kb=false is_internal=false
while [[ $# -gt 0 ]]; do
case "$1" in
--comment|-c) comment="$2"; shift 2 ;;
--status-id) status_id="$2"; shift 2 ;;
--publish-kb) publish_kb=true; shift ;;
--internal) is_internal=true; shift ;;
*) die "Unknown option: $1" ;;
esac
done
[[ -n "$comment" ]] || die "--comment is required"
local body
body=$(jq -n \
--arg comment "$comment" \
--argjson publish_kb "$publish_kb" \
--argjson is_internal "$is_internal" \
'{Comment: $comment, IsPublishToKnowledgeBase: $publish_kb, IsInternal: $is_internal}')
[[ -n "$status_id" ]] && body=$(echo "$body" | jq --argjson v "$status_id" '. + {StatusId: $v}')
local response
response=$(bms_curl POST "/v2/servicedesk/tickets/${ticket_id}/resolve" -d "$body")
echo "$response" | jq -r '"Ticket \("'"$ticket_id"'") resolved."'
# Log success
local args_json result_json
args_json=$(jq -n --argjson ticket_id "$ticket_id" --argjson comment "$comment" '{ticket_id: $ticket_id, comment: $comment}')
result_json=$(jq -n --argjson tid "$ticket_id" '{ticket_id: $tid}')
log_action "tickets.resolve" "$args_json" "$result_json" "success"
}
cmd_delete() {
local ids=("$@")
[[ ${#ids[@]} -gt 0 ]] || die "Usage: bms tickets delete <id> [id2 ...]"
local response=""
if [[ ${#ids[@]} -eq 1 ]]; then
response=$(bms_curl DELETE "/v2/servicedesk/tickets/${ids[0]}")
echo "Deleted ticket ${ids[0]}"
else
local body
body=$(printf '%s\n' "${ids[@]}" | jq -Rs 'split("\n") | map(select(. != "")) | map(tonumber) | {Ids: .}')
response=$(bms_curl DELETE "/v2/servicedesk/tickets" -d "$body")
echo "Deleted tickets: ${ids[*]}"
fi
# Check success (DELETE often returns { success: true } or empty)
local success
success=$(echo "$response" | jq -r '.success // .Success // ""')
if [[ "$success" != "true" ]] && [[ -n "$response" ]] && echo "$response" | jq -e . >/dev/null 2>&1; then
# If response is JSON but not success=true, treat as failure
echo "Delete operation may have failed:" >&2
echo "$response" | jq . >&2
local args_json result_json
args_json=$(jq -n --argjson ids "${ids}" '{ids: $ids}')
result_json=$(jq -n '{error: "delete_failed", response: ("$response" | fromjson? // "$response")}')
log_action "tickets.delete" "$args_json" "$result_json" "error"
exit 1
fi
# Log success
local args_json result_json
args_json=$(jq -n --argjson ids "${ids}" '{ids: $ids}')
result_json=$(jq -n --argjson deleted_ids "${ids}" '{deleted_ids: $deleted_ids}')
log_action "tickets.delete" "$args_json" "$result_json" "success"
}
# ─── Dispatch ────────────────────────────────────────────────────────────────
subcmd="${1:-list}"
[[ $# -gt 0 ]] && shift
case "$subcmd" in
list) cmd_list "$@" ;;
get) cmd_get "$@" ;;
create) cmd_create "$@" ;;
update) cmd_update "$@" ;;
note) cmd_note "$@" ;;
assign) cmd_assign "$@" ;;
resolve) cmd_resolve "$@" ;;
delete) cmd_delete "$@" ;;
*)
echo "Usage: bms tickets <list|get|create|update|note|assign|resolve|delete>" >&2
exit 1
;;
esac
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
export PYTHONPATH="${REPO_ROOT}/src${PYTHONPATH:+:$PYTHONPATH}"
exec python3 -m openclaw_bms tickets "$@"