Skip to content

Commit 47d78c5

Browse files
committed
feat(affordance): per-command usage guidance system (markdown source)
Adds the affordance mechanism: per-command usage guidance authored as one markdown file per domain under affordance/, surfaced in `--help` and the schema output and read directly at runtime (lazy, cached). See affordance/README.md for the format; contact is included as a worked example. Remaining domains land in follow-up changes. Also trims a couple of redundant lines from the contact skill.
1 parent 602c788 commit 47d78c5

23 files changed

Lines changed: 1152 additions & 173 deletions

affordance/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Affordance
2+
3+
Per-command usage guidance for the CLI, authored as one markdown file per domain
4+
(`<service>.md`). It is surfaced in `lark-cli <command> --help` and in the
5+
`schema` output, and read directly at runtime (lazy, cached) — there is no build
6+
step. Maintain these files alongside `skills/` and `shortcuts/`.
7+
8+
## Format
9+
10+
A small, fixed markdown subset; each file describes one domain:
11+
12+
# <domain> optional `> skill: <name>` applies to every command below
13+
## <command> the command as typed, minus `lark-cli <domain>`
14+
<lead paragraph> when to use this command
15+
### Avoid when when not to use it / which command to use instead
16+
### Prerequisites what you must have first (e.g. an id, and where it comes from)
17+
### Tips gotchas and constraints
18+
### Examples **description** lines, each followed by a fenced command
19+
### <other heading> a custom section; flows through verbatim
20+
21+
Reference another command with `[[command]]` — it renders as `command` in help.
22+
Under `Avoid when` it means "use that one instead"; under `Prerequisites`
23+
("… from [[command]]") it means "get the input there first".
24+
25+
## Example
26+
27+
## messages get
28+
Fetch the full content of a single message by id.
29+
30+
### Avoid when
31+
- Reading several at once → use [[messages batch_get]]
32+
33+
### Prerequisites
34+
- message_id from [[messages list]]
35+
36+
### Examples
37+
38+
**Fetch one message**
39+
```bash
40+
lark-cli mail user_mailbox.messages get --message-id "<id>"
41+
```
42+
43+
## Notes
44+
45+
- Write plain prose; the only convention is wrapping command references in `[[ ]]`.
46+
- Keep it concise and high-signal — don't restate field/flag names, id types, or
47+
anything the schema and flags already show; the agent infers the rest.
48+
- Command-form headings resolve to method ids via the registry, so plural resource
49+
names (`messages`) map to the singular method id (`message`) automatically.

affordance/contact.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# contact
2+
> skill: lark-contact
3+
4+
## user_profiles batch_query
5+
Bulk-fetch personal status and signature for user ids you already have.
6+
7+
### Avoid when
8+
- Need more than status/signature (name, dept, email), or don't have the open_id yet → use [[+search-user]]
9+
10+
### Tips
11+
- Off by default — set include_personal_status / include_description to true under query_option
12+
- ids in user_ids must match --user-id-type (default open_id)
13+
14+
### Examples
15+
16+
**Bulk-query status and signature**
17+
```bash
18+
lark-cli contact user_profiles batch_query --data '{"user_ids":["ou_3a8b****6a7b"],"query_option":{"include_personal_status":true,"include_description":true}}'
19+
```

cmd/api/api.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,21 @@ func NewCmdApiWithContext(ctx context.Context, f *cmdutil.Factory, runF func(*AP
6767

6868
cmd := &cobra.Command{
6969
Use: "api <method> <path>",
70-
Short: "Generic Lark API requests",
71-
Args: cobra.ExactArgs(2),
70+
Short: "Raw HTTP escape hatch — call any endpoint by path (fallback when no typed command exists)",
71+
Long: `Raw HTTP escape hatch: send any Lark API request by HTTP method + path.
72+
73+
Prefer the typed domain command when one exists — it validates parameters,
74+
shows the Risk level, gates destructive calls behind --yes, and carries usage
75+
guidance that this raw command does not. If a domain command covers your task
76+
(browse with ` + "`lark-cli <domain> --help`" + `), use it instead of this.
77+
78+
Reach for ` + "`api`" + ` only for endpoints that have no typed command yet (e.g.
79+
newer/preview APIs), where you already have the HTTP path from the Lark docs.
80+
81+
Examples:
82+
lark-cli api GET /open-apis/calendar/v4/calendars
83+
lark-cli api POST /open-apis/im/v1/messages --params '{"receive_id_type":"open_id"}' --data @body.json`,
84+
Args: cobra.ExactArgs(2),
7285
RunE: func(cmd *cobra.Command, args []string) error {
7386
opts.Method = strings.ToUpper(args[0])
7487
opts.Path = args[1]

cmd/build.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@ func buildInternal(ctx context.Context, inv cmdutil.InvocationContext, opts ...B
170170
rootCmd.SetOut(cfg.streams.Out)
171171
rootCmd.SetErr(cfg.streams.ErrOut)
172172

173+
// Root-only usage template (curated Usage synopsis + skills footer); see
174+
// rootUsageTemplate.
175+
rootCmd.SetUsageTemplate(rootUsageTemplate)
176+
173177
installTipsHelpFunc(rootCmd)
174178
rootCmd.SilenceErrors = true
175179
// SilenceUsage as a static field (not only in PersistentPreRun) so it also
@@ -205,6 +209,8 @@ func buildInternal(ctx context.Context, inv cmdutil.InvocationContext, opts ...B
205209
}
206210
shortcuts.RegisterShortcutsWithContext(ctx, rootCmd, f)
207211

212+
groupRootCommands(rootCmd)
213+
208214
installUnknownSubcommandGuard(rootCmd)
209215

210216
if mode := f.ResolveStrictMode(ctx); mode.IsActive() && !cfg.skipStrictMode {

cmd/root.go

Lines changed: 100 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import (
1111
"sort"
1212
"strings"
1313

14+
"github.com/larksuite/cli/cmd/service"
1415
"github.com/larksuite/cli/errs"
1516
"github.com/larksuite/cli/extension/platform"
1617
"github.com/larksuite/cli/internal/build"
18+
"github.com/larksuite/cli/internal/cmdmeta"
1719
"github.com/larksuite/cli/internal/cmdpolicy"
1820
"github.com/larksuite/cli/internal/cmdutil"
1921
"github.com/larksuite/cli/internal/deprecation"
@@ -28,43 +30,60 @@ import (
2830

2931
const rootLong = `lark-cli — Lark/Feishu CLI tool.
3032
31-
USAGE:
32-
lark-cli <command> [subcommand] [method] [options]
33-
lark-cli api <method> <path> [--params <json>] [--data <json>]
34-
lark-cli schema <service.resource.method>
33+
AGENT QUICKSTART (driving this as an agent? start here):
34+
Browse commands: lark-cli <domain> --help # +shortcuts (preferred) and raw API resources
35+
Inspect a call: lark-cli schema <service>.<resource>.<method> # params, types, scopes, examples
36+
Prefer a +shortcut over the raw API resource when one matches the task.
37+
Risk: each command's --help shows read | write | high-risk-write;
38+
high-risk-write needs --yes, only after the user confirms.
39+
On any API call: --jq <expr> filters JSON output, --dry-run previews the request (runs nothing).
3540
36-
EXAMPLES:
37-
# View upcoming events
38-
lark-cli calendar +agenda
41+
EXAMPLES (one per command style, in order of preference):
42+
lark-cli calendar +agenda # +shortcut — a high-level task, prefer these
43+
lark-cli mail user_mailbox.messages list --user-mailbox-id me # typed command for one API method
44+
lark-cli schema mail.user_mailbox.messages.list # inspect a method's params before calling
45+
lark-cli api GET /open-apis/calendar/v4/calendars # raw escape hatch — any endpoint by HTTP path`
3946

40-
# List calendar events
41-
lark-cli calendar events instance_view --params '{"calendar_id":"primary","start_time":"1700000000","end_time":"1700086400"}'
47+
// rootUsageTemplate is cobra's default usage template with two root-only
48+
// additions gated on {{if not .HasParent}}: a curated multi-form Usage synopsis
49+
// (replacing cobra's generic "[flags] / [command]") and a human skills-setup
50+
// footer. Subcommands render the stock template unchanged. The rest is verbatim
51+
// cobra so the command groups and flags are untouched.
52+
const rootUsageTemplate = `{{if .HasParent}}Usage:{{if .Runnable}}
53+
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
54+
{{.CommandPath}} [command]{{end}}{{else}}Usage:
55+
lark-cli <command> [subcommand] [method] [flags]
56+
lark-cli api <method> <path> [--params <json>] [--data <json>]
57+
lark-cli schema <service.resource.method>{{end}}{{if gt (len .Aliases) 0}}
4258
43-
# Search users
44-
lark-cli contact +search-user --query "John"
59+
Aliases:
60+
{{.NameAndAliases}}{{end}}{{if .HasExample}}
4561
46-
# Generic API call
47-
lark-cli api GET /open-apis/calendar/v4/calendars
62+
Examples:
63+
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
4864
49-
AI AGENT SKILLS:
50-
lark-cli pairs with AI agent skills (Claude Code, etc.) that
51-
teach the agent Lark API patterns, best practices, and workflows.
65+
Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
66+
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}
5267
53-
Install all skills:
54-
npx skills add larksuite/cli -g -y
68+
{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}}
69+
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}}
5570
56-
Or pick specific domains:
57-
npx skills add larksuite/cli -s lark-calendar -y
58-
npx skills add larksuite/cli -s lark-im -y
71+
Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
72+
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
5973
60-
Learn more: https://github.com/larksuite/cli#agent-skills
74+
Flags:
75+
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
6176
62-
COMMUNITY:
63-
GitHub: https://github.com/larksuite/cli
64-
Issues: https://github.com/larksuite/cli/issues
65-
Docs: https://open.feishu.cn/document/
77+
Global Flags:
78+
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
6679
67-
More help: lark-cli <command> --help`
80+
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
81+
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
82+
83+
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}{{if not .HasParent}}
84+
85+
Skills setup (one-time, humans): npx skills add larksuite/cli -g -y — https://github.com/larksuite/cli#agent-skills{{end}}
86+
`
6887

6988
// Execute runs the root command and returns the process exit code.
7089
// rawInvocationArgs holds os.Args[1:] captured at Execute() entry. cobra's
@@ -529,6 +548,49 @@ func availableSubcommandNames(cmd *cobra.Command) (available, deprecated []strin
529548
return available, deprecated
530549
}
531550

551+
// Root command help groups, so an agent sees content domains, agent tooling, and
552+
// CLI management as distinct blocks instead of one flat alphabetical dump.
553+
const (
554+
groupDomains = "lark-domains"
555+
groupTooling = "agent-tooling"
556+
groupManagement = "cli-management"
557+
)
558+
559+
// groupRootCommands classifies root's direct children into the help groups,
560+
// called once after all commands are registered. Unclassified commands fall to
561+
// cobra's "Additional Commands" section.
562+
func groupRootCommands(root *cobra.Command) {
563+
root.AddGroup(
564+
&cobra.Group{ID: groupDomains, Title: "Lark domains:"},
565+
&cobra.Group{ID: groupTooling, Title: "Agent tooling:"},
566+
&cobra.Group{ID: groupManagement, Title: "CLI management:"},
567+
)
568+
tooling := map[string]bool{"api": true, "schema": true, "skills": true}
569+
management := map[string]bool{"auth": true, "config": true, "profile": true, "doctor": true, "update": true}
570+
for _, c := range root.Commands() {
571+
if c.GroupID != "" {
572+
continue
573+
}
574+
switch {
575+
case tooling[c.Name()]:
576+
c.GroupID = groupTooling
577+
case management[c.Name()]:
578+
c.GroupID = groupManagement
579+
case isLarkDomain(c):
580+
c.GroupID = groupDomains
581+
}
582+
}
583+
}
584+
585+
// isLarkDomain reports whether a root child is a Lark domain (service-sourced or
586+
// shortcut-tagged), not CLI tooling. Mirrors service.PrepareDomainHelp.
587+
func isLarkDomain(c *cobra.Command) bool {
588+
if src, _ := cmdmeta.SourceOf(c); src == cmdmeta.SourceService {
589+
return true
590+
}
591+
return cmdmeta.Domain(c) != ""
592+
}
593+
532594
// flagDidYouMean is the root FlagErrorFunc (inherited by all subcommands). It
533595
// converts cobra's flag-parse errors into a typed validation envelope: an
534596
// unknown flag gets a focused "did you mean" hint (so agents recover even when
@@ -610,6 +672,17 @@ func installTipsHelpFunc(root *cobra.Command) {
610672
defer func() { f.Hidden = true }()
611673
}
612674
}
675+
// Domain and method commands compose their agent guidance into Long lazily
676+
// here (shortcuts attach after service registration); both skip the generic
677+
// bottom-of-help append below.
678+
if service.PrepareDomainHelp(cmd, embeddedSkillContent) {
679+
defaultHelp(cmd, args)
680+
return
681+
}
682+
if service.PrepareMethodHelp(cmd) {
683+
defaultHelp(cmd, args)
684+
return
685+
}
613686
defaultHelp(cmd, args)
614687
out := cmd.OutOrStdout()
615688
if level, ok := cmdutil.GetRisk(cmd); ok {

cmd/root_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,13 @@ func TestPersistentPreRunE_ConfigSubcommands(t *testing.T) {
7676
}
7777

7878
func TestRootLong_AgentSkillsLinkTargetsReadmeSection(t *testing.T) {
79-
if !strings.Contains(rootLong, "https://github.com/larksuite/cli#agent-skills") {
80-
t.Fatalf("root help should link to the README Agent Skills section, got:\n%s", rootLong)
79+
// The human skills-install guidance now lives in the root usage-template
80+
// footer (below the command list), not in the agent-facing Long.
81+
if !strings.Contains(rootUsageTemplate, "https://github.com/larksuite/cli#agent-skills") {
82+
t.Fatalf("root help footer should link to the README Agent Skills section, got:\n%s", rootUsageTemplate)
8183
}
82-
if strings.Contains(rootLong, "https://github.com/larksuite/cli#install-ai-agent-skills") {
83-
t.Fatalf("root help should not reference the removed install-ai-agent-skills anchor, got:\n%s", rootLong)
84+
if strings.Contains(rootUsageTemplate, "https://github.com/larksuite/cli#install-ai-agent-skills") {
85+
t.Fatalf("root help should not reference the removed install-ai-agent-skills anchor, got:\n%s", rootUsageTemplate)
8486
}
8587
}
8688

0 commit comments

Comments
 (0)