Skip to content

Commit f6eda3c

Browse files
authored
Merge pull request #227 from flashcatcloud/feat/im-chat-automation
feat: create automations bound to the current IM chat
2 parents 6cf6614 + f9dde4d commit f6eda3c

8 files changed

Lines changed: 216 additions & 32 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.18-0.20261015103612-f84d9ddaca1a
6+
github.com/flashcatcloud/go-flashduty v0.15.18-0.20261016025836-93c9482173b5
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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
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.18-0.20261015103612-f84d9ddaca1a h1:+oqnFVWaZlC7yGMu+wl0QpBwEOGdOG+a6hFagXSHYuM=
5-
github.com/flashcatcloud/go-flashduty v0.15.18-0.20261015103612-f84d9ddaca1a/go.mod h1:YpHiTYXR5NXBI/rGRZfUy537XMkhdCkwA8NW1QoRHwk=
4+
github.com/flashcatcloud/go-flashduty v0.15.17-0.20261014102327-2f2d5497df53 h1:v6tVZmttBD67/gcdQym1SmY1TavYXd95WU3AQAog9VQ=
5+
github.com/flashcatcloud/go-flashduty v0.15.17-0.20261014102327-2f2d5497df53/go.mod h1:YpHiTYXR5NXBI/rGRZfUy537XMkhdCkwA8NW1QoRHwk=
6+
github.com/flashcatcloud/go-flashduty v0.15.18-0.20261016025836-93c9482173b5 h1:e5ApVt+44vsgUvakvkDn51tjIBC53hRqHiw863O9iTw=
7+
github.com/flashcatcloud/go-flashduty v0.15.18-0.20261016025836-93c9482173b5/go.mod h1:YpHiTYXR5NXBI/rGRZfUy537XMkhdCkwA8NW1QoRHwk=
68
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
79
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
810
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=

‎internal/cli/automation.go‎

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ func newAutomationCreateCmd() *cobra.Command {
4949
promptFile string
5050
environmentKind string
5151
environmentID string
52+
runIn string
5253
timezone string
5354
)
5455

@@ -58,15 +59,31 @@ func newAutomationCreateCmd() *cobra.Command {
5859
Long: curatedLong(`Create an AI SRE Automation.
5960
6061
By default the rule is enabled. Use --disabled only when the user explicitly
61-
asks to create it disabled. team_id=0 means personal scope; --team-id >0 creates
62-
the rule under that team. The scope is immutable after creation.
62+
asks to create it disabled. Omit --team-id to let the server inherit the live
63+
IM conversation scope when available; --team-id 0 explicitly selects personal
64+
scope and --team-id >0 selects that team. The scope is immutable after creation.
65+
66+
When this command is invoked from a live IM conversation, omitting --run-in
67+
lets the server bind the Automation to that current chat. Use --run-in web to
68+
run it in the web console instead. Do not supply chat, platform, integration,
69+
or session identifiers: the server resolves and authorizes the current chat.
70+
Each ordinary current-chat run starts a fresh execution session and, where the
71+
IM integration creates threads, a fresh IM thread. It remains bound to the
72+
original chat and its immutable scope, rather than an old session or thread.
73+
Write the task prompt with the needed chat context; do not refer to an earlier
74+
session or say "above". A retry of the same run reuses that run's execution
75+
identity. If the bound chat's scope no longer matches the rule, the run is
76+
skipped. War-room Automations keep their existing behavior.
6377
6478
Schedule helpers build a 5-field cron expression evaluated in the rule's
6579
timezone. Pass --timezone when the user names one. When --timezone is
6680
omitted, leave it unset so the server uses the caller's member timezone,
6781
then the account timezone, then Asia/Shanghai. Do not send a timezone
6882
default from the CLI. Pass the user's local wall-clock time to --at or
6983
--cron-expr; do not convert it to UTC first.
84+
Custom cron uses five fields, but its minute must be one fixed integer from
85+
0 through 59. The fastest supported schedule is once per hour; minute
86+
wildcards, ranges, lists, and steps are unsupported.
7087
7188
For HTTP POST-only rules, pass --http-post-trigger without a schedule; the CLI
7289
sends a valid placeholder cron and disables the schedule trigger.`, "Automations", "RuleWriteCreate"),
@@ -86,6 +103,12 @@ the rule under that team. The scope is immutable after creation.
86103
if strings.TrimSpace(taskPrompt) == "" {
87104
return fmt.Errorf("one of --prompt or --prompt-file is required")
88105
}
106+
if cmd.Flags().Changed("run-in") {
107+
runIn = strings.TrimSpace(runIn)
108+
if runIn != "current-chat" && runIn != "web" {
109+
return fmt.Errorf("--run-in must be current-chat or web")
110+
}
111+
}
89112

90113
effectiveScheduleEnabled := scheduleEnabled
91114
cron, err := resolveAutomationCreateCron(cmd, schedule, at, weekday, cronExpr, httpPostTrigger, &effectiveScheduleEnabled)
@@ -95,7 +118,6 @@ the rule under that team. The scope is immutable after creation.
95118

96119
req := &flashduty.AutomationRuleCreateRequest{
97120
Name: name,
98-
TeamID: teamID,
99121
CronExpr: cron,
100122
Enabled: !disabled,
101123
ScheduleTriggerEnabled: flashduty.Bool(effectiveScheduleEnabled),
@@ -104,6 +126,12 @@ the rule under that team. The scope is immutable after creation.
104126
EnvironmentKind: strings.TrimSpace(environmentKind),
105127
EnvironmentID: strings.TrimSpace(environmentID),
106128
}
129+
if cmd.Flags().Changed("team-id") {
130+
req.TeamID = flashduty.Int64(teamID)
131+
}
132+
if cmd.Flags().Changed("run-in") {
133+
req.RunIn = flashduty.String(runIn)
134+
}
107135
if cmd.Flags().Changed("timezone") {
108136
req.Timezone = strings.TrimSpace(timezone)
109137
}
@@ -117,21 +145,23 @@ the rule under that team. The scope is immutable after creation.
117145
}
118146

119147
cmd.Flags().StringVar(&name, "name", "", "Automation name")
120-
cmd.Flags().Int64Var(&teamID, "team-id", 0, "Scope team ID; 0 means personal scope")
148+
cmd.Flags().Int64Var(&teamID, "team-id", 0, "Scope team ID; omit to inherit IM scope when available, or pass 0 for personal scope")
149+
cmd.Flags().StringVar(&runIn, "run-in", "", "Execution destination: current-chat or web; omit for the server default")
121150
cmd.Flags().StringVar(&schedule, "schedule", "", "Schedule helper: hourly, daily, weekly, or cron")
122151
cmd.Flags().StringVar(&at, "at", "", "Local time in HH:MM (rule's timezone); for hourly schedules, only the minute is used. "+automationTimezoneNote)
123152
cmd.Flags().StringVar(&weekday, "weekday", "", "Weekday for weekly schedules: sun, mon, tue, wed, thu, fri, sat, or 0-7")
124-
cmd.Flags().StringVar(&cronExpr, "cron-expr", "", "Exact 5-field cron expression in the rule's timezone; overrides --schedule helpers. "+automationTimezoneNote)
153+
cmd.Flags().StringVar(&cronExpr, "cron-expr", "", "Supported 5-field cron expression in the rule's timezone. The minute must be a fixed 0-59 integer, so the fastest schedule is hourly; overrides --schedule helpers. "+automationTimezoneNote)
125154
cmd.Flags().StringVar(&timezone, "timezone", "", "IANA timezone name cron_expr is evaluated in, for example Asia/Shanghai. The server must be able to load it; an invalid value is rejected. Omit to use the caller's member timezone, then the account timezone, then Asia/Shanghai.")
126155
cmd.Flags().BoolVar(&disabled, "disabled", false, "Create the Automation disabled")
127156
cmd.Flags().BoolVar(&scheduleEnabled, "schedule-enabled", true, "Whether the schedule trigger is enabled")
128157
cmd.Flags().BoolVar(&httpPostTrigger, "http-post-trigger", false, "Create and enable an HTTP POST trigger")
129-
cmd.Flags().StringVar(&prompt, "prompt", "", "Task prompt sent to the AI SRE agent")
158+
cmd.Flags().StringVar(&prompt, "prompt", "", "Self-contained task prompt for the fresh AI SRE execution; include needed chat context and do not refer to earlier messages or 'above'")
130159
cmd.Flags().StringVar(&promptFile, "prompt-file", "", "Read task prompt from a file, or - for stdin")
131160
cmd.Flags().StringVar(&environmentKind, "environment-kind", "", "Runtime environment kind: cloud or byoc; empty means automatic")
132161
cmd.Flags().StringVar(&environmentID, "environment-id", "", "BYOC Runner ID when --environment-kind=byoc")
133162
registerEnumFlag(cmd, "schedule", "hourly", "daily", "weekly", "cron")
134163
registerEnumFlag(cmd, "environment-kind", "cloud", "byoc")
164+
registerEnumFlag(cmd, "run-in", "current-chat", "web")
135165
return cmd
136166
}
137167

@@ -336,7 +366,7 @@ func newAutomationUpdateCmd() *cobra.Command {
336366
cmd.Flags().StringVar(&schedule, "schedule", "", "Schedule helper: hourly, daily, weekly, or cron")
337367
cmd.Flags().StringVar(&at, "at", "", "Local time in HH:MM (rule's timezone); for hourly schedules, only the minute is used. "+automationTimezoneNote)
338368
cmd.Flags().StringVar(&weekday, "weekday", "", "Weekday for weekly schedules: sun, mon, tue, wed, thu, fri, sat, or 0-7")
339-
cmd.Flags().StringVar(&cronExpr, "cron-expr", "", "Exact 5-field cron expression in the rule's timezone; overrides --schedule helpers. "+automationTimezoneNote)
369+
cmd.Flags().StringVar(&cronExpr, "cron-expr", "", "Supported 5-field cron expression in the rule's timezone. The minute must be a fixed 0-59 integer, so the fastest schedule is hourly; overrides --schedule helpers. "+automationTimezoneNote)
340370
cmd.Flags().StringVar(&timezone, "timezone", "", "IANA timezone name cron_expr is evaluated in. The server must be able to load it; an invalid value is rejected. Omit to leave the stored timezone unchanged, including a legacy empty string. An explicit empty string is stored as UTC, not the account default.")
341371
cmd.Flags().BoolVar(&enableRule, "enable", false, "Enable the Automation")
342372
cmd.Flags().BoolVar(&disableRule, "disable", false, "Disable the Automation")

‎internal/cli/automation_test.go‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,65 @@ func TestAutomationCreateDailyDefaultsEnabled(t *testing.T) {
3434
assertBody(t, stub.lastBody, "prompt", "Summarize yesterday's incidents")
3535
}
3636

37+
func TestAutomationCreateExecutionDestinationWireContract(t *testing.T) {
38+
t.Run("omits server selected destination", func(t *testing.T) {
39+
saveAndResetGlobals(t)
40+
stub := newGFStub(t)
41+
42+
_, err := execCommand(
43+
"automation", "create",
44+
"--name", "Daily SRE brief",
45+
"--prompt", "Summarize yesterday's incidents",
46+
"--json",
47+
)
48+
if err != nil {
49+
t.Fatalf("[automation-create-omitted-destination] unexpected error: %v", err)
50+
}
51+
for _, field := range []string{"run_in", "team_id"} {
52+
if _, ok := stub.lastBody[field]; ok {
53+
t.Fatalf("[automation-create-omitted-destination] %s must be omitted, body=%#v", field, stub.lastBody)
54+
}
55+
}
56+
})
57+
58+
t.Run("sends explicit chat and personal scope", func(t *testing.T) {
59+
saveAndResetGlobals(t)
60+
stub := newGFStub(t)
61+
62+
_, err := execCommand(
63+
"automation", "create",
64+
"--name", "Daily SRE brief",
65+
"--prompt", "Summarize yesterday's incidents",
66+
"--run-in", "current-chat",
67+
"--team-id", "0",
68+
"--json",
69+
)
70+
if err != nil {
71+
t.Fatalf("[automation-create-current-chat] unexpected error: %v", err)
72+
}
73+
assertBody(t, stub.lastBody, "run_in", "current-chat")
74+
assertBody(t, stub.lastBody, "team_id", float64(0))
75+
})
76+
77+
t.Run("rejects invalid destination before request", func(t *testing.T) {
78+
saveAndResetGlobals(t)
79+
stub := newGFStub(t)
80+
81+
_, err := execCommand(
82+
"automation", "create",
83+
"--name", "Daily SRE brief",
84+
"--prompt", "Summarize yesterday's incidents",
85+
"--run-in", "other-chat",
86+
)
87+
if err == nil || !strings.Contains(err.Error(), "--run-in must be current-chat or web") {
88+
t.Fatalf("[automation-create-invalid-destination] err = %v", err)
89+
}
90+
if stub.requests != 0 {
91+
t.Fatalf("[automation-create-invalid-destination] request must not reach API, requests=%d", stub.requests)
92+
}
93+
})
94+
}
95+
3796
func TestAutomationScheduleHelpDocumentsTimezone(t *testing.T) {
3897
saveAndResetGlobals(t)
3998

@@ -60,6 +119,30 @@ func TestAutomationScheduleHelpDocumentsTimezone(t *testing.T) {
60119
}
61120
}
62121

122+
func TestAutomationCreateHelpDocumentsFreshIMExecutionAndScheduleLimit(t *testing.T) {
123+
saveAndResetGlobals(t)
124+
125+
out, err := execCommand("automation", "create", "--help")
126+
if err != nil {
127+
t.Fatalf("[automation-create-help] unexpected error: %v", err)
128+
}
129+
for _, want := range []string{
130+
"fresh execution session",
131+
"fresh IM thread",
132+
"Write the task prompt with the needed chat context",
133+
"say \"above\"",
134+
"scope no longer matches the rule",
135+
"skipped. War-room Automations keep their existing behavior",
136+
"The fastest supported schedule is once per hour",
137+
"wildcards, ranges, lists, and steps are unsupported",
138+
"Self-contained task prompt for the fresh AI SRE execution",
139+
} {
140+
if !strings.Contains(out, want) {
141+
t.Fatalf("[automation-create-help] missing %q\n%s", want, out)
142+
}
143+
}
144+
}
145+
63146
func TestAutomationCreateTimezone(t *testing.T) {
64147
saveAndResetGlobals(t)
65148
stub := newGFStub(t)
@@ -279,6 +362,18 @@ func TestAutomationUpdateDoesNotExposeScopeFlag(t *testing.T) {
279362
}
280363
}
281364

365+
func TestSafariAutomationRuleUpdateDoesNotExposeExecutionTargetFlags(t *testing.T) {
366+
saveAndResetGlobals(t)
367+
newGFStub(t)
368+
369+
for _, flag := range []string{"--team-id", "--run-in"} {
370+
_, err := execCommand("safari", "automation-rule-update", "auto_123", flag, "0")
371+
if err == nil || !strings.Contains(err.Error(), "unknown flag") {
372+
t.Fatalf("[safari-automation-update-target] %s err = %v", flag, err)
373+
}
374+
}
375+
}
376+
282377
func TestAutomationFireSendsBearerToken(t *testing.T) {
283378
saveAndResetGlobals(t)
284379
stub := newGFStub(t)

0 commit comments

Comments
 (0)