Every failure on the authentication, authorization, and configuration
path now surfaces as a typed structured error instead of an ad-hoc
envelope. Users and scripts that consume CLI output get:
- a fixed nine-category taxonomy on the wire, each mapped to a
stable shell exit code (authentication/authorization/config = 3,
network = 4, internal = 5, policy = 6, confirmation = 10)
- identity-aware detail fields (missing_scopes, requested_scopes,
granted_scopes, console_url, log_id, retryable, hint) carried
uniformly on the envelope
- a single canonical policy envelope at exit 6; the legacy
auth_error carve-out is retired
- per-subtype canonical message + hint that preserves Lark's
diagnostic phrasing and routes recovery to the right actor:
app developer (app_scope_not_applied), user (missing_scope,
token_scope_insufficient, user_unauthorized), or tenant admin
(app_unavailable, app_disabled)
- wrong app credentials classify as config/invalid_client whether
surfaced by the Open API endpoint (99991543) or the tenant
access-token mint endpoint (10003 / 10014), instead of
collapsing to a transport error or api/unknown
- local shortcut scope preflight emits the same
authorization/missing_scope envelope (identity + deterministic
missing-scope set) used by the post-call permission path, so AI
consumers read the same structured shape from precheck and from
server-returned permission denial
- streaming download/upload failures keep the same network subtype
split (timeout / TLS / DNS / transport) as the non-stream path
instead of collapsing every cause to a generic transport failure
- console_url is carried only on the bot-perspective
app_scope_not_applied envelope (where the recovery action is
"developer applies the scope at the developer console"); the
user-perspective missing_scope envelope drops the field, since
the only actionable user recovery is `lark-cli auth login --scope`
and pointing an end user at a console they cannot modify is
misleading
- bind workflows (Hermes / OpenClaw / lark-channel) flatten dynamic
Type tags to wire 'config' with the original module name kept
as a metric label
All 10 typed errors are cause-bearing, nil-safe on .Error() and
.Unwrap(), and defensively clone slice setter inputs. Four lint
rules (CheckNilSafeError / CheckBuilderImmutable / CheckUnwrapSymmetry
/ CheckBuildAPIErrorArms) lock these invariants on migrated paths.
Fix 3 occurrences of --minute-token (singular) to --minute-tokens
(plural) in lark-vc-recording.md to match the actual CLI flag
definition in minutes_download.go.
Add a new --types flag (string_slice; values from {group, p2p}) to
+chat-list, backed by the new GET /open-apis/im/v1/chats `types` query
parameter. Accepts CSV (--types group,p2p) and repeated-flag forms
(--types group --types p2p).
Defaults to groups-only (backward compatible). Under user identity,
p2p single chats appear with chat_mode="p2p" plus p2p_target_type /
p2p_target_id fields. Under bot identity:
- --types=p2p alone is rejected at validation
- --types=p2p,group is silently downgraded to types=group (no runtime
notice; skill docs document this contract)
Updates Shortcut.Description, lark-im SKILL.md (frontmatter trigger
+ shortcut table row), and the chat-list reference doc with command
examples, the new parameter, output field documentation, and a
dedicated "Bot identity and p2p" section.
Change-Id: I637ce23b3c6ce4ec350f0ac26dbac8120761bb71
Follow-up to #1095. The reactions auto-enrichment shipped, but on busy chats the strictly-serial per-resource fetches in EnrichReactions, ExpandThreadReplies, and merge_forward expansion stretched the command's wall time above 14s — enough that wrapper agents (30–60s wall-clock budgets) saw timeouts even though the CLI itself never errored. This PR parallelizes all three with the same bounded-concurrency pattern, batches the follow-up contact-API sender resolution so it doesn't fan back out into a serial stall, and fixes two correctness bugs that surfaced during review. Scoped to convert_lib/{reactions,thread,merge,content_convert}.go + tests + the 4 shortcut Execute hooks + the reference doc.
Change-Id: I0206d10ad204382170bd42aec67f82578923736e
- Pull messages now auto-call im.reactions.batch_query and attach a
reactions block (counts + details) to each message. Stops AI from
misjudging "user already reacted" as "no response yet" and
re-sending duplicate reactions. Server caps queries[] at 20 per
call, so messages are split into batches of size <= 20.
- Edited messages additionally surface update_time. The server echoes
update_time == create_time for unedited messages too, so the field
is only emitted when updated == true; otherwise every message
output would look "edited". The value is read via an explicit
string assertion + TrimSpace so empty strings are filtered properly
(the previous `v != ""` was a no-op for non-string types).
- All four message-pulling shortcuts (+messages-mget,
+chat-messages-list, +messages-search, +threads-messages-list) get
a --no-reactions opt-out flag for callers that want to skip the
extra round-trip.
- Each shortcut declares im:message.reactions:read on its
UserScopes/BotScopes (or Scopes for the user-only search command) so
the auth flow covers the new dependency.
- Each shortcut's --dry-run output now lists the
reactions/batch_query call (or omits it when --no-reactions is set),
so callers can audit the full set of API calls before execution.
- Warnings go through runtime.IO().ErrOut (forbidigo lint requires
IOStreams over os.Stderr in shortcut code).
- Duplicate message_id inputs (e.g. mget --message-ids om_a,om_a)
attach the reactions block to every entry while still querying the
API only once per distinct id.
- EnrichReactions walks msg["thread_replies"] recursively, and mget/
chat-messages-list call it after ExpandThreadReplies, so replies
receive reactions in the same batched call as their parent message.
- When the batch_query call fails or returns per-message failures,
the affected messages get reactions_error=true (mirroring the
thread_replies_error flag from thread.go) so consumers can
distinguish "fetch failed" from "no reactions exist" by reading
stdout alone, without depending on the stderr warning channel.
- lark-im skill docs: the default-enrichment contract lives in a
standalone references/lark-im-message-enrichment.md so the generated
SKILL.md can't strand it on regeneration. The four read references
and the raw reactions API reference link to it, and the template
source skill-template/domains/im.md carries a durable pointer.
Change-Id: Ia9ea74b11945644262bb25c6503fb9b2003c6c98
Parse keywords from minutes artifacts API in vc +notes and document
the field in lark-vc skill references.
Co-authored-by: Cursor <cursoragent@cursor.com>
The standup workflow and the +get-my-tasks reference both implied a
"pending todo summary" use case but did not pass --complete=false in
the example commands. As a result, completed tasks were surfaced into
standup/daily summaries as if they were still pending.
This change updates the workflow and reference docs only — the
underlying command behavior is unchanged.
Closes#993
* feat(apps): replace +html-publish cwd hard-reject with credential-file scan
The previous --path == "." block was a coarse heuristic: it caught the
common foot-gun of publishing a repo root, but also rejected legitimate
clean cwds, and let a ./dist with a forgotten .env ship the secret
through anyway (the sensitive-paths scanner was advisory and never ran
on the Execute path).
Move the gate from path shape to path content:
- Validate now walks --path candidates and rejects publishes that
include well-known credential files (.env / .env.* / .npmrc / .netrc
/ .git-credentials / .aws/credentials / .gcloud/credentials* /
.docker/config.json / .kube/config). Living in Validate (not DryRun)
means dry-run returns non-zero on hit too, so the dry-run preview
matches Execute.
- Narrow the credential pattern set. .git/, SSH private keys, *.pem
and *.key are out of scope -- they're not env-token files and the
false-positive rate (public certs, docs about key formats) is high.
- Add --allow-sensitive as the escape hatch for legitimate cases
(e.g. a docs site shipping .env.example on purpose). DryRun surfaces
the waived list in sensitive_waived so the caller can relay it.
- Drop the cwd defense-in-depth in runHTMLPublish. A clean cwd is now
a valid publish target.
The lark-apps skill and the html-publish reference are updated to
describe the new gate, the override flag, and the patterns now
explicitly out of scope.
* feat(apps): drop .gcloud/* from credential-file scan
The .gcloud/credentials pattern matched a non-existent path: gcloud's
actual config dir is ~/.config/gcloud/ (XDG-based), and the real
credential files there are credentials.db / access_tokens.db /
application_default_credentials.json -- none of which would land under
a .gcloud/ segment in a publish payload.
Drop the rule rather than fix it: the realistic gcloud foot-gun would
require recognizing the .config/gcloud/* tree by file basename, which
is a broader change than the targeted env/cred scan in this PR. The
remaining 7 patterns (.env / .env.* / .npmrc / .netrc /
.git-credentials / .aws/credentials / .docker/config.json /
.kube/config) cover the common Node/Python/CLI-tooling foot-guns.
* fix(apps): close credential-scan bypass when --path is the parent dir itself
isSensitiveRelPath anchors cloud-SDK matchers on adjacent parent/file
segments (.aws/credentials, .docker/config.json, .kube/config), but
walker strips that parent via filepath.Rel when --path is the conventional
parent dir (e.g. ./.aws), yielding a bare RelPath="credentials" that
slipped through silently. Same bypass for the single-file form
--path ./.aws/credentials (walker sets RelPath = Base(rootPath)).
Wrap the scan in isSensitiveCandidate: keep the fast RelPath scan, and
on miss fall back to filepath.Abs(AbsPath) so the parent segment is
visible again. isSensitiveRelPath itself is unchanged; existing tests
still pin its pure-function contract.
* fix(apps): drop filepath.Abs from sensitive scan to satisfy forbidigo lint
The previous fix called filepath.Abs(c.AbsPath) — banned by the repo's
forbidigo rule because shortcuts must not reach into the filesystem for
path resolution.
Reframe the same fix without fs access: re-prepend the root's basename
(or, for the single-file form, the parent dir's basename of rootPath)
to RelPath and re-scan only the parent-anchored credential pairs
(.aws/credentials, .docker/config.json, .kube/config). Leaf matchers
(.env / .npmrc / ...) stay scoped to RelPath — incidentally closing a
latent false-positive where --path /home/alice/.env/dist would have
flagged every file under it just because .env appeared in the
absolute path.
* fix(apps): read app object from data.app for +create and +update
The Miaoda OpenAPI returns the application object nested under
data.app for both POST /apps and PATCH /apps/{appId}. The CLI text
helper was reading common.GetString(data, "app_id"), which yields an
empty string against the wire format -- so `lark-cli apps +create
--format pretty` printed `created: ` with no ID.
Navigate the new nested path via GetString(data, "app", "app_id") for
both create and update. Update unit-test mocks to wrap the response
under `app`. Refresh the lark-apps skill references (example response
shape + jq paths) so agents reading them follow the right path.
Wire format is passed through to the user's JSON envelope untouched
-- no unwrapping in CLI. Consumers reading the response should use
.data.app.app_id.
The GET /apps list endpoint is unchanged: per the design doc its
items[] are flat objects, no wrapper.
* docs(apps): add required --app-type HTML to scenario 2 snippet
The "用户没有 app_id" snippet in lark-apps-html-publish.md was missing
the required --app-type flag, so copy-pasting it triggered Validate
("--app-type is required") and left $APP empty -- the following
+html-publish then failed with --app-id "". Bring the snippet in line
with every other apps +create example in the skill.
* docs(apps): simplify auth-recovery rule to error.type == missing_scope
Every apps shortcut declares Scopes, so the precheck path in
shortcuts/common/runner.go:825 is always the one that fires on scope
violations and the envelope's error.type is the stable discriminator.
Drop the keyword-sniffing of error.hint, the chain explanation, and the
bot caveat — they all reduce to one boolean: error.type == "missing_scope"
→ run `lark-cli auth login --domain apps`.
Also collapse the corresponding bullet in 快速决策 to point at this rule.
Per issue #1049 (third point), wiki +node-get used --token while sibling
commands (+node-delete / +node-copy / +move) use --node-token. The
inconsistency forced humans and AI agents to remember which adjacent
command takes which flag.
Make --node-token the canonical flag and keep --token as a hidden,
deprecated alias so existing scripts continue to work. pflag's
MarkDeprecated prints "Flag --token has been deprecated, use --node-token
instead" to stderr on use, guiding callers to migrate. Conflict between
the two with different values is rejected upfront.
Skills docs (lark-wiki, lark-base) updated to prefer --node-token.
Change-Id: I3415a98f079613c0b1a0b989cf54a09cbb8986fb
- description: switch from trigger-word enumeration to a general
principle (any HTML artifact intended to be independently accessible
falls under this skill; defer the deploy-vs-demo decision to the
skill body)
- surface apps +access-scope-get in prerequisites list and Shortcuts
table so agents can find the read side of access-scope
- add "writing HTML hard constraints" section: index.html is the
required entry filename, --path cannot equal cwd (both are CLI-side
hard rejects that previously only lived in the html-publish ref)
* feat(apps): gate apps domain off on Lark brand
The Miaoda apps OpenAPI is Feishu-only. On Lark brand:
- shortcut subtree is registered + hidden, RunE returns a structured
brand-restriction error so users see a clear message instead of
cobra's generic "unknown command"
- auth login `--domain apps` is treated as unknown; `--domain all`
skips apps; help text omits it
- scope collection skips apps shortcuts so spark:* scopes are never
requested
The leaf-stub pattern mirrors internal/cmdpolicy/apply.go::installDenyStub
(DisableFlagParsing + ArbitraryArgs + leaf-level PersistentPreRunE
override) so cobra can't short-circuit the stub with a missing-flag or
parent-PreRunE detour.
Change-Id: I5817e87ae6fedabdb5faf05d0d32ea988f7effc9
- +member-add: wrap POST /spaces/{id}/members; --member-type / --member-role
enums, optional --need-notification query (omitted entirely when the flag
is unset, instead of forcing need_notification=false), my_library
resolution under --as user, flattened single-member output
- +member-remove: wrap DELETE /spaces/{id}/members/{member_id}; surfaces the
required member_type + member_role body the API expects, my_library
resolution, fallback to echoing the caller's inputs when the API omits
the member echo
- +member-list: wrap GET /spaces/{id}/members; reuses the +space-list /
+node-list pagination contract (single page by default, --page-all walks
every page capped by --page-limit, --page-token resumes a cursor)
- All three reject bot identity + my_library upfront with a clear hint and
declare the narrowest scope the API accepts (wiki:member:create /
wiki:member:update / wiki:member:retrieve) so tokens carrying only the
narrow scope are not false-rejected by the exact-string preflight
- skill docs: reference pages for the three new shortcuts + SKILL.md
shortcuts table; switch the membership flow guidance from raw
`wiki members create` to the new +member-add path
Change-Id: I158a86aa7f00bb7cecc7a4e99346f3fb151b3c09
docs +search is in maintenance and will be removed; cloud-space resource
discovery is consolidated onto drive +search. Two related doc/help fixes:
1. Redirect guidance: docs +search -> drive +search
- skill-template/domains/{doc,sheets}.md
- lark-base/SKILL.md: --filter '{"doc_types":["BITABLE"]}' -> --doc-types bitable
- lark-sheets/SKILL.md: body + frontmatter description, add drive-search ref link
Same server API, equivalent capability; only flattens the entry from
nested --filter JSON to flags. reference links repointed to lark-drive.
2. Fix creator_ids/--mine semantic: creator -> owner
The server matches creator_ids (incl. --mine / --creator-ids) by owner
(document owner), not original creator, despite the OpenAPI field name.
- shortcuts/drive/drive_search.go: --help Desc and Tip
- lark-drive/references/lark-drive-search.md: identity section, params, rules, examples
- lark-drive/SKILL.md: top-level guidance
- lark-doc/references/lark-doc-search.md: creator_ids usage note (now self-consistent)
Wire field name creator_ids kept (aligned with the server).
Docs/help strings only, no logic change; gofmt / go vet / package build pass.
Change-Id: If3ebf5a247b7e38b58050c677dc888a310f1c6b6