Skip to content

Commit 098b751

Browse files
authored
Merge pull request #222 from flashcatcloud/codex/public-integration-contract-cli-20261012
fix(integrations): sync public API contract and skill commands
2 parents 84e7bb5 + 05f5245 commit 098b751

6 files changed

Lines changed: 251 additions & 6 deletions

File tree

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/flashcatcloud/flashduty-cli
33
go 1.26.0
44

55
require (
6-
github.com/flashcatcloud/go-flashduty v0.15.15
6+
github.com/flashcatcloud/go-flashduty v0.15.16
77
github.com/mattn/go-runewidth v0.0.30
88
github.com/spf13/cobra v1.10.2
99
github.com/spf13/pflag v1.0.10

‎go.sum‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
22
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
33
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
4-
github.com/flashcatcloud/go-flashduty v0.15.15 h1:jMgWfpuXqdyHArSvUtf9ATwFFB7s49hgN7Rv+c9PxAY=
5-
github.com/flashcatcloud/go-flashduty v0.15.15/go.mod h1:YpHiTYXR5NXBI/rGRZfUy537XMkhdCkwA8NW1QoRHwk=
4+
github.com/flashcatcloud/go-flashduty v0.15.16 h1:h04Z0NGxB4ujlfKliVPD8QhgLgBObeAoRMz9rXJpnes=
5+
github.com/flashcatcloud/go-flashduty v0.15.16/go.mod h1:YpHiTYXR5NXBI/rGRZfUy537XMkhdCkwA8NW1QoRHwk=
66
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
77
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
88
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package cli
2+
3+
import (
4+
"encoding/json"
5+
"reflect"
6+
"testing"
7+
)
8+
9+
func integrationContractFixture() map[string]any {
10+
return map[string]any{
11+
"integration_id": 42, "data_source_id": 42, "account_id": 100, "exclusive_data_source_id": 0,
12+
"name": "Contract fixture", "description": "", "team_id": 7,
13+
"category": "im", "plugin_type": "slack_app", "plugin_type_name": "Slack", "plugin_id": 9,
14+
"ref_id": "a_100", "status": "enabled", "created_at": 1700000000, "updated_at": 1700000001, "last_time": 0,
15+
"creator_id": 11, "updated_by": 12, "no_editable": false, "editable": true,
16+
"settings": map[string]any{"headers": map[string]any{"Authorization": "fixture-not-a-real-secret"}},
17+
"integration_key": "fixture-not-a-real-key",
18+
}
19+
}
20+
21+
func TestIntegrationListPreservesConfigurationAndPermissions(t *testing.T) {
22+
saveAndResetGlobals(t)
23+
stub := newGFStub(t)
24+
fixture := integrationContractFixture()
25+
stub.data = map[string]any{"p": 1, "limit": 100, "total": 1, "items": []any{fixture}}
26+
out, err := execCommand("integration", "list", "--json")
27+
if err != nil {
28+
t.Fatal(err)
29+
}
30+
var result map[string]any
31+
if err := json.Unmarshal([]byte(out), &result); err != nil {
32+
t.Fatal(err)
33+
}
34+
items, ok := result["items"].([]any)
35+
if !ok || len(items) != 1 {
36+
t.Fatalf("unexpected list output: %s", out)
37+
}
38+
item := items[0].(map[string]any)
39+
for field := range fixture {
40+
if _, ok := item[field]; !ok {
41+
t.Errorf("list dropped backend field %s", field)
42+
}
43+
}
44+
for _, field := range []string{"settings", "integration_key", "editable", "no_editable"} {
45+
if !reflect.DeepEqual(item[field], fixture[field]) {
46+
t.Errorf("%s changed: got %#v, want %#v", field, item[field], fixture[field])
47+
}
48+
}
49+
if stub.lastPath != "/integration/list" {
50+
t.Fatalf("request path = %s", stub.lastPath)
51+
}
52+
}
53+
54+
func TestIntegrationInfoPreservesConfigurationAndTestResult(t *testing.T) {
55+
for _, withTest := range []bool{false, true} {
56+
t.Run(map[bool]string{false: "absent test", true: "failed test"}[withTest], func(t *testing.T) {
57+
saveAndResetGlobals(t)
58+
stub := newGFStub(t)
59+
fixture := integrationContractFixture()
60+
for _, field := range []string{"account_id", "data_source_id", "exclusive_data_source_id"} {
61+
delete(fixture, field)
62+
}
63+
if withTest {
64+
fixture["test"] = map[string]any{"success": false, "message": "fixture plugin test failed"}
65+
}
66+
stub.data = fixture
67+
out, err := execCommand("integration", "info", "42", "--json")
68+
if err != nil {
69+
t.Fatal(err)
70+
}
71+
var result map[string]any
72+
if err := json.Unmarshal([]byte(out), &result); err != nil {
73+
t.Fatal(err)
74+
}
75+
for _, field := range []string{"settings", "integration_key", "plugin_id", "editable", "creator_id", "updated_by", "no_editable"} {
76+
if _, ok := result[field]; !ok {
77+
t.Errorf("info dropped backend field %s", field)
78+
}
79+
}
80+
if !reflect.DeepEqual(result["settings"], fixture["settings"]) || result["integration_key"] != fixture["integration_key"] {
81+
t.Errorf("info changed configuration or key: %s", out)
82+
}
83+
if !reflect.DeepEqual(result["test"], fixture["test"]) {
84+
t.Errorf("test result changed: %s", out)
85+
}
86+
if !withTest {
87+
if _, ok := result["test"]; ok {
88+
t.Errorf("absent test was fabricated: %s", out)
89+
}
90+
}
91+
})
92+
}
93+
}
94+
95+
func TestIntegrationCreatePreservesKeyAndPluginTest(t *testing.T) {
96+
for _, mode := range []string{"data", "flags"} {
97+
t.Run(mode, func(t *testing.T) {
98+
saveAndResetGlobals(t)
99+
stub := newGFStub(t)
100+
stub.data = map[string]any{"integration_id": 42, "integration_key": "fixture-key", "test": map[string]any{"success": false, "message": "fixture plugin test failed"}}
101+
args := []string{"integration", "create", "--json", "--data", `{"plugin_type":"standard.alert","integration_key":"00000000000000000000000000000000100","is_test":true}`}
102+
if mode == "flags" {
103+
args = []string{"integration", "create", "--json", "--plugin-type", "standard.alert", "--integration-key", "00000000000000000000000000000000100", "--is-test"}
104+
}
105+
out, err := execCommand(args...)
106+
if err != nil {
107+
t.Fatal(err)
108+
}
109+
if stub.lastBody["integration_key"] != "00000000000000000000000000000000100" || stub.lastBody["is_test"] != true {
110+
t.Errorf("create dropped key or test flag: %#v", stub.lastBody)
111+
}
112+
var result map[string]any
113+
if err := json.Unmarshal([]byte(out), &result); err != nil {
114+
t.Fatal(err)
115+
}
116+
if !reflect.DeepEqual(result["test"], stub.data.(map[string]any)["test"]) {
117+
t.Errorf("create lost failed plugin test: %s", out)
118+
}
119+
})
120+
}
121+
}
122+
123+
func TestIntegrationUpdatePreservesTestAndExplicitEmptyHeaders(t *testing.T) {
124+
saveAndResetGlobals(t)
125+
stub := newGFStub(t)
126+
_, err := execCommand("integration", "update", "42", "--team-id", "0", "--is-test", "--data", `{"settings":{"headers":{}}}`)
127+
if err != nil {
128+
t.Fatal(err)
129+
}
130+
if stub.lastBody["is_test"] != true || stub.lastBody["team_id"] != float64(0) {
131+
t.Errorf("update lost test flag or explicit team clear: %#v", stub.lastBody)
132+
}
133+
want := map[string]any{"headers": map[string]any{}}
134+
if !reflect.DeepEqual(stub.lastBody["settings"], want) {
135+
t.Errorf("update changed explicit empty headers: %#v", stub.lastBody)
136+
}
137+
}

‎skills/flashduty/SKILL.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: flashduty
33
version: "3.0"
4-
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/<domain>.md for exact verbs & flags BEFORE running flashduty. Don't guess or --help-dance."
4+
description: "USE FIRST for Flashduty tasks — status pages, incidents, alerts, integrations, on-call, monitors, automations, RUM, sourcemaps, members. `flashduty` CLI = the whole API. ALWAYS load this skill + read reference/<domain>.md for exact verbs & flags BEFORE running flashduty. Don't guess or --help-dance."
55
allowed-tools: bash, read
66
hidden: true # internal-only: withheld from skills.sh public discovery (Safari embeds this skill directly).
77
---
@@ -76,7 +76,8 @@ Some asks span several commands. For those the skill ships a script that fetches
7676
| datasource 数据源 = a system Flashmonit queries (Prometheus / Loki / SQL / SLS) / connect a datasource 连接数据源 / SLS project / logstore / Redis / MongoDB / Kafka / database overview 数据库概览 / locks 锁 / slowlog 慢日志 | **`reference/monit-datasource.md`** |
7777
| automation / 自动化 / 定时 AI SRE / scheduled AI task / daily brief / weekly report / webhook trigger / POST trigger / chat-created automation | **`reference/automation.md`** |
7878
| metric/log query / 指标查询 / 日志查询 / PromQL / LogsQL / SQL / trend 趋势 / log clustering 日志聚类 / datasource RCA 数据源排查 | **`reference/monit-query.md`** |
79-
| channel / 协作空间 / collaboration space / 频道 / integration 集成 / 告警来源 alert source / alert grouping 告警分组 | **`reference/channel.md`** |
79+
| public integration / 公共集成 / 集成中心 / integration 集成 / alert source 告警来源 / integration key 集成 Key / IM bot / webhook configuration | **`reference/integration.md`** |
80+
| channel / 协作空间 / collaboration space / 频道 / channel-owned dedicated integration 协作空间专属集成 / alert grouping 告警分组 | **`reference/channel.md`** |
8081
| dispatch rule 分派策略 / 分派规则 / escalation rule 升级规则 / notify layers 通知层级 / who gets paged | **`reference/escalation.md`** |
8182
| silence 静默 / 屏蔽 / inhibit 抑制 / drop rule 丢弃 / noise reduction 降噪 / maintenance silence 维护窗口静默 | **`reference/noise.md`** |
8283
| enrichment / 数据加工 / 富化 / label mapping 字段映射 / extraction 提取 / mapping schema 集成 schema | **`reference/enrichment.md`** |

‎skills/flashduty/reference/channel.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,5 +133,5 @@ Escalation, silence, inhibit and unsubscribe rules live on their own cards: `ref
133133

134134
- **`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 <channel-id>` shows the shorter positional form; the flag is an equally valid alternative, and if both are given the flag value wins.
135135
- **`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.
136-
- **`--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.
136+
- **`--plugin-ids` subscribes integrations that already exist.** Public integration creation, configuration, and inbound keys are available through the `integration` commands; read `reference/integration.md`. Channel-owned dedicated integrations are outside that public API surface and are managed in the console. `channel info` reports the channel's own fields, not integration credentials.
137137
- **`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.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# flashduty integration — command card
2+
3+
Prereq: read `SKILL.md`. These commands manage **public/account-level integrations**, including supported alert, change, IM, and webhook types. They do not manage channel-owned dedicated integrations (`c_` references); use the console for those. For channel configuration and subscriptions, read `reference/channel.md`.
4+
5+
## Read before changing configuration
6+
7+
- Start with `type-list` and `list`. Create only a `plugin_type` whose type entry has `supports_api_create: true`.
8+
- List/info return stored `settings` and decrypted `integration_key` without masking. Prefer `--fields` projections for discovery; do not paste secrets into a report or unrelated tool. An integration key authenticates inbound pushes; it is not the account App Key used by this CLI.
9+
- `editable` describes object permission; writes also require Integrations Manage. `no_editable: true` marks a read-only integration. Do not infer write authorization from read access.
10+
- Before update/delete or key rotation, get per-target confirmation and save `info` to the protected workspace backup described in `SKILL.md`. Confirm references before deletion. Rotating the key invalidates the old key immediately; update the sender configuration as part of the approved change.
11+
- Metadata-only updates preserve omitted fields. `--team-id 0` deliberately clears team ownership. Submitted `settings` replace type-specific configuration; known sensitive fields omitted or set to `******` preserve their stored value. An explicit `headers: {}` clears headers.
12+
- `--is-test` on create, or on update with `settings`, returns plugin test results. Inspect `test.success` and `test.message`: HTTP success is not plugin-test success. An absent `test` means no test result was returned, not a successful test.
13+
14+
## Discovery
15+
16+
```bash
17+
flashduty integration type-list --category event.alert --output-format toon
18+
flashduty integration list --fields integration_id,name,plugin_type,team_id,editable,no_editable --output-format toon
19+
flashduty integration info 42 --fields integration_id,name,plugin_type,team_id,editable,no_editable --output-format toon
20+
```
21+
22+
## Approved configuration changes
23+
24+
Use the exact type-specific settings provided for the target integration; do not guess credential keys. `--integration-key` is optional on create; omitting it generates a key. If provided, it must satisfy the account suffix, length, and uniqueness checks.
25+
26+
```bash
27+
# Only after confirmation; inspect the returned integration_id/key and test result.
28+
flashduty integration create --plugin-type standard.alert --name "Production alerts" --is-test --data '{"settings":{}}' --json
29+
# Only after backing up integration 42 and confirming this rename.
30+
flashduty integration update 42 --name "Production alerts (primary)" --json
31+
```
32+
33+
Do not add `--is-test` to a metadata-only update expecting a plugin test; settings must be submitted for an update test. Disable/enable preserve configuration. Delete may fail with `ReferenceExist` when other objects still reference the integration.
34+
35+
<!-- GENERATED:integration START · 由 flashduty __dump-commands 同步 · 勿手改 fence 内 -->
36+
37+
### create
38+
Create integration
39+
- `--description` string — Free-form description, at most 499 characters. (≤499 chars)
40+
- `--integration-key` string — Optional pre-generated key: 35 characters, ending with the account ID modulo 1000 padded to three digits; it must not already be in use. Omit to generate a key. (35-35 chars)
41+
- `--is-test` bool — Run the plugin test during creation; false when omitted. Inspect the returned 'test.success' for the test outcome.
42+
- `--name` string — Integration name, 2–49 characters when nonempty; defaults to the integration type name when omitted or empty. (2-49 chars)
43+
- `--plugin-type` string (required) — Integration type. Must be one listed by 'POST /integration/type/list' with 'supports_api_create: true'.
44+
- `--team-id` int64 — Owning team ID; omit or use '0' for no team. A nonzero ID must belong to the current account. (min 0)
45+
- body-only (`--data`): settings (object)
46+
- response: single object (`data` unwrapped to the top level) — fields: integration_id (integer); integration_key (string); test (object)
47+
48+
### delete <integration-id>
49+
Delete integration
50+
- `<integration-id>` (positional, required) int64 — Integration ID. (min 1)
51+
52+
### disable <integration-id>
53+
Disable integration
54+
- `<integration-id>` (positional, required) int64 — Integration ID. (min 1)
55+
56+
### enable <integration-id>
57+
Enable integration
58+
- `<integration-id>` (positional, required) int64 — Integration ID. (min 1)
59+
60+
### info <integration-id>
61+
Get integration detail
62+
- `<integration-id>` (positional, required) int64 — Integration ID. (min 1)
63+
- response: single object (`data` unwrapped to the top level) — fields: category (string); created_at (string); creator_id (integer); description (string); editable (boolean); integration_id (integer); integration_key (string); last_time (string); name (string); no_editable (boolean); plugin_id (integer); plugin_type (string); plugin_type_name (string); ref_id (string); settings (object); status (string); team_id (integer); test (object); updated_at (string); updated_by (integer)
64+
65+
### key-rotate <integration-id>
66+
Rotate integration key
67+
- `<integration-id>` (positional, required) int64 — Integration ID. (min 1)
68+
- response: single object (`data` unwrapped to the top level) — fields: integration_key (string)
69+
70+
### list
71+
List integrations
72+
- `--asc` bool — Sort ascending when true, descending when false.
73+
- `--category` string — Filter by category. Accepts a comma-separated list.
74+
- `--is-my-team` bool — Limit the result to integrations owned by your teams.
75+
- `--limit` int64 — Page size. Defaults to 100, maximum 100. (1-100)
76+
- `--name` string — Filter by integration name.
77+
- `--orderby` string — Sort field. Defaults to 'created_at'; 'plugin_type' is sorted by the underlying plugin. · enum: created_at | updated_at | name | plugin_type | status
78+
- `--page` int64 — Page number, 1-based. (min 1)
79+
- `--plugin-type` string — Filter by integration type. Accepts a comma-separated list.
80+
- `--ref-ids` stringSlice — Filter by source reference IDs. Each value must start with 'c_' (channel), 'a_' (account) or 'w_'.
81+
- `--search-after-ctx` string
82+
- `--status` string — Filter by status. Accepts a comma-separated list.
83+
- `--team-ids` intSlice — Filter by team IDs. With 'is_my_team', the values narrow that set further.
84+
- `--type` string — Deprecated. Merged into 'plugin_type' when both are set.
85+
- response: `{items: [...], limit, p, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items fields: account_id (integer); category (string); created_at (string); creator_id (integer); data_source_id (integer); description (string); editable (boolean); exclusive_data_source_id (integer); integration_id (integer); integration_key (string); last_time (string); name (string); no_editable (boolean); plugin_id (integer); plugin_type (string); plugin_type_name (string); ref_id (string); settings (object); status (string); team_id (integer); updated_at (string); updated_by (integer)
86+
87+
### type-list
88+
List integration types
89+
- `--asc` bool — Sort ascending when 'true' (the default); descending when 'false'.
90+
- `--category` string — Filter by category. Accepts a comma-separated list, for example 'event.alert,event.change'.
91+
- `--limit` int64 — Page size. Defaults to 20, maximum 100. (1-100)
92+
- `--orderby` string — Sort field. When omitted, types are returned in console ranking order. · enum: id | created_at | updated_at | name | type
93+
- `--page` int64 — Page number, 1-based. (min 1)
94+
- `--search-after-ctx` string
95+
- response: `{items: [...], limit, p, total}` page wrapper — pipe `--json | jq '.items[]'` (NOT top-level `.[]`) — items fields: category (string); plugin_type (string); plugin_type_logo_url (string); plugin_type_name (string); status (string); supports_api_create (boolean)
96+
97+
### update <integration-id>
98+
Update integration
99+
- `--description` string — New description, at most 499 characters. (≤499 chars)
100+
- `<integration-id>` (positional, required) int64 — Integration ID. (min 1)
101+
- `--is-test` bool — Run the plugin test when submitting 'settings'; no test is performed for a metadata-only update.
102+
- `--name` string — New name, 2–49 characters. (2-49 chars)
103+
- `--team-id` int64 — New owning team ID; '0' clears the team assignment. (min 0)
104+
- body-only (`--data`): settings (object)
105+
- response: same shape as `info <integration-id>` above
106+
107+
<!-- GENERATED:integration END -->

0 commit comments

Comments
 (0)