From ea4d919c50de9e377c5b7a467c397110c989ae0c Mon Sep 17 00:00:00 2001 From: ysyneu Date: Thu, 8 Oct 2026 05:06:17 -0700 Subject: [PATCH 1/2] feat: name the CLI after the command word it was invoked as The root command now takes its name from os.Args[0], so a copy installed under another name (install.sh INSTALLED_NAME) shows that name in help, examples, flag usages, errors, update notices and completion scripts. Help and error text stays authored with the canonical "flashduty" and is rewritten to the invoked name at startup; the default name runs the tree unchanged. Completion scripts are now generated by the installed binary under its own name, so install.sh no longer rewrites them with sed. The bundled skill and its scripts now call the CLI by its default name, flashduty, and help strings that used another name now use flashduty. --- cmd/flashduty/main.go | 10 +++- install.sh | 17 ++---- internal/cli/audit.go | 2 +- internal/cli/incident.go | 4 +- internal/cli/incident_summary_script_test.go | 14 ++--- internal/cli/incident_test.go | 2 +- internal/cli/oncall.go | 2 +- internal/cli/root.go | 54 ++++++++++++++++--- internal/cli/root_test.go | 51 ++++++++++++++++++ internal/cli/update.go | 2 +- internal/skilldoc/generate.go | 2 +- .../incident_comment_guidance_test.go | 6 +-- skills/flashduty/SKILL.md | 16 +++--- skills/flashduty/reference/alert.md | 28 +++++----- skills/flashduty/reference/automation.md | 26 ++++----- skills/flashduty/reference/calendar.md | 20 +++---- skills/flashduty/reference/change.md | 10 ++-- skills/flashduty/reference/channel.md | 14 ++--- skills/flashduty/reference/enrichment.md | 28 +++++----- skills/flashduty/reference/escalation.md | 20 +++---- skills/flashduty/reference/field.md | 14 ++--- skills/flashduty/reference/filters.md | 2 +- skills/flashduty/reference/incident.md | 48 ++++++++--------- skills/flashduty/reference/insight.md | 30 +++++------ skills/flashduty/reference/member.md | 36 ++++++------- .../flashduty/reference/monit-datasource.md | 12 ++--- skills/flashduty/reference/monit-probe.md | 4 +- skills/flashduty/reference/monit-query.md | 10 ++-- skills/flashduty/reference/monit-rule.md | 16 +++--- skills/flashduty/reference/monit.md | 6 +-- skills/flashduty/reference/noise.md | 12 ++--- skills/flashduty/reference/postmortem.md | 18 +++---- skills/flashduty/reference/role.md | 30 +++++------ skills/flashduty/reference/route.md | 20 +++---- skills/flashduty/reference/rum.md | 22 ++++---- skills/flashduty/reference/safari.md | 14 ++--- skills/flashduty/reference/schedule.md | 28 +++++----- skills/flashduty/reference/sourcemap.md | 10 ++-- skills/flashduty/reference/status-page.md | 18 +++---- skills/flashduty/reference/team.md | 22 ++++---- skills/flashduty/reference/template.md | 34 ++++++------ skills/flashduty/scripts/incident-summary.sh | 8 +-- 42 files changed, 414 insertions(+), 328 deletions(-) create mode 100644 internal/cli/root_test.go diff --git a/cmd/flashduty/main.go b/cmd/flashduty/main.go index 9a7711c..4e6c240 100644 --- a/cmd/flashduty/main.go +++ b/cmd/flashduty/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "path/filepath" "runtime/debug" "strings" @@ -29,7 +30,14 @@ func main() { date = "unknown" } cli.SetVersionInfo(version, commit, date) - if err := cli.Execute(); err != nil { + // The CLI names itself after the command word the user typed, so a copy + // installed under another name (install.sh INSTALLED_NAME) shows that name + // in help, errors and completion scripts. + name := strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe") + if name == "" || name == "." || name == string(filepath.Separator) { + name = "flashduty" + } + if err := cli.Execute(name); err != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", err) os.Exit(1) } diff --git a/install.sh b/install.sh index c3d1b6e..c08e323 100755 --- a/install.sh +++ b/install.sh @@ -134,17 +134,6 @@ detect_shell() { 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 @@ -161,7 +150,7 @@ setup_completion() { 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" && { + "${BIN}" completion fish > "${dir}/${INSTALLED_NAME}.fish" && { info "Installed fish completion to ${dir}/${INSTALLED_NAME}.fish (restart fish to load)" return 0 } @@ -171,7 +160,7 @@ setup_completion() { 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}" && { + "${BIN}" completion bash > "${dir}/${INSTALLED_NAME}" && { info "Installed bash completion to ${dir}/${INSTALLED_NAME} (needs the bash-completion package; restart bash to load)" return 0 } @@ -183,7 +172,7 @@ setup_completion() { "/usr/local/share/zsh/site-functions" \ "/usr/share/zsh/site-functions"; do if [ -d "${dir}" ] && [ -w "${dir}" ]; then - gen_completion zsh > "${dir}/_${INSTALLED_NAME}" && { + "${BIN}" 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 diff --git a/internal/cli/audit.go b/internal/cli/audit.go index 5c586b4..127846e 100644 --- a/internal/cli/audit.go +++ b/internal/cli/audit.go @@ -104,7 +104,7 @@ func newAuditSearchCmd() *cobra.Command { cmd.Flags().StringVar(&since, "since", "7d", "Start time") cmd.Flags().StringVar(&until, "until", "now", "End time") cmd.Flags().Int64Var(&person, "person", 0, "Filter by person ID") - cmd.Flags().StringVar(&operation, "operation", "", "Filter by exact operation name(s) from 'fduty audit operation-list' (e.g. monitRule:write:update); comma-separate to match several in one call. Prefixes do NOT match (\"monitRule\" returns nothing).") + cmd.Flags().StringVar(&operation, "operation", "", "Filter by exact operation name(s) from 'flashduty audit operation-list' (e.g. monitRule:write:update); comma-separate to match several in one call. Prefixes do NOT match (\"monitRule\" returns nothing).") cmd.Flags().IntVar(&limit, "limit", 20, "Max results (max 99)") cmd.Flags().IntVar(&page, "page", 1, "Page number") diff --git a/internal/cli/incident.go b/internal/cli/incident.go index 589ee0e..cd7851f 100644 --- a/internal/cli/incident.go +++ b/internal/cli/incident.go @@ -80,7 +80,7 @@ func newIncidentListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List incidents", - Long: curatedLong("List incidents matching the given filters. The --since/--until window must be < 31 days; --limit max is 100. In json/toon mode, rows default to the compact fields incident_id,num,title,incident_severity,progress,start_time,channel_id,detail_url; pass --fields to choose a different projection.\n\nSee also: fduty insight for aggregated metrics (MTTA, MTTR, noise reduction), fduty insight incident-list for metric-rich filtered incident rows, and fduty insight incident-export for CSV incident exports.", "Incidents", "List"), + Long: curatedLong("List incidents matching the given filters. The --since/--until window must be < 31 days; --limit max is 100. In json/toon mode, rows default to the compact fields incident_id,num,title,incident_severity,progress,start_time,channel_id,detail_url; pass --fields to choose a different projection.\n\nSee also: flashduty insight for aggregated metrics (MTTA, MTTR, noise reduction), flashduty insight incident-list for metric-rich filtered incident rows, and flashduty insight incident-export for CSV incident exports.", "Incidents", "List"), RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { startTime, err := timeutil.Parse(since) @@ -1004,7 +1004,7 @@ const maxIncidentVerifyConcurrency = 8 // symbol instead of pinning a copy of the sentence: reword this constant and // every caller (production and test) picks up the new wording automatically, // with nothing left to fall out of sync. -const commentVerificationGuidance = "The write API already reported success for the whole batch (a single POST covering every requested incident), and not finding a match here does not mean a comment is missing — this check can only confirm presence, never confirm absence. Do not write the comment again, for the full batch or for the incident(s) listed above alone: either risks a duplicate on a write that most likely already landed. Run `fduty incident timeline ` on the listed incident(s) to check by hand before deciding on anything further" +const commentVerificationGuidance = "The write API already reported success for the whole batch (a single POST covering every requested incident), and not finding a match here does not mean a comment is missing — this check can only confirm presence, never confirm absence. Do not write the comment again, for the full batch or for the incident(s) listed above alone: either risks a duplicate on a write that most likely already landed. Run `flashduty incident timeline ` on the listed incident(s) to check by hand before deciding on anything further" // commentVerificationNotFoundDetailFmt is the per-incident detail appended to // commentVerificationGuidance's problem list when the page walk completes diff --git a/internal/cli/incident_summary_script_test.go b/internal/cli/incident_summary_script_test.go index 6a15337..20f1589 100644 --- a/internal/cli/incident_summary_script_test.go +++ b/internal/cli/incident_summary_script_test.go @@ -19,12 +19,12 @@ func TestIncidentSummaryScriptCompactOutput(t *testing.T) { t.Fatalf("resolve repository root: %v", err) } script := filepath.Join(root, "skills", "flashduty", "scripts", "incident-summary.sh") - log := filepath.Join(t.TempDir(), "fduty.log") - bin := filepath.Join(t.TempDir(), "fduty") - if err := os.WriteFile(bin, []byte("#!/usr/bin/env bash\nprintf '%s\\n' \"$*\" >> \"$FDUTY_LOG\"\nprintf 'compact result\\n'\n"), 0o755); err != nil { - t.Fatalf("write fake fduty: %v", err) + log := filepath.Join(t.TempDir(), "flashduty.log") + bin := filepath.Join(t.TempDir(), "flashduty") + if err := os.WriteFile(bin, []byte("#!/usr/bin/env bash\nprintf '%s\\n' \"$*\" >> \"$FAKE_FLASHDUTY_LOG\"\nprintf 'compact result\\n'\n"), 0o755); err != nil { + t.Fatalf("write fake flashduty: %v", err) } - t.Setenv("FDUTY_LOG", log) + t.Setenv("FAKE_FLASHDUTY_LOG", log) t.Setenv("PATH", filepath.Dir(bin)+string(os.PathListSeparator)+os.Getenv("PATH")) output, err := exec.Command("bash", script, "inc-1").CombinedOutput() @@ -33,11 +33,11 @@ func TestIncidentSummaryScriptCompactOutput(t *testing.T) { } invocations, err := os.ReadFile(log) if err != nil { - t.Fatalf("read fake fduty log: %v", err) + t.Fatalf("read fake flashduty log: %v", err) } lines := strings.FieldsFunc(strings.TrimSpace(string(invocations)), func(r rune) bool { return r == '\n' }) if len(lines) != 7 { - t.Fatalf("fduty calls = %d, want 7 (six reads + the start_time probe):\n%s", len(lines), invocations) + t.Fatalf("flashduty calls = %d, want 7 (six reads + the start_time probe):\n%s", len(lines), invocations) } wantDetail := "incident detail inc-1 --fields incident_id,num,title,incident_severity,progress,ai_summary,root_cause,resolution,alert_cnt,start_time,channel_id,detail_url --output-format toon" if lines[0] != wantDetail { diff --git a/internal/cli/incident_test.go b/internal/cli/incident_test.go index e4eec5a..0c9f7b5 100644 --- a/internal/cli/incident_test.go +++ b/internal/cli/incident_test.go @@ -106,7 +106,7 @@ func TestCommandIncidentListHelpSurfacesInsightIncidentExport(t *testing.T) { if err != nil { t.Fatalf("incident list --help: %v", err) } - if !strings.Contains(out, "fduty insight incident-export") { + if !strings.Contains(out, "flashduty insight incident-export") { t.Fatalf("help output missing incident export discovery hint:\n%s", out) } } diff --git a/internal/cli/oncall.go b/internal/cli/oncall.go index 2f7a99e..fbcc7e2 100644 --- a/internal/cli/oncall.go +++ b/internal/cli/oncall.go @@ -33,7 +33,7 @@ func newOncallWhoCmd() *cobra.Command { cmd := &cobra.Command{ Use: "who", Short: "Show who is currently on call", - Long: curatedLong("Show who is currently on call across schedules within a time window, optionally filtered by team or schedule name. The table output already resolves person_ids to display names; when you have raw person_ids elsewhere, batch-resolve them with 'fduty person infos ...' (NOT by paginating 'fduty member list' — person_id and member_id are different id namespaces).", "Schedules", "List"), + Long: curatedLong("Show who is currently on call across schedules within a time window, optionally filtered by team or schedule name. The table output already resolves person_ids to display names; when you have raw person_ids elsewhere, batch-resolve them with 'flashduty person infos ...' (NOT by paginating 'flashduty member list' — person_id and member_id are different id namespaces).", "Schedules", "List"), RunE: func(cmd *cobra.Command, args []string) error { return runCommand(cmd, args, func(ctx *RunContext) error { startTime, err := timeutil.Parse(since) diff --git a/internal/cli/root.go b/internal/cli/root.go index 845289d..2ad094d 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -3,14 +3,17 @@ package cli import ( "context" "encoding/json" + "errors" "fmt" "io" "os" + "regexp" "strconv" "strings" "github.com/flashcatcloud/go-flashduty" "github.com/spf13/cobra" + "github.com/spf13/pflag" toon "github.com/toon-format/toon-go" "golang.org/x/term" @@ -57,7 +60,7 @@ var rootCmd = &cobra.Command{ } updateNotice = nil updateCheckWarning = "" - if cmd.CommandPath() == "flashduty update" { + if cmd.CommandPath() == cmd.Root().Name()+" update" { return nil } if update.IsManagedByRunner() { @@ -70,7 +73,7 @@ var rootCmd = &cobra.Command{ result, err := checkForUpdateAutoFn(versionStr) if err != nil { if update.IsTimeout(err) { - updateCheckWarning = "auto update check timeout, please run 'flashduty update --check' manually" + updateCheckWarning = "auto update check timeout, please run '" + cmd.Root().Name() + " update --check' manually" } else { updateNotice = update.StateHasUpdate(versionStr) } @@ -89,9 +92,9 @@ var rootCmd = &cobra.Command{ _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "\n%s\n", updateCheckWarning) } if updateNotice != nil { - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "\nA new version of flashduty is available: v%s -> %s\n", - update.StripV(updateNotice.CurrentVersion), updateNotice.LatestVersion) - _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "To update, run: flashduty update\n") + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "\nA new version of %s is available: v%s -> %s\n", + cmd.Root().Name(), update.StripV(updateNotice.CurrentVersion), updateNotice.LatestVersion) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "To update, run: %s update\n", cmd.Root().Name()) } }, } @@ -162,9 +165,44 @@ func init() { attachSafariSkillUpload(rootCmd) } -// Execute runs the root command. -func Execute() error { - return rootCmd.Execute() +// Execute runs the root command as name, the command word the binary was +// invoked as. Help and error text is authored with the canonical "flashduty"; +// under any other name the tree's text and the returned error are rewritten +// to it, and cobra derives completion scripts from the root's name. +func Execute(name string) error { + if name == rootCmd.Name() { + return rootCmd.Execute() + } + renameCommandText(rootCmd, name) + rootCmd.Use = name + if err := rootCmd.Execute(); err != nil { + return errors.New(renameCLI(err.Error(), name)) + } + return nil +} + +// renameCommandText rewrites the CLI name in cmd's help text and flag usages, +// recursively. +func renameCommandText(cmd *cobra.Command, name string) { + cmd.Short = renameCLI(cmd.Short, name) + cmd.Long = renameCLI(cmd.Long, name) + cmd.Example = renameCLI(cmd.Example, name) + rename := func(f *pflag.Flag) { f.Usage = renameCLI(f.Usage, name) } + cmd.Flags().VisitAll(rename) + cmd.PersistentFlags().VisitAll(rename) + for _, c := range cmd.Commands() { + renameCommandText(c, name) + } +} + +// cliWord matches "flashduty" used as a command word: followed by a space, and +// not part of a path, domain or longer identifier ("go-flashduty", +// "~/.flashduty"). +var cliWord = regexp.MustCompile(`(^|[^\w./~-])flashduty `) + +// renameCLI replaces every command-word "flashduty" in s with name. +func renameCLI(s, name string) string { + return cliWord.ReplaceAllString(s, "${1}"+name+" ") } // newClient creates a go-flashduty client using the current factory. diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go new file mode 100644 index 0000000..5938249 --- /dev/null +++ b/internal/cli/root_test.go @@ -0,0 +1,51 @@ +package cli + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestRenameCLI(t *testing.T) { + cases := []struct{ in, want string }{ + {"flashduty incident list", "fduty incident list"}, + {"run 'flashduty login' first", "run 'fduty login' first"}, + {"Run `flashduty incident timeline `", "Run `fduty incident timeline `"}, + {" flashduty team list\n flashduty team get 1", " fduty team list\n fduty team get 1"}, + {"cat rows.csv | flashduty enrichment upload", "cat rows.csv | fduty enrichment upload"}, + {"flashduty is managed by flashduty-runner", "fduty is managed by flashduty-runner"}, + // Not the command word: product name, paths, domains, identifiers. + {"Flashduty CLI", "Flashduty CLI"}, + {"go-flashduty client", "go-flashduty client"}, + {"~/.flashduty config", "~/.flashduty config"}, + {"skills/flashduty cards", "skills/flashduty cards"}, + {"flashduty.example.com", "flashduty.example.com"}, + {"flashduty", "flashduty"}, + } + for _, c := range cases { + if got := renameCLI(c.in, "fduty"); got != c.want { + t.Errorf("renameCLI(%q) = %q, want %q", c.in, got, c.want) + } + } +} + +func TestRenameCommandText(t *testing.T) { + root := &cobra.Command{Use: "flashduty", Long: "Run 'flashduty login'."} + child := &cobra.Command{Use: "list", Short: "List", Example: " flashduty team list"} + child.Flags().String("id", "", "from 'flashduty team list'") + root.PersistentFlags().String("x", "", "see 'flashduty login'") + root.AddCommand(child) + + renameCommandText(root, "fduty") + + for _, c := range []struct{ got, want string }{ + {root.Long, "Run 'fduty login'."}, + {child.Example, " fduty team list"}, + {child.Flags().Lookup("id").Usage, "from 'fduty team list'"}, + {root.PersistentFlags().Lookup("x").Usage, "see 'fduty login'"}, + } { + if c.got != c.want { + t.Errorf("got %q, want %q", c.got, c.want) + } + } +} diff --git a/internal/cli/update.go b/internal/cli/update.go index 9597540..7b0d20e 100644 --- a/internal/cli/update.go +++ b/internal/cli/update.go @@ -66,7 +66,7 @@ func runInstaller(cmd *cobra.Command) error { return fmt.Errorf("update failed: %w", err) } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nUpdate complete. Run 'flashduty version' to verify.\n") + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nUpdate complete. Run '%s version' to verify.\n", cmd.Root().Name()) return nil } diff --git a/internal/skilldoc/generate.go b/internal/skilldoc/generate.go index 713772a..20cf8d2 100644 --- a/internal/skilldoc/generate.go +++ b/internal/skilldoc/generate.go @@ -10,7 +10,7 @@ import ( // Fence markers. The generator owns ONLY the text between these; intent→verb // routing, worked examples, and gotchas are hand-written outside the fence. const ( - fenceStartFmt = "" + fenceStartFmt = "" fenceEndFmt = "" ) diff --git a/internal/skilldoc/incident_comment_guidance_test.go b/internal/skilldoc/incident_comment_guidance_test.go index 136cf25..f46db3d 100644 --- a/internal/skilldoc/incident_comment_guidance_test.go +++ b/internal/skilldoc/incident_comment_guidance_test.go @@ -17,7 +17,7 @@ func TestIncidentCardCommentWorkflow(t *testing.T) { body := string(card) quotedHeredoc := "cat > \"$COMMENT_FILE\" <<'FDUTY_COMMENT_7F3A9C2E_EOF'" - commentCommand := `fduty incident comment "$ID" --comment-file "$COMMENT_FILE"` + commentCommand := `flashduty incident comment "$ID" --comment-file "$COMMENT_FILE"` previous := -1 for _, requirement := range []string{quotedHeredoc, commentCommand} { @@ -63,7 +63,7 @@ func TestIncidentCommentFileHeredocPreservesMarkdown(t *testing.T) { } dir := t.TempDir() - script := `fduty() { + script := `flashduty() { # Mimic --comment-file: print the referenced file's bytes verbatim. cat "$5" } @@ -76,7 +76,7 @@ Use ` + "`kubectl get pod`" + ` to inspect the restart. COMMENT_EOF The follow-up is still pending. FDUTY_COMMENT_7F3A9C2E_EOF -fduty incident comment "$ID" --comment-file "$COMMENT_FILE" +flashduty incident comment "$ID" --comment-file "$COMMENT_FILE" ` output, err := exec.Command("bash", "-c", script).CombinedOutput() diff --git a/skills/flashduty/SKILL.md b/skills/flashduty/SKILL.md index fcf7daf..5b3c05b 100644 --- a/skills/flashduty/SKILL.md +++ b/skills/flashduty/SKILL.md @@ -1,20 +1,20 @@ --- name: flashduty version: "3.0" -description: "USE FIRST for Flashduty tasks — status pages, incidents, alerts, on-call, monitors, automations, RUM, sourcemaps, members. `fduty` CLI = the whole API. ALWAYS load this skill + read reference/.md for exact verbs & flags BEFORE running fduty. Don't guess or --help-dance." +description: "USE FIRST for Flashduty tasks — status pages, incidents, alerts, on-call, monitors, automations, RUM, sourcemaps, members. `flashduty` CLI = the whole API. ALWAYS load this skill + read reference/.md for exact verbs & flags BEFORE running flashduty. Don't guess or --help-dance." allowed-tools: bash, read hidden: true # internal-only: withheld from skills.sh public discovery (Safari embeds this skill directly). --- # Flashduty CLI -`fduty` is your interface to Flashduty — invoke it from `bash`. This SKILL.md is a **router**: shared model + conventions below, then a domain index. For a real task, **read the one `reference/.md` card first** — it carries every command, flag, enum, and the worked flow for that domain, so you operate without `--help` trial-and-error or guessing command names. +`flashduty` is your interface to Flashduty — invoke it from `bash`. This SKILL.md is a **router**: shared model + conventions below, then a domain index. For a real task, **read the one `reference/.md` card first** — it carries every command, flag, enum, and the worked flow for that domain, so you operate without `--help` trial-and-error or guessing command names. ## Auth & availability - **Auth.** Set your Flashduty app key once — `export FLASHDUTY_APP_KEY=` — or pass `--app-key ` per call. Then just call the verb. - **No curl for the API.** The CLI is the only supported path to Flashduty — never hand-roll an HTTP call. -- **If `fduty: command not found`** (rare — it is normally on PATH at startup): install from the Flashduty CDN into a user-writable dir (no sudo, no hang), then tell the user — don't work around it: `curl -sSL https://static.flashcat.cloud/flashduty-cli/install.sh | FLASHDUTY_INSTALL_DIR="$HOME/.local/bin" INSTALLED_NAME=fduty sh && export PATH="$HOME/.local/bin:$PATH"`. +- **If `flashduty: command not found`** (rare — it is normally on PATH at startup): install from the Flashduty CDN into a user-writable dir (no sudo, no hang), then tell the user — don't work around it: `curl -sSL https://static.flashcat.cloud/flashduty-cli/install.sh | FLASHDUTY_INSTALL_DIR="$HOME/.local/bin" sh && export PATH="$HOME/.local/bin:$PATH"`. - **If `jq` is missing** and you genuinely need JSON filtering, it is fine to install it into a user-writable dir the same way (`$HOME/.local/bin`) and continue. But first ask whether you can avoid `jq` entirely by using `--output-format toon`, `--fields`, a returned `total`, or a server-side aggregation verb (`insight`, `rule-counter-*`, etc.). ## Data model — 3 layers @@ -37,19 +37,19 @@ Append `--output-format toon` to read commands: it drops the per-row repeated ke ## Timestamps — convert before quoting -fduty renders timestamp fields (created_at / updated_at / start_time / …) as **RFC3339 strings in the process's local timezone** in `--json` and `toon` output; unset values are `null` in `--json` (the string `0` in `toon`). In the AI-SRE runner the process timezone is **Etc/UTC** (the env block's `Environment Timezone`), so those strings come out **UTC** — not the user's local time. The env block's `User Timezone` (Asia/Shanghai for zh-CN accounts) is the zone the user reads. +flashduty renders timestamp fields (created_at / updated_at / start_time / …) as **RFC3339 strings in the process's local timezone** in `--json` and `toon` output; unset values are `null` in `--json` (the string `0` in `toon`). In the AI-SRE runner the process timezone is **Etc/UTC** (the env block's `Environment Timezone`), so those strings come out **UTC** — not the user's local time. The env block's `User Timezone` (Asia/Shanghai for zh-CN accounts) is the zone the user reads. -**Rule: before quoting ANY fduty timestamp to the user, convert it to the user's timezone** (Asia/Shanghai for zh-CN accounts; the runner env block states `User Timezone`). Report the converted time and label it 北京时间/本地时间. Never present a raw UTC wall-clock as if it were the user's local time — e.g. a change logged at `2026-08-13T13:05:03Z` happened at **21:05 北京时间**, not 13:05. A timestamp you can't convert must be reported with its original offset and an explicit "(UTC)" tag. +**Rule: before quoting ANY flashduty timestamp to the user, convert it to the user's timezone** (Asia/Shanghai for zh-CN accounts; the runner env block states `User Timezone`). Report the converted time and label it 北京时间/本地时间. Never present a raw UTC wall-clock as if it were the user's local time — e.g. a change logged at `2026-08-13T13:05:03Z` happened at **21:05 北京时间**, not 13:05. A timestamp you can't convert must be reported with its original offset and an explicit "(UTC)" tag. ## Command names — don't guess, read the card -The hot path: **read the domain card** (index below) for the exact verb + flags. Command groups are hyphenated (`status-page`, `alert-event`), not concatenated (`statuspage`) — guessing the wrong form costs a failed call. For a command outside the cards, derive it from its API path: **group = first path segment, verb = the rest joined by `-`** (`POST /status-page/change/create` → `fduty status-page change-create`), then confirm with `fduty --help`. Pass nested-object / array fields as JSON via `--data '{...}'`; typed scalar flags override matching `--data` keys. +The hot path: **read the domain card** (index below) for the exact verb + flags. Command groups are hyphenated (`status-page`, `alert-event`), not concatenated (`statuspage`) — guessing the wrong form costs a failed call. For a command outside the cards, derive it from its API path: **group = first path segment, verb = the rest joined by `-`** (`POST /status-page/change/create` → `flashduty status-page change-create`), then confirm with `flashduty --help`. Pass nested-object / array fields as JSON via `--data '{...}'`; typed scalar flags override matching `--data` keys. **Positional arguments.** A card heading like `### change-create ` means that id can always be passed **positionally**, as the first bare argument (`change-create 5759… --type incident`). On generated/raw-passthrough commands the matching `--` flag (e.g. `--page-id`) is an equally valid alternative — if both are given, the flag wins. A few curated commands (`incident detail`, `automation get`) are positional-only, with no matching flag at all. A heading with no `<…>` takes all inputs as flags. `--help` settles any doubt. -## fduty answers directly — don't grep or browse +## flashduty answers directly — don't grep or browse -Configuration, permission-model, enrichment, monitor, and on-call questions are answered by `fduty` itself (the cards + the live commands). Do **not** grep external documentation or browse the web for something the CLI covers — that usually returns staler information than the live API. Read the card, run the verb. +Configuration, permission-model, enrichment, monitor, and on-call questions are answered by `flashduty` itself (the cards + the live commands). Do **not** grep external documentation or browse the web for something the CLI covers — that usually returns staler information than the live API. Read the card, run the verb. ## Safety — confirm before mutating diff --git a/skills/flashduty/reference/alert.md b/skills/flashduty/reference/alert.md index a00c144..bd808fb 100644 --- a/skills/flashduty/reference/alert.md +++ b/skills/flashduty/reference/alert.md @@ -1,4 +1,4 @@ -# fduty alert — command card +# flashduty alert — command card Prereq: `SKILL.md` read. Read verbs are free. `merge` is **irreversible** (alerts cannot be un-merged). `pipeline-upsert` **replaces** the full pipeline config. Confirm IDs before either. @@ -27,15 +27,15 @@ Prereq: `SKILL.md` read. Read verbs are free. `merge` is **irreversible** (alert ```bash # 1. list contributing alerts (from the incident domain) -fduty incident alerts --output-format toon +flashduty incident alerts --output-format toon # 2. inspect the worst alert -fduty alert get --output-format toon +flashduty alert get --output-format toon # 3. trace raw events deduplicated into that alert -fduty alert events --output-format toon +flashduty alert events --output-format toon # 4. view state transitions (mute/severity changes/operator actions) -fduty alert feed --output-format toon +flashduty alert feed --output-format toon # 5. for a time-window view across alerts, alert-event list is compact by default -fduty alert-event list --channel --since 1h --limit 30 --output-format toon +flashduty alert-event list --channel --since 1h --limit 30 --output-format toon ``` Structured `alert-event list` output stays below 16 KiB: when the requested page would overflow, only the leading rows that fit are emitted — every value intact — and a stderr note says how many of the rows were emitted, so heed it before assuming the page is complete (narrow `--fields` or lower `--limit` to fit more rows per page). A trailing `...` on a value, with a stderr note naming the clipped fields, appears only when one row alone exceeds the budget — heed it before matching on that value, because the clipped text is what a `jq` filter sees. In json/toon mode rows default to the compact projection `event_id,alert_id,event_severity,event_status,event_time,title` (a stderr note says so when it applies); any other response field is one `--fields` away — a key missing from the output means it wasn't selected, not that the server omits it. @@ -44,14 +44,14 @@ Structured `alert-event list` output stays below 16 KiB: when the requested page ```bash # 1. find active critical alerts in the last 4 hours -fduty alert list --severity Critical --active --since 4h --output-format toon +flashduty alert list --severity Critical --active --since 4h --output-format toon # 2. merge (IRREVERSIBLE) — alert IDs are POSITIONAL; --incident-id is a flag # comment text comes from a file, never an inline shell argument (see incident.md's comment workflow) printf '%s' 'Related disk alerts' > /tmp/merge-comment.txt -fduty alert merge --incident-id --comment-file /tmp/merge-comment.txt +flashduty alert merge --incident-id --comment-file /tmp/merge-comment.txt ``` - + ### event-list List events for an alert @@ -194,7 +194,7 @@ as `payments-api / disk_used`. Prefer the `[TPL]` form: it is explicit about where values come from and does not hard-code separators. **Labels the template reads must already exist.** Enrichment runs BEFORE the -pipeline, so labels produced by `fduty enrichment upsert` (extraction, +pipeline, so labels produced by `flashduty enrichment upsert` (extraction, composition, mapping) are available here — but a label produced by a LATER pipeline rule is not, and a typo just renders `` into the title. @@ -202,7 +202,7 @@ pipeline rule is not, and a typo just renders `` into the title. - **All alert verbs are positional except `list` and the two-ID `merge` flag.** Every verb with `` in its `use` form takes that ID as the first bare argument — do NOT pass `--alert-id`. The single exception: `merge` takes the first alert ID positionally AND requires `--incident-id` as a flag (two different IDs, different roles). - **`alert get` vs `alert info`, `alert events` vs `alert-event list`:** both pairs exist; prefer `get`/`events` (shorter, no extra flag); `info`/`event-list` accept `--alert-id` as a flag override for scripting. -- **No server-side title filter on `list`.** To search by title, use `--json` and pipe to `jq`: `fduty alert list --json | jq '.[] | select(.title | test("disk";"i"))'` +- **No server-side title filter on `list`.** To search by title, use `--json` and pipe to `jq`: `flashduty alert list --json | jq '.[] | select(.title | test("disk";"i"))'` - **`list`'s structured output has no `total`/page metadata** — its `--json`/`toon` response is a bare TOP-LEVEL array (see the `list` fence entry above), not a `{items, total}` wrapper. To count matches, project the narrowest field with `--fields` and count elements, or use a wrapper-style verb whose fence shows `total` (e.g. `list-by-ids`). Don't paginate page 1/2/3... just to count alerts — narrow the query instead (`--active`, `--recovered`, `--severity`, `--channel`, `--since`). - **Use `--fields` when hunting IDs, not full rows.** If the task is "find alert IDs / titles / channels / severities", project only those fields first, then drill into one alert with `get` / `events`. Dumping every field for 100 alerts wastes tokens and hides the one row you need. - **`list` time window cap is 31 days**; `--limit` max is 100. For broader queries use `insight` domain. @@ -213,7 +213,7 @@ pipeline rule is not, and a typo just renders `` into the title. ```bash # Find active Critical alerts in a specific channel and view the noisiest one -fduty alert list --severity Critical --active --channel 98765 --since 2h --output-format toon -fduty alert get --output-format toon -fduty alert events --output-format toon +flashduty alert list --severity Critical --active --channel 98765 --since 2h --output-format toon +flashduty alert get --output-format toon +flashduty alert events --output-format toon ``` diff --git a/skills/flashduty/reference/automation.md b/skills/flashduty/reference/automation.md index 747636e..f6f6003 100644 --- a/skills/flashduty/reference/automation.md +++ b/skills/flashduty/reference/automation.md @@ -1,4 +1,4 @@ -# fduty automation - command card +# flashduty automation - command card Prereq: `SKILL.md` read. Automations create AI SRE sessions on a schedule or through an HTTP POST trigger. `create`, `update`, `delete`, and `fire` mutate or start work. If the user directly asks for that action and provides enough detail, treat it as confirmation and do not ask again. @@ -43,7 +43,7 @@ Prereq: `SKILL.md` read. Automations create AI SRE sessions on a schedule or thr ## Hot flow - create from chat ```bash -fduty automation create \ +flashduty automation create \ --name "Daily SRE brief" \ --team-id \ --schedule daily \ @@ -57,7 +57,7 @@ If the user did not specify a team, omit `--team-id` for personal scope. If the ## Hot flow - create an HTTP POST trigger ```bash -fduty automation create \ +flashduty automation create \ --name "Webhook triage" \ --http-post-trigger \ --prompt-file ./automation-prompt.md \ @@ -67,13 +67,13 @@ fduty automation create \ The response can include `http_post_trigger_id`, `http_post_trigger_url`, and one-time `http_post_token`. Tell the user to store the token; it cannot be retrieved later. Rotate it with: ```bash -fduty automation update --rotate-http-post-token --output-format toon +flashduty automation update --rotate-http-post-token --output-format toon ``` ## Hot flow - exact cron ```bash -fduty automation create \ +flashduty automation create \ --name "Weekday 08:05 review" \ --cron-expr "5 8 * * 1-5" \ --prompt "Review open incidents and alert noise before the workday." \ @@ -83,19 +83,19 @@ fduty automation create \ ## Manage and inspect ```bash -fduty automation list --scope all --limit 20 --output-format toon -fduty automation get --output-format toon -fduty automation runs --since 7d --output-format toon +flashduty automation list --scope all --limit 20 --output-format toon +flashduty automation get --output-format toon +flashduty automation runs --since 7d --output-format toon -fduty automation update --disable --output-format toon -fduty automation update --enable --cron-expr "30 1 * * *" --output-format toon -fduty automation delete --force +flashduty automation update --disable --output-format toon +flashduty automation update --enable --cron-expr "30 1 * * *" --output-format toon +flashduty automation delete --force ``` ## Fire an HTTP POST trigger ```bash -fduty automation fire \ +flashduty automation fire \ --token "$FLASHDUTY_AUTOMATION_TRIGGER_TOKEN" \ --text "manual validation run" \ --output-format toon @@ -103,7 +103,7 @@ fduty automation fire \ The trigger API has no idempotency key: retry only when the failed call is known not to have reached the server. Do not invent a token. If it is missing, rotate the trigger token through `update` or ask the user to provide it through their secure shell/environment, not in chat. - + ### create Create an Automation diff --git a/skills/flashduty/reference/calendar.md b/skills/flashduty/reference/calendar.md index 9caee89..ced285b 100644 --- a/skills/flashduty/reference/calendar.md +++ b/skills/flashduty/reference/calendar.md @@ -1,4 +1,4 @@ -# fduty calendar — command card +# flashduty calendar — command card Prereq: `SKILL.md` read. **`delete` is irreversible** (calendar + all its events gone). `event-delete` is irreversible per event. Reads are free; confirm IDs before any delete. @@ -28,21 +28,21 @@ Public holiday calendars (e.g. `zh-cn.china.official`) are read-only — list th ```bash # 1. Find available public-holiday cal IDs for your locale -fduty calendar list --kind region.official.holiday --output-format toon +flashduty calendar list --kind region.official.holiday --output-format toon # 2. Create a personal calendar that inherits CN public holidays, Mon–Fri workdays -fduty calendar create --cal-name "Ops Workdays" \ +flashduty calendar create --cal-name "Ops Workdays" \ --timezone Asia/Shanghai \ --workdays 1,2,3,4,5 \ --extra-cal-ids zh-cn.china.official # → returns cal_id; save it # 3. Mark a make-up workday (補班) on a Saturday -fduty calendar event-upsert --summary "補班 (New Year)" \ +flashduty calendar event-upsert --summary "補班 (New Year)" \ --start-at 2026-01-17 --end-at 2026-01-18 --is-off false # 4. Mark a custom holiday (non-working) -fduty calendar event-upsert --summary "Team offsite" \ +flashduty calendar event-upsert --summary "Team offsite" \ --start-at 2026-03-20 --end-at 2026-03-22 --is-off true ``` @@ -50,13 +50,13 @@ fduty calendar event-upsert --summary "Team offsite" \ ```bash # List all events in January 2026 -fduty calendar event-list --year 2026 --month 1 --output-format toon +flashduty calendar event-list --year 2026 --month 1 --output-format toon # Delete a specific event (get event_id from the list above) -fduty calendar event-delete --cal-id --event-id +flashduty calendar event-delete --cal-id --event-id ``` - + ### create Create calendar @@ -129,7 +129,7 @@ Update calendar ## Gotchas -- **`cal-id` is POSITIONAL on `info`, `update`, `delete`, `event-list`, `event-upsert`** — pass it as the first bare argument: `fduty calendar info `. On `event-delete` both `--cal-id` and `--event-id` are flags (no positional — `use` is bare `event-delete`). +- **`cal-id` is POSITIONAL on `info`, `update`, `delete`, `event-list`, `event-upsert`** — pass it as the first bare argument: `flashduty calendar info `. On `event-delete` both `--cal-id` and `--event-id` are flags (no positional — `use` is bare `event-delete`). - **`event-upsert` creates OR updates** — omit `--event-id` to create; supply it to edit an existing event. The returned `event_id` is what to save for future edits or deletes. - **`list` defaults to `--kind personal`** — you will NOT see public-holiday calendars unless you pass `--kind region.official.holiday`. Add `--no-locale` to see all locales, not just yours. - **`delete` removes the calendar and ALL its events** — confirm `cal_id` with `list` first; irreversible. @@ -140,7 +140,7 @@ Update calendar Mark the Spring Festival week (2026) as non-working in a personal calendar: ```bash -fduty calendar event-upsert cal.abc123 \ +flashduty calendar event-upsert cal.abc123 \ --summary "Spring Festival" \ --start-at 2026-01-28 --end-at 2026-02-04 \ --is-off true \ diff --git a/skills/flashduty/reference/change.md b/skills/flashduty/reference/change.md index cad5043..ee398f7 100644 --- a/skills/flashduty/reference/change.md +++ b/skills/flashduty/reference/change.md @@ -1,4 +1,4 @@ -# fduty change — command card +# flashduty change — command card Prereq: `SKILL.md` read. One read-only verb. Change events are the "what changed" signal you correlate against an incident during fault analysis. @@ -16,13 +16,13 @@ Prereq: `SKILL.md` read. One read-only verb. Change events are the "what changed ```bash # Pull recent changes in the incident's window, then eyeball label/time overlap with the incident. -fduty change list --since 24h --output-format toon +flashduty change list --since 24h --output-format toon # Narrow by the integration or channel that emitted them, or by a keyword: -fduty change list --since 48h --integration --query "deploy" --output-format toon +flashduty change list --since 48h --integration --query "deploy" --output-format toon ``` - + ### list List changes @@ -52,5 +52,5 @@ List changes ```bash # Changes in the last 6h on a specific integration, newest first -fduty change list --since 6h --integration 5759613685214 --output-format toon +flashduty change list --since 6h --integration 5759613685214 --output-format toon ``` diff --git a/skills/flashduty/reference/channel.md b/skills/flashduty/reference/channel.md index 690f106..ca1e7a4 100644 --- a/skills/flashduty/reference/channel.md +++ b/skills/flashduty/reference/channel.md @@ -1,4 +1,4 @@ -# fduty channel — command card +# flashduty channel — command card Prereq: `SKILL.md` read. Read verbs are free; `create`, `update`, `delete`, `disable`, `enable` mutate state — confirm before acting. `delete` is **irreversible**. @@ -27,16 +27,16 @@ Rules INSIDE a channel have their own cards: escalation / 分派策略 → `refe ## Hot flow — create a channel ```bash -# 1. find owning team-id (from `fduty team list --output-format toon`) -fduty channel list --output-format toon +# 1. find owning team-id (from `flashduty team list --output-format toon`) +flashduty channel list --output-format toon # 2. create the channel (no positional; --channel-name and --team-id are required) -fduty channel create --channel-name "production-api" --team-id \ +flashduty channel create --channel-name "production-api" --team-id \ --auto-resolve-timeout 3600 --auto-resolve-mode trigger # → returns channel_id; next, add an escalation rule so incidents page someone: # see reference/escalation.md ``` - + ### create Create channel @@ -114,7 +114,7 @@ Update channel `severity` — or a label written with its `labels.` prefix.** A bare label name is rejected: `equals: [["service"]]` returns `equal service is not supported` (400). This is the most common way a grouping update fails, and it bites - hardest right after `fduty enrichment upsert` creates a new label — the label + hardest right after `flashduty enrichment upsert` creates a new label — the label exists, but it is `labels.` here, never the bare name. Escalation, silence, inhibit and unsubscribe rules live on their own cards: `reference/escalation.md` and `reference/noise.md`. @@ -123,5 +123,5 @@ Escalation, silence, inhibit and unsubscribe rules live on their own cards: `ref - **`channel-id` can be passed positionally or via `--channel-id` — both work** on every verb of this card (`info`, `infos`, `update`, `delete`, `disable`, `enable`). The fence heading `### verb ` shows the shorter positional form; the flag is an equally valid alternative, and if both are given the flag value wins. - **`channel create` requires `--channel-name` and `--team-id`** even though they are not marked `required` in the flag list — the server rejects the request without them. -- **`--plugin-ids` subscribes integrations that already exist; no `fduty` verb creates or configures one.** Creating an integration (email, webhook, an alert source) and reading its inbound address/token live in the console only — they are not part of the open API this CLI speaks, so they are absent from the command tree by design. When the user asks how to *connect* something, answer from the product documentation and point at the console; do not dump and grep the command tree looking for a verb that cannot exist. `channel info` likewise reports the channel's own fields, not integration credentials. +- **`--plugin-ids` subscribes integrations that already exist; no `flashduty` verb creates or configures one.** Creating an integration (email, webhook, an alert source) and reading its inbound address/token live in the console only — they are not part of the open API this CLI speaks, so they are absent from the command tree by design. When the user asks how to *connect* something, answer from the product documentation and point at the console; do not dump and grep the command tree looking for a verb that cannot exist. `channel info` likewise reports the channel's own fields, not integration credentials. - **`delete` on a channel is irreversible** — all rules within it (escalation, silence, inhibit, drop) are also removed. Confirm the `channel-id` against `list` before proceeding. diff --git a/skills/flashduty/reference/enrichment.md b/skills/flashduty/reference/enrichment.md index 01b12de..011baed 100644 --- a/skills/flashduty/reference/enrichment.md +++ b/skills/flashduty/reference/enrichment.md @@ -1,11 +1,11 @@ -# fduty enrichment — command card +# flashduty enrichment — command card Prereq: `SKILL.md` read. Read verbs are free. **`upsert` fully replaces all rules for an integration** (atomic, irreversible in the sense that the previous ruleset is gone); `mapping-schema-delete`, `mapping-api-delete`, `mapping-data-truncate`, and `mapping-data-upload` are irreversible — confirm IDs before running. ## Route here when "告警丰富 / 字段提取 / 标签映射 / 标签组合 / 标签删除 / 映射表 / 映射 API / enrichment rules / label extraction / label composition / mapping schema / lookup table / alert enrichment" → **enrichment**, NOT `route` (routing = which channel an alert goes to) or `template` (notification rendering). You need two kinds of IDs: -- **`integration-id`** (int64) — the integration that produces alerts. Get a real one from **`fduty alert list`** (every alert carries `integration_id` + `integration_name`). It is **NOT** a `channel_id` — `channel list` does not surface integration IDs, so never feed channel IDs here. +- **`integration-id`** (int64) — the integration that produces alerts. Get a real one from **`flashduty alert list`** (every alert carries `integration_id` + `integration_name`). It is **NOT** a `channel_id` — `channel list` does not surface integration IDs, so never feed channel IDs here. - **`schema-id`** / **`api-id`** (MongoDB ObjectID hex string) — from `mapping-schema-list` / `mapping-api-list`. If no specific integration is in scope, ask which one — do **not** enumerate every channel/integration ID and probe each (most 400 `Integration ... not found`; that is not a discovery strategy). @@ -38,7 +38,7 @@ If no specific integration is in scope, ask which one — do **not** enumerate e ```bash # 1. Create schema: define which alert labels are the lookup keys and what labels will be added -fduty enrichment mapping-schema-create \ +flashduty enrichment mapping-schema-create \ --schema-name "service-owner-map" \ --source-labels service \ --result-labels owner_team,oncall_email \ @@ -46,31 +46,31 @@ fduty enrichment mapping-schema-create \ # → returns schema_id (hex string); save it # 2. Populate rows (up to 1000 per call; docs array via --data) -fduty enrichment mapping-data-upsert \ +flashduty enrichment mapping-data-upsert \ --data '{"docs":[{"service":"payments","owner_team":"platform","oncall_email":"platform@example.com"},{"service":"auth","owner_team":"identity","oncall_email":"identity@example.com"}]}' # 3. Verify rows landed -fduty enrichment mapping-data-list --output-format toon +flashduty enrichment mapping-data-list --output-format toon ``` ## Hot flow — attach enrichment rules to an integration ```bash # 1. Find a real integration ID — alerts carry integration_id + integration_name -fduty alert list --limit 20 --output-format toon +flashduty alert list --limit 20 --output-format toon # 2. Check existing rules before replacing -fduty enrichment info --output-format toon +flashduty enrichment info --output-format toon # 3. Upsert rules (full replacement; rules array via --data) -fduty enrichment upsert \ +flashduty enrichment upsert \ --data '{"rules":[{"kind":"mapping","settings":{"mapping_type":"schema","schema_id":"","result_labels":["owner_team","oncall_email"]}},{"kind":"composition","settings":{"result_label":"summary","template":"[{{.owner_team}}] {{.title}}"}}]}' # 4. Confirm the new ruleset -fduty enrichment info --output-format toon +flashduty enrichment info --output-format toon ``` - + ### info Get enrichment rules @@ -205,8 +205,8 @@ Each rule may have an optional `if` AND-filter: `[{"key":"env","oper":"IN","vals ## Gotchas - **`upsert` replaces the entire ruleset atomically.** There is no "add one rule" verb. Always read with `info` first, then reconstruct the full `rules` array before calling `upsert`. Omitting a rule deletes it silently. -- **`integration-id` is POSITIONAL on `info`, `list`, and `upsert`** — pass it as the first bare argument (e.g. `fduty enrichment upsert 12345 --data '...'`), not as `--integration-id`. Similarly, `schema-id` and `api-id` are POSITIONAL on all verbs where the `use` shows `` or ``. -- **`list` accepts multiple integration IDs as positional args** (`use: list [...]`) — pass them space-separated: `fduty enrichment list 101 102 103`. +- **`integration-id` is POSITIONAL on `info`, `list`, and `upsert`** — pass it as the first bare argument (e.g. `flashduty enrichment upsert 12345 --data '...'`), not as `--integration-id`. Similarly, `schema-id` and `api-id` are POSITIONAL on all verbs where the `use` shows `` or ``. +- **`list` accepts multiple integration IDs as positional args** (`use: list [...]`) — pass them space-separated: `flashduty enrichment list 101 102 103`. - **`mapping-data-upsert` requires `docs` via `--data`** — this array cannot be expressed as flat flags. Each doc must include all `source_labels` AND all `result_labels` fields for the schema, or the row is rejected. - **`mapping-schema-create` requires Pro plan** — creating a schema on a free account returns a plan-gate error, not a 404. - **`mapping-data-truncate` wipes all rows immediately** — there is no undo. Use `mapping-data-download` to export a backup CSV first if the data matters. @@ -218,8 +218,8 @@ Each rule may have an optional `if` AND-filter: `[{"key":"env","oper":"IN","vals ```bash # Read current rules for integration 42 (positional), then extend them -fduty enrichment info 42 --output-format toon +flashduty enrichment info 42 --output-format toon # → copy the existing rules[] array, append the new rule, then upsert the full set: -fduty enrichment upsert 42 \ +flashduty enrichment upsert 42 \ --data '{"rules":[,{"kind":"drop","settings":{"drop_labels":["raw_body","_meta"]}}]}' ``` diff --git a/skills/flashduty/reference/escalation.md b/skills/flashduty/reference/escalation.md index ac768a8..ee8dc44 100644 --- a/skills/flashduty/reference/escalation.md +++ b/skills/flashduty/reference/escalation.md @@ -1,4 +1,4 @@ -# fduty channel escalation rules — 分派策略 +# flashduty channel escalation rules — 分派策略 Prereq: `SKILL.md` read. `escalate-rule-list` / `escalate-rule-info` are free reads; `escalate-rule-create/update/delete/enable/disable` mutate who gets @@ -12,8 +12,8 @@ card. Escalation rules live INSIDE a channel (协作空间) and pick the PEOPLE notified once an incident lands there — NOT `reference/route.md` (alert routing picks the *channel*), NOT `reference/schedule.md` (on-call schedules are a notify *target* referenced from layers). Key IDs: **`channel-id` -(int)** from `fduty channel list`; **`rule-id` (MongoDB ObjectID string)** -from `escalate-rule-list`; **`template-id`** from `fduty template list`. +(int)** from `flashduty channel list`; **`rule-id` (MongoDB ObjectID string)** +from `escalate-rule-list`; **`template-id`** from `flashduty template list`. ## Intent → verb @@ -30,15 +30,15 @@ from `escalate-rule-list`; **`template-id`** from `fduty template list`. ```bash # 1. find the channel and its existing rules -fduty channel escalate-rule-list --output-format toon +flashduty channel escalate-rule-list --output-format toon # 2. add the rule (layers is required via --data) -# API field `person_ids` expects member IDs from `fduty member list`. -fduty channel escalate-rule-create \ +# API field `person_ids` expects member IDs from `flashduty member list`. +flashduty channel escalate-rule-create \ --channel-id --rule-name "P1 on-call" --template-id \ --data '{"layers":[{"target":{"person_ids":[],"by":{"critical":["voice","sms"],"warning":["feishu"]}},"notify_step":5,"max_times":3,"escalate_window":30}]}' ``` - + ### escalate-rule-create Create escalation rule @@ -115,9 +115,9 @@ Update escalation rule ## Worked example — inspect a channel's escalation policy ```bash -fduty channel list --name "payments" --output-format toon +flashduty channel list --name "payments" --output-format toon # → find channel_id (e.g. 4201) -fduty channel escalate-rule-list 4201 --output-format toon +flashduty channel escalate-rule-list 4201 --output-format toon # → find rule_id (MongoDB ObjectID string, e.g. "6643abc123def456789012aa") -fduty channel escalate-rule-info --channel-id 4201 --rule-id "6643abc123def456789012aa" --output-format toon +flashduty channel escalate-rule-info --channel-id 4201 --rule-id "6643abc123def456789012aa" --output-format toon ``` diff --git a/skills/flashduty/reference/field.md b/skills/flashduty/reference/field.md index 3cca5d5..306c2e9 100644 --- a/skills/flashduty/reference/field.md +++ b/skills/flashduty/reference/field.md @@ -1,4 +1,4 @@ -# fduty field — command card +# flashduty field — command card Prereq: `SKILL.md` read. Read verbs (`list`, `info`) are free. `delete` is **irreversible** — double-check the field-id before running it. @@ -23,10 +23,10 @@ You need a **`field_id`** (24-char hex ObjectID) — get it from `field list`. ```bash # 1. Check what already exists (avoid duplicate display-name) -fduty field list --output-format toon +flashduty field list --output-format toon # 2. Create a single-select field (field-type + value-type + options are all required here) -fduty field create \ +flashduty field create \ --display-name "Root Cause" \ --field-name "root_cause" \ --field-type single_select \ @@ -35,11 +35,11 @@ fduty field create \ # → returns field_id; save it. # 3. Later: add an option (pass the full replacement list) -fduty field update \ +flashduty field update \ --options "hardware failure" --options "software bug" --options "human error" --options "network issue" ``` - + ### create Create field @@ -91,7 +91,7 @@ Update field ## Gotchas -- **`delete`, `info`, `update` take `` positionally or via `--field-id`** — both work, e.g. `fduty field delete ` or `fduty field delete --field-id `. If both are given, the flag wins. +- **`delete`, `info`, `update` take `` positionally or via `--field-id`** — both work, e.g. `flashduty field delete ` or `flashduty field delete --field-id `. If both are given, the flag wins. - **`--options` replaces the whole list on `update`** — omitting it leaves options unchanged, but a partial list silently drops the missing values. Always pass the full desired set. - **`--field-name` is the machine key** (`[a-zA-Z0-9_]`, starts with letter/underscore, ≤40 chars). It is the stable identifier for downstream enrichment rules — choose it carefully; it cannot be renamed. - **`delete` is permanent and cascades** — any enrichment rules that reference the field by `field_name` will lose their target. Confirm the name against `field list` before deleting. @@ -101,7 +101,7 @@ Update field ```bash # Create a checkbox field (value-type must be bool; options must be omitted) -fduty field create \ +flashduty field create \ --display-name "Needs Postmortem" \ --field-name "needs_postmortem" \ --field-type checkbox \ diff --git a/skills/flashduty/reference/filters.md b/skills/flashduty/reference/filters.md index eea21a4..3bcd9e9 100644 --- a/skills/flashduty/reference/filters.md +++ b/skills/flashduty/reference/filters.md @@ -67,7 +67,7 @@ alert's. Always write the canonical key. ## Building filters from incident labels (scoping a rule to one incident) To scope a rule to one incident's blast radius, build a single AND group from -that incident's own data (`fduty incident detail `): +that incident's own data (`flashduty incident detail `): 1. Start the group with a severity condition: `{"key":"severity","oper":"IN","vals":[""]}`. diff --git a/skills/flashduty/reference/incident.md b/skills/flashduty/reference/incident.md index 4d9a265..b718f11 100644 --- a/skills/flashduty/reference/incident.md +++ b/skills/flashduty/reference/incident.md @@ -1,4 +1,4 @@ -# fduty incident — command card +# flashduty incident — command card Prereq: `SKILL.md` read. Read verbs are free. **Mutating verbs notify responders or alter state** — confirm scope first. `merge` and `remove` are **irreversible**; `remove` permanently deletes. @@ -11,7 +11,7 @@ Prereq: `SKILL.md` read. Read verbs are free. **Mutating verbs notify responders | want | verb | |---|---| | list / search active incidents | `list` | -| CSV export of incidents | `fduty insight incident-export` | +| CSV export of incidents | `flashduty insight incident-export` | | look up by 6-char UI num | `info --num ` | | full detail + AI summary for a 24-char id | `detail ` (narrative) or `info --incident-id ` (same endpoint) | | get structured data for one or more ids | `get [...]` | @@ -47,19 +47,19 @@ Prereq: `SKILL.md` read. Read verbs are free. **Mutating verbs notify responders ```bash # 1. Find unacknowledged critical incidents (last 4h) -fduty incident list --severity Critical --progress Triggered --since 4h --fields incident_id,num,title,incident_severity,progress,start_time,channel_id,detail_url --output-format toon +flashduty incident list --severity Critical --progress Triggered --since 4h --fields incident_id,num,title,incident_severity,progress,start_time,channel_id,detail_url --output-format toon # 2. Get AI summary + full detail (use the 24-char incident_id from step 1) -fduty incident detail --fields incident_id,num,title,incident_severity,progress,ai_summary,root_cause,resolution,alert_cnt,start_time,channel_id,detail_url --output-format toon +flashduty incident detail --fields incident_id,num,title,incident_severity,progress,ai_summary,root_cause,resolution,alert_cnt,start_time,channel_id,detail_url --output-format toon # 3. See contributing alerts -fduty incident alerts +flashduty incident alerts # 4. Check for prior similar incidents (channel-backed only; see Gotchas) -fduty incident similar --limit 5 --output-format toon +flashduty incident similar --limit 5 --output-format toon # 5. Acknowledge ownership -fduty incident ack +flashduty incident ack # 6. Post a status comment — content goes into a file, never a shell argument ID= @@ -69,10 +69,10 @@ cat > "$COMMENT_FILE" <<'FDUTY_COMMENT_7F3A9C2E_EOF' Root cause identified: DB failover. Fix deploying. FDUTY_COMMENT_7F3A9C2E_EOF -fduty incident comment "$ID" --comment-file "$COMMENT_FILE" +flashduty incident comment "$ID" --comment-file "$COMMENT_FILE" # 7. Resolve with root-cause note -fduty incident resolve --root-cause "DB primary failover delay" --resolution "Failover completed; latency normal." +flashduty incident resolve --root-cause "DB primary failover delay" --resolution "Failover completed; latency normal." ``` Projected `similar` lists stay below 16 KiB: when the page would overflow, only the leading rows that fit are emitted — every value intact — and a stderr note says how many rows were emitted. A trailing `...` in a list row, with a stderr note naming the clipped fields, appears only when one row alone exceeds the budget. `detail --fields` is different: it never shortens values — the projection must fit within 8 KiB as requested or the command fails and names the largest fields, so drop some fields (or drop `--fields` for the full unbounded detail) and retry. @@ -95,13 +95,13 @@ If you fetch the pieces by hand instead, run **all seven** — they are cheap re ```bash ID= # 24-char id from `incident list` -fduty incident detail "$ID" --fields incident_id,num,title,incident_severity,progress,ai_summary,root_cause,resolution,alert_cnt,start_time,channel_id,detail_url --output-format toon # ① 详情 + AI summary + alert counts + channel -fduty incident alerts "$ID" # ② contributing alerts (detail's embedded alerts are empty here) -fduty incident timeline "$ID" # ④ timeline (or `incident feed "$ID"` for the paginated view) -fduty incident similar "$ID" --limit 5 --output-format toon # ⑤ similar past incidents (channel-backed; see Gotchas; compact by default) -fduty incident post-mortem-list --channel-ids # ⑥ post-mortems for this incident's channel (verb card: reference/postmortem.md) -fduty change list --since 24h # ③ correlated changes — by shared labels + time; see reference/change.md -fduty incident list --since --until --limit 50 --fields incident_id,num,title,incident_severity,progress,start_time,channel_id --output-format toon # ⑦ concurrent incidents — all channels, any progress, ±15 min around this incident's start_time (from ①) +flashduty incident detail "$ID" --fields incident_id,num,title,incident_severity,progress,ai_summary,root_cause,resolution,alert_cnt,start_time,channel_id,detail_url --output-format toon # ① 详情 + AI summary + alert counts + channel +flashduty incident alerts "$ID" # ② contributing alerts (detail's embedded alerts are empty here) +flashduty incident timeline "$ID" # ④ timeline (or `incident feed "$ID"` for the paginated view) +flashduty incident similar "$ID" --limit 5 --output-format toon # ⑤ similar past incidents (channel-backed; see Gotchas; compact by default) +flashduty incident post-mortem-list --channel-ids # ⑥ post-mortems for this incident's channel (verb card: reference/postmortem.md) +flashduty change list --since 24h # ③ correlated changes — by shared labels + time; see reference/change.md +flashduty incident list --since --until --limit 50 --fields incident_id,num,title,incident_severity,progress,start_time,channel_id --output-format toon # ⑦ concurrent incidents — all channels, any progress, ±15 min around this incident's start_time (from ①) ``` > **Never report a result you didn't fetch.** Do not write "返回空" / "无" / a count for any aspect whose command is **absent from your tool-call history this turn** — write `未查询 — 可运行 ` instead. "Empty" is a claim only a command you actually ran can make; inventing it is the worst failure mode of a fault summary. @@ -112,19 +112,19 @@ fduty incident list --since --until --limit 50 --fields ```bash # Merge two duplicate incidents into a primary (IRREVERSIBLE — confirm first) -fduty incident merge --source , +flashduty incident merge --source , # Record post-incident narrative on the primary -fduty incident reset \ +flashduty incident reset \ --root-cause "Redis OOM on shard-3" \ --impact "Checkout latency P99 >5s for 12 min" \ --resolution "Increased memory limit; deployed hot patch" # Review the event timeline -fduty incident timeline +flashduty incident timeline ``` - + ### ack [...] Acknowledge incident @@ -509,20 +509,20 @@ Update a work item - **`merge` is irreversible**: source incidents are absorbed into target permanently. Always list and confirm both IDs before running. - **`remove --force`** bypasses the interactive confirmation prompt — never pass `--force` unless the user has explicitly said so. - **`assign` needs `--data` for the nested `assigned_to` object** (either `person_ids` or `escalate_rule_id`). Pass member IDs from `member list` in the API field: `--data '{"incident_ids":[""],"assigned_to":{"person_ids":[101]}}'`. `reassign --person ` is simpler for direct member assignment. -- **Responders live in the `responders` array on `detail` / `get` / `list` records — names come resolved, no `member list` join needed.** Canonical extraction: `fduty incident detail --json | jq -r '.responders[] | "\(.person_name) <\(.email)>"'`. On `get` the same array sits one level down because `get` prints a top-level array: `jq -r '.[0].responders[] | ...'`. Each responder object carries `person_id`, `person_name` (server-resolved display name), `email`, `assigned_at`, `acknowledged_at` (`0` until acknowledged), and `as` (role label). Two selection traps: `list` omits `responders` from its default compact projection — add it via `--fields ...,responders`; and `detail --fields` projects only what you name — include `responders` in the list or the key is absent (absent ≠ empty). In plain-text mode both `detail` and single-id `get` already print a `Responders: name1, name2` line. +- **Responders live in the `responders` array on `detail` / `get` / `list` records — names come resolved, no `member list` join needed.** Canonical extraction: `flashduty incident detail --json | jq -r '.responders[] | "\(.person_name) <\(.email)>"'`. On `get` the same array sits one level down because `get` prints a top-level array: `jq -r '.[0].responders[] | ...'`. Each responder object carries `person_id`, `person_name` (server-resolved display name), `email`, `assigned_at`, `acknowledged_at` (`0` until acknowledged), and `as` (role label). Two selection traps: `list` omits `responders` from its default compact projection — add it via `--fields ...,responders`; and `detail --fields` projects only what you name — include `responders` in the list or the key is absent (absent ≠ empty). In plain-text mode both `detail` and single-id `get` already print a `Responders: name1, name2` line. ## Worked example ```bash # Start: a prod alert paged out; you have the 6-char num "A3F9B1" from Slack. # Step 1: resolve the num to full id and get AI summary in one call. -fduty incident info --num A3F9B1 --output-format toon +flashduty incident info --num A3F9B1 --output-format toon # Step 2: acknowledge so teammates see it's being handled. -fduty incident ack +flashduty incident ack # Step 3: after fix, resolve with context. -fduty incident resolve \ +flashduty incident resolve \ --root-cause "Misconfigured health-check threshold after deploy" \ --resolution "Reverted threshold; all pods healthy." ``` diff --git a/skills/flashduty/reference/insight.md b/skills/flashduty/reference/insight.md index d653d83..05417ca 100644 --- a/skills/flashduty/reference/insight.md +++ b/skills/flashduty/reference/insight.md @@ -1,4 +1,4 @@ -# fduty insight — command card +# flashduty insight — command card Prereq: `SKILL.md` read. All `insight` verbs are **read-only** — no mutations, no confirmations needed. @@ -8,7 +8,7 @@ Prereq: `SKILL.md` read. All `insight` verbs are **read-only** — no mutations, "per-person notification counts / 通知次数 / 通知量 (how many notifications did a person receive)" → **insight responder** (`total_notifications`); per-person delivery outcome detail lives in `incident timeline` `i_notify` entries (`person_id` + `failed_reason` only — no answer/接通 field), not here. -Do **not** hand-aggregate from `alert list` / `incident list` — `insight` does server-side aggregation and gives authoritative numbers. Key IDs you may need: `--team-ids` and `--channel-ids` from `fduty channel list` or `fduty team list`; `--responder-ids` from `fduty member list`. +Do **not** hand-aggregate from `alert list` / `incident list` — `insight` does server-side aggregation and gives authoritative numbers. Key IDs you may need: `--team-ids` and `--channel-ids` from `flashduty channel list` or `flashduty team list`; `--responder-ids` from `flashduty member list`. ## Intent → verb @@ -31,20 +31,20 @@ Do **not** hand-aggregate from `alert list` / `incident list` — `insight` does ```bash # account-level roll-up for past 30 days -fduty insight account --start-time 30d --end-time now --output-format toon +flashduty insight account --start-time 30d --end-time now --output-format toon # per-team and per-channel breakdowns (same flags) -fduty insight team --start-time 30d --end-time now --output-format toon -fduty insight channel --start-time 30d --end-time now --output-format toon +flashduty insight team --start-time 30d --end-time now --output-format toon +flashduty insight channel --start-time 30d --end-time now --output-format toon # who responded slowest (per-responder MTTA) -fduty insight responder --start-time 30d --end-time now --output-format toon +flashduty insight responder --start-time 30d --end-time now --output-format toon # top-10 noisiest check sources this week -fduty insight top-alerts --label check --since 7d --output-format toon +flashduty insight top-alerts --label check --since 7d --output-format toon # per-incident list with MTTA/MTTR (uses --since not --start-time) -fduty insight incidents --since 30d --limit 50 --output-format toon +flashduty insight incidents --since 30d --limit 50 --output-format toon ``` ## Hot flow — 月报 CSV export @@ -52,15 +52,15 @@ fduty insight incidents --since 30d --limit 50 --output-format toon ```bash # incident-export takes epoch seconds ONLY (int64, not relative strings) S=$(date -v-30d +%s); E=$(date +%s) -fduty insight incident-export --start-time $S --end-time $E > incidents.csv +flashduty insight incident-export --start-time $S --end-time $E > incidents.csv # responder/channel/team-export accept relative strings -fduty insight responder-export --start-time 30d --end-time now > responders.csv -fduty insight channel-export --start-time 30d --end-time now > channels.csv -fduty insight team-export --start-time 30d --end-time now > teams.csv +flashduty insight responder-export --start-time 30d --end-time now > responders.csv +flashduty insight channel-export --start-time 30d --end-time now > channels.csv +flashduty insight team-export --start-time 30d --end-time now > teams.csv ``` - + ### account Get account-level insight @@ -380,13 +380,13 @@ Both families accept: relative duration (`30d`, `24h`), `now`, `+7d`, a date, or - **`--aggregate-unit`** (on `account`, `alert-topk-by-label`, `channel`, `responder`, `team` and their exports) splits results into time buckets: `day` / `week` / `month`. When set, the window must span ≥24 h; `day` additionally caps the range at 31 days. - **`top-alerts` and `alert-topk-by-label` return the same top-K breakdown** (`label`, `hours`, `total_alert_cnt`, `total_alert_event_cnt`) — `alert-topk-by-label` is the superset (adds `--team-ids`/`--channel-ids`/severity filters and `--start-time`/`--end-time`). Pick one; don't call both for the same question. - **`responder` has no `--limit`/`--page`** — one call returns every responder's rollup for the account. For account-wide load analysis, use that single response; don't cap it and re-fetch for the rest. -- **Iterating on a `jq` filter? Save `--json` once, then re-run `jq` against the saved file.** Each `fduty insight ...` invocation is a real backend query — re-running the whole command per filter tweak multiplies OLAP load for nothing and risks a timeout on a heavy window. +- **Iterating on a `jq` filter? Save `--json` once, then re-run `jq` against the saved file.** Each `flashduty insight ...` invocation is a real backend query — re-running the whole command per filter tweak multiplies OLAP load for nothing and risks a timeout on a heavy window. ## Worked example — identify noisiest check sources ```bash # Top-20 noisiest check sources in the past 7 days, sorted by raw event count -fduty insight alert-topk-by-label \ +flashduty insight alert-topk-by-label \ --label check \ --k 20 \ --orderby total_alert_event_cnt \ diff --git a/skills/flashduty/reference/member.md b/skills/flashduty/reference/member.md index 1d60a1c..e05c229 100644 --- a/skills/flashduty/reference/member.md +++ b/skills/flashduty/reference/member.md @@ -1,10 +1,10 @@ -# fduty member — command card +# flashduty member — command card Prereq: `SKILL.md` read. `invite` sends invitation emails immediately (up to 20 per call). `delete` is **irreversible** — it removes the member from the organization. Default safety check rejects deletes when the member is referenced by escalation rules, schedules, team membership, etc. (pass `--is-force` to bypass). A member provisioned via SSO cannot be deleted at all, even with `--is-force` — disable SSO management for them first. `role-update` **replaces** all role assignments atomically; `role-grant`/`role-revoke` are additive/subtractive. ## Route here when -"成员 / 邀请 / 用户 / 角色 / member / invite / user profile / role assignment / org roster" → **member**. Sibling domains: `team` (team membership lists, not org-level members); `role` (role definitions — get role IDs here first); `person` (resolve a `person_id` → name with `fduty person infos …`, e.g. ids returned by `schedule`/`oncall`/`incident`/`alert` output). Key IDs: **`member_id` (int)** from `member list`; **`role_id` (int)** from `fduty role list`. +"成员 / 邀请 / 用户 / 角色 / member / invite / user profile / role assignment / org roster" → **member**. Sibling domains: `team` (team membership lists, not org-level members); `role` (role definitions — get role IDs here first); `person` (resolve a `person_id` → name with `flashduty person infos …`, e.g. ids returned by `schedule`/`oncall`/`incident`/`alert` output). Key IDs: **`member_id` (int)** from `member list`; **`role_id` (int)** from `flashduty role list`. ## Intent → verb @@ -24,35 +24,35 @@ Prereq: `SKILL.md` read. `invite` sends invitation emails immediately (up to 20 ```bash # 1. find available role IDs -fduty role list --output-format toon +flashduty role list --output-format toon # 2. invite up to 20 members in one call; members array MUST go via --data -fduty member invite \ +flashduty member invite \ --data '{"members":[{"email":"alice@example.com","member_name":"Alice","role_ids":[]},{"email":"bob@example.com","member_name":"Bob","role_ids":[]}]}' # → returns items[].member_id for each new member # 3. confirm they appear (status will be 'pending' until invite accepted) -fduty member list --query "alice" --output-format toon +flashduty member list --query "alice" --output-format toon ``` ## Hot flow — role change for an existing member ```bash # 1. look up the member -fduty member list --query "alice" --output-format toon +flashduty member list --query "alice" --output-format toon # note member_id and current account_role_ids # 2a. add a role without disturbing others (role-id is POSITIONAL) -fduty member role-grant --member-id +flashduty member role-grant --member-id # 2b. OR: set the complete new role list (role-ids positional; replaces ALL roles) -fduty member role-update --member-id +flashduty member role-update --member-id # 3. verify -fduty member list --query "alice" --output-format toon +flashduty member list --query "alice" --output-format toon ``` - + ### delete Delete member @@ -135,12 +135,12 @@ Update member roles ```bash jq -n --rawfile h /tmp/report.html --argjson pids '[,]' \ '{html: $h, subject: "", person_ids: $pids}' \ - | fduty member notify --data - + | flashduty member notify --data - ``` -- **Resolving a `person_id` → name: use `fduty person infos …`, NOT `member list`.** `schedule`/`oncall`/`incident`/`alert` output returns `person_id`s, a **different namespace from `member_id`**. `fduty person infos` (the sibling `person` group) batch-resolves any number of `person_id`s to `person_name` in one call (rows under `.items[]`). Matching `member list` rows on `member_id == ` is wrong, and paginating the full roster to find them silently misses people on later pages. +- **Resolving a `person_id` → name: use `flashduty person infos …`, NOT `member list`.** `schedule`/`oncall`/`incident`/`alert` output returns `person_id`s, a **different namespace from `member_id`**. `flashduty person infos` (the sibling `person` group) batch-resolves any number of `person_id`s to `person_name` in one call (rows under `.items[]`). Matching `member list` rows on `member_id == ` is wrong, and paginating the full roster to find them silently misses people on later pages. - **`invite` members array is body-only — use `--data`.** Individual members cannot be passed as flat flags; the `members` array (with nested `role_ids`, `email`, `phone`, etc.) lives only in the JSON body. Up to 20 members per call. -- **`info-reset ` can be passed positionally or via `--member-id`** — both work: `fduty member info-reset --member-name "New Name"` or `fduty member info-reset --member-id --member-name "New Name"`. If both are given, the flag wins. -- **`role-grant` / `role-revoke` / `role-update` — role IDs can be passed positionally or via `--role-ids`.** Positional is shorter: `fduty member role-grant [...] --member-id `, or pass `--role-ids ,` instead. If both are given, the flag wins. +- **`info-reset ` can be passed positionally or via `--member-id`** — both work: `flashduty member info-reset --member-name "New Name"` or `flashduty member info-reset --member-id --member-name "New Name"`. If both are given, the flag wins. +- **`role-grant` / `role-revoke` / `role-update` — role IDs can be passed positionally or via `--role-ids`.** Positional is shorter: `flashduty member role-grant [...] --member-id `, or pass `--role-ids ,` instead. If both are given, the flag wins. - **`role-update` is a full replacement.** List current roles with `member list` first; omitting a role removes it. - **`delete` default is safe** (checks escalation rules / schedules / team membership). If it rejects with a reference error, review those references before using `--is-force`. An SSO-provisioned member rejects unconditionally — `--is-force` does not override that check. - **Empty `member list` result is authoritative** — if `--query` returns nothing the member does not exist; do not widen the query. @@ -151,16 +151,16 @@ Look up a member then promote them to a new role: ```bash # find member -fduty member list --query "carol" --output-format toon +flashduty member list --query "carol" --output-format toon # → member_id=4217, account_role_ids=[2] # find the admin role ID -fduty role list --output-format toon +flashduty role list --output-format toon # → role_id=1 is "Admin" # grant admin role (keeps existing role 2) -fduty member role-grant 1 --member-id 4217 +flashduty member role-grant 1 --member-id 4217 # confirm -fduty member list --query "carol" --output-format toon +flashduty member list --query "carol" --output-format toon ``` diff --git a/skills/flashduty/reference/monit-datasource.md b/skills/flashduty/reference/monit-datasource.md index 1334dd3..f8def13 100644 --- a/skills/flashduty/reference/monit-datasource.md +++ b/skills/flashduty/reference/monit-datasource.md @@ -1,4 +1,4 @@ -# fduty monit — datasources +# flashduty monit — datasources Prereq: `SKILL.md` + `reference/monit.md` read. Datasources are what every other Flashmonit surface points at: a rule evaluates one, a probe queries one. @@ -6,7 +6,7 @@ Prereq: `SKILL.md` + `reference/monit.md` read. Datasources are what every other "数据源 / 连接数据源 / SLS project / logstore" or "datasource / connect a datasource / SLS discovery" → this card, **when the datasource is something Flashmonit queries** (Prometheus, Loki, VictoriaLogs, SQL engines, SLS). -**"Datasource" is two unrelated things in this product — check which one the user means.** This card is `POST /monit/datasource/*`: the Flashmonit config surface, i.e. the systems Flashmonit *queries*. On-call has its own, older use of the word: the top-level `datasource` group is `POST /datasource/*` and holds IM-integration plumbing (`fduty datasource im-war-room-enabled-list`, `fduty datasource im-person-try-link`), while the On-call **integrations** that *receive* alerts into a channel live in `reference/channel.md`. So 接入告警 / 集成 / 告警来源 → On-call, not here; "连一个 Prometheus / Loki / MySQL 上来查" → here. +**"Datasource" is two unrelated things in this product — check which one the user means.** This card is `POST /monit/datasource/*`: the Flashmonit config surface, i.e. the systems Flashmonit *queries*. On-call has its own, older use of the word: the top-level `datasource` group is `POST /datasource/*` and holds IM-integration plumbing (`flashduty datasource im-war-room-enabled-list`, `flashduty datasource im-person-try-link`), while the On-call **integrations** that *receive* alerts into a channel live in `reference/channel.md`. So 接入告警 / 集成 / 告警来源 → On-call, not here; "连一个 Prometheus / Loki / MySQL 上来查" → here. **Mutating:** `datasource-create`, `datasource-update`, `datasource-delete` — confirm before running. **`datasource-delete` is irreversible**; confirm the target with `datasource-info` first. @@ -33,9 +33,9 @@ Use the selected `id`, never an Agent locator. `enabled=true` is required; `aler Invoke tools through `monit-query` (see `reference/monit-query.md`); the generated `monit datasource-tools-invoke` below is the spec-mirror equivalent entry for the same endpoint. ```bash -fduty monit datasource-list --type redis_node --output-format json \ +flashduty monit datasource-list --type redis_node --output-format json \ | jq '[.[] | {id,name,type_ident,address,edge_cluster_name,enabled,alerting_enabled}]' -fduty monit-query 12345 --tool redis_node.overview --output-format json +flashduty monit-query 12345 --tool redis_node.overview --output-format json ``` One call invokes one named tool. The CLI unwraps HTTP data to `{datasource_id,tool,data,summary?,truncated?}`. The endpoint has no tool catalog: use the datasource-specific skill reference for static names and parameters. Examples include `mysql.lock_contention`, `postgres.activity`, `redis_node.slowlog`, `kafka.consumer_lag`, `elasticsearch.cat`, `prometheus.metric_trends`, `loki.log_patterns`, and `victorialogs.log_patterns`. Do not guess tool parameters. @@ -47,14 +47,14 @@ Tools require all currently routable Edge sessions in the selected cluster to su The same invoke entry also runs query tools named `.query` for ten datasource types: `prometheus`, `mysql`, `postgres`, `oracle`, `clickhouse`, `elasticsearch`, `loki`, `victorialogs`, `sls`, `tencent_cls`. The tool prefix must match the datasource type. `params` is a per-datasource structure: always `expr` plus `execution` (`kind: instant|range|window`, `from_ms`/`to_ms` Unix milliseconds; `range` also needs `max_data_points`). Log types can add `limit`/`direction`; `sls` requires `project`/`logstore`; `tencent_cls` requires `region`/`topic_id`/`syntax`. SQL types take a single read-only statement with `window` execution. ```bash -fduty monit-query 12345 --tool prometheus.query --output-format json --params - <<'FDUTY' +flashduty monit-query 12345 --tool prometheus.query --output-format json --params - <<'FDUTY' {"expr":"sum by (job) (rate(http_requests_total[5m]))","execution":{"kind":"instant","to_ms":1757462400000}} FDUTY ``` Query `data` is the complete Explore result: `format` is `explore_result.v1` and `result.kind` is `samples`, `frames`, or `logs`; log results keep `applied_limit` and `has_more`. Query tools never synthesize `summary` or `truncated`. Query tools require Edge Explore support (protocol v0.68.0); unsupported clusters fail with `edge_upgrade_required`, `mixed_edge_versions`, or `edge_version_unknown` — report as returned, never fall back to another endpoint automatically. - + ### datasource-create Create datasource diff --git a/skills/flashduty/reference/monit-probe.md b/skills/flashduty/reference/monit-probe.md index 6983e69..ea339b7 100644 --- a/skills/flashduty/reference/monit-probe.md +++ b/skills/flashduty/reference/monit-probe.md @@ -1,4 +1,4 @@ -# fduty monit — datasource queries and diagnostics +# flashduty monit — datasource queries and diagnostics Read only the card for the selected task. Use a configured datasource for metrics, logs and database/middleware diagnostics. @@ -12,7 +12,7 @@ Datasource tools use `datasource_id`, one tool per call and static tool guidance Investigations use named datasource tools. Trend/pattern tools take explicit `params.time_range` Unix seconds (up to six hours), while database overview tools observe the current server. Source evidence is not a confirmed root cause. Read warning/truncation fields before interpreting results. - + ### query-data Query structured data diff --git a/skills/flashduty/reference/monit-query.md b/skills/flashduty/reference/monit-query.md index 845b4ef..6855f86 100644 --- a/skills/flashduty/reference/monit-query.md +++ b/skills/flashduty/reference/monit-query.md @@ -1,9 +1,9 @@ -# fduty monit-query — datasource tool invocation +# flashduty monit-query — datasource tool invocation `monit-query` is the unified tool-invocation command for a configured datasource (`POST /monit/datasource/tools/invoke`). One call runs one named tool — there is no separate query vs. diagnostic split. The generated `monit datasource-tools-invoke` is the spec-mirror equivalent entry (see `reference/monit-datasource.md`). ```bash -fduty monit-query --tool '' [--account-id ] [--params ''] +flashduty monit-query --tool '' [--account-id ] [--params ''] ``` - `` — numeric ID from `monit datasource-list` (use the ID, never a name; several configurations may share an address). @@ -14,15 +14,15 @@ Query-tool `params` always carry `expr` plus `execution` (`kind: instant|range|w ```bash # query tool: PromQL instant evaluation -fduty monit-query 12345 --tool prometheus.query --output-format json --params - <<'FDUTY' +flashduty monit-query 12345 --tool prometheus.query --output-format json --params - <<'FDUTY' {"expr":"sum by (job) (rate(http_requests_total[5m]))","execution":{"kind":"instant","to_ms":1757462400000}} FDUTY # diagnostic tool: no params needed -fduty monit-query 12345 --tool redis_node.overview --output-format json +flashduty monit-query 12345 --tool redis_node.overview --output-format json ``` - + ### monit-query Invoke a datasource query or diagnostic tool diff --git a/skills/flashduty/reference/monit-rule.md b/skills/flashduty/reference/monit-rule.md index b50556d..919a9e2 100644 --- a/skills/flashduty/reference/monit-rule.md +++ b/skills/flashduty/reference/monit-rule.md @@ -1,4 +1,4 @@ -# fduty monit — alert rules +# flashduty monit — alert rules Prereq: `SKILL.md` + `reference/monit.md` read. This is the largest Flashmonit surface: rule CRUD, the folder tree, counters, and change history. @@ -27,8 +27,8 @@ Prereq: `SKILL.md` + `reference/monit.md` read. This is the largest Flashmonit s Use `rule-list-basic --folder-id ` only when a real folder ID is available from the user or trusted context; it lists direct rules, not descendants. Add `--include-descendants` (optionally `--query` / `--limit`, capped at 100) to enumerate subtree rules as id/folder_id/name rows. Omitting the ID or passing `0` returns "Folder not found". ```bash -fduty monit rule-list-basic --folder-id --output-format toon -fduty monit rule-list-basic --folder-id --include-descendants --limit 100 --output-format toon +flashduty monit rule-list-basic --folder-id --output-format toon +flashduty monit rule-list-basic --folder-id --include-descendants --limit 100 --output-format toon ``` The public CLI cannot discover the folder tree itself. If folder IDs are unavailable, report that limit rather than guessing IDs or treating a partial rule list as complete. @@ -59,18 +59,18 @@ The public CLI cannot discover the folder tree itself. If folder IDs are unavail ```bash # 1. list direct rules in a folder whose ID is separately known -fduty monit rule-list-basic --folder-id --output-format toon +flashduty monit rule-list-basic --folder-id --output-format toon # 2. enumerate subtree rules (id/folder_id/name only, capped at 100) -fduty monit rule-list-basic --folder-id --include-descendants --limit 100 --output-format toon +flashduty monit rule-list-basic --folder-id --include-descendants --limit 100 --output-format toon # 3. get full config of one rule -fduty monit rule-v2-info --id --output-format toon +flashduty monit rule-v2-info --id --output-format toon # 4. disable several rules at once without touching other fields -fduty monit rule-update-fields --ids , --fields enabled --enabled false +flashduty monit rule-update-fields --ids , --fields enabled --enabled false ``` - + ### rule-audit-detail Get rule audit snapshot diff --git a/skills/flashduty/reference/monit.md b/skills/flashduty/reference/monit.md index 630ec84..86d691a 100644 --- a/skills/flashduty/reference/monit.md +++ b/skills/flashduty/reference/monit.md @@ -1,10 +1,10 @@ -# fduty monit — command card +# flashduty monit — command card Prereq: `SKILL.md` read. Flashmonit is five separate surfaces sharing one command group, so this card is an index: **read the card for the surface you need, not all of them.** ## Route here when -"监控规则 / 告警规则 / 数据源 / PromQL查询 / 日志查询 / 诊断" or "alert rule / datasource / metric query / log pattern / diagnose" → **monit**. NOT `incident` (that domain = the alert graph after rules fire), and **"数据源" here means a system Flashmonit queries** — On-call 集成 / 告警来源 is a different surface (`reference/channel.md`), and the top-level `datasource` group (`fduty datasource im-war-room-enabled-list`) is On-call IM plumbing, not this one. +"监控规则 / 告警规则 / 数据源 / PromQL查询 / 日志查询 / 诊断" or "alert rule / datasource / metric query / log pattern / diagnose" → **monit**. NOT `incident` (that domain = the alert graph after rules fire), and **"数据源" here means a system Flashmonit queries** — On-call 集成 / 告警来源 is a different surface (`reference/channel.md`), and the top-level `datasource` group (`flashduty datasource im-war-room-enabled-list`) is On-call IM plumbing, not this one. ## Which card @@ -18,7 +18,7 @@ Key IDs are shared across all of them: **rule ID (int)** from `rule-list-basic`; Read verbs are free. Mutating verbs change state — confirm before running; each card flags its own, and marks the irreversible ones. - + ### dashboard-create Create dashboard diff --git a/skills/flashduty/reference/noise.md b/skills/flashduty/reference/noise.md index b9858d0..951bff6 100644 --- a/skills/flashduty/reference/noise.md +++ b/skills/flashduty/reference/noise.md @@ -1,4 +1,4 @@ -# fduty channel noise rules — silence / inhibit / drop +# flashduty channel noise rules — silence / inhibit / drop Prereq: `SKILL.md` read. Read verbs (`*-rule-list`) are free; every `silence-rule-*`, `inhibit-rule-*`, `unsubscribe-rule-*` create / update / @@ -9,7 +9,7 @@ enable / disable / delete mutates state — confirm before acting. "静默 / 屏蔽 / 抑制 / 丢弃 / 降噪 / 维护窗口 / silence / mute / inhibit / suppress / drop / discard / noise reduction / maintenance window" → this card. These rules live INSIDE a channel (协作空间): **`channel-id` (int)** -from `fduty channel list` (channel management: `reference/channel.md`); +from `flashduty channel list` (channel management: `reference/channel.md`); **`rule-id` (MongoDB ObjectID string)** from the matching `*-rule-list`. Escalation / 分派策略 → `reference/escalation.md`. @@ -37,22 +37,22 @@ construction rules and the valid key set. ```bash # 1. inspect the incident to silence around — pulls incident_severity + labels -fduty incident detail --output-format toon +flashduty incident detail --output-format toon # 2. channel-id is POSITIONAL on silence-rule-create (see use: "silence-rule-create ") # filters is one AND group: a severity condition plus one labels. condition # per distinguishing label — id-shaped/long/date-shaped/noise-key label values are # dropped, not passed through (construction rules: reference/filters.md). -fduty channel silence-rule-create \ +flashduty channel silence-rule-create \ --rule-name "planned-maintenance-2026-07-01" \ --is-auto-delete \ --data '{"time_filter":{"start_time":1751328000,"end_time":1751371200},"filters":[[{"key":"severity","oper":"IN","vals":["Critical"]},{"key":"labels.service","oper":"IN","vals":["payments-api"]},{"key":"labels.env","oper":"IN","vals":["prod"]}]]}' # 3. verify — read back `filters` to confirm the conditions round-tripped -fduty channel silence-rule-list --output-format toon +flashduty channel silence-rule-list --output-format toon ``` - + ### inhibit-rule-create Create inhibit rule diff --git a/skills/flashduty/reference/postmortem.md b/skills/flashduty/reference/postmortem.md index 868c0a1..ed841eb 100644 --- a/skills/flashduty/reference/postmortem.md +++ b/skills/flashduty/reference/postmortem.md @@ -1,4 +1,4 @@ -# fduty incident post-mortems — command card +# flashduty incident post-mortems — command card Prereq: `SKILL.md` read. `post-mortem-list` / `post-mortem-info` / `post-mortem-template-list` / `post-mortem-template-info` are free reads; @@ -14,7 +14,7 @@ linked to 1–10 incidents; the incidents themselves (triage, resolve, merge) are `reference/incident.md`. Key IDs: **`post-mortem-id` (string)** from `post-mortem-list` (deterministic hash of the linked incident set); **`template-id` (string)** from `post-mortem-template-list`; -**`incident-id` (24-char MongoDB ObjectID)** from `fduty incident list`. +**`incident-id` (24-char MongoDB ObjectID)** from `flashduty incident list`. ## Intent → verb @@ -35,9 +35,9 @@ are `reference/incident.md`. Key IDs: **`post-mortem-id` (string)** from ```bash # 1. pick a template -fduty incident post-mortem-template-list --output-format toon +flashduty incident post-mortem-template-list --output-format toon # 2. initialize the report from the incident (returns post_mortem_id in meta) -fduty incident post-mortem-init --template-id +flashduty incident post-mortem-init --template-id # 3. write the narrative — Markdown goes into a file, then content-reset BODY_FILE=$(mktemp) cat > "$BODY_FILE" <<'FDUTY_PM_7F3A9C2E_EOF' @@ -46,13 +46,13 @@ cat > "$BODY_FILE" <<'FDUTY_PM_7F3A9C2E_EOF' ## Root cause ... FDUTY_PM_7F3A9C2E_EOF -fduty incident post-mortem-content-reset --markdown-file "$BODY_FILE" +flashduty incident post-mortem-content-reset --markdown-file "$BODY_FILE" # 4. follow-ups + publish -fduty incident post-mortem-follow-ups-reset --follow-ups "Add alert on replica lag; tune failover timeout" -fduty incident post-mortem-status-reset --status published +flashduty incident post-mortem-follow-ups-reset --follow-ups "Add alert on replica lag; tune failover timeout" +flashduty incident post-mortem-status-reset --status published ``` - + ### post-mortem-basics-reset Update post-mortem basics @@ -147,7 +147,7 @@ Update post-mortem title ## Gotchas - **Post-mortem verbs live under the `incident` command group** — - `fduty incident post-mortem-list`; there is no standalone post-mortem + `flashduty incident post-mortem-list`; there is no standalone post-mortem group. - **`post-mortem-id` ≠ `incident-id`**: init takes incident IDs and returns the report; every later verb takes the `post-mortem-id` from diff --git a/skills/flashduty/reference/role.md b/skills/flashduty/reference/role.md index 62114e3..3529420 100644 --- a/skills/flashduty/reference/role.md +++ b/skills/flashduty/reference/role.md @@ -1,4 +1,4 @@ -# fduty role — command card +# flashduty role — command card Prereq: `SKILL.md` read. Read verbs are free; `delete` is **irreversible** — confirm the role-id first. `upsert --permission-ids` **replaces** the entire permission set on an existing role. @@ -26,39 +26,39 @@ Prereq: `SKILL.md` read. Read verbs are free; `delete` is **irreversible** — c ```bash # 1. Browse available permissions with role membership annotation -fduty role permission-list --with-all --output-format toon +flashduty role permission-list --with-all --output-format toon # 2. Create the role with chosen permission IDs (note: ids from step 1) -fduty role upsert --role-name "Incident Responder" \ +flashduty role upsert --role-name "Incident Responder" \ --description "Read incidents and manage on-call." \ --permission-ids 101,102,305 # 3. Find the new role ID -fduty role list --output-format toon +flashduty role list --output-format toon # 4. Find member IDs to assign (member-id is POSITIONAL, role-id is a flag) -fduty member list --output-format toon +flashduty member list --output-format toon # 5. Grant role to members (first member-id is positional; additional ids space-separated) -fduty role member-grant --role-id -# Grant to multiple: fduty role member-grant --role-id +flashduty role member-grant --role-id +# Grant to multiple: flashduty role member-grant --role-id ``` ## Hot flow — audit and update an existing role ```bash # 1. Find the role -fduty role list --output-format toon +flashduty role list --output-format toon # 2. Inspect current permissions (is_granted shows which are currently set) -fduty role permission-list --role-ids --with-all --output-format toon +flashduty role permission-list --role-ids --with-all --output-format toon # 3. Update permissions (--permission-ids is the FULL replacement set) -fduty role upsert --role-id --role-name "Incident Responder" \ +flashduty role upsert --role-id --role-name "Incident Responder" \ --permission-ids 101,102,305,410 ``` - + ### delete Delete a role @@ -123,8 +123,8 @@ Create or update a role ## Gotchas -- **`delete`, `disable`, `enable`, `info` take `` positionally or via `--role-id`** — both work: `fduty role delete ` or `fduty role delete --role-id `. If both are given, the flag wins. -- **`member-grant` / `member-revoke`: `` is POSITIONAL (one or more space-separated); `--role-id` is a flag** — easy to flip. Example: `fduty role member-grant 123 456 --role-id 7`. Member IDs can also be passed via `--member-ids` instead of the positional (same fold-then-override rule). +- **`delete`, `disable`, `enable`, `info` take `` positionally or via `--role-id`** — both work: `flashduty role delete ` or `flashduty role delete --role-id `. If both are given, the flag wins. +- **`member-grant` / `member-revoke`: `` is POSITIONAL (one or more space-separated); `--role-id` is a flag** — easy to flip. Example: `flashduty role member-grant 123 456 --role-id 7`. Member IDs can also be passed via `--member-ids` instead of the positional (same fold-then-override rule). - **`upsert --permission-ids` replaces the full set** on update — omitting it clears all permissions. Always read `permission-list --role-ids --with-all` first to get the current set before modifying. - **`upsert` with no `--role-id` (or `--role-id 0`) creates; with `--role-id N` updates** — the verb doubles as create and update; check for an existing role with `list` to avoid accidental duplicates. - **`delete` is irreversible** — members who had this role lose its permissions immediately. Prefer `disable` to park a role without destroying it. @@ -134,7 +134,7 @@ Create or update a role ```bash # Revoke a role from a single member -fduty role member-revoke --role-id +flashduty role member-revoke --role-id # Revoke from multiple members in one call -fduty role member-revoke --role-id +flashduty role member-revoke --role-id ``` diff --git a/skills/flashduty/reference/route.md b/skills/flashduty/reference/route.md index d3cff99..06cf97f 100644 --- a/skills/flashduty/reference/route.md +++ b/skills/flashduty/reference/route.md @@ -1,12 +1,12 @@ -# fduty route — command card +# flashduty route — command card Prereq: `SKILL.md` read. `upsert` is a **full replacement** of the rule — it overwrites all cases; always read first and pass `--version` for optimistic concurrency. ## Route here when "路由规则 / 告警路由 / 集成路由 / 分派到频道 / route rule / alert routing / integration routing / which channel gets alerts" → **route**. Key IDs needed: -- **`integration-id`** (int) — the integration the rule belongs to. Get a real one from **`fduty alert list`** (every alert carries `integration_id` + `integration_name`). It is **NOT** a `channel_id`; `channel list` does not surface integration IDs. If none is in scope, ask which integration rather than probing IDs. -- **`channel-id`** (int) — the target channel matched alerts route to; from `fduty channel list`. +- **`integration-id`** (int) — the integration the rule belongs to. Get a real one from **`flashduty alert list`** (every alert carries `integration_id` + `integration_name`). It is **NOT** a `channel_id`; `channel list` does not surface integration IDs. If none is in scope, ask which integration rather than probing IDs. +- **`channel-id`** (int) — the target channel matched alerts route to; from `flashduty channel list`. Do NOT use `route` for scheduling (→ `schedule`), templates (→ `template`), or channel management (→ `channel`). @@ -22,11 +22,11 @@ Do NOT use `route` for scheduling (→ `schedule`), templates (→ `template`), ```bash # 1. Read the current rule; note the returned `version` field for concurrency control. -fduty route info --output-format toon +flashduty route info --output-format toon # 2. Upsert: route critical alerts to channel 101, all others to channel 102 (default). # Pass the `version` from step 1 to prevent races. -fduty route upsert --version \ +flashduty route upsert --version \ --data '{ "cases": [ { @@ -39,15 +39,15 @@ fduty route upsert --version \ }' # 3. Verify -fduty route info --output-format toon +flashduty route info --output-format toon ``` ```bash # Bulk read — check rules for several integrations at once (positional ids). -fduty route list --output-format toon +flashduty route list --output-format toon ``` - + ### info Get routing rule detail @@ -90,10 +90,10 @@ Upsert routing rule ```bash # Read current rule for integration 5000, then add a name-mapping case for team-based routing. -fduty route info --output-format toon +flashduty route info --output-format toon # → note current version, e.g. 3 -fduty route upsert --version 3 \ +flashduty route upsert --version 3 \ --data '{ "cases": [ { diff --git a/skills/flashduty/reference/rum.md b/skills/flashduty/reference/rum.md index e872ead..98136ba 100644 --- a/skills/flashduty/reference/rum.md +++ b/skills/flashduty/reference/rum.md @@ -1,4 +1,4 @@ -# fduty rum — command card +# flashduty rum — command card Prereq: `SKILL.md` read. Read verbs are free. `application-create` / `application-update` / `application-delete` / `issue-update` mutate state — confirm before running. `application-delete` is **irreversible**. @@ -31,35 +31,35 @@ Prereq: `SKILL.md` read. Read verbs are free. `application-create` / `applicatio ```bash # 1. find the app (application_id is a string) -fduty rum application-list --query "checkout" --output-format toon +flashduty rum application-list --query "checkout" --output-format toon # 2. list open errors in the last 7 days (both time flags required, MILLISECOND epoch) NOW=$(date +%s000) WEEK_AGO=$(( $(date +%s) - 604800 ))000 -fduty rum issue-list \ +flashduty rum issue-list \ --application-ids \ --start-time $WEEK_AGO --end-time $NOW \ --statuses for_review --orderby error_count \ --output-format toon # 3. get full detail of the top issue -fduty rum issue-info --output-format toon +flashduty rum issue-info --output-format toon # 4. mark resolved after fix is confirmed -fduty rum issue-update --status resolved --suspected-cause code.exception +flashduty rum issue-update --status resolved --suspected-cause code.exception ``` ## Hot flow — create a new RUM application ```bash # team-id is POSITIONAL (use: "application-create "); other fields are flags -fduty rum application-create \ +flashduty rum application-create \ --application-name "Checkout Web" \ --type browser # → returns application_id + client_token for SDK init ``` - + ### application-create Create application @@ -386,20 +386,20 @@ Regression: a `resolved` issue that recurs gets a `regression{}` object on its r - **`alerting` and `tracing` are nested objects** — configure them via `--data '{"alerting":{...},"tracing":{...}}'`; there are no flat flags for their sub-fields. Scalar flags (`--application-name`, `--type`, …) override matching `--data` keys. - **Application records hold CONFIG only** — no traffic volume, error-rate, or session-count fields. For trend data, query `monit` RUM series. - **Empty `issue-list` is authoritative** — a filter returning no items means no matching issues, not a missing feature. Do not widen the query or guess. -- **No `rum sourcemap` subcommand** — sourcemap lookup and stack enrichment are top-level: read `reference/sourcemap.md` and use `fduty sourcemap ...`. +- **No `rum sourcemap` subcommand** — sourcemap lookup and stack enrichment are top-level: read `reference/sourcemap.md` and use `flashduty sourcemap ...`. ## Worked example ```bash # Find the worst unreviewed crash in the "payment" app this week, then mark it resolved -APP_ID=$(fduty rum application-list --query "payment" --output-format json | jq -r '.items[0].application_id') +APP_ID=$(flashduty rum application-list --query "payment" --output-format json | jq -r '.items[0].application_id') NOW=$(date +%s000) WEEK_AGO=$(( $(date +%s) - 604800 ))000 -fduty rum issue-list \ +flashduty rum issue-list \ --application-ids "$APP_ID" \ --start-time $WEEK_AGO --end-time $NOW \ --statuses for_review --orderby session_count \ --limit 1 --output-format json | jq -r '.items[0].issue_id' # → paste the returned issue_id below -fduty rum issue-update --status resolved --suspected-cause code.exception +flashduty rum issue-update --status resolved --suspected-cause code.exception ``` diff --git a/skills/flashduty/reference/safari.md b/skills/flashduty/reference/safari.md index 664ffb9..9c29279 100644 --- a/skills/flashduty/reference/safari.md +++ b/skills/flashduty/reference/safari.md @@ -1,8 +1,8 @@ -# fduty safari — command card +# flashduty safari — command card Prereq: `SKILL.md` read. This is the **AI-SRE platform self-management** group: install/configure the account's own **MCP servers (connectors)**, **skills**, and **A2A agents**, plus inspect **sessions**. Mutating verbs (`create`, `update`, `delete`, `upload`) change account configuration — confirm before running. `delete` is **irreversible**. -> Registering an MCP server is THIS group (`fduty safari mcp-server-create`) — **not** a tool search. A tool search only discovers callable tools on servers already connected to you; it can neither register nor configure one. +> Registering an MCP server is THIS group (`flashduty safari mcp-server-create`) — **not** a tool search. A tool search only discovers callable tools on servers already connected to you; it can neither register nor configure one. ## Route here when @@ -32,17 +32,17 @@ Pass the nested `env` / `headers` objects through `--data` (they have no scalar ```bash # stdio (local process): command + args + secrets via env -fduty safari mcp-server-create --data '{"server_name":"GitHub Tools","transport":"stdio","description":"Read issues and pull requests from GitHub.","command":"npx","args":["-y","@modelcontextprotocol/server-github"],"env":{"GITHUB_TOKEN":"ghp_xxx"},"team_id":0,"status":"enabled"}' +flashduty safari mcp-server-create --data '{"server_name":"GitHub Tools","transport":"stdio","description":"Read issues and pull requests from GitHub.","command":"npx","args":["-y","@modelcontextprotocol/server-github"],"env":{"GITHUB_TOKEN":"ghp_xxx"},"team_id":0,"status":"enabled"}' # remote (streamable-http) with per-user OAuth — oauth_metadata stays empty (auto-discovered + DCR at runtime) -fduty safari mcp-server-create --data '{"server_name":"Aliyun OpenAPI","transport":"streamable-http","description":"Alibaba Cloud OpenAPI MCP.","url":"https://openapi-mcp.example.com/mcp","auth_mode":"per_user_oauth","team_id":0,"status":"enabled"}' +flashduty safari mcp-server-create --data '{"server_name":"Aliyun OpenAPI","transport":"streamable-http","description":"Alibaba Cloud OpenAPI MCP.","url":"https://openapi-mcp.example.com/mcp","auth_mode":"per_user_oauth","team_id":0,"status":"enabled"}' # confirm it registered, then inspect its live tool catalogue -fduty safari mcp-server-list --output-format toon -fduty safari mcp-server-get --data '{"server_id":"mcp_xxx"}' +flashduty safari mcp-server-list --output-format toon +flashduty safari mcp-server-get --data '{"server_id":"mcp_xxx"}' ``` - + ### a2a-agent-create Create A2A agent diff --git a/skills/flashduty/reference/schedule.md b/skills/flashduty/reference/schedule.md index 1bbef4f..f30fa80 100644 --- a/skills/flashduty/reference/schedule.md +++ b/skills/flashduty/reference/schedule.md @@ -1,4 +1,4 @@ -# fduty schedule — command card +# flashduty schedule — command card Prereq: `SKILL.md` read. **Read verbs are free. `delete` is irreversible — confirm IDs before executing. `create` / `update` immediately change the live rotation — confirm scope first.** @@ -27,37 +27,37 @@ Prereq: `SKILL.md` read. **Read verbs are free. `delete` is irreversible — con ```bash # 1. Find the schedule ID(s) — scope by name or team; don't fetch every schedule -fduty schedule list --query "SRE" --output-format toon -# or, for several: fduty schedule list --team-ids --output-format toon +flashduty schedule list --query "SRE" --output-format toon +# or, for several: flashduty schedule list --team-ids --output-format toon # 2. Current on-call for each schedule — a tiny now-window yields the live shift -fduty schedule info --start now --end +1h --output-format toon +flashduty schedule info --start now --end +1h --output-format toon ``` `--output-format toon` is for reading, not piping — it is **not** jq-parseable. If you need to filter/extract with `jq`, use `--json` instead. -`schedule info`'s on-call groups carry `person_ids` (integers), not names. Resolve every id in **one batch call** with `fduty person infos` (the sibling `person` group — takes positional ids or `--person-ids`): +`schedule info`'s on-call groups carry `person_ids` (integers), not names. Resolve every id in **one batch call** with `flashduty person infos` (the sibling `person` group — takes positional ids or `--person-ids`): ```bash # person_ids come straight from the schedule/oncall output above -fduty person infos [ …] --output-format toon +flashduty person infos [ …] --output-format toon # → rows under .items[] with person_id + person_name; join on person_id client-side ``` -**`person_id` ≠ `member_id` — do NOT resolve schedule/oncall people via `member list`.** They are different id namespaces, so matching `member list` rows on `member_id == ` is wrong, and paginating the full roster (often 20+ pages) silently drops people who land on later pages — a real prod miss. Always feed the `person_id`s to `fduty person infos`. If a lookup genuinely fails, report the bare `person_id` rather than guessing. +**`person_id` ≠ `member_id` — do NOT resolve schedule/oncall people via `member list`.** They are different id namespaces, so matching `member list` rows on `member_id == ` is wrong, and paginating the full roster (often 20+ pages) silently drops people who land on later pages — a real prod miss. Always feed the `person_id`s to `flashduty person infos`. If a lookup genuinely fails, report the bare `person_id` rather than guessing. ## Hot flow — inspect a schedule's upcoming shifts ```bash -fduty schedule list --query "SRE" --output-format toon # find the schedule_id -fduty schedule info --start now --end +7d --output-format toon # next 7 days +flashduty schedule list --query "SRE" --output-format toon # find the schedule_id +flashduty schedule info --start now --end +7d --output-format toon # next 7 days ``` ## Hot flow — create a schedule via --data ```bash # Layers are deeply nested; pass the full body via --data; scalar flags override matching keys. -fduty schedule create --schedule-name "SRE Weekly" --team-id \ +flashduty schedule create --schedule-name "SRE Weekly" --team-id \ --data '{ "layers": [{ "layer_name": "Week rotation", @@ -93,10 +93,10 @@ fduty schedule create --schedule-name "SRE Weekly" --team-id \ "update_by": 0 }] }' -# → returns schedule_id; verify with: fduty schedule info --start now --end +7d +# → returns schedule_id; verify with: flashduty schedule info --start now --end +7d ``` - + ### by-person Get member on-call status @@ -189,7 +189,7 @@ Update schedule ## Gotchas -- **`info` takes `` positionally or via `--schedule-id`** — both work: `fduty schedule info 123 --start now --end +7d` or `fduty schedule info --schedule-id 123 --start now --end +7d`. If both are given, the flag wins. **`infos` and `delete` have no singular `--schedule-id` flag** — only the plural `--schedule-ids` (comma-separated), which folds the same way as the positional list; passing `--schedule-id` there is an unknown-flag error, not a rejected alternative. +- **`info` takes `` positionally or via `--schedule-id`** — both work: `flashduty schedule info 123 --start now --end +7d` or `flashduty schedule info --schedule-id 123 --start now --end +7d`. If both are given, the flag wins. **`infos` and `delete` have no singular `--schedule-id` flag** — only the plural `--schedule-ids` (comma-separated), which folds the same way as the positional list; passing `--schedule-id` there is an unknown-flag error, not a rejected alternative. - **`create` / `update` / `preview` take all inputs as flags** (no positional). `update` requires `--schedule-id` as a flag to identify the target. - **`layers` is body-only.** There is no per-layer typed flag — you must pass the entire `layers` array via `--data`. Scalar top-level flags (`--schedule-name`, `--team-id`) override matching `--data` keys. - **`list` without `--start`/`--end` omits computed shifts** — only schedule metadata is returned. Pass both flags (≤45 day span) to get rotation slots in the list response. @@ -201,5 +201,5 @@ Update schedule ```bash # Find my own on-call windows for the next two weeks -fduty schedule self --start now --end +14d --output-format toon +flashduty schedule self --start now --end +14d --output-format toon ``` diff --git a/skills/flashduty/reference/sourcemap.md b/skills/flashduty/reference/sourcemap.md index 23f3e27..540128a 100644 --- a/skills/flashduty/reference/sourcemap.md +++ b/skills/flashduty/reference/sourcemap.md @@ -1,4 +1,4 @@ -# fduty sourcemap — command card +# flashduty sourcemap — command card Prereq: `SKILL.md` read. These verbs are read-only/debugging helpers. They do not upload, delete, or mutate sourcemap records. @@ -17,7 +17,7 @@ Prereq: `SKILL.md` read. These verbs are read-only/debugging helpers. They do no ```bash # 1. Confirm the service/version/type actually has uploaded mapping files -fduty sourcemap list \ +flashduty sourcemap list \ --type browser \ --services checkout-web \ --start-time 1712000000000 \ @@ -25,12 +25,12 @@ fduty sourcemap list \ --output-format toon # 2. Enrich the minified stack trace. Use --data for multiline stack payloads. -fduty sourcemap stack-enrich \ +flashduty sourcemap stack-enrich \ --data '{"type":"browser","service":"checkout-web","version":"1.0.0","near":3,"stack":"TypeError: Cannot read properties of undefined\n at render (https://cdn.example.com/app.min.js:1:2345)"}' \ --output-format toon ``` - + ### list List sourcemaps @@ -70,7 +70,7 @@ Enrich a stack trace ## Gotchas -- **Top-level group:** use `fduty sourcemap ...`, not `fduty rum sourcemap ...`. +- **Top-level group:** use `flashduty sourcemap ...`, not `flashduty rum sourcemap ...`. - **`stack-enrich` needs exact upload identity:** `type`, `service`, and `version` must match the uploaded sourcemap/dSYM metadata. - **Use `--data` for stack traces.** Multiline stacks are easier and safer as JSON body payloads than shell-escaped flags. - **Empty `list` is authoritative** for the supplied filters; re-check service/version/type from the RUM app or build metadata before changing the time window. diff --git a/skills/flashduty/reference/status-page.md b/skills/flashduty/reference/status-page.md index aba1f1d..7f4150c 100644 --- a/skills/flashduty/reference/status-page.md +++ b/skills/flashduty/reference/status-page.md @@ -1,4 +1,4 @@ -# fduty status-page — command card +# flashduty status-page — command card Prereq: `SKILL.md` read. **SKILL.md + this card = full competence on status pages — no `--help` needed.** Read verbs are free; any `change-*` create/update with `--notify-subscribers` pages subscribers immediately — confirm scope first. @@ -29,25 +29,25 @@ Structure mutations (`component-upsert` / `component-delete` / `section-upsert` ```bash # page-id is POSITIONAL here (see fence headings: `### change-active-list `); change-id stays a flag. # 1. find the page + impacted component IDs -fduty status-page list --output-format toon +flashduty status-page list --output-format toon # 2. confirm nothing already open (empty = nothing open; if one exists, reuse its change_id) -fduty status-page change-active-list --type incident +flashduty status-page change-active-list --type incident # 3. open it (page-id positional; scalars as flags; the required `updates` array via --data); save change_id -fduty status-page change-create --type incident \ +flashduty status-page change-create --type incident \ --title "API latency elevated" --status investigating --description "Investigating elevated latency." \ --data '{"updates":[{"status":"investigating","description":"Team is investigating.","component_changes":[{"component_id":"","status":"degraded"}]}]}' # 4. post progress: investigating → identified → monitoring (change-timeline-create takes BOTH ids as flags) -fduty status-page change-timeline-create --page-id --change-id \ +flashduty status-page change-timeline-create --page-id --change-id \ --status identified --description "Root cause identified." # 5. resolve — every referenced component MUST go back to operational -fduty status-page change-timeline-create --page-id --change-id \ +flashduty status-page change-timeline-create --page-id --change-id \ --status resolved --description "Recovered." \ --data '{"component_changes":[{"component_id":"","status":"operational"}]}' # 6. confirm closed -fduty status-page change-active-list --type incident +flashduty status-page change-active-list --type incident ``` - + ### change-active-list List active status page events @@ -284,7 +284,7 @@ Update status page ## Worked example — open an incident ```bash -fduty status-page change-create --type incident \ +flashduty status-page change-create --type incident \ --title "Web Console Degraded" --status investigating \ --description "Investigating degraded performance on the web console." \ --data '{"updates":[{"status":"investigating","description":"Team is investigating.","component_changes":[{"component_id":"","status":"degraded"}]}]}' diff --git a/skills/flashduty/reference/team.md b/skills/flashduty/reference/team.md index 2f891fa..7ea3bc5 100644 --- a/skills/flashduty/reference/team.md +++ b/skills/flashduty/reference/team.md @@ -1,12 +1,12 @@ -# fduty team — command card +# flashduty team — command card Prereq: `SKILL.md` read. **SKILL.md + this card = full competence on teams — no `--help` needed.** Read verbs are free; `delete` is **irreversible** (always `--force` in scripted contexts); `update --person-ids` **replaces** the entire member list — dangerous without a prior `get`. ## Route here when "团队 / 成员管理 / 创建团队 / 查找团队 / HR同步 / team ID / person ID归属" → **team**. Key IDs: -- **`team_id` (int64)** — from `fduty team list` or `team get --name`. -- **`--person-ids` inputs are member IDs** — look up via `fduty member list --query ` (member card, not here). The API field is named `person_ids`, but team membership expects member IDs. +- **`team_id` (int64)** — from `flashduty team list` or `team get --name`. +- **`--person-ids` inputs are member IDs** — look up via `flashduty member list --query ` (member card, not here). The API field is named `person_ids`, but team membership expects member IDs. NOT this card: on-call schedules (oncall), incidents (incident), channels (channel). @@ -27,24 +27,24 @@ NOT this card: on-call schedules (oncall), incidents (incident), channels (chann ```bash # 1. Check name doesn't already exist -fduty team list --name "SRE Platform" --output-format toon +flashduty team list --name "SRE Platform" --output-format toon # 2. Create with initial members (member IDs from member list) -fduty team create --name "SRE Platform" --description "Site Reliability" \ +flashduty team create --name "SRE Platform" --description "Site Reliability" \ --person-ids 1001,1002,1003 # 3. Verify — note the returned team_id -fduty team get --name "SRE Platform" --output-format toon +flashduty team get --name "SRE Platform" --output-format toon ``` ## Hot flow — update members safely ```bash # ALWAYS read current members before --person-ids (it REPLACES, not appends) -fduty team get --id --output-format toon +flashduty team get --id --output-format toon # Then pass the FULL desired set (existing + new) -fduty team update --id --person-ids 1001,1002,1003,1004 +flashduty team update --id --person-ids 1001,1002,1003,1004 ``` - + ### create Create a new team @@ -125,14 +125,14 @@ Create or update a team - **`--person-ids` on `update` / `create` / `upsert` is a full replacement**, not an append. Read the current list with `get --id` first, or you will silently remove members. - **`get` vs `info`** — both fetch a single team; `get` accepts `--id`/`--name`/`--ref-id`; `get []` also allows the ID as a positional arg. `info` uses `--team-id`/`--team-name`/`--ref-id` flags only. Prefer `get` for interactive lookup. - **`delete` is irreversible** and requires confirmation unless `--force` is set. Always confirm the correct `--id` (not `--name`) in scripts to avoid name-collision accidents. -- **`infos` accepts IDs either way** — space-separated positional args or comma-separated `--team-ids`: `fduty team infos 101 102 103` or `fduty team infos --team-ids 101,102,103`. If both are given, the flag wins. +- **`infos` accepts IDs either way** — space-separated positional args or comma-separated `--team-ids`: `flashduty team infos 101 102 103` or `flashduty team infos --team-ids 101,102,103`. If both are given, the flag wins. - **`upsert` requires `--team-name`** even when updating by `--team-id`; omitting it returns a validation error. ## Worked example ```bash # Idempotent HR-sync upsert: create "Payments" or reset its membership if it already exists -fduty team upsert --team-name "Payments" \ +flashduty team upsert --team-name "Payments" \ --description "Payments engineering" \ --person-ids 2001,2002,2003 \ --reset-if-name-exist \ diff --git a/skills/flashduty/reference/template.md b/skills/flashduty/reference/template.md index 41e440e..5f01e07 100644 --- a/skills/flashduty/reference/template.md +++ b/skills/flashduty/reference/template.md @@ -1,4 +1,4 @@ -# fduty template — command card +# flashduty template — command card Prereq: `SKILL.md` read. Read verbs are free. `create`, `update`, `delete` mutate account-wide notification templates — confirm before running. `delete ` is **irreversible**. @@ -29,25 +29,25 @@ channel — but read the clearing caveat under Gotchas before you try to empty o ```bash # 1. Fetch the built-in preset as a starting point (channel enum below) -fduty template get-preset --channel feishu --output-format toon +flashduty template get-preset --channel feishu --output-format toon # 2. Save the source, edit in an editor, then validate from file -fduty template validate --channel feishu --file ./feishu.tpl +flashduty template validate --channel feishu --file ./feishu.tpl # 3. Preview with a real incident for realistic rendering (no file — inline content) -fduty template preview \ +flashduty template preview \ --type feishu \ --content "$(cat ./feishu.tpl)" \ --incident-id # 4. Create the template (template-name unique per account) -fduty template create \ +flashduty template create \ --template-name "Critical-Feishu-v2" \ --feishu "$(cat ./feishu.tpl)" \ --team-id 0 # 5. Verify -fduty template info --output-format toon +flashduty template info --output-format toon ``` ## Hot flow — change one channel on an existing template @@ -59,25 +59,25 @@ fduty template info --output-format toon T= # POSITIONAL on update/info/delete; --template-name always required # 1. Pull the current source of the channel you are changing -fduty template info "$T" --json > /tmp/tpl.json +flashduty template info "$T" --json > /tmp/tpl.json jq -r '.feishu_app' /tmp/tpl.json > /tmp/feishu_app.tpl # …edit /tmp/feishu_app.tpl… # 2. Preview the edited source against a REAL incident before writing jq -n --rawfile c /tmp/feishu_app.tpl \ '{type:"feishu_app", content:$c, incident_id:""}' \ - | fduty template preview --data - + | flashduty template preview --data - # 3. Write that one channel. NEVER move the body through "$(...)": command substitution # strips every trailing newline, so a body ending in a blank line is silently shortened. jq -n --rawfile feishu_app /tmp/feishu_app.tpl \ --arg t "$T" --arg n "" \ '{template_id:$t, template_name:$n, feishu_app:$feishu_app}' \ - | fduty template update --data - + | flashduty template update --data - # 4. Verify the body round-tripped byte-for-byte — cmp catches a silent truncation that # "the field is still non-empty" would not. -fduty template info "$T" --json | jq -r '.feishu_app' | cmp - /tmp/feishu_app.tpl \ +flashduty template info "$T" --json | jq -r '.feishu_app' | cmp - /tmp/feishu_app.tpl \ && echo "round-trip OK" ``` @@ -85,7 +85,7 @@ Every channel you did not name is untouched — that is the server contract now, Verify the body you wrote anyway: `cmp` is what separates "wrote the right bytes" from "wrote something non-empty". - + ### create Create a template @@ -219,8 +219,8 @@ Note: `create` / `update` flags use **hyphenated** names (`--dingtalk-app`, `--f it.) `--template-name` is still required on every update even when unchanged. - **To CLEAR a channel you must send it as an explicit empty string** — omitting it now means "keep", not "clear". `--dingtalk-app ''` is the intent, but a flag set to the empty - string was dropped before it reached the wire in `fduty` **older than v1.4.2**, which - makes clearing a silent no-op on those builds. Check `fduty --version` first; if it is + string was dropped before it reached the wire in `flashduty` **older than v1.4.2**, which + makes clearing a silent no-op on those builds. Check `flashduty --version` first; if it is older, clear via `--data` with the field spelled out — `--data '{"template_id":"…", "template_name":"…","dingtalk_app":""}'` — and confirm with `info --json` that the channel actually went empty. @@ -232,7 +232,7 @@ Note: `create` / `update` flags use **hyphenated** names (`--dingtalk-app`, `--f - **`--feishu-app-card-v2-table-enabled` uses pointer semantics on `update`** — unlike the plain string channel-content flags, it patches the table-rendering setting only when the flag is explicitly passed; omit it to leave the existing setting untouched. It is a plain bool on `create` (no prior setting to preserve). - **`list` returns every channel's full template source for every row** — a few dozen templates blow past a tool-output cap in one call. Never render it directly: go to a - file and project. `fduty template list --limit 100 --json > /tmp/tpl_list.json && jq -r + file and project. `flashduty template list --limit 100 --json > /tmp/tpl_list.json && jq -r '.items[] | [.template_id, .template_name, .team_id] | @tsv' /tmp/tpl_list.json`. - **`delete` is permanent.** The built-in preset (`template_id = 000000000000000000000001`) can be addressed by that sentinel ID in `info` and `delete` — don't delete it. - **`validate` reads from a local `--file`; `preview` takes inline `--content`.** They are complementary: `validate` gives size-vs-limit diagnostics; `preview` renders against real or mock incident data. @@ -243,9 +243,9 @@ Note: `create` / `update` flags use **hyphenated** names (`--dingtalk-app`, `--f ```bash # Browse variables available in templates, then validate a draft -fduty template variables --category core --output-format toon -fduty template validate --channel slack --file ./slack-draft.tpl --incident +flashduty template variables --category core --output-format toon +flashduty template validate --channel slack --file ./slack-draft.tpl --incident # On success, create it -fduty template create --template-name "Ops-Slack-Alert" --slack "$(cat ./slack-draft.tpl)" +flashduty template create --template-name "Ops-Slack-Alert" --slack "$(cat ./slack-draft.tpl)" # → returns template_id; assign it to a channel in the escalation policy UI. ``` diff --git a/skills/flashduty/scripts/incident-summary.sh b/skills/flashduty/scripts/incident-summary.sh index b0d64c4..347c453 100644 --- a/skills/flashduty/scripts/incident-summary.sh +++ b/skills/flashduty/scripts/incident-summary.sh @@ -9,7 +9,7 @@ # # Section ⑥ lists recent post-mortems account-wide. To scope them to THIS incident's # channel, read channel_id from the projected detail section and re-run: -# fduty incident post-mortem-list --channel-ids +# flashduty incident post-mortem-list --channel-ids # # Note: errexit (-e) is intentionally NOT set — every section must run even if one # command fails, so the summary stays as complete as possible. Each command's own @@ -25,7 +25,7 @@ fi # Project detail explicitly because its default table includes unbounded narrative # fields. The other read verbs use their compact default renderers; raw toon would # dump every empty field plus heavy blobs like a change's labels.steps. -run() { echo "===== fduty $* ====="; fduty "$@" 2>&1; echo; } +run() { echo "===== flashduty $* ====="; flashduty "$@" 2>&1; echo; } run incident detail "$ID" --fields incident_id,num,title,incident_severity,progress,ai_summary,root_cause,resolution,alert_cnt,start_time,channel_id,detail_url --output-format toon # ① 详情 + AI summary + alert counts + channel run incident alerts "$ID" # ② contributing alerts @@ -38,7 +38,7 @@ run change list --since 24h # ③ correlated changes (shared l # start_time falls within ±15 min of this one's. Alert grouping runs per channel, so one root # cause spanning several services/channels arrives as several incidents; this is the only # section that looks sideways at them. The reference incident itself appears in the list. -START_RAW=$(fduty incident detail "$ID" --json 2>/dev/null | jq -r '.start_time // empty') +START_RAW=$(flashduty incident detail "$ID" --json 2>/dev/null | jq -r '.start_time // empty') START_TS="" if [ -n "$START_RAW" ]; then # The CLI renders start_time as RFC3339 in its local zone. GNU date (sandbox / Linux runners) @@ -51,6 +51,6 @@ if [ -n "$START_TS" ]; then echo "# ⑦ concurrent incidents: every incident in this account (any channel, any progress) started within ±15 min of $START_RAW — includes $ID itself; the trailing note carries the window total" run incident list --since "$((START_TS - 900))" --until "$((START_TS + 900))" --limit 50 --fields incident_id,num,title,incident_severity,progress,start_time,channel_id --output-format toon else - echo "===== ⑦ concurrent incidents: SKIPPED — could not read start_time of $ID; run: fduty incident list --since --until --limit 50 --output-format toon =====" + echo "===== ⑦ concurrent incidents: SKIPPED — could not read start_time of $ID; run: flashduty incident list --since --until --limit 50 --output-format toon =====" echo fi From 543a68b02ca9cf491d1a9dfb516285e3c9870854 Mon Sep 17 00:00:00 2001 From: ysyneu Date: Thu, 8 Oct 2026 05:16:13 -0700 Subject: [PATCH 2/2] fix: update replaces the running binary under its own name `update` now resolves the running binary (os.Executable, symlinks resolved) and passes its directory and file name to the installer as FLASHDUTY_INSTALL_DIR and INSTALLED_NAME, so a copy installed under another name or directory is replaced in place instead of a fresh default-named copy being installed elsewhere. install.ps1 honors INSTALLED_NAME and moves an existing .exe aside before installing, since Windows cannot overwrite a running executable. Also: treat "$" in the invoked name literally when rewriting help text, strip the ".exe" suffix case-insensitively, and note why the version line keeps its fixed "flashduty version" prefix. --- cmd/flashduty/main.go | 5 ++++- install.ps1 | 13 +++++++++++-- install.sh | 1 + internal/cli/root.go | 2 +- internal/cli/root_test.go | 4 ++++ internal/cli/update.go | 13 ++++++++++++- internal/cli/version.go | 2 ++ internal/update/check.go | 18 ++++++++++++++---- internal/update/check_test.go | 32 ++++++++++++++++++++------------ 9 files changed, 69 insertions(+), 21 deletions(-) diff --git a/cmd/flashduty/main.go b/cmd/flashduty/main.go index 4e6c240..d9128ab 100644 --- a/cmd/flashduty/main.go +++ b/cmd/flashduty/main.go @@ -33,7 +33,10 @@ func main() { // The CLI names itself after the command word the user typed, so a copy // installed under another name (install.sh INSTALLED_NAME) shows that name // in help, errors and completion scripts. - name := strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe") + name := filepath.Base(os.Args[0]) + if ext := filepath.Ext(name); strings.EqualFold(ext, ".exe") { + name = strings.TrimSuffix(name, ext) + } if name == "" || name == "." || name == string(filepath.Separator) { name = "flashduty" } diff --git a/install.ps1 b/install.ps1 index cc9e418..96a1827 100644 --- a/install.ps1 +++ b/install.ps1 @@ -4,6 +4,7 @@ # Environment variables: # FLASHDUTY_VERSION - specific version to install (e.g. "v0.1.2") # FLASHDUTY_INSTALL_DIR - install directory (default: $HOME\.flashduty\bin) +# INSTALLED_NAME - installed command name (default: flashduty) # MIRROR_URL - fetch release assets from this https mirror prefix. # Default: https://static.flashcat.cloud/flashduty-cli. # The mirror must replicate @@ -17,7 +18,8 @@ $ErrorActionPreference = "Stop" $Repo = "flashcatcloud/flashduty-cli" $Binary = "flashduty-cli.exe" -$InstalledName = "flashduty.exe" +$CommandName = if ($env:INSTALLED_NAME) { $env:INSTALLED_NAME -replace '\.exe$', '' } else { "flashduty" } +$InstalledName = "$CommandName.exe" # By default release downloads are fetched from the Flashcat CDN. Set MIRROR_URL # to another prefix to override, or to an empty string to force GitHub fallback. @@ -153,6 +155,13 @@ try { } $DestPath = Join-Path $InstallDir $InstalledName + if (Test-Path $DestPath) { + # A running .exe can't be overwritten but can be renamed: move it aside + # so `update` can replace the binary it is running from. + $OldPath = "$DestPath.old" + Remove-Item -Path $OldPath -Force -ErrorAction SilentlyContinue + Move-Item -Path $DestPath -Destination $OldPath -Force + } Move-Item -Path $BinaryPath -Destination $DestPath -Force Write-Info "Installed to $DestPath" @@ -165,7 +174,7 @@ try { Write-Info "Added $InstallDir to user PATH (restart your terminal for it to take effect)" } - Write-Info "Run 'flashduty version' to verify" + Write-Info "Run '$CommandName version' to verify" } finally { Remove-Item -Path $TmpDir -Recurse -Force -ErrorAction SilentlyContinue } diff --git a/install.sh b/install.sh index c08e323..ee7527e 100755 --- a/install.sh +++ b/install.sh @@ -5,6 +5,7 @@ # Environment: # FLASHDUTY_VERSION Install a specific version (e.g. v0.1.2). Default: latest. # FLASHDUTY_INSTALL_DIR Install directory. Default: /usr/local/bin. +# INSTALLED_NAME Installed command name. Default: flashduty. # MIRROR_URL Fetch release assets from this https mirror prefix. # Default: https://static.flashcat.cloud/flashduty-cli. # The mirror must replicate diff --git a/internal/cli/root.go b/internal/cli/root.go index 2ad094d..d89a676 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -202,7 +202,7 @@ var cliWord = regexp.MustCompile(`(^|[^\w./~-])flashduty `) // renameCLI replaces every command-word "flashduty" in s with name. func renameCLI(s, name string) string { - return cliWord.ReplaceAllString(s, "${1}"+name+" ") + return cliWord.ReplaceAllString(s, "${1}"+strings.ReplaceAll(name, "$", "$$")+" ") } // newClient creates a go-flashduty client using the current factory. diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 5938249..a1e04c5 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -27,6 +27,10 @@ func TestRenameCLI(t *testing.T) { t.Errorf("renameCLI(%q) = %q, want %q", c.in, got, c.want) } } + // "$" in the name is literal text, not a regexp group reference. + if got, want := renameCLI("run flashduty login", "fd$1"), "run fd$1 login"; got != want { + t.Errorf("renameCLI with $ in name = %q, want %q", got, want) + } } func TestRenameCommandText(t *testing.T) { diff --git a/internal/cli/update.go b/internal/cli/update.go index 7b0d20e..16c5151 100644 --- a/internal/cli/update.go +++ b/internal/cli/update.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "runtime" "github.com/spf13/cobra" @@ -54,13 +55,23 @@ func newUpdateCmd() *cobra.Command { } func runInstaller(cmd *cobra.Command) error { + // The installer replaces the binary that is running, under its own name + // and directory, not a default-named copy elsewhere. + binPath, err := os.Executable() + if err == nil { + binPath, err = filepath.EvalSymlinks(binPath) + } + if err != nil { + return fmt.Errorf("locate the running binary: %w", err) + } + name, args := installerCommandSpec(runtime.GOOS, update.InstallShellURL(), update.InstallPowerShellURL()) c := exec.Command(name, args...) c.Stdout = cmd.OutOrStdout() c.Stderr = cmd.ErrOrStderr() c.Stdin = os.Stdin - c.Env = update.InstallerEnv(os.Environ()) + c.Env = update.InstallerEnv(os.Environ(), binPath) if err := c.Run(); err != nil { return fmt.Errorf("update failed: %w", err) diff --git a/internal/cli/version.go b/internal/cli/version.go index e7c72c0..08ad9ff 100644 --- a/internal/cli/version.go +++ b/internal/cli/version.go @@ -45,6 +45,8 @@ func newVersionCmd() *cobra.Command { _, _ = fmt.Fprintln(out, string(b)) return } + // Fixed "flashduty version" prefix whatever the invoked name: tests, + // the issue template and external tooling match on it. _, _ = fmt.Fprintf(out, "flashduty version %s (%s) built %s\n", versionStr, commitStr, dateStr) }, } diff --git a/internal/update/check.go b/internal/update/check.go index ffc71ed..102412c 100644 --- a/internal/update/check.go +++ b/internal/update/check.go @@ -54,15 +54,25 @@ func UpdateBaseURL() string { func InstallShellURL() string { return UpdateBaseURL() + "/install.sh" } func InstallPowerShellURL() string { return UpdateBaseURL() + "/install.ps1" } -func InstallerEnv(base []string) []string { - env := make([]string, 0, len(base)+1) +// InstallerEnv returns base with the installer's MIRROR_URL, +// FLASHDUTY_INSTALL_DIR and INSTALLED_NAME pointed at the update source and at +// binPath, the running binary, so the installer replaces that binary in place +// instead of installing a default-named copy elsewhere. +func InstallerEnv(base []string, binPath string) []string { + env := make([]string, 0, len(base)+3) for _, item := range base { - if strings.HasPrefix(item, "MIRROR_URL=") { + if strings.HasPrefix(item, "MIRROR_URL=") || + strings.HasPrefix(item, "FLASHDUTY_INSTALL_DIR=") || + strings.HasPrefix(item, "INSTALLED_NAME=") { continue } env = append(env, item) } - return append(env, "MIRROR_URL="+UpdateBaseURL()) + return append(env, + "MIRROR_URL="+UpdateBaseURL(), + "FLASHDUTY_INSTALL_DIR="+filepath.Dir(binPath), + "INSTALLED_NAME="+filepath.Base(binPath), + ) } func latestPointerURL() string { diff --git a/internal/update/check_test.go b/internal/update/check_test.go index 51731af..da3fe6b 100644 --- a/internal/update/check_test.go +++ b/internal/update/check_test.go @@ -300,23 +300,31 @@ func TestUpdateBaseURLAndInstallerURLs(t *testing.T) { } } -func TestInstallerEnvPassesUpdateBaseAsMirrorURL(t *testing.T) { +func TestInstallerEnvTargetsUpdateBaseAndRunningBinary(t *testing.T) { t.Setenv("FLASHDUTY_UPDATE_BASE_URL", "https://mirror.example.com/fduty/") t.Setenv("MIRROR_URL", "") - env := InstallerEnv([]string{"PATH=/bin", "MIRROR_URL=https://old.example.com"}) - want := "MIRROR_URL=https://mirror.example.com/fduty" - found := 0 - for _, item := range env { - if strings.HasPrefix(item, "MIRROR_URL=") { - found++ - if item != want { - t.Fatalf("MIRROR_URL entry = %q, want %q", item, want) + binPath := filepath.Join("opt", "tools", "fduty") + env := InstallerEnv([]string{ + "PATH=/bin", + "MIRROR_URL=https://old.example.com", + "FLASHDUTY_INSTALL_DIR=/usr/local/bin", + "INSTALLED_NAME=flashduty", + }, binPath) + for key, want := range map[string]string{ + "MIRROR_URL": "https://mirror.example.com/fduty", + "FLASHDUTY_INSTALL_DIR": filepath.Join("opt", "tools"), + "INSTALLED_NAME": "fduty", + } { + var got []string + for _, item := range env { + if v, ok := strings.CutPrefix(item, key+"="); ok { + got = append(got, v) } } - } - if found != 1 { - t.Fatalf("found %d MIRROR_URL entries, want 1 in %#v", found, env) + if len(got) != 1 || got[0] != want { + t.Errorf("%s entries = %q, want exactly [%q]", key, got, want) + } } }