Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ flashduty config set base_url URL # Override API endpoint

| Flag | Description |
|------|-------------|
| `--json` | Output as JSON instead of table |
| `--output-format` | Output format: `table` (default), `json`, or `toon` (compact, fewer tokens) |
| `--json` | Output as JSON (alias for `--output-format json`) |
| `--no-trunc` | Do not truncate long fields in table output |
| `--base-url` | Override the API base URL |

Expand Down Expand Up @@ -272,12 +273,18 @@ inc_def456 High memory usage Warning Processing Staging 2026
Showing 2 results (page 1, total 2).
```

**JSON (`--json`):** Machine-parseable, full data, no truncation.
**JSON (`--json` / `--output-format json`):** Machine-parseable, full data, no truncation.

```bash
flashduty incident list --json | jq '.[].title'
```

**TOON (`--output-format toon`):** Token-Oriented Object Notation — full data, no truncation, but drops the per-row repeated keys that JSON emits for uniform arrays, so list output costs materially fewer tokens. Preferred for LLM/agent consumption. Not directly `jq`-able; use `--json` when you need to pipe into `jq`.

```bash
flashduty incident list --output-format toon
```

**No truncation (`--no-trunc`):** Table with full field content.

---
Expand Down
120 changes: 120 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,122 @@ resolve_version() {
echo "${version}"
}

# --- shell completion (best-effort, non-intrusive) ---

# The user's interactive shell decides which completion script we need —
# completion varies by shell, not by OS/arch. Empty when it's not one we support.
detect_shell() {
case "$(basename "${SHELL:-}" 2>/dev/null)" in
bash) echo "bash" ;;
zsh) echo "zsh" ;;
fish) echo "fish" ;;
*) echo "" ;;
esac
}

# Emit the completion script for the shell named in $1. Cobra bakes the root
# command name "flashduty" into the script (#compdef / complete -c / function
# names); when installed under a different name, rewrite every occurrence so the
# completion binds to the actual command (the runtime dispatch line already uses
# the typed command word, so it needs no rewrite). The `|` sed delimiter is safe
# because a binary name can't contain it, and the rewrite is a no-op for the
# default "flashduty".
gen_completion() {
"${BIN}" completion "$1" | sed "s|flashduty|${INSTALLED_NAME}|g"
}

# Install completion for the current shell into a directory the shell already
# auto-loads, without ever editing the user's rc files. zsh has no guaranteed
# writable fpath dir, so it only succeeds when a standard site-functions dir is
# already writable (e.g. a Homebrew install); otherwise we point at the binary's
# own per-shell setup instructions.
setup_completion() {
[ "${OS}" = "Windows" ] && return 0
sh_name=$(detect_shell)
[ -z "${sh_name}" ] && return 0
"${BIN}" completion "${sh_name}" >/dev/null 2>&1 || return 0

case "${sh_name}" in
fish)
dir="${XDG_CONFIG_HOME:-${HOME}/.config}/fish/completions"
mkdir -p "${dir}" 2>/dev/null || true
if [ -w "${dir}" ]; then
gen_completion fish > "${dir}/${INSTALLED_NAME}.fish" && {
info "Installed fish completion to ${dir}/${INSTALLED_NAME}.fish (restart fish to load)"
return 0
}
fi
;;
bash)
dir="${XDG_DATA_HOME:-${HOME}/.local/share}/bash-completion/completions"
mkdir -p "${dir}" 2>/dev/null || true
if [ -w "${dir}" ]; then
gen_completion bash > "${dir}/${INSTALLED_NAME}" && {
info "Installed bash completion to ${dir}/${INSTALLED_NAME} (needs the bash-completion package; restart bash to load)"
return 0
}
fi
;;
zsh)
for dir in \
"${HOMEBREW_PREFIX:-/opt/homebrew}/share/zsh/site-functions" \
"/usr/local/share/zsh/site-functions" \
"/usr/share/zsh/site-functions"; do
if [ -d "${dir}" ] && [ -w "${dir}" ]; then
gen_completion zsh > "${dir}/_${INSTALLED_NAME}" && {
info "Installed zsh completion to ${dir}/_${INSTALLED_NAME}"
info " Run 'rm -f ~/.zcompdump*' and restart zsh to load."
return 0
}
fi
done
;;
esac

# Couldn't auto-install into an auto-loaded dir (the common zsh case: no
# writable fpath dir, and we never edit ~/.zshrc). Print the exact,
# copy-pasteable steps so the user can finish setup in one go.
print_manual_completion "${sh_name}"
}

# Print a concrete, copy-pasteable recipe to enable completion for $1, used when
# setup_completion can't drop the script into an auto-loaded directory. Plain
# stdout (no "[flashduty]" prefix) so the commands paste cleanly.
print_manual_completion() {
name="${INSTALLED_NAME}"
info "Shell completion was not auto-installed. To enable it for $1, run:"
case "$1" in
zsh)
cat <<EOF

mkdir -p ~/.zsh/completions
${name} completion zsh > ~/.zsh/completions/_${name}
echo 'fpath=(~/.zsh/completions \$fpath)' >> ~/.zshrc # one-time
rm -f ~/.zcompdump* && exec zsh

EOF
;;
bash)
cat <<EOF

mkdir -p ~/.local/share/bash-completion/completions
${name} completion bash > ~/.local/share/bash-completion/completions/${name}
# requires the bash-completion package; then restart bash

EOF
;;
fish)
cat <<EOF

mkdir -p ~/.config/fish/completions
${name} completion fish > ~/.config/fish/completions/${name}.fish
# then restart fish

EOF
;;
esac
}

# --- main ---

main() {
Expand Down Expand Up @@ -201,6 +317,10 @@ main() {
info " export PATH=\"${INSTALL_DIR}:\$PATH\"" ;;
esac

# Best-effort shell completion; never fail the install over it.
BIN="${INSTALL_DIR}/${INSTALLED_NAME}"
setup_completion || true

info "Run '${INSTALLED_NAME} version' to verify"
}

Expand Down
1 change: 1 addition & 0 deletions internal/cli/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ func newAlertListCmd() *cobra.Command {

cmd.Flags().StringVar(&severity, "severity", "", "Filter: Critical,Warning,Info")
cmd.Flags().BoolVar(&active, "active", false, "Show active only")
registerEnumFlag(cmd, "severity", severityEnum...)
cmd.Flags().BoolVar(&recovered, "recovered", false, "Show recovered only")
cmd.Flags().StringVar(&channel, "channel", "", "Comma-separated channel IDs")
cmd.Flags().BoolVar(&muted, "muted", false, "Show ever-muted only")
Expand Down
1 change: 1 addition & 0 deletions internal/cli/alert_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func newAlertEventListCmd() *cobra.Command {

cmd.Flags().StringVar(&severity, "severity", "", "Filter: Critical,Warning,Info (comma-separated)")
cmd.Flags().StringVar(&channel, "channel", "", "Comma-separated channel IDs")
registerEnumFlag(cmd, "severity", severityEnum...)
cmd.Flags().StringVar(&integrationType, "integration-type", "", "Comma-separated integration types")
cmd.Flags().StringVar(&since, "since", "1h", "Start time")
cmd.Flags().StringVar(&until, "until", "now", "End time")
Expand Down
16 changes: 16 additions & 0 deletions internal/cli/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package cli

import "github.com/spf13/cobra"

// severityEnum is the closed set of incident/alert severities, shared by every
// --severity flag.
var severityEnum = []string{"Critical", "Warning", "Info"}

// registerEnumFlag makes <flag>'s value tab-complete to a fixed set and
// suppresses the default filename completion. The error only fires on an
// unknown flag name (a programmer error), so it is ignored.
func registerEnumFlag(cmd *cobra.Command, flag string, values ...string) {
_ = cmd.RegisterFlagCompletionFunc(flag, func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
return values, cobra.ShellCompDirectiveNoFileComp
})
}
4 changes: 4 additions & 0 deletions internal/cli/incident.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ func newIncidentListCmd() *cobra.Command {

cmd.Flags().StringVar(&progress, "progress", "", "Filter: Triggered,Processing,Closed")
cmd.Flags().StringVar(&severity, "severity", "", "Filter: Critical,Warning,Info")
registerEnumFlag(cmd, "progress", "Triggered", "Processing", "Closed")
registerEnumFlag(cmd, "severity", severityEnum...)
cmd.Flags().Int64Var(&channelID, "channel", 0, "Filter by channel ID")
cmd.Flags().StringVar(&query, "query", "", "Free-text search across title/labels/content (also resolves a 24-char incident ID or 6-char incident num to a direct lookup)")
cmd.Flags().StringVar(&nums, "nums", "", "Comma-separated short incident ids (num, the 6-char id shown in the UI) to filter by")
Expand Down Expand Up @@ -338,6 +340,7 @@ func newIncidentCreateCmd() *cobra.Command {
cmd.Flags().StringVar(&title, "title", "", "Incident title (required, 3-200 chars)")
cmd.Flags().StringVar(&severity, "severity", "", "Severity: Critical, Warning, Info (required)")
cmd.Flags().Int64Var(&channelID, "channel", 0, "Channel ID")
registerEnumFlag(cmd, "severity", severityEnum...)
cmd.Flags().StringVar(&description, "description", "", "Description (max 6144 chars)")
cmd.Flags().IntSliceVar(&assign, "assign", nil, "Person IDs to assign (use 'flashduty member list' to look up IDs)")

Expand Down Expand Up @@ -429,6 +432,7 @@ func newIncidentUpdateCmd() *cobra.Command {
cmd.Flags().StringVar(&description, "description", "", "New description")
cmd.Flags().StringVar(&severity, "severity", "", "New severity: Critical, Warning, Info")
cmd.Flags().StringArrayVar(&fieldFlags, "field", nil, "Custom field: key=value (repeatable)")
registerEnumFlag(cmd, "severity", severityEnum...)

return cmd
}
Expand Down
2 changes: 2 additions & 0 deletions internal/cli/monit_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func newMonitQueryDiagnoseCmd() *cobra.Command {

cmd.Flags().StringVar(&dsType, "ds-type", "", "Datasource type: prometheus|victorialogs|loki|mysql (required)")
cmd.Flags().StringVar(&dsName, "ds-name", "", "Datasource name as configured (required)")
registerEnumFlag(cmd, "ds-type", "prometheus", "victorialogs", "loki", "mysql")
cmd.Flags().StringVar(&timeStart, "time-start", "15m", "Window start (relative '15m'/'1h', unix seconds, or 'now')")
cmd.Flags().StringVar(&timeEnd, "time-end", "now", "Window end (relative, unix seconds, or 'now'; span capped at 6h)")
cmd.Flags().StringVar(&inputQuery, "input-query", "", "Filter-only log query OR matrix PromQL (required)")
Expand Down Expand Up @@ -134,6 +135,7 @@ func newMonitQueryRowsCmd() *cobra.Command {

cmd.Flags().StringVar(&dsType, "ds-type", "", "Datasource type (required)")
cmd.Flags().StringVar(&dsName, "ds-name", "", "Datasource name (required)")
registerEnumFlag(cmd, "ds-type", "prometheus", "victorialogs", "loki", "mysql")
cmd.Flags().StringVar(&expr, "expr", "", "Query expression (required)")
cmd.Flags().StringSliceVar(&argsKV, "args", nil, "Arg entries KEY=VALUE (repeatable; values must be strings per monit-query contract)")

Expand Down
1 change: 1 addition & 0 deletions internal/cli/postmortem.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func newPostmortemListCmd() *cobra.Command {

cmd.Flags().StringVar(&status, "status", "", "Filter: drafting or published")
cmd.Flags().StringVar(&channel, "channel", "", "Comma-separated channel IDs")
registerEnumFlag(cmd, "status", "drafting", "published")
cmd.Flags().StringVar(&team, "team", "", "Comma-separated team IDs")
cmd.Flags().StringVar(&since, "since", "", "Created after (time filter)")
cmd.Flags().StringVar(&until, "until", "", "Created before (time filter)")
Expand Down
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func init() {
rootCmd.PersistentFlags().StringVar(&flagAppKey, "app-key", "", "Override app key")
rootCmd.PersistentFlags().StringVar(&flagBaseURL, "base-url", "", "Override base URL")
_ = rootCmd.PersistentFlags().MarkHidden("app-key")
registerEnumFlag(rootCmd, "output-format", "table", "json", "toon")

rootCmd.AddCommand(newVersionCmd())
rootCmd.AddCommand(newLoginCmd())
Expand Down
1 change: 1 addition & 0 deletions internal/cli/status_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func newStatusPageChangesCmd() *cobra.Command {

cmd.Flags().Int64Var(&pageID, "page-id", 0, "Page ID (required)")
cmd.Flags().StringVar(&changeType, "type", "", "Change type: incident or maintenance (required)")
registerEnumFlag(cmd, "type", "incident", "maintenance")
_ = cmd.MarkFlagRequired("page-id")
_ = cmd.MarkFlagRequired("type")

Expand Down
1 change: 1 addition & 0 deletions internal/cli/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ func newTemplateFunctionsCmd() *cobra.Command {
}

cmd.Flags().StringVar(&funcType, "type", "all", "Filter: custom, sprig, or all")
registerEnumFlag(cmd, "type", "custom", "sprig", "all")

return cmd
}
11 changes: 8 additions & 3 deletions skills/flashduty-shared/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: flashduty-shared
version: 1.0.0
description: "Flashduty CLI foundation: authentication (login, app_key, config), the 3-layer noise reduction model (Alert Event to Alert to Incident), global flags (--json, --no-trunc), output modes (table, JSON, vertical detail), pagination (--limit, --page), time parsing (relative, absolute, unix, future durations), reference data lookups (member, team, channel, field, escalation-rule), and safety rules. Prerequisite for all other flashduty-* skills. Use when setting up flashduty-cli, encountering auth errors, looking up IDs, or needing to understand the Flashduty data model."
description: "Flashduty CLI foundation: authentication (login, app_key, config), the 3-layer noise reduction model (Alert Event to Alert to Incident), global flags (--output-format, --json, --no-trunc), output modes (table, JSON, TOON, vertical detail), pagination (--limit, --page), time parsing (relative, absolute, unix, future durations), reference data lookups (member, team, channel, field, escalation-rule), and safety rules. Prerequisite for all other flashduty-* skills. Use when setting up flashduty-cli, encountering auth errors, looking up IDs, or needing to understand the Flashduty data model."
metadata:
requires:
bins: ["flashduty"]
Expand Down Expand Up @@ -107,7 +107,8 @@ These flags are available on **every** command via Cobra `PersistentFlags`:

| Flag | Type | Default | Effect |
|------|------|---------|--------|
| `--json` | bool | `false` | Output as JSON instead of table |
| `--output-format` | string | `table` | Output format: `table`, `json`, or `toon` (compact, fewer tokens) |
| `--json` | bool | `false` | Output as JSON (alias for `--output-format json`) |
| `--no-trunc` | bool | `false` | Do not truncate long values in table output |
| `--app-key` | string | `""` | Override app key (hidden flag) |
| `--base-url` | string | `""` | Override base URL |
Expand All @@ -124,10 +125,14 @@ Human-readable aligned columns. Long values are truncated with `...` unless `--n
Showing 20 results (page 1, total 142).
```

### JSON (`--json`)
### JSON (`--json` / `--output-format json`)

Machine-readable full output. No truncation. Suitable for piping to `jq`. Success messages are wrapped as `{"message": "..."}`.

### TOON (`--output-format toon`)

Token-Oriented Object Notation — machine-readable full output, no truncation, encoded via the same path as the Flashduty MCP server. For uniform arrays (list commands) it emits the field keys once as a header instead of repeating them on every row, so list output costs materially fewer tokens than JSON. **Preferred when an LLM/agent reads the output.** Not directly `jq`-able — use `--json` when you need `jq` field selection.

### Vertical Detail

Used automatically for single-item lookups (e.g., `flashduty incident get <id>` with one ID). Displays key-value pairs vertically instead of a table row.
Expand Down
Loading