- Assemble applinks via net/url to ensure proper encoding
- Normalize message position values across more numeric types
- Avoid leaking null message_app_link; assemble when missing
- Update unit tests to assert URL semantics and cover edge cases
Change-Id: Ic473cb563c8a648c4f6677c32b25b9f371a0f84e
Adds a new top-level safety section "数据真实性与操作合规" to the
lark-mail skill via the canonical generation pipeline:
- skill-template/domains/mail.md (source) — adds the section to the
domain introduction file that gen-skills.py renders into SKILL.md.
- skills/lark-mail/SKILL.md (regenerated product) — produced by
`make gen-skills project=mail` from larksuite-cli-registry against
the modified mail.md source.
Why both files: skills/lark-mail/SKILL.md is auto-generated from
skill-template/domains/mail.md + registry-conf/skill-meta.yaml +
output/from_meta/mail.json. Editing only SKILL.md would be reverted on
the next `make gen-skills` run because SKILL.md has no AUTO-GENERATED
markers and falls into the "no markers -> overwrite whole file" branch
in scripts/gen-skills.py.
The section adds 3 hard constraints on agent behavior:
- empty result is a valid answer; do not fabricate IDs or placeholders
- explicit action preview before destructive write operations
(delete / trash / batch_trash / cancel_scheduled_send / rules.*)
- reversible modifications (label / read state / folder move) are
exempt from the preview requirement
Addresses recurring evaluation failures (c03/c04/c06/c09/c14/c19~c24/c40)
where the agent fabricated IDs or auto-executed destructive operations.
The --as flag displayed (default "bot"), (default "user"), or
(default "auto") in help text, but ResolveAs() never uses the cobra
default — it resolves identity via credential config and auto-detect.
The displayed default misled users into thinking a fixed identity was
used when --as was omitted.
Set cobra default to empty string so no (default ...) suffix appears.
Also remove "auto" from visible options since --as auto is equivalent
to omitting --as entirely.
Change-Id: I51ba550a6697eb3675a29f5cee4d0010e0a1cc16
Users who install or upgrade lark-cli via make install, go install, or
direct binary download end up with a binary but no AI agent skills,
degrading agent UX. This PR adds a startup-time skills version drift
notice (injected into JSON envelope _notice.skills, mirroring the
existing _notice.update pattern) and unifies lark-cli update's skills
sync across all three branches (npm / manual / already-latest) with
stamp-based dedup, so any explicit update invocation keeps skills in
sync regardless of how the binary was installed.
Changes:
- new internal/skillscheck package: notice (StaleNotice + atomic
pending), stamp (~/.lark-cli/skills.stamp), skip (CI / DEV /
non-release / LARKSUITE_CLI_NO_SKILLS_NOTIFIER opt-out), check
(synchronous Init)
- cmd/root.go: rename setupUpdateNotice -> setupNotices, compose
output.PendingNotice returning {update?, skills?}; capture
build.Version locally before spawning the async update goroutine
- cmd/update/update.go: add runSkillsAndStamp helper with stamp-based
dedup; rewire the three branches through shared applySkillsResult /
emitSkillsTextHints helpers; add skills_status block to --check JSON
output as a pure report (no side effects)
- internal/update: export IsRelease(version) bool / IsCIEnv() bool
for cross-package reuse; refresh UpdateInfo.Message to append
', run: lark-cli update' so both notices recommend the same fix
- AGENTS.md: add Notification Opt-Outs section documenting
LARKSUITE_CLI_NO_UPDATE_NOTIFIER and LARKSUITE_CLI_NO_SKILLS_NOTIFIER
- internal/binding/types.go: bump default exec-provider timeout from
5s to 10s (out-of-scope flake fix for TestResolveExecRef_JSONResponse
under heavy parallel test load)
AI agents running inside OpenClaw / Hermes were routinely creating a parallel
app via `config init --new` instead of binding to the agent's existing app,
because every "not configured" hint and several deny errors hard-coded
`config init` regardless of workspace. Once bound, the same agents could
silently grant themselves user identity (impersonation) without the user
ever seeing a risk message in chat.
Changes:
- Introduce `core.NotConfiguredError` / `NoActiveProfileError` /
`reconfigureHint` helpers that branch on `CurrentWorkspace()`. In agent
workspaces they point at `lark-cli config bind --help` (a help page, not
a ready-to-run command) so AI must read the binding workflow and confirm
identity preset with the user before acting. In local terminals they
preserve the previous `config init --new` guidance.
- Migrate every `config init` hint that should be workspace-aware:
RequireConfigForProfile, default credential provider, credential provider
fallback, secret-resolve mismatch, config show, strict-mode entry-point
errors, default-as, profile use/rename/remove, auth list, doctor's
config_file check (which now also wraps the OS-level "no such file"
noise into the user-shaped "not configured" message).
- Refuse `config init` when run inside an OpenClaw / Hermes workspace by
default; add `--force-init` for the rare case the user genuinely wants
a parallel app. Without this guard, hint fixes were undone the moment
AI ignored them.
- Rewrite the strict-mode deny errors in cmd/auth/login.go, cmd/prune.go,
and internal/cmdutil/factory.go. The previous "AI agents are strictly
prohibited from modifying this setting" terminated AI reasoning while
providing no real gate. New errors point at `config strict-mode --help`
with the legitimate confirmation flow and explicitly note that switching
does NOT require re-bind. Integration test envelopes updated.
- Tighten `config bind --help` and `config strict-mode --help` to encode
the user-confirmation discipline directly: identity preset semantics
(bot-only vs user-default), "DO NOT switch without explicit user
confirmation", and a cross-reference clarifying that `config bind` is
for changing the underlying app while `config strict-mode` is the
policy-only switch (resolves an ambiguity an audit run found).
- Surface user-identity (impersonation) risk at every config write that
newly grants it, by reusing the canonical IdentityEscalationMessage
string from bind_messages.go:
- `noticeUserDefaultRisk` fires on flag-mode bind landing on
user-default, including the first-time case `warnIdentityEscalation`
misses (it requires a previous bot lock).
- `setStrictMode` warns when transitioning bot → user or bot → off
(newly permits user identity); stays quiet on narrowing changes
and on off → user (off already permitted user).
- Add tests: notconfigured_test.go (workspace branches),
init_guard_test.go (refuse + --force-init bypass), bind_warning_test.go
(user-default warning fires; bot-only does not), strict_mode_warning_test.go
(5 transitions covering both warn and no-warn paths).
Two follow-ups intentionally deferred: the keychain master-key hint at
internal/keychain/keychain.go:42 still suggests `config init` because the
keychain package can't import core (would be circular); fixing requires
either parameterizing the hint via callback or extracting workspace into
its own package. The lark-shared skill doc still tells AI to run
`config init` for first-time setup; updating the skill is in scope for
a follow-up PR.
Change-Id: I02273e044d9e061d211ceaa4f3ed5a3fb28325b3
* fix(auth): handle missing scopes and device flow improvements
* fix: remove redundant error return in login scope handler
* test(auth): rename test for zero interval default case
* fix: increase device code polling timeout from 180 to 600 seconds
* feat(base): support batch record get and delete
* fix(base): address batch record PR feedback
* docs(base): refine record skill routing
* refactor(base): use batch record get and delete only
* refactor(base): share record selection normalization
* docs(base): clarify record get field projection help
* feat(drive): pre-flight per-text-element byte limit for +add-comment
The open-platform comment API returns an opaque [1069302] Invalid or
missing parameters whenever a single reply_elements[i] text exceeds
its implicit byte budget. The error does not name which element failed
or that length is the cause, so callers resort to binary-search
debugging.
Empirically: Chinese text up to ~80 chars (~240 bytes) lands; ~130
chars (~390 bytes) fails. Set the pre-flight limit to 300 bytes which
sits safely inside the known-good zone.
- parseCommentReplyElements now rejects any text element whose UTF-8
byte length exceeds 300, with an ExitError naming the element index
(#N, 1-based) and both the rune and byte counts, plus an ErrWithHint
recommending the correct remediation (split into multiple text
elements — the comment UI renders them as one contiguous comment).
- The previous 1000-rune check is removed: it was too lenient (a
Chinese text under that cap would still fail server-side).
- skills/lark-drive/references/lark-drive-add-comment.md documents
the per-element limit and the correct split pattern so agents
avoid constructing oversized single elements upstream.
Addresses Case 12 in the 踩坑列表 doc.
* fix(drive): correct +add-comment hint to match actual escape coverage
`escapeCommentText` only expands `<` and `>` (each → 4 bytes via
`<` / `>`); `&` is intentionally left as-is. Both the over-limit
hint and the inline comment in `parseCommentReplyElements` previously
claimed `&` was also escaped, with a "4-5 bytes each" range that
implicitly assumed `&` (5 bytes) — a string of 300 `&` chars
would actually fit in the budget, but a user reading the hint would
think otherwise and pre-emptively split it.
Code:
- Hint string ends with `Note: '<' and '>' are HTML-escaped and
counted in their escaped form (4 bytes each).` (was: included `&`
and "4-5 bytes")
- Inline comment above the budget check now matches:
`escapeCommentText only expands '<' and '>' (each becomes 4 bytes:
< / >); '&' is intentionally left as-is.`
Tests (regression):
- New `300 ampersands accepted (escapeCommentText leaves '&' as-is)`
subtest pins that 300 `&` chars stay within budget. Without the fix
this also passed (function was always correct), but the hint was
lying — the test pins the budget contract loud and clear.
- New `TestParseCommentReplyElementsHintMatchesEscape` asserts the
hint string itself: must mention `'<' and '>'` / `4 bytes`, must NOT
mention `'&'` / `&` / `4-5 bytes`. Catches a future drift if
`escapeCommentText` is changed without updating the hint, or
vice-versa.
The skill md (`skills/lark-drive/references/lark-drive-add-comment.md`)
already had the right wording (`每个 < 或 > 占 4 字节`), so it was the
in-Go strings that drifted; this commit aligns code with doc.
* fix(drive): rewrite +add-comment length cap to match real server behavior
The original PR set a 300-byte per-element pre-flight check, justified
by the empirical pattern "~80 Chinese chars succeeds, ~130 fails". A
fresh round of probing the live `/open-apis/drive/v1/files/{token}/
new_comments` endpoint with a real docx shows that pattern does not
reproduce, and the actual contract is very different:
- 10000 ASCII / 10000 Chinese / 10000 '<' (escaped to 40000 bytes)
in a single text element: all OK
- 10001 of any of the above in a single text element: [1069302]
- 5000 + 5000 across two text elements (total 10000): OK
- 5000 + 5001 across two text elements (total 10001): [1069302]
- 4000 + 4000 + 4000 across three (total 12000): [1069302]
Two consequences:
1. The cap is *10000 runes total across all reply_elements text*, not
300 bytes per element. The old check rejected legitimate input
anywhere from ~100 to 10000 Chinese chars (≈100x too aggressive).
2. The hint that recommended "split the content across multiple
{\"type\":\"text\",\"text\":\"...\"} elements" was actively wrong —
splitting doesn't bypass a total cap. A user told to split a
10001-char message into 5000+5001 hits the same opaque [1069302].
This commit:
- Replaces `maxCommentTextElementBytes = 300` with
`maxCommentTotalRunes = 10000`. The constant's doc comment records
the probe matrix above so future maintainers know how it was
derived.
- Switches the measurement from `len(escapeCommentText(input.Text))`
to `utf8.RuneCountInString(input.Text)`. Server counts raw runes;
byte width and post-escape form are irrelevant. The escape itself
still happens — `<` and `>` still get rendered literally — but it
no longer participates in the length check.
- Tracks a running `totalRunes` across the whole reply_elements array
and bails at the first element that pushes the cumulative total
over the 10000-rune budget, with index reporting that points at the
offending element.
- Rewrites the over-cap hint to (a) name the actual 10000-rune budget,
(b) explicitly say splitting does NOT help, (c) drop the wrong
"comment UI still renders them as one contiguous comment" framing
that implied splitting was a workaround.
- Adds a `TestParseCommentReplyElementsHintForbidsSplitAdvice`
watchdog that fails if any future drift puts the discredited split
advice back into the hint.
Tests: 11 cases on TestParseCommentReplyElementsTextLength covering
single-element boundary (ASCII / Chinese / angle brackets at exactly
10000 and at 10001), multi-element total cap (5000+5000 OK, 5000+5001
rejected with index pointing at element #2), early-element-overshoot
indexing (first element at 10001 reports index #1, not the trailing
element), and mention_user not double-counting toward the cap.
Skill md updated: removes the 300-byte / "split into multiple
elements" advice; documents the 10000-rune total cap with a note that
the schema currently advertises 1-1000 chars and is out of date,
plus a procedure for re-probing if the server-side limit ever moves.
Manual API verification: rebuilt binary and posted comments at
boundary lengths — all OK cases (100 / 5000 / 10000 chars, 5000+5000
split) accepted by server; over-cap cases (10001 / 10100 single, and
5000+5001 split) rejected by the new pre-flight before reaching the
network.
---------
Co-authored-by: fangshuyu <fangshuyu@bytedance.com>
* feat(doc): expand callout type= shorthand into background-color and border-color
When users write <callout type="warning" emoji="📝"> without an explicit
background-color, the Feishu doc renders the block with no color. This
commit adds fixCalloutType() which maps the semantic type= attribute to
the corresponding background-color/border-color pair accepted by create-doc.
- warning → light-yellow/yellow
- info/note → light-blue/blue
- tip/success/check → light-green/green
- error/danger → light-red/red
- caution → light-orange/orange
- important → light-purple/purple
Explicit background-color or border-color attributes are always preserved.
The fix is applied via prepareMarkdownForCreate() in both +create and
+update paths, and also inside fixExportedMarkdown() for round-trip fidelity.
* refactor(doc): replace silent callout type→color injection with hint output
Per reviewer feedback (SunPeiYang996), silently rewriting user Markdown is
the wrong layer for this adaptation. The type→color mapping is not part of
the Feishu spec, and covert transforms make debugging harder.
Replace fixCalloutType() (which rewrote the Markdown) with WarnCalloutType()
which leaves the Markdown unchanged and instead writes a hint line to stderr
for each callout tag that has type= but no background-color, telling the user
the recommended explicit attributes to add:
hint: callout type="warning" has no background-color; consider: background-color="light-yellow" border-color="yellow"
Also fixes CodeRabbit feedback: the type= regex now accepts both single-quoted
and double-quoted attribute values (type='warning' and type="warning").
* fix(doc): harden background-color detection in WarnCalloutType
CodeRabbit flagged that the previous strings.Contains(attrs,
"background-color=") check missed forms like 'background-color =
"light-red"' with whitespace around the equals sign. Replace with a
regex that tolerates optional whitespace, and add a regression test.
* fix(doc): close real review gaps left over after rebase
PR #467's review thread had three substantive comments
(`fangshuyu-768`, 2026-04-21) that the prior reply messages claimed
were fixed in commit 7d4b556 — but that commit no longer exists on the
branch (lost in a rebase / squash), and the head still ships the
original buggy code. This commit makes the fixes real.
Three behavior fixes in shortcuts/doc/markdown_fix.go:
1. (#5) Tighten the type= and background-color= regex anchors. \b sits
at any word/non-word boundary, and `-` is a non-word char, so
`\btype=` also matched the suffix of `data-type=` — a tag like
`<callout data-type="warning">` would emit a bogus light-yellow
hint. Switched both regexes to `(?:^|\s)…` so a real attribute
separator is required. The same anchor on background-color closes
the symmetric case where a `data-background-color=` attribute
would silently suppress the real hint.
2. (#4) WarnCalloutType is now a fence-aware line walker. Previously
the regex ran over the entire markdown body, so a callout sample
inside a documentation code fence (```markdown … ```) would
generate a phantom stderr hint every time the docs mentioned the
feature. The walker tracks fence state via the existing
codeFenceOpenMarker / isCodeFenceClose helpers from
docs_update_check.go, which handle both backtick and tilde fences
per CommonMark §4.5.
3. (#3) Drop the ReplaceAllStringFunc-as-iterator pattern. The
previous code routed callout iteration through a rewrite primitive
whose rebuilt-string return value was discarded, then ran the same
regex a second time inside the callback to recover the capture
groups. New scanCalloutTagsForWarning helper uses
FindAllStringSubmatch — one pass, no thrown-away allocation,
intent matches the surface (read-only scan, not a mutator).
Tests: 5 new TestWarnCalloutType subtests pin each contract:
- data-type attribute does not trigger hint (#5)
- data-background-color does not suppress hint (#5, symmetric)
- callout inside backtick fence emits no hint (#4)
- callout inside tilde fence emits no hint (#4)
- callout after fence close still emits hint (#4, fence-state reset)
All 14 TestWarnCalloutType cases pass; go vet / golangci-lint
--new-from-rev=origin/main both clean.
* feat(base): add record read SOP guidance
1. Add a unified lark-base record read SOP for get/search/list routing, field projection, temporary view querying, pagination, matrix result binding, and link field reads.
2. Inline command-focused parameter guidance into +record-get, +record-search, and +record-list help, including examples, JSON shape, view scope, projection, and limit constraints.
3. Preserve base shortcut flag order in help output and add tests covering record read help guidance.
4. Remove the single-method record read skill references in favor of the unified SOP.
* test(base): remove stale record list fixture
* fix(base): scan record markdown output
* fix(base): fallback record markdown output
* fix(base): unify base token wording in shortcuts and skills
* feat(drive): add +pull shortcut to mirror a Drive folder onto local
Adds `drive +pull`, a one-way Drive → local mirror command. It
recursively lists --folder-token, downloads each type=file entry
into --local-dir at the matching relative path, and optionally
deletes local files absent from the remote (mirror semantics).
Implementation notes:
- Listing recurses through subfolders with the standard 200-page
pagination loop. Online docs (docx, sheet, bitable, mindnote,
slides) and shortcuts are skipped since there is no equivalent
local binary to write back. Folder tree is reproduced under
--local-dir, with parent directories auto-created by FileIO.Save.
- Per-file --if-exists=overwrite (default) | skip controls how
pre-existing local files are treated; the framework's enum guard
rejects any other value.
- --delete-local is the only destructive flag and is bound to --yes
in Validate: --delete-local without --yes is rejected upfront so
no listing or download even runs. --delete-local --yes performs
downloads first, then walks --local-dir and removes regular files
not present in the remote map. This matches the spec doc's
"high-risk-write" intent for --delete-local without making the
default pull path require confirmation.
- --local-dir is funneled through validate.SafeLocalFlagPath so
errors reference --local-dir instead of the framework default
--file. FileIO().Stat then enforces existence and IsDir.
- Scopes: drive:drive.metadata:readonly + drive:file:download. The
broader drive:drive is disabled by enterprise policy in some
tenants.
- Listing helper (drivePullListRemote) is duplicated locally rather
than reused from drive_status.go because that change is still in
open PR #692; once it merges, both can be lifted into a shared
drive package helper. TODO marker is left in the code.
Tests cover six unit scenarios (happy-path with nested subfolder +
docx skipping, --if-exists=skip, --delete-local rejection without
--yes, --delete-local --yes deletes orphans, absolute-path
rejection, bad enum) and four E2E dry-run scenarios (request shape,
absolute path rejection, --delete-local --yes guard, missing
required flag).
* docs(skills): document drive +pull in lark-drive skill
Adds references/lark-drive-pull.md covering parameters, output schema
(summary + per-item action breakdown), the type=file scoping rule,
the --if-exists policy matrix, and the --delete-local + --yes safety
contract. Calls out the network-traffic caveat (pull is full-download,
unlike +status which only fetches when both sides have the file) and
the cwd boundary on --local-dir.
Wires +pull into the Shortcuts table in SKILL.md.
* fix(drive): walk +pull on canonical absolute root to close symlink/.. escape
Same root cause as the +status fix: --local-dir was validated through
SafeLocalFlagPath but the walk used the user-supplied raw string.
SafeLocalFlagPath returns the original value (the canonical form is
discarded), and SafeInputPath itself relies on filepath.Clean for
normalization, which shrinks "link/.." to "." purely as string
manipulation. The kernel then resolves "link/.." through the symlink
target's parent at walk time, putting the traversal outside cwd.
For +pull the bug is more dangerous than for +status because it
travels through --delete-local --yes — a raw walk would let the
delete pass land on files outside cwd.
Fix:
- In Execute, resolve --local-dir via validate.SafeInputPath to get a
canonical absolute path, and resolve "." the same way for cwd.
- Convert the resolved root back to a cwd-relative form
(filepath.Rel) for download targets so FileIO.Save's existing
SafeOutputPath check (which rejects absolute paths) still applies.
- For --delete-local, walk the canonical absolute root, then delete
via the absolute path. Both values come from the validated
safeRoot, so kernel path resolution cannot redirect a delete to a
file outside the canonical subtree.
- drivePullWalkLocal now returns absolute paths instead of rel paths;
the caller computes the rel_path via filepath.Rel against safeRoot
for output / remote-set membership checks.
Adds TestDrivePullDeleteLocalDoesNotEscapeViaSymlinkParentRef as a
regression: it stages an "escape" sibling directory containing a
sentinel file, adds a "link" symlink in cwd pointing into it, and
runs +pull --delete-local --yes against an empty remote with
--local-dir "link/..". The sentinel must survive (proving --delete
did not escape) and the in-cwd file must be removed (proving the
walk did run).
* test(drive): pin walker / download behavior on +pull symlink corner cases
Adds three regressions on top of the canonical-root walk fix:
- TestDrivePullSkipsSymlinkInsideRoot: a child symlink inside the
validated root pointing to a sibling temp dir. Under
--delete-local --yes with an empty remote, the sentinel inside the
target must survive (walker did not follow the child symlink) and
the in-cwd file must be deleted (walker did run).
- TestDrivePullSurvivesCircularSymlinkInsideRoot: a child symlink
pointing at one of its ancestors. The walk must terminate so the
test does not hang on the per-test timeout.
- TestDrivePullDownloadDoesNotEscapeViaSymlinkParentRef: pins the
download half of the fix. With --local-dir "link/.." the canonical
root resolves to cwd, so the remote file must land in cwd, not
inside the symlink target's parent. The preexisting sentinel inside
the escape directory must remain untouched.
* fix(drive): +pull --delete-local must not unlink local files shadowed by online docs
CodeRabbit (PR #696) flagged that the --delete-local pass treated any
local path missing from `remoteFiles` as orphaned, but `remoteFiles` only
records type=file entries. If Drive held a docx/sheet/shortcut at the
same rel_path as a local file, the local file would be unlinked even
though Drive still owned that path.
drivePullListRemote now returns two views:
- files: rel_path -> file_token, type=file only (download/skip set)
- allPaths: every entry's rel_path regardless of type
The download loop continues to consume `files`; the --delete-local pass
consults `allPaths`, so an online-doc shadow of a local filename keeps
the local file safe.
Also routes the local walk and the delete through the vfs abstraction
(vfs.ReadDir + vfs.Remove) instead of filepath.WalkDir + os.Remove.
This drops the //nolint:forbidigo justifications and lines up with how
internal/keychain and internal/registry already do filesystem I/O. The
recursive vfs.ReadDir walker preserves the same "do not follow child
symlinks" semantics that filepath.WalkDir gave us, so the canonical-root
escape protections in 240b772 stay intact.
Adds TestDrivePullDeleteLocalPreservesLocalFileShadowedByOnlineDoc as a
direct regression: Drive serves keep.txt (file) plus notes.docx (docx),
local has both keep.txt and a hand-edited notes.docx; --delete-local
--yes must download keep.txt, leave notes.docx untouched, and report
deleted_local=0.
* fix(drive): count +pull delete failures in summary.failed
CodeRabbit (PR #696) flagged that both delete_failed branches in the
--delete-local pass appended an item but left the `failed` counter at
zero, so the JSON summary could legitimately report `"failed": 0` after
a partially-failed mirror. Increment failed in both branches (the
filepath.Rel error path and the vfs.Remove error path) so summary.failed
reflects every item flagged delete_failed in items[].
Adds TestDrivePullDeleteLocalCountsFailureInSummary, which forces
vfs.Remove to fail by chmod-ing the local dir 0o555 right before the
run and restoring 0o755 in t.Cleanup so t.TempDir teardown still works.
* fix(drive): swap +pull walk/remove back to filepath/os to satisfy depguard
The previous fix-up commits used vfs.ReadDir + vfs.Remove inside the
+pull shortcut, which depguard's "shortcuts-no-vfs" rule rejects:
shortcuts cannot import internal/vfs directly. CI lint failed on the
import line.
Restore the same pattern used in drive_status.go and the prior +pull
walker:
- filepath.WalkDir to enumerate files under the canonical absolute
root, gated by //nolint:forbidigo with a comment explaining why.
- os.Remove for the actual delete, also gated by //nolint:forbidigo.
The canonical-root safety still holds: validate.SafeInputPath bounds
the walk root inside cwd before WalkDir runs, and WalkDir's default
"do not follow child symlinks" policy is preserved. The two earlier
fixes (drivePullListRemote returning allPaths so online-doc shadows
do not look orphaned, and incrementing failed on delete_failed) stay
in place.
`go test ./shortcuts/drive/...` and `golangci-lint run
--new-from-rev=origin/main` are both clean.
* fix(drive): record remote folder rel_path in +pull allPaths
Follow-up to 45fe4e3. The folder branch in drivePullListRemote merged
descendant rel_paths into allPaths but never recorded the folder's own
rel_path, so a local regular file with the same name as a remote
folder still looked orphaned and got unlinked under --delete-local.
Adds the missing allPaths[rel] for the folder case and a regression:
TestDrivePullDeleteLocalPreservesLocalFileShadowedByRemoteFolder
stages a Drive containing a folder named shadow alongside a
downloadable file, with the local side holding a regular file named
shadow; --delete-local --yes must download keep.txt and leave the
shadow file untouched.
* fix(drive): +pull pagination + dir/file conflict + skill doc symlink claim
Codex review on PR #696 surfaced three issues; addressed in one go:
1. drivePullListRemote only honored next_page_token. The shared
common.PaginationMeta helper accepts both page_token and
next_page_token; switched +pull over so a backend reply using
page_token no longer makes the lister stop at page 1 (which would
silently drop later remote files from both download and
--delete-local).
2. --if-exists=skip swallowed mirror conflicts. The skip/overwrite
branch only checked Stat success, so a local directory shadowing a
remote regular file was reported as action=skipped. Now Stat's
IsDir() is checked first; the conflict surfaces as action=failed
with a message naming the directory, under both --if-exists=skip
and --if-exists=overwrite, and increments summary.failed.
3. Skill doc told callers to soft-link the target into cwd if they
wanted to pull from outside cwd. That is wrong: SafeInputPath
evaluates symlinks before the cwd check, so a symlink pointing
out-of-tree is rejected. Replaced the bogus shortcut with the
actually viable options (switch the agent working directory,
physically move/copy the target, or skip the comparison).
Two new regressions:
- TestDrivePullSurfacesDirectoryFileMirrorConflict — table test over
both policies asserting failed=1, no skipped, action=failed, plus
the 'is a directory' hint in the error message.
- TestDrivePullPaginationHandlesPageTokenField — first page returns
page_token (not next_page_token) with has_more=true; asserts both
pages are fetched and both files land on disk.
* fix(drive): +pull exits non-zero on item failures; gate --delete-local
Two PR-696 review fixes:
- Item-level failures (download error, dir/file conflict, delete error)
now surface as a structured partial_failure ExitError instead of a
success envelope with summary.failed > 0. Exit code becomes non-zero
and error.detail still carries the {summary, items[]} payload, so
AI / script callers can detect the failure via the exit code without
reaching into the JSON body.
- A failed download pass now skips the --delete-local walk entirely.
Previously +pull would continue removing local-only files even when
the download phase had partially failed, leaving the mirror in a
half-synced state (some Drive files missing locally AND some
local-only files unlinked). Re-runs after fixing the download error
recover cleanly.
Skill doc / shortcut description / flag desc updated to call the
operation a one-way file-level mirror, since --delete-local only
unlinks regular files and does not prune empty local directories left
behind by remote folder deletes (true directory-level mirroring is
explicitly out of scope).
Tests: existing dir/file-conflict and delete-failure cases now assert
the partial_failure ExitError shape; new test covers the
"download fails => --delete-local skipped" gating contract.
* refactor(drive): consolidate folder-listing helpers into listRemoteFolder
Closes the post-#692 / post-#709 TODO that lived in drive_pull.go (and
the matching note in drive_push.go): both #692 and #709 are now on main,
so the three near-identical recursive Drive folder listers can collapse
into one.
New shared helper in shortcuts/drive/list_remote.go:
driveRemoteEntry { FileToken, Type, RelPath }
listRemoteFolder(ctx, runtime, folderToken, relBase) -> map[rel]entry
Returns one entry per Drive item (every type), keyed by rel_path.
Subfolders are descended into and the folder's own entry is recorded so
callers can reason about "this rel_path is occupied by a folder"
without re-listing. Pagination via common.PaginationMeta is unchanged.
Each shortcut now derives its own per-shortcut view from the unified
listing:
- drive_status.go: collapses to remoteFiles (Type=="file" -> token) for
the content-hash diff.
- drive_pull.go: derives remoteFiles (Type=="file") for the download
set, plus remotePaths (every rel_path) as the --delete-local guard.
- drive_push.go: derives remoteFiles (Type=="file") for upload /
overwrite / orphan-delete, plus remoteFolders (Type=="folder") for
the create_folder cache. drivePushRemoteEntry was a duplicate of
driveRemoteEntry's first two fields and is dropped; the few call
sites that read .FileToken keep working unchanged.
Per-shortcut copies removed:
- drive_status.go: listRemoteForStatus, joinRelStatus,
driveStatusListPageSize/FileType/FolderType
- drive_pull.go: drivePullListRemote, drivePullJoinRel,
drivePullListPageSize/FileType/FolderType
- drive_push.go: drivePushListRemote, drivePushJoinRel,
drivePushListPageSize/FileType/FolderType, drivePushRemoteEntry
drive_push_test.go's TestDrivePushHelpersRelPath is retargeted at the
shared joinRelDrive; the docstrings on the same-name-conflict tests
were tweaked to refer to "the remoteFiles view" instead of the
just-removed drivePushListRemote.
Net diff: +1 new file, -207 net lines across the four touched files.
All existing unit + e2e dry-run tests pass without behavioral change;
the rel_path / pagination / type-filter contracts each shortcut depends
on are preserved by construction.
* feat(cmdutil): support @file for --params/--data (issue #705)
Inline JSON values for --params/--data are mangled by Windows
PowerShell 5's CommandLineToArgvW. Stdin (-) was the only escape
hatch but supports just one flag at a time.
Extend ResolveInput to accept @<path> (read JSON from a file) and
@@... (escape for a literal @-prefixed value), mirroring the
shortcuts framework's resolveInputFlags semantics. With this, both
--params and --data can be sourced from files in the same call,
sidestepping shell quoting on every platform.
- internal/cmdutil/resolve.go: add @path / @@ handling, trim file
content like stdin does, error on empty path or empty file
- internal/cmdutil/resolve_test.go: cover file read, whitespace
trim, missing file, empty path, empty content, @@ escape, plus
ParseJSONMap / ParseOptionalBody integration through @file
- cmd/api/api.go, cmd/service/service.go: update --params/--data
help text to mention @file
Change-Id: I366aa0f5783fbec6f05403f7f542505098a98c82
* refactor(cmdutil): route @file through fileio.FileIO abstraction
The first cut of @file support called os.ReadFile directly inside
ResolveInput, bypassing the codebase's fileio.FileIO abstraction
(SafeInputPath validation, pluggable provider). That diverged from
how every other file-reading path works: BuildFormdata for --file
uploads and the shortcuts framework's resolveInputFlags both go
through fileio.FileIO.Open with explicit fileio.ErrPathValidation
handling.
Re-route @file through the same path:
- ResolveInput, ParseJSONMap, ParseOptionalBody now take a
fileio.FileIO; @path uses fileIO.Open which goes through
SafeInputPath (control-char rejection, abs-path rejection,
symlink-escape check) — same security posture as --file
- cmd/api and cmd/service callsites pass
Factory.ResolveFileIO(ctx); the upload path now reuses the
resolved fileIO instead of resolving twice
- Path-validation errors surface as
`--params: invalid file path "...": ...` distinct from
`--params: cannot read file "...": ...` for genuine I/O errors
- Nil fileIO with an @path returns a clear
"file input (@path) is not available" error
- Tests use localfileio.LocalFileIO with TestChdir(t, dir),
matching the existing fileupload_test.go pattern; absolute-path
rejection and nil-fileIO are covered
This makes the feature behave identically under any FileIO
provider (including server mode) instead of being silently bound
to the local filesystem.
Change-Id: I878c4e8fb03f43f1f19afad75ec3af9cdab7a7f9
* refactor(cmdutil): share at-file input handling
Change-Id: I92a6eb6ea8fd02054bf8f4925cd81807449d5e51
* feat(drive): add +push shortcut for one-way local → Drive mirror
Mirrors a local directory onto a Drive folder: walks --local-dir,
recursively lists --folder-token, mirrors local subdirectory structure
(including empty dirs) onto Drive via create_folder, and for each
rel_path uploads new files, overwrites already-present files, or skips
them per --if-exists. With --delete-remote --yes, any Drive type=file
entry absent locally is removed; Lark native cloud docs (docx/sheet/
bitable/mindnote/slides) and shortcuts are never overwritten or deleted.
Overwrite hits POST /open-apis/drive/v1/files/upload_all with the
existing file_token in the form body and the response's `version` is
propagated to items[].version, mirroring the markdown +overwrite
contract. Files >20MB fall back to the 3-step
upload_prepare/upload_part/upload_finish path with a single shared fd
reused via io.NewSectionReader per block.
Output is a {summary, items[]} envelope; items[].action is one of
uploaded / overwritten / skipped / folder_created / deleted_remote /
failed / delete_failed.
--delete-remote is bound to --yes upfront in Validate, same pattern as
+pull's --delete-local: a stray flag never silently deletes anything.
Path safety reuses the canonical-root walk + SafeInputPath mechanics
from the sibling +status / +pull commands.
Scopes: drive:drive.metadata:readonly + drive:file:upload +
space:folder:create. space:document:delete is intentionally NOT in the
default set — the framework's pre-flight scope check would otherwise
block plain pushes and dry-runs for callers that haven't granted delete;
--delete-remote --yes relies on the runtime DELETE call to surface
missing_scope. The skill ref calls out the scope so users running
mirror sync can grant it upfront.
13 unit tests cover the upload/overwrite/skip/delete matrix, online-doc
protection, same-name conflict between local file and native cloud doc,
empty-directory mirroring, multipart, scope/path validation, and helper
correctness. 4 dry-run e2e tests pin the request shape.
* fix(drive +push): address review — failure semantics, default skip, scope pre-check, mirror wording
- Item-level failures now bump the exit code via output.ErrBare(ExitAPI)
while keeping the structured items[] envelope on stdout. The
--delete-remote phase is skipped entirely when any upload / overwrite /
folder step fails, so a partial upload never proceeds to delete remote
orphans (a half-synced state).
- Default --if-exists flipped from "overwrite" to the safer "skip": the
upload_all overwrite-version protocol field is still rolling out, so
the default no longer fails a first push against a pre-populated
folder. Callers must opt into "overwrite" explicitly.
- --delete-remote --yes now triggers a conditional space:document:delete
scope pre-check in Validate via the new RuntimeContext.EnsureScopes
helper, so a missing grant fails the run before any upload — instead
of after the upload phase, which would leave orphans uncleaned.
- Description, Tips and skill doc rewritten to call this a file-level
mirror (not a directory mirror): the command does not remove
remote-only directories or close gaps in directory structure that
exists only on Drive.
Tests:
- new TestDrivePushDefaultsToSkipForExistingRemote pins the new default
- new TestDrivePushSkipsDeleteAfterUploadFailure pins the half-sync
guard and the non-zero exit on item-level failure
- new TestDrivePushExitsZeroOnCleanRun pins the inverse
- existing tests that relied on the old overwrite default now opt in
explicitly with --if-exists=overwrite
- TestDrivePushOverwriteWithoutVersionFails updated to assert
*output.ExitError with Code=ExitAPI
- new TestDrive_PushDryRunAcceptsDeleteRemoteWithYes (e2e) symmetric to
the existing reject-without-yes test, pinning that EnsureScopes is a
silent no-op when the resolver has no scope metadata
* fix(drive +push): close remaining CodeRabbit comments
Three small follow-ups on the +push review thread that were still
open after the earlier failure-semantics / default-skip / scope
pre-check fix:
- drivePushUploadAll now extracts data.file_token before checking
larkCode, and surfaces the returned token on the partial-success
path (non-zero code + non-empty file_token). Without this, a backend
response where bytes already landed but code != 0 would force the
caller to fall back to entry.FileToken and silently lose the actual
Drive token, defeating the overwrite-error token-stability handling
in Execute.
- TestDrivePushOverwriteWithoutVersionFails switched from "tok_keep"
to "tok_keep_new" in the upload_all stub and now asserts that the
returned token (not entry.FileToken) lands in items[].file_token —
pins the contract that a regression to the fallback branch would
otherwise pass silently.
- New TestDrivePushOverwritePartialSuccessSurfacesReturnedToken pins
the new partial-success branch end-to-end.
- drive_push_dryrun_test.go: tightened the three Validate / cobra
rejections from `exit != 0` to exact codes — `exit == 2` for the
two Validate-stage rejections (--local-dir absolute,
--delete-remote without --yes), `exit == 1` for the cobra
required-flag check (--folder-token missing). Locks in failure
classification so a regression that misroutes the error layer
doesn't slip through.
* feat(drive): add +status shortcut for content-hash diff
Adds `drive +status`, a read-only diff primitive that walks --local-dir,
recursively lists --folder-token, and reports four buckets — new_local,
new_remote, modified, unchanged — by SHA-256 content hash.
Implementation notes:
- Drive's list/metas APIs do not expose a content hash, so files
present on both sides are downloaded via DoAPIStream and hashed in
memory (sha256 + io.Copy, no disk write). Files only on one side are
not fetched. The command stays Risk: "read".
- Only Drive entries with type=file participate. Online docs (docx,
sheet, bitable, mindnote, slides) and shortcuts are skipped — there
is no equivalent local binary to hash against.
- --local-dir is funneled through the framework's
validate.SafeLocalFlagPath helper so that absolute paths and any ..
that escapes cwd are rejected with --local-dir in the error message
(rather than the internal default --file). FileIO().Stat() then
enforces existence and the IsDir check.
- Local walk uses filepath.WalkDir behind a //nolint:forbidigo comment.
The runtime FileIO interface has no walker today and shortcuts can't
import internal/vfs; SafeInputPath has already bounded the walk root
inside cwd, so the bare walk is acceptable until a runtime-level
walker lands.
- Scopes: drive:drive.metadata:readonly (list folders) +
drive:file:download (fetch files for hashing). The broader
drive:drive scope is disabled by enterprise policy in some tenants;
this narrower pair was verified end-to-end.
Tests cover the four-bucket categorization with a nested subfolder and
docx/shortcut filtering, plus validation errors for missing local-dir,
non-directory local-dir, and absolute-path local-dir.
* docs(skills): document drive +status in lark-drive skill
Adds references/lark-drive-status.md covering parameters, output
schema, the type=file scoping rule, and the network-traffic caveat
(hash is streamed in memory, but bytes still cross the wire).
Notes that --local-dir is bounded to cwd by the CLI's path validation,
and that when a user wants to compare a directory outside cwd the
agent should ask the user to relocate or to switch the agent's working
directory rather than `cd`-ing on its own.
Wires +status into the Shortcuts table in SKILL.md.
* test(drive): cover --folder-token validation and add +status dry-run E2E
Addresses two CodeRabbit review comments on PR #692:
- Adds TestDriveStatusRejectsEmptyFolderToken and
TestDriveStatusRejectsMalformedFolderToken so the Validate-stage
required-check and the ResourceName format guard for --folder-token
are exercised, not just --local-dir.
- Adds tests/cli_e2e/drive/drive_status_dryrun_test.go which drives
the real binary in dry-run mode and asserts:
* the request shape (GET /open-apis/drive/v1/files with
folder_token in the dry-run envelope), plus the description text,
* --local-dir absolute paths are rejected by Validate (which still
runs under --dry-run) with --local-dir surfaced in the message,
* cobra's required-flag enforcement rejects a missing
--folder-token before any custom validation.
* fix(drive): walk +status on canonical absolute root to close symlink/.. escape
Reported in PR review: --local-dir was validated through
SafeLocalFlagPath, but the actual walk used the user-supplied raw
string. SafeLocalFlagPath returns the original value (it only checks
the path through SafeInputPath and discards the canonical form), and
SafeInputPath itself relies on filepath.Clean for path normalization.
filepath.Clean shrinks "link/.." to "." purely as string manipulation,
so the validator sees a path inside cwd. The kernel, however, resolves
"link/.." through the symlink target's parent — which is outside cwd
and is what filepath.WalkDir actually traverses.
Fix: in Execute, resolve --local-dir via validate.SafeInputPath to
get the canonical absolute path (this one fully evaluates symlinks
across the entire path), and walk that path. Each absolute walk hit
is converted to a cwd-relative form via filepath.Rel against
validate.SafeInputPath(".") so FileIO.Open's existing SafeInputPath
guard (which rejects absolute paths) still applies.
Adds TestDriveStatusDoesNotEscapeViaSymlinkParentRef as a regression:
it stages an "escape" sibling directory containing a sentinel file,
adds a "link" symlink in cwd pointing into the escape directory, and
runs +status with --local-dir "link/..". Without this fix, the raw
walk visits the sentinel and leaks it into new_local; with the fix,
the walk stays inside the canonical cwd.
A standalone repro confirms the underlying behavior: raw
filepath.WalkDir("link/..", ...) traversed dozens of unrelated files
in the kernel-resolved parent directory; walking the canonical root
visits only the legitimate cwd contents.
* test(drive): pin walker behavior on child / circular symlinks for +status
Adds two corner-case regressions to back up the canonical-root walk fix:
- TestDriveStatusSkipsSymlinkInsideRoot: a child symlink under
--local-dir that points to a sibling temp dir outside cwd. WalkDir's
default policy must report it as a non-regular entry so the callback
skips it, and the sentinel inside the target must not surface in
new_local. This pins the contract our caller relies on (walk
declines to follow child symlinks even when the canonical root
resolves cleanly).
- TestDriveStatusSurvivesCircularSymlinkInsideRoot: a child symlink
pointing back at one of its ancestors. The walk must terminate and
surface the legitimate sibling file; if WalkDir ever followed the
loop, the per-test timeout would catch it.
* fix(drive): close +status review gaps from Codex (pagination, doc, live E2E)
Three independent fixes flagged on PR #692:
1. Route the recursive Drive folder listing through common.PaginationMeta
instead of reading next_page_token directly. The shared helper accepts
both page_token and next_page_token, matching what okr/im already do
and keeping +status safe against a backend field rename. Adds
TestDriveStatusPaginatesRemoteListing, which serves a 2-page response
where page 1 advertises the cursor as next_page_token and page 2 as
page_token; either spelling alone would silently drop one page.
2. The skill doc previously suggested "or symlink the target into cwd"
as a workaround for cwd-relative --local-dir. SafeInputPath calls
filepath.EvalSymlinks before checking isUnderDir(canonicalCwd), so
any symlink whose final target sits outside cwd still gets rejected
as `unsafe file path`. Rewrite the section so agents stop steering
users into a path that always errors out.
3. Add tests/cli_e2e/drive/drive_status_workflow_test.go — the live
E2E that AGENTS.md requires for new shortcuts. Seeds a real Drive
folder with three uploaded files (unchanged.txt, modified.txt,
remote-only.txt), seeds a local tree with matching/diverging
content plus a local-only.txt, runs +status, and asserts each of
the four buckets contains exactly the file we expect with the
right file_token. Cleanup of every uploaded file plus the parent
folder is registered through the existing best-effort cleanup
helpers. Coverage table bumped: drive +status moves to ✓ and the
denominator goes from 28→29 to account for the new shortcut.
Codex also flagged the local-side filepath.WalkDir as a vfs-bypass.
Investigated: the depguard rule shortcuts-no-vfs explicitly forbids
shortcuts from importing internal/vfs (see commit c1b0bed on the
+pull branch where the same migration was rejected by CI). The
filepath.WalkDir + nolint:forbidigo pattern in walkLocalForStatus is
the lint-required convention until FileIO grows a walker, so leaving
it as-is.
Support minutes +upload to generate a minute from an uploaded media file token.
Change-Id: I59c0719a39541134e395a23262aea7f387105715
Co-authored-by: calendar-assistant <calendar-assistant@users.noreply.github.com>
## Summary
Add explicit guidance on the parent `docs` command so agents pick the right
lark-doc API version. Without this, agents that have an older lark-doc skill
installed can mistakenly mix v2 flags into a v1 flow.
## Changes
- Add `--api-version` help flag and a Tips section to `docs` so `lark docs --help`
(and `--api-version v2`) explain when v2 should be used.
- Refresh the lark-doc skill references and `docs_fetch_v2` keyword flag
description for clarity.
- Add `shortcuts/register_test.go` covering the new docs help wiring.
## Test Plan
- [x] Unit tests pass (`go test ./shortcuts/...`)
- [x] Manual local verification confirms the `lark docs --help` and
`lark docs --help --api-version v2` commands work as expected
## Related Issues
- None
Change-Id: Id3b3196e6a069bb52f95a6fc679b8258313faf3d
The Windows extraction step relied on `powershell -Command Expand-Archive`,
which fails when:
- Microsoft.PowerShell.Archive (a script module) cannot be loaded due to
PSModulePath shadowing (Store-installed pwsh injecting WindowsApps
paths) or ExecutionPolicy Restricted (issue #603), or
- the temp directory contains characters that corrupt PowerShell string
parsing (e.g. a single quote in TEMP).
Switch to a two-tier extraction:
1. Primary: Add-Type System.IO.Compression.FileSystem +
[ZipFile]::ExtractToDirectory. Bypasses the PowerShell module system
entirely. .NET 4.5+, available on Win 8 / Server 2012 by default and
widely on Win 7 SP1.
2. Fallback: Expand-Archive -LiteralPath, kept for the rare host without
.NET 4.5 but with PS 5.0+ (e.g. Win 7 SP1 with WMF 5).
Both paths pass file paths through env vars ($env:LARK_CLI_ARCHIVE /
$env:LARK_CLI_DEST) so quoting / wildcard chars in the path can no longer
break command parsing. -LiteralPath ensures Expand-Archive treats the value
literally rather than as a wildcard pattern. $ErrorActionPreference='Stop'
makes non-terminating cmdlet errors propagate as non-zero exit codes.
Also drop `stdio: "ignore"` so the actual PowerShell error surfaces in the
postinstall log when both paths fail, instead of leaving users with
"Command failed: powershell ..." with no detail.
Verified on Windows 10 + PS 5.1:
- Reproduced #603 with shadow Microsoft.PowerShell.Archive +
Restricted ExecutionPolicy: original install.js fails, patched
install.js succeeds.
- Reproduced single-quote-in-TEMP path corruption: original fails,
patched succeeds.
- Fallback path verified end-to-end with primary forced to fail.
- Normal-environment install: no regression.
* feat(contact +search-user): add --queries multi-name fanout
Add --queries CSV flag to lark-cli contact +search-user for parallel
multi-name fanout (up to 20 entries, partial-failure tolerant).
Output shape in fanout mode:
- data.users[] rows carry matched_query (string)
- data.queries[] sidecar lists each input with {query, error?, has_more}
- top-level data.has_more removed (per-query in queries[])
- error is omitempty; absent on success
Single --query mode is byte-for-byte unchanged (regression-guarded).
--queries is mutually exclusive with --query and --user-ids; bool
filters propagate to every sub-request.
Workers run with WaitGroup + buffered semaphore + index-slot writes;
each has defer recover() converting panics to internal error: ... in
the sidecar (no stack to stderr). Pre-canceled context returns
context canceled without making the request.
All-failed exit propagates first failure's HTTP/API code via ErrAPI;
falls back to ExitInternal for transport/parse/panic/ctx-canceled
(avoids emitting code 0, which means success in the Lark protocol).
HTTP non-200 ErrMsg now includes truncated response body for diagnosis.
Drive-by: signature field is now omitempty (mostly empty in practice).
Infrastructure:
- internal/httpmock gains BodyFilter/OnMatch/Reusable/CapturedBodies
hooks to support concurrent stub-driven tests
- internal/output adds 'users' to knownArrayFields so CSV picks the
primary array correctly
Change-Id: I3c14195fb8e094ae150002d90c36a0e4a0cc97d0
* fix(config/init): use parseBrand(opts.Brand) instead of hardcoded BrandFeishu in --new mode
The --new flag was ignoring the --brand flag and always passing BrandFeishu
to runCreateAppFlow. Now it correctly uses parseBrand(opts.Brand) to
respect the user's --brand parameter (e.g., --brand lark for international).
Change-Id: I1d4d78b3d586142b0210e6ceaeeb467b14e9c1a1
Add --queries CSV flag to lark-cli contact +search-user for parallel
multi-name fanout (up to 20 entries, partial-failure tolerant).
Output shape in fanout mode:
- data.users[] rows carry matched_query (string)
- data.queries[] sidecar lists each input with {query, error?, has_more}
- top-level data.has_more removed (per-query in queries[])
- error is omitempty; absent on success
Single --query mode is byte-for-byte unchanged (regression-guarded).
--queries is mutually exclusive with --query and --user-ids; bool
filters propagate to every sub-request.
Workers run with WaitGroup + buffered semaphore + index-slot writes;
each has defer recover() converting panics to internal error: ... in
the sidecar (no stack to stderr). Pre-canceled context returns
context canceled without making the request.
All-failed exit propagates first failure's HTTP/API code via ErrAPI;
falls back to ExitInternal for transport/parse/panic/ctx-canceled
(avoids emitting code 0, which means success in the Lark protocol).
HTTP non-200 ErrMsg now includes truncated response body for diagnosis.
Drive-by: signature field is now omitempty (mostly empty in practice).
Infrastructure:
- internal/httpmock gains BodyFilter/OnMatch/Reusable/CapturedBodies
hooks to support concurrent stub-driven tests
- internal/output adds 'users' to knownArrayFields so CSV picks the
primary array correctly
Change-Id: I3c14195fb8e094ae150002d90c36a0e4a0cc97d0
* feat(install): enhance binary URL resolution with environment variable support
* fix(install): defer mirror resolution into install() to surface friendly errors
resolveMirrorUrl was called at module scope, so an invalid
LARK_CLI_DOWNLOAD_HOST (e.g. file://) threw before the try/catch in the
postinstall entrypoint, dumping a raw stack trace instead of the recovery
guidance with proxy/registry/host-override options.
Move resolution into install() via getMirrorUrl() so the throw is caught
and the user sees the actionable help text.
* fix(install): keep npmmirror fallback when npm_config_registry is set
resolveMirrorUrl returned a single URL, so any non-default
npm_config_registry replaced the npmmirror fallback entirely. Corporate
npm proxies (Verdaccio, Artifactory, Nexus) often only serve npm package
metadata and don't host /-/binary/<pkg>/..., turning previously-working
installs into 404s when GitHub is unreachable.
Switch to resolveMirrorUrls returning an ordered chain:
- LARK_CLI_DOWNLOAD_HOST set → [override] only (explicit user choice;
no silent leak to npmmirror).
- Otherwise → [derived_from_registry?, npmmirror_default]; npmmirror
is always the final entry, restoring the pre-PR safety net.
install() now walks [GITHUB_URL, ...mirrorUrls] and stops at the first
success.
* fix(install): skip GitHub when LARK_CLI_DOWNLOAD_HOST is set
The download loop unconditionally tried GITHUB_URL first, even when the
user explicitly named a download host. In locked-down networks, probing
github.com can trigger DLP / firewall alerts and contradicts the
explicit-override semantics ("use only this host, nothing else").
When LARK_CLI_DOWNLOAD_HOST is set, the chain is now just [override].
When it isn't, behavior is unchanged: [GITHUB_URL, derived?, npmmirror].
* refactor(install): drop LARK_CLI_DOWNLOAD_HOST env override
Issue #640 only asked for --registry to influence the binary download.
The LARK_CLI_DOWNLOAD_HOST escape hatch was added speculatively for
locked-down networks but is YAGNI — users in those environments already
have npm-level mirrors (--registry) or proxy controls (https_proxy).
Removing it shrinks the surface area:
- delete parseDownloadBase() and its strict https-only validation
- drop the install() branch that skipped GitHub on explicit override
- simplify failure-help message to two recovery options
Resolution chain becomes [GITHUB, derived_from_npm_config_registry?,
npmmirror_default]. The npmmirror tail still preserves the pre-PR safety
net when a corp registry doesn't actually serve /-/binary/<pkg>/...
End-to-end verified on Linux + Windows via real `npm install -g <tgz>`:
all four user scenarios pass, with the issue #640 path (--registry=
npmmirror + GitHub blocked) finishing in 2s on Linux / 6s on Windows.
* feat(ics): add RFC 5545 iCalendar generator and parser
Add shortcuts/mail/ics package:
- builder.go: generates METHOD:REQUEST ICS with VEVENT, ORGANIZER,
ATTENDEE, DTSTART/DTEND with timezone, UID, and X-LARK-MAIL-DRAFT
- parser.go: parses ICS into ParsedEvent struct, detects IsLarkDraft
- Handles CN quoting, control-char sanitization, email validation,
line folding per RFC 5545, and TZID edge cases
Change-Id: I01d13285a57a5a4de50891c54d655efa8423c3c1
* feat(mail): support calendar events in emails
- Add --event-summary/start/end/location flags to +send, +reply,
+reply-all, +forward, +draft-create
- Build ICS and embed as text/calendar in multipart/alternative
- Validate event time range and enforce --event/--send-time mutual
exclusion (extracted into validateEventSendTimeExclusion)
- CalendarBody() in emlbuilder places ICS correctly
- Exclude BCC from ATTENDEE list
Change-Id: Icf9e49ababebc4e8fcf36760ab613c64938c2744
* feat(mail): X-LARK-MAIL-DRAFT and read-only calendar guard
- ics.Build() writes X-LARK-MAIL-DRAFT:TRUE so Feishu client
recognizes CLI-created calendar events as editable
- ics.ParseEvent() detects IsLarkDraft field
- +draft-edit rejects --set-event-* on calendars without
X-LARK-MAIL-DRAFT marker (read-only after send)
- Export FindPartByMediaType from draft package for cross-package use
- Add set_calendar/remove_calendar patch ops with full test coverage
Change-Id: I7d547a4b40880e8d4ee3fecf68864d6ea89e66cd
* feat(mail): forward preserves original calendar ICS
When forwarding an email that contains a calendar event (body_calendar),
pass through the original ICS bytes as text/calendar part if no new
--event-* flags are specified.
Change-Id: I67d2e82604eaf969cee8c7e0bedcf32198d12d57
* docs(mail): document calendar invitation feature
- Add --event-* params to +send, +reply, +reply-all, +forward,
+draft-create, +draft-edit reference docs
- Add calendar_event output section to +message reference
- Add calendar invitation workflow to skill-template/domains/mail.md
- Regenerate SKILL.md via gen-skills
Change-Id: Iccacd06990d91e1cf3beb896d5b772d27e5e29ff
* fix(mail): reject --set-event-start/end/location without --set-event-summary
Change-Id: Icb651ff28ede526ff96b22e7b304b7bdea86d01f
Co-Authored-By: AI
* fix(mail): include --event-location in validateEventFlags; fix stale comment
Change-Id: I2f47016b6bfa11957dfe2c8c499cf36737efba53
Co-Authored-By: AI
* fix(mail): clear stale headers when wrapping single-leaf body in multipart/alternative
Change-Id: I29fe883c9151570f7939d372523b128cbea0b1ed
Co-Authored-By: AI
* fix(mail): add method=REQUEST to text/calendar MIME part created by set_calendar
Change-Id: I4d23674e20e4c42adab36385ff5ee8bb6d97625d
Co-Authored-By: AI
* fix(mail): use post-edit recipients for ICS attendees when --set-to combined with --set-event-*
Change-Id: I659e06635dd043f798d2f2e90d7dbca6e13d7f3d
Co-Authored-By: AI
* fix(mail): cover add_recipient/remove_recipient in ICS attendee resolution
Extract effectiveRecipients() to replay all three recipient op types
(set_recipients, add_recipient, remove_recipient) before building the
ICS for set_calendar, so patch-file recipient changes are reflected in
ATTENDEE metadata.
Change-Id: I3a7a55f96df8fac7d924a4dbeedd5b3d0d9d443c
Co-Authored-By: AI
* fix(mail): derive method= from ICS body in writeCalendarPart instead of hardcoding REQUEST
Passthrough ICS (e.g. forwarded METHOD:CANCEL) previously emitted a
Content-Type with method=REQUEST, disagreeing with the body. Now
extractICSMethod() scans the ICS for METHOD: and falls back to REQUEST
when absent, keeping existing behavior for our own generated ICS.
Change-Id: I4bf6c3755a189a436c2d26b082372d9f838f4051
Co-Authored-By: AI
* fix(mail): normalize calendar_event start/end to UTC in output
Callers expect RFC 3339 UTC strings; source ICS with TZID offsets
previously emitted +08:00 instead of Z.
Change-Id: I88bd4b925f8fc3b4f569e41712ae58ab50d94a2f
Co-Authored-By: AI
* fix(mail): make ICS parser case-insensitive and handle parameterized property names
RFC 5545 §3.1 allows any case and optional parameters on all property
names. Unify UID/SUMMARY/LOCATION/DTSTART/etc. to compare via
strings.ToUpper(name) and add HasPrefix checks for the NAME; form,
consistent with how ORGANIZER and ATTENDEE were already handled.
Change-Id: I7dc642dd210a3256f2189a901a2d9518ea284815
Co-Authored-By: AI
* docs(base): align base skills and view config contracts
1. Rework the lark-base source-of-truth docs around canonical field, cell, record and view payload shapes.
2. Refresh view, workflow, lookup and related references against current openapi behavior and remove stale or broken guidance.
3. Remove dead array-wrapper handling from view sort/group setters and add unit plus dry-run e2e coverage for object-only input.
* docs(base): drop view config code changes from doc refactor
1. Revert the temporary Base view config Go and test adjustments so this PR only keeps lark-base skill and reference updates.
2. Preserve the documentation contract changes while leaving runtime behavior unchanged from the pre-refactor implementation.
* docs(base): revert temporary view config code cleanup
1. Restore the pre-refactor Base view config Go paths and related unit tests so this PR keeps runtime behavior unchanged.
2. Leave the lark-base skill and reference updates in place as the only intended product change in this branch.
* docs(base): fix progress color typo
* docs(base): trim padding in reference docs
1. Remove obviously excessive alignment spaces from base reference examples and operator lists.
2. Shorten a few overlong separator rows in the formula guide to reduce low-value formatting noise.
3. Keep the changes scoped to four lark-base reference files without changing documented behavior.
* docs(base): clarify field description guidance
* test: isolate dry-run e2e config state
* chore: update data-query prompt
* docs(base): simplify formula filter guidance
* docs(base): drop stage field mention from data query
* revert: keep e2e changes scoped to base docs
* docs(base): clarify dashboard field type wording
* docs(base): trim number filter operators
`im chats link` is registered as a regular service method (no
`risk: high-risk-write` annotation), so the framework does not register
the `--yes` flag on it. Setting `Yes: true` on the e2e Request makes
the runner append `--yes`, which cobra rejects with `unknown flag:
--yes` before the request is ever issued — the rest of the assertions
then fall through with empty stdout.
The flag was added in #633 alongside the risk-tiering rollout that
covered other workflows that genuinely flipped to high-risk-write.
For chats link the API call (creating a chat share link with a
configurable validity period) is not destructive and was never
re-classified, so the line is just leftover from that pass. Drop it
to restore the e2e green; if we ever decide to gate share-link
creation behind confirmation we can re-add it together with the
metadata flip.
Change-Id: Ieb094407a7f0fa18cd130a9d80c7146274b5ecc7
* feat(contact +search-user): add search filters and richer profile fields
- Filter results by chat history, employment status, tenant boundary,
or enterprise email presence; keyword is now optional so filter-only
queries ("list all my external contacts") work end-to-end.
- Each result now carries multilingual names, contact email, activation
state, whether you've chatted with them, tenant context, user
signature, and a hit-highlight line that surfaces the matched segment
and the user's department path.
- Always-empty legacy columns and fields the new backend no longer
returns are dropped.
- Also fixes the contact +get-user skill doc, which previously
instructed callers to pass --table (a flag that never existed); now
correctly documents --format table and the full --format enum.
* refactor(lark-contact): clean up search-user code, tighten skill docs
- contact_search_user.go / _test.go: simplify and clarify
- SKILL.md: focus description on user-facing trigger scenarios;
rework decision table; trim notes to load-bearing constraints
- references/lark-contact-search-user.md: add flag table covering
all four bool filters; add multi-filter examples; clean up
output field contract (drop server <h> tag implementation detail)
- references/lark-contact-get-user.md: trim to two real use cases
(self via user identity; full profile of others via bot identity);
point user-mode-by-id users to +search-user instead
- .golangci.yml: replace package-level deny on net/http with a
symbol-level forbidigo rule. Constants (http.MethodPost,
http.StatusOK) and helpers (http.StatusText) were never the
intent; only Client / NewRequest / Get / Post / Do etc. are now
blocked in shortcuts/, matching the rule's actual purpose
Change-Id: Ic42043d3f4c1b675800e48229c7ba2e970da26fe
* fix(contact +search-user): align query limit and reject empty user-ids
API rejects queries longer than 50 characters; local cap was 64 runes,
producing confusing "passed local validation but server-rejected"
behaviour. Lower the cap to 50 and rename the constant accordingly.
Also reject --user-ids inputs that parse to zero entries (",,,",
" , , ", ","): SplitCSV silently dropped empty segments, so the
shortcut sent an empty body to the API and returned indeterminate
results.
Change-Id: Ib34fe897023e175bf4c657273bdb49a33d2f083b
---------
Co-authored-by: liangshuo-1 <266696938+liangshuo-1@users.noreply.github.com>
Add BuildResourceURL helper and wire it into doc/sheets/drive/base/wiki
create paths so callers always receive a clickable link, even when the
backend response (MCP degraded path or upstream OpenAPI) returns an
empty URL field. The fallback uses the brand-standard host
(www.feishu.cn / www.larksuite.com), which redirects to the tenant
domain.
Affected entries:
- docs +create v1 / v2
- sheets +create
- drive +create-folder / +import / +upload (newly exposes url)
- wiki +node-create (newly exposes url)
drive +create-shortcut is intentionally skipped because the URL form
depends on the underlying file kind, which the shortcut payload does
not carry.
* feat(risk): implement confirmation for high-risk write operations
* feat(risk): streamline confirmation for high-risk write operations
* feat(risk): document approval protocol for high-risk write operations
* feat(risk): refine confirmation protocol for high-risk write operations
* feat(risk): remove redundant variable declaration in risk test
* feat(risk): add 'Yes' flag to various test cases for confirmation
The previous default (atomic.Bool zero-value = enabled) meant any
*cobra.Command built without first calling configureFlagCompletions
leaked into cobra's package-global flagCompletionFunctions map. Bench
runs (scripts/bench_build) showed hundreds of KB and thousands of
objects retained per Build call.
Flip the semantics so the zero-value matches the safe default:
- Rename internal var to flagCompletionsEnabled (zero = disabled).
- Rename public API to SetFlagCompletionsEnabled / FlagCompletionsEnabled.
- Update call sites in cmd/root.go and scripts/bench_build/main.go.
- Add cmd.TestBuild_DefaultNoCompletionLeak: asserts that, with no
setter call at all, repeated cmd.Build invocations stay under 50 KB
and 500 objects per build (observed: ~0.7 KB, 3 objs/build). This
closes the gap that let the wrong default ship — every previous
test explicitly Set the switch before exercising it.
Change-Id: Ifefb04af5fd45eea9676a344a64ad071b6a4cd1a
* feat(event): add event subscription & consume system with orphan bus detection
Introduces end-to-end Feishu event consumption via a new `lark-cli event`
command family. Users can subscribe to and consume real-time events
(IM messages, chat/member lifecycle, reactions, ...) in a forked bus
daemon architecture with orphan detection, reflected + overrideable JSON
schemas, and AI-friendly `--json` / `--jq` output.
Commands
--------
- `event list [--json]` list subscribable EventKeys
- `event schema <key>` Parameters + Output Schema + auth info
- `event consume <key>` foreground blocking consume; SIGINT/SIGTERM
/stdin-EOF shutdown; `--max-events` /
`--timeout` bounded; `--jq` projection;
`--output-dir` spool; `--param` KV inputs
- `event status [--fail-on-orphan] [--json]` bus daemon health
- `event stop [--all] [--force] [--json]` stop bus daemon(s)
- `event _bus` (hidden) forked daemon entrypoint
Architecture
------------
- Bus daemon (internal/event/bus): per-AppID forked process that holds
the Feishu long-poll connection and fans events out to 1..N local
consumers over an IPC socket. Drop-oldest backpressure, TOCTOU-safe
cleanup via AcquireCleanupLock, idle-timeout self-shutdown, graceful
SIGTERM.
- Consume client (internal/event/consume): fork+dial the daemon,
handshake, remote preflight (HTTP /open-apis/event/v1/connection),
JQ projection, sequence-gap detection, health probe. Bounded
execution (`--max-events` / `--timeout`) for AI/script usage.
- Wire protocol (internal/event/protocol): newline-delimited JSON
frames with 1 MB size cap and 5 s write deadlines. Hello / HelloAck /
PreShutdownCheck / Shutdown / StatusQuery control messages.
- Orphan detection (internal/event/busdiscover): OS process-table scan
(ps on Unix, PowerShell on Windows) with two-gate cmdline filter
(lark-cli + event _bus) that naturally rejects pid-reused unrelated
processes.
- Transport (internal/event/transport): Unix socket on darwin/linux,
Windows named pipe on windows.
- Schema system (internal/event, internal/event/schemas): SchemaDef with
mutually-exclusive Native (framework wraps V2 envelope) or Custom
(zero-touch) specs. Reflection reads `desc` / `enum` / `kind` struct
tags, with array elements diving into `items`. FieldOverrides overlay
engine addresses paths via JSON Pointer (including `/*` array
wildcard) and runs post-reflect, post-envelope. Lint guards orphan
override paths.
- IM events (events/im): 11 keys — receive / read / recalled, chat and
member lifecycle, reactions — all with per-field open_id / union_id /
user_id / chat_id / message_id / timestamp_ms format annotations.
Robustness
----------
- Bus idle-timer race fix: re-check live conn count under lock before
honoring the tick; Stop+drain before Reset per timer contract.
- Protocol frame cap: replace `br.ReadBytes('\n')` with `ReadFrame` that
rejects frames > MaxFrameBytes (1 MB). Closes a DoS path where any
local peer could grow the reader's buffer unbounded.
- Control-message writes gated by WriteTimeout (5 s) so a wedged peer
kernel buffer can't stall writers indefinitely.
- Consume signal goroutine: `signal.Stop` + `ctx.Done` select, no leak
across repeated invocations in the same process.
- JQ pre-flight compile so bad expressions fail before the bus fork and
any server-side PreConsume side effects.
- `f.NewAPIClient`'s `*core.ConfigError` now passes through unwrapped
so the actionable "run lark-cli config init" hint reaches the user.
Subprocess / AI contract
------------------------
- `event consume` emits `[event] ready event_key=<key>` on stderr once
the bus handshake completes and events will flow. Parent processes
block-read stderr until this line before reading stdout — no `sleep`
fallback needed.
- All list-like commands have `--json` for structured consumption.
- Skill docs in `skills/lark-event/` (SKILL.md + references/) brief AI
agents on the command surface, JQ against Output Schema, bounded
execution, and subprocess lifecycle.
Testing
-------
Unit tests across bus/hub, consume loop, protocol codec, dedup,
registry, transport (Unix + Windows), schema reflection, field
overrides, pointer resolver. Integration tests cover fork startup,
shutdown, orphan detection, probe, stdin EOF, preflight, bounded
execution, and Windows busdiscover PowerShell compatibility.
Change-Id: Ib69d6d8409b33b99790081e273d4b5b01b7dbf80
* fix(event): address CodeRabbit findings + lift patch coverage above 60%
CodeRabbit comments (PR #654)
-----------------------------
1. bus/dedup: IsDuplicate dropped legitimate (post-TTL) events after
cleanupExpired fired. The run-every-1000-inserts cleanup removed
TTL-expired IDs from the `seen` map but left them in the ring;
IsDuplicate's ring-scan fallback then rediscovered them and falsely
reported "duplicate", and bus.Publish silently dropped the event.
Removed the ring-scan branch — `seen` is the sole authority, the ring
only bounds map size via overflow eviction. New regression test
TestDedupFilter_TTLExpiryAfterCleanupRunRespected exercises the 10-
insert + cleanup path and guards the fix.
2. consume/remote_preflight: the decoder only read `data.online_instance_
cnt`. A non-zero business code with no data payload decoded to 0 and
callers treated it as "verified zero", forking a local bus that would
duplicate events. Added Code / Msg fields and promoted code != 0 into
an error so the caller distinguishes verified-zero from check-failed.
3. cmd/event/stop: swapped os.ReadDir / os.Stat to vfs.ReadDir / vfs.Stat
in discoverAppIDs per project guideline (enables test mocking). New
TestDiscoverAppIDs_* lifts discoverAppIDs from 0% to 100%.
4. cmd/event/appmeta_err: narrowed authURLPattern from
feishu.cn|feishu.net|larksuite.com|larkoffice.com to the two hosts
consoleScopeGrantURL actually produces. Kept the allowlist pinned to
ResolveEndpoints' output with a comment flagging the synchrony.
5. cmd/event/list: moved "No EventKeys registered." and "Use 'event
schema <key>' for details." hints to stderr so `event list | jq`
style pipelines don't ingest them as data.
6. cmd/event/schema: runSchema is a RunE entry point; swapped the bare
fmt.Errorf on resolveSchemaJSON failure to output.Errorf so AI
agents parse a structured error envelope.
Coverage bumps (patch ~50% -> ~60%)
-----------------------------------
internal/event/consume/loop_test.go: loop.go was 0% at patch time.
New tests cover consumeLoop end-to-end via net.Pipe (events -> sink,
max-events -> ctx.Done -> PreShutdownCheck/Ack), seq-gap warning,
jq filtering + early compile failure, isTerminalSinkError classifier.
Takes consumeLoop from 0% to ~74%.
internal/event/protocol/messages_test.go: all NewXxx constructors,
Encode/Decode roundtrip per message type, EncodeWithDeadline deadline
enforcement, ReadFrame MaxFrameBytes rejection + EOF propagation.
Takes protocol from 28% to ~86%.
Also bundles small UX polish:
- cmd/event/consume: --output-dir flag doc flags path-traversal behavior;
jq-validation failures now re-wrap with an event-specific hint
pointing at `event schema` for payload shape.
- internal/event/consume.validateParams: error now names the EventKey
and lists valid param names inline so AI callers recover without a
second `event schema` round-trip.
- skills/lark-event: description expanded to mention
listener/subscribe/consume synonyms + the IM scope set explicitly;
lark-event-im reference polished; obsolete lark-event-subscribe
reference removed.
Verified with go test -race -timeout 120s across ./cmd/event/...,
./events/..., ./internal/event/...; gofmt clean; go vet clean.
Change-Id: I3837b8645ea1d7529c9a8fd4c2bbfa965ae1b519
* test(event): cover format helpers + cobra factories
Adds cmd/event/format_helpers_test.go covering the pure output helpers
and factory wire-ups that RunE-level tests would need a live bus to
exercise:
- writeStopJSON: shape assertions + nil → [] (scripts expecting
.results | length must not see null).
- writeStopText: stdout vs stderr routing — stopped / no-bus lines to
stdout, refused / errored lines to stderr.
- busState.String: all three discriminator values.
- humanizeDuration: each bucket boundary (seconds / minutes / hours / days).
- writeStatusText: covers stateNotRunning / stateRunning (with consumer
table) / stateOrphan (with kill hint).
- writeStatusJSON: orphan entry carries suggested_action + issue;
running entry must NOT carry those fields (hint-leak guard for
scripts that key on issue != "").
- exitForOrphan: flag-off never errors; flag-on errors iff any orphan
is present, with ExitValidation code.
- NewCmdConsume / NewCmdStatus / NewCmdStop / NewCmdList / NewCmdBus:
flag registration + RunE presence, so review catches flag-name drift.
NewCmdBus check also pins Hidden=true.
Lifts cmd/event coverage 51.7% → 61.1%; aggregate event-package
coverage crosses the 60% codecov patch threshold (62% locally).
Change-Id: I9ecf3d905a8f9607b9441ee8a61e746496e2be63
* fix(event): address lint + deadcode CI failures
4 golangci-lint findings + 1 deadcode finding flagged on PR #654.
lint
----
1. cmd/event/stop.go:86 (ineffassign): `targets := []string{}` is
overwritten by both branches of the `if o.all` below, so the empty-
slice initializer is dead. Switched to `var targets []string`.
2. cmd/event/consume.go nilerr: the user-identity scope preflight
swallows a non-nil ResolveToken error and returns nil. This is
intentional — a missing/expired user token must not block consume;
the bus handshake will surface the real auth error with actionable
hints. Added `//nolint:nilerr` with a 4-line comment pinning the
reasoning.
3. events/im/message_receive.go:62 nilerr: malformed JSON payload
returns the original bytes + nil so consumers still see the event
(the WARN breadcrumb lives in the outer loop). Added
`//nolint:nilerr` with a one-line comment.
4. internal/event/schemas/fromtype_test.go:26 unused: `unexportedStr`
is a reflection-test fixture — its presence (not value) exercises
the FromType skip-unexported path verified at the "unexported
field should not be in schema" assertion. Added `//nolint:unused`
and a 4-line comment pointing at the guarded assertion.
deadcode
--------
5. internal/event/testutil/testutil.go: NewTCPFake has no callers in
the repo. Removed the constructor plus the `inner == nil` TCP-mode
branches from Listen / Dial / Cleanup. FakeTransport now only
supports the wrapped-overlay mode (NewWrappedFake), which is the
one every existing test uses. Doc comment simplified accordingly.
Verified locally: go test -race -timeout 120s across ./cmd/event/...,
./events/..., ./internal/event/... all green; gofmt clean; go vet
clean.
Change-Id: Ie8a2270827a0bde6b8159ab70aaf5c1e9ca7d5b9
* fix(event): drop stale enum + simplify protocol test type helper
- events/im/message_receive.go: dropped the `enum` tag on
ImMessageReceiveOutput.MessageType. convertlib registers many more
message types than the old 11-item list (video / location /
calendar / todo / vote / hongbao / merge_forward / folder / ...),
so a partial enum would tell AI consumers that valid values like
"video" are invalid and produce false-negative JQ filters.
- internal/event/protocol/messages_test.go: collapsed the
typeOf → reflectTypeName → stringType chain in
TestEncode_DecodeRoundtripAllTypes to a single fmt.Sprintf("%T", v).
The hand-maintained type switch silently returned "<unknown>" for
any new message type, which would have let future Decode bugs slip
past the roundtrip assertion. Also removed a dead `cases` table at
the top of TestConstructors_PinTypeField left over from an earlier
refactor.
Change-Id: I831e96f8417e80637596030d652a559de0d33122
* docs(event): polish skill docs + rename root_path_hint to jq_root_path
- skills/lark-event/SKILL.md, lark-event-im.md: translated to English,
reorganized around a top-level "Core commands" table, scenario
recipes tightened.
- cmd/event/schema.go: renamed the writeSchemaJSON hint field
RootPathHint / "root_path_hint" -> JQRootPath / "jq_root_path" to
make its purpose (a jq path prefix) obvious at the call site; no
external consumer depends on the old name yet.
Change-Id: I00c14061ca33caedc0975bfeadc4b26d3dcd314d
* chore(event): strip excessive comments
Change-Id: I8f44f36f5dbdba3ef95dfc67069dc796232f91ec
* fix(event): dedup self-eviction race + protocol oversized-frame test
dedup: in IsDuplicate, the ring-slot eviction step deleted seen[id] even
when ring[pos] equalled the freshly-recorded id (post-TTL reinsertion
landing on its own historical slot). Net result: ring still held id but
seen did not, so the next IsDuplicate(id) returned false and the
duplicate was delivered. Skip the delete when old == eventID. New
TestDedupFilter_SelfEvictionPreservesFreshEntry pins the invariant by
pre-loading the ring slot and asserting the second call still reports
duplicate.
protocol: TestReadFrame_RejectsOversized used strings.Contains feeding
t.Logf, so any non-nil error passed — including a future regression
that returned io.ErrUnexpectedEOF while silently keeping the buffer
unbounded. Promoted MaxFrameBytes overflow to a sentinel
ErrFrameTooLarge and the test now asserts via errors.Is.
Change-Id: I50281dad392152b0ca083fd30c38eb0695e63bd3
* docs(event): clarify .content shape per message_type + add sender filter recipe
Change-Id: I619fd15c1a362e42e6602fd3e3316bbc75eddc5e
* fix(event): replace cmdline-regex bus discovery with PID file + close concurrent fork race
Bus discovery previously walked the OS process table and parsed `--profile cli_*` from
cmdline; the regex rejected any non-cli_ profile name (D-03a). Replace with per-AppID
bus.pid + bus.alive.lock under events/<AppID>/, probed via try-lock. AppID round-trips
through the directory name, so the profile-vs-AppID confusion is gone by construction.
Also fix B-07 (two consumers each fork an independent bus, halving event delivery):
- forkBus holds bus.fork.lock until child is dial-able, not just until cmd.Start
- bus daemon takes alive.lock before binding the socket; cleanup-TOCTOU race can no
longer leave two listeners on different inodes
status.go renders an orphan with PID=0 distinctly (live bus but pid file unreadable)
so we never print "Action: kill 0".
Change-Id: I3bf0a6cf1d91fb274ac5a6df83d66896aafb291f
* style(event): gofmt bus.go
Trailing blank line introduced when appending acquireAliveLock helper.
Change-Id: I4ae1b4a4363dc6c89dcbd6a170f4563117490ba3
* fix(event): swap os.Remove/Rename for vfs.* and silence forbidigo on internal diagnostics
golangci-lint forbidigo blocks os.* in internal/. Switch the pid-file write to vfs.Remove/vfs.Rename and add a nolint marker on the two stderr diagnostics in busdiscover, matching the existing pattern in consume/*.
Change-Id: Ia6768be62aefeb8ca40f991d3130a78ef2ec0ea5
* fix(event): cross-platform --all + clean SIGPIPE shutdown for consume
- stop --all: replace bus.sock-file probe with busdiscover lock-based
scan; previously skipped Windows entirely (named-pipe transport, no
socket on disk) and misidentified Unix stale sockets as live. Same
win for `event status` (shares discoverAppIDs).
- consume: ignore SIGPIPE so a closed stdout pipe (e.g. `... | head -n 1`)
surfaces as EPIPE error and reaches the existing isTerminalSinkError
cleanup path (log "output pipe closed", lastForKey query, hub
unregister), instead of being killed by Go's default fd 1/2 SIGPIPE
handler with exit 141 and zero deferred cleanup.
Build-tagged: real on unix, no-op on windows (no SIGPIPE there).
Change-Id: I453b19f05c489fd9d5c1a9ba3bdc35e127c15b83
* docs(event): translate IM EventKey descriptions and field tags to English
Aligns with the rest of the codebase (titles, struct names, README) which
are already in English. Surfaces in `event list` / `event schema` and is
also consumed by AI agents.
- events/im/message_receive.go: 11 desc tags on ImMessageReceiveOutput
- events/im/native.go: 10 description fields on Native EventKeys
- events/im/register.go: im.message.receive_v1 Description
Change-Id: I6f46950b4793f137e0129c1f06019a3419195443
* docs(event): drop misleading AuthTypes[0] auto-default claim
The KeyDefinition comment and SKILL.md flag table both stated that
`--as auto` resolves to `AuthTypes[0]`. It does not — ResolveAs goes
through global rules (config default_as / credential hint / `bot`
fallback) without consulting the EventKey. AuthTypes is only used by
CheckIdentity as a post-resolve whitelist.
Reword the field comment to plain whitelist semantics and have SKILL.md
defer `--as` documentation to lark-shared.
Change-Id: Ia5d3d3790aed05813a0fa72d6b43518224e2055b
* revert(comments): restore original comments on 3rd-party files
e61482a stripped comments across 105 files. Restore the four files
authored by others (cmd/build.go, shortcuts/common/{types,runner}.go,
shortcuts/event/subscribe.go) to their pre-strip state so unrelated
documentation isn't churned in this PR.
Change-Id: Ie2527b06bfaf5b3861b0b9dff1e19bbfe7dde456
* fix(e2e/wiki): pass obj_type when deleting wiki nodes in cleanup
The wiki node DELETE endpoint now rejects requests without obj_type
(API error 99992402: "obj_type is required"), causing TestWiki_NodeWorkflow
cleanup to fail on every run. Forward the obj_type from the create/copy
response into the delete query params so cleanup succeeds.
* fix(e2e/wiki): delete cleanup wiki nodes via drive v1 endpoint
The wiki v2 DELETE /spaces/{space_id}/nodes/{node_token} endpoint is
undocumented and rejects requests with `obj_type is required` even when
obj_type is forwarded as a query parameter (see actions run #25005966144).
Switch cleanup to the documented path: delete the underlying drive file
via DELETE /drive/v1/files/{obj_token}?type=<obj_type>, which removes the
backing document and the wiki node in one call.
Change-Id: Ieb93b1f92ea758d8b80bcfdd4f20b2be8f35a0bd
* fix(e2e/wiki): pass obj_type to wiki delete in body, not query
Previous attempts:
- query (?obj_type=docx) → API still rejects with 99992402 obj_type
required (the wiki delete-node endpoint reads it from the body, not
the query string).
- drive v1 fallback → bot identity does not have drive write scope and
returns 1061004 forbidden, so we cannot reuse drive's delete API for
the cleanup helpers.
Add example commands for file types declared in the supported-conversions
table but absent from the command examples section: .docx/.doc, .txt,
.html, .xls -> sheet, and .csv -> sheet.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(strict-mode): reject explicit --as instead of silently overriding it
ResolveAs checked strict mode before the --as flag, so `--as bot` under strict=user
was silently rewritten to user. Reorder so explicit --as is returned as-is and CheckStrictMode rejects the conflict (exit=2). Implicit paths (--as auto / unset) are still forced by
strict mode.
* fix(strict-mode): fix CI
Project management for Lark/Feishu is provided by the standalone
meegle-cli (https://github.com/larksuite/meegle-cli), which requires a
separate install. Surface it in the Features table so users can
discover the capability without expecting it to ship inside lark-cli.
Add --at-chatter-ids flag to shortcuts/im/im_messages_search.go that
passes filter.at_chatter_ids to the search API, restricting results to
messages that @mention any of the given user open_ids. Messages that
* feat(pagination): preserve pagination state on truncation and natural end
* feat(pagination): drop page_token from merged output to reflect aggregate view
Expose doc_wiki/search v2 under the drive domain via explicit flags
(--query, --edited-since, --commented-since, --opened-since,
--created-since, --mine, --creator-ids, --doc-types, --folder-tokens,
--space-ids, ...) instead of a nested JSON filter, so natural-language
queries from AI agents map 1:1 to discrete flags.
Time handling:
- my_edit_time and my_comment_time are snapped to the hour (floor/ceil)
with a stderr notice, since those fields are aggregated at hour
granularity server-side. create_time passes through as-is.
- open_time has a server-side 3-month cap per request. When
--opened-since / --opened-until span exceeds 90 days, the CLI narrows
the request to the most recent 90-day slice and emits a stderr notice
listing every remaining slice's --opened-* values so the agent can
re-invoke for older ranges. Spans over 365 days are rejected up front
to bound runaway slicing.
Flag ergonomics:
- --doc-types accepts mixed case; values are normalized to upper case
before validation and before being sent to the server.
- --sort default is translated to the server enum DEFAULT_TYPE (every
other sort value upper-cases 1:1).
Error hints:
- Lark code 99992351 (referenced open_id outside the app's contact
visibility) is enriched with a +search-specific hint that
distinguishes API scope from contact visibility and points at
--creator-ids / --sharer-ids as the likely source.
Skill docs:
- new reference at skills/lark-drive/references/lark-drive-search.md,
including the open_time slicing protocol and the paginate-within-
slice-before-switching agent playbook.
- lark-drive/SKILL.md routes resource-discovery to drive +search.
- lark-doc/SKILL.md and lark-doc-search.md mark docs +search as
deprecated and point users at drive +search.
Change-Id: I36d620045809b448446d4fdbdfa923b05794da19
* feat(mail): add +share-to-chat shortcut to share emails as IM cards
Two-step API (create share token → send card) wrapped in a single
shortcut. Supports message-id/thread-id, five receive-id-type variants
(chat_id, open_id, user_id, union_id, email), and dry-run mode.
Change-Id: Ic7b8c01c0d25fef262f35be92555f1fd019bd679
Co-Authored-By: AI
* fix(mail): regenerate SKILL.md from skill-template instead of manual edit
Add missing safety rule 8 (draft link rule) to skill-template/domains/mail.md
so it survives regeneration. SKILL.md is now produced by `make gen-skills`
in the registry repo rather than hand-edited.
Change-Id: I9cf3605deae8b6de2042e40819fedc304967e78e
Co-Authored-By: AI
* fix(mail): add docstrings and use real validation path in tests
- Add Go doc comments to exported symbols for docstring coverage
- Rewrite tests to exercise MailShareToChat.Validate via RuntimeContext
instead of duplicating validation logic
- Replace hand-rolled containsStr with strings.Contains
- Add httpmock stubs for execute and error path tests
Change-Id: Ic781494f61e9e844224185844bce7b0c48e8e200
Co-Authored-By: AI
* test(mail): add dry-run E2E test for +share-to-chat
Validate request shape (method, URL, mailbox path) under --dry-run
with fake credentials. Covers message-id, thread-id, and custom
mailbox variants.
Change-Id: Iae87bf141cbe4f312d3e9b1fca4ba175052c5c35
Co-Authored-By: AI
* fix(mail): include request body and params in dry-run output
DryRun now mirrors Execute: the share-token POST shows message_id or
thread_id, and the send POST shows receive_id_type and receive_id.
E2E test updated to assert these fields. Also fix strconv.Itoa usage.
Change-Id: I00f8770fd5a12b7354986c5e5077f97cfe5d6653
* style(mail): gofmt dry-run test file
Change-Id: I47dc6a9a47252dcfb7853737f88dfdaef65a0ae7
* test(mail): assert exact API call count in dry-run test
Change-Id: I9f4a1a183b55d03f5248eb4adddfddb08037ca95
End-to-end RFC 3798 Message Disposition Notification support, covering
both sides of the receipt flow — requesting a receipt when composing, and
responding to one (send or decline) when reading.
Request side (compose)
- New --request-receipt flag on +send / +reply / +reply-all / +forward /
+draft-create / +draft-edit. When set, the outgoing EML carries a
Disposition-Notification-To header (RFC 3798) addressed to the resolved
sender. Recipient mail clients may prompt the user, auto-send a receipt,
or silently ignore — delivery is not guaranteed.
- requireSenderForRequestReceipt gates the flag against a controlled
sender address resolved BEFORE the orig.headTo fallback in +reply /
+reply-all / +forward, so the DNT cannot silently land on someone else
in CC / shared-mailbox flows.
Response side
- +send-receipt: build a system-templated reply for messages carrying the
READ_RECEIPT_REQUEST label (-607). Subject / recipient / sent / read
time layout matches the Lark client; body is non-customizable — receipt
bodies are system templates by industry convention; free-form notes
belong in +reply. Risk:"high-risk-write" + --yes required.
- +decline-receipt: clear READ_RECEIPT_REQUEST without sending anything
(mirrors the client's "不发送" / "Don't send" button). Idempotent on
re-run; Risk:"write" — no --yes needed.
Read-path hints
- +message / +messages / +thread emit a stderr hint when surfacing a
mail carrying READ_RECEIPT_REQUEST, exposing BOTH response paths
(+send-receipt --yes / +decline-receipt) so agents present a real
choice to the user instead of silently auto-sending.
Guard rails
- +send / +reply / +reply-all / +forward stay draft-by-default and
require --confirm-send to send, gated by a dynamic scope check for
mail:user_mailbox.message:send (absent from the default scope set so
draft-only flows don't need the sensitive permission).
- All header-bound user input (sender / display name / recipient /
subject) goes through CR/LF rejection plus Bidi / zero-width / line-
separator guards, mirroring emlbuilder.validateHeaderValue, to block
header injection and visual spoofing.
- Hint output strips terminal control characters (CR, LF) from any
untrusted field embedded into the user-visible suggestion.
Backend coupling
- Outgoing receipt EML carries the private header
X-Lark-Read-Receipt-Mail: 1. The data-access backend parses it into
BodyExtra.IsReadReceiptMail; DraftSend then applies READ_RECEIPT_SENT
(-608) and clears READ_RECEIPT_REQUEST (-607) from the original
message, closing the client-side banner.
- en receipts require backend TCC SubjectPrefixListForAdvancedSearch to
include "Read Receipt:" for conversation-view aggregation; zh prefix
("已读回执:") is already configured.
Docs: new reference pages for +send-receipt / +decline-receipt;
--request-receipt noted on each compose-side reference; SKILL.md
workflow (section 9) describes the full privacy-safe decision tree on
both sides.
Tests cover emlbuilder DispositionNotificationTo / IsReadReceiptMail
helpers, receiptMetaLabels (zh / en), buildReceiptSubject, text and HTML
body generators (with HTML escaping and Bidi guards), header-injection
defenses, sender-resolution gating (CC-only / shared-mailbox regression),
hint emission paths, and the full +send-receipt / +decline-receipt happy
+ idempotent paths via httpmock.
Update im +chat-messages-list to request only thread root messages from /open-apis/im/v1/messages by default. This aligns the shortcut request shape with topic-group usage and makes the intended API behavior explicit in both runtime params and dry-run output.
Change-Id: I3901b27e70b0e4db506ff199eb03c96fcf98671d
Adds an `--api-version v2` path to the docs shortcuts, backed by the
`docs_ai/v1/documents` OpenAPI. DocxXML is the default document format
and Markdown is available as an alternative. Content input is unified
across the three shortcuts via `--content` + `--doc-format`. The v1
(MCP) path is preserved for backward compatibility and now prints a
deprecation notice on use.
Shortcuts:
- `docs +create --api-version v2`: create a document from XML or
Markdown, with optional `--parent-token` or `--parent-position`.
Bot identity continues to auto-grant the current CLI user
full_access on the new document.
- `docs +fetch --api-version v2`: adds `--detail simple|with-ids|full`
for export granularity and `--scope full|outline|range|keyword|section`
for partial reads, along with `--context-before` / `--context-after`,
`--max-depth`, and `--revision-id`.
- `docs +update --api-version v2`: introduces structured operations
via `--command`: `str_replace`, `block_delete`, `block_insert_after`,
`block_copy_insert_after`, `block_replace`, `block_move_after`,
`overwrite`, `append`.
Framework support in `shortcuts/common`:
- `OutRaw` / `OutFormatRaw` emit the JSON envelope with HTML escaping
disabled so XML/HTML document bodies are preserved verbatim.
- New `Shortcut.PostMount` hook runs after a cobra.Command is fully
configured; used here to install a version-aware help function
that hides flags belonging to the inactive `--api-version`.
Also refreshes the lark-doc skill pack (SKILL.md, create/fetch/update
references, new lark-doc-xml and lark-doc-md references, style and
workflow guides), README examples, and downstream skill call sites
(lark-drive, lark-vc, lark-whiteboard, lark-workflow-meeting-summary,
lark-event).
Change-Id: Ide2d86b190a4e21095ae29096e7fb00031d80489
Give each AI Agent (OpenClaw, Hermes) its own lark-cli workspace so
its Feishu calls don't overwrite the developer's local config or
collide with other Agents.
lark-cli config bind [--source openclaw|hermes] [--app-id <id>]
[--identity bot-only|user-default] [--force]
Key capabilities:
- Source auto-detected from OPENCLAW_* / HERMES_* env signals; config
written to ~/.lark-cli/<agent>/, isolated per Agent.
- Two identity presets: 'bot-only' (flag-mode default) and
'user-default'. Flag mode rejects silent bot→user escalation
without --force; TUI prompts are exempt.
- Agent-friendly stdout JSON with 'identity' + 'message' for
next-step branching.
- 'config show' and 'doctor' expose the bound 'workspace'.
- OpenClaw SecretRef resolution: plain / ${VAR} / file:+JSON Pointer
/ exec:.
* refactor: make install.js side-effect-free on require
Change-Id: I5444e3f34642d7c0740b6422a70ca6921a85e363
* feat: add getExpectedChecksum with unit tests
Change-Id: I87548be25d30c384e743da17b1d161b9d9f0ea87
* feat: add verifyChecksum with unit tests
Change-Id: Ifc2067bf1b824b02257dba7b53716fbe18d0f6b6
* feat: harden download with host allowlist and checksum verification
Change-Id: I2580782866049f1f62a2597e86b7bf59d0e50925
* ci: bundle checksums.txt in npm package for install verification
Change-Id: I2d7c44d9d5b9075158f63c0f8cf66c1e0abe3d8d
* ci: use triggering tag and verify checksums.txt presence in release workflow
Address CodeRabbit review: use GITHUB_REF_NAME instead of parsing
package.json to avoid version drift, and add explicit file check to
fail loudly if checksums.txt is missing or empty.
Change-Id: I8a5658412b6afc338ad2a642baba146cceafd0fc
* feat: streaming hash, allowlist tests, and malformed-line coverage
- verifyChecksum: switch from readFileSync to streaming 64KB chunks
to avoid loading entire archive (10-100MB) into memory
- Export and test assertAllowedHost: 7 cases covering allowed hosts,
rejection, case normalization, port handling, invalid URL
- Add ALLOWED_HOSTS comment clarifying it only gates initial URL
- Add getExpectedChecksum tests for malformed/tab-separated lines
Change-Id: Ida639def89c242b3b261a76effae08fd414a10dc
* refactor(base): enforce field-map record upsert input
1. Reject top-level fields wrappers in base +record-upsert input and keep request bodies as field maps.
2. Replace record-upsert tests with Map<FieldNameOrID, CellValue> input and assert the outgoing body has no fields wrapper.
3. Consolidate Base record value documentation around lark-base-cell-value and update record command references.
* refactor(base): use common record JSON parsing for upsert
1. Remove the dedicated record-upsert parser and restore the shared record JSON object validation path.
2. Keep record-upsert dry-run and execution as raw JSON object passthrough.
3. Drop the test assertion that rejected a top-level fields key for record-upsert.
* docs(base): refine record cell value guidance
1. Align record CellValue examples with live behavior for date, URL, user, link, select, numeric styles, and readonly fields.
2. Remove misleading user_id_type and execution identity prompts from record-writing guidance.
3. Keep record JSON file input guidance generic and avoid documenting environment-specific stdin or path limits.
Introduces `lark-cli slides +replace-slide`, a shortcut over the
native `xml_presentation.slide.replace` API for element-level editing
of existing Lark Slides pages. Callers pass a JSON array of parts and
the CLI handles URL resolution, XML hygiene, client-side validation,
and 3350001 hint enrichment.
Why a dedicated shortcut
The native API has three sharp edges every caller hits:
1. URL formats. Users have /slides/<token> or /wiki/<token> URLs, not
bare xml_presentation_id.
2. Undocumented XML hygiene. `block_replace` requires id=<block_id> on
the replacement root; <shape> requires <content/>. Missing either
returns a catch-all 3350001 with no guidance.
3. 3350001 is a catch-all on the backend with no actionable message.
Code
shortcuts/slides/slides_replace_slide.go (new)
- Flags: --presentation (bare token | /slides/ URL | /wiki/ URL),
--slide-id, --parts (JSON array, max 200), --revision-id (-1 for
current, specific number for optimistic locking), --tid,
--as user|bot.
- Validation (pre-API): [1,200] item cap; action restricted to
block_replace / block_insert (str_replace rejected); per-action
required fields (block_id for block_replace, insertion for
block_insert); per-field string type-assertion guards on the
decoded JSON so a numeric/bool payload fails fast with a targeted
error.
- XML hygiene:
* injects id="<block_id>" on block_replace replacement roots;
* auto-expands self-closing <shape/> and injects <content/> on
shapes for SML 2.0 compliance.
Dry-run surfaces injection errors and renders the same
path-encoded presentationID that Execute sends.
- On backend 3350001 attaches a generic common-causes checklist
(missing block_id / invalid XML / coords out of 960×540).
shortcuts/slides/helpers.go
- ensureXMLRootID: regex tightened to `(?:^|\s)id` so data-id and
xml:id are not matched as root id.
- ensureShapeHasContent: regex `<content(?:\s|/|>)` avoids false
positives like <contention/>; self-closing branch preserves
trailing siblings.
shortcuts/slides/shortcuts.go: register SlidesReplaceSlide.
Tests (package coverage 89.4%; parseReplaceParts and
injectBlockReplaceIDs both reach 100%)
- helpers_test.go: regex edge cases, id override semantics, content
auto-inject across self-closing and open-tag shapes.
- slides_replace_slide_test.go: parameter validation table, URL
resolution (slides / wiki), mixed block_replace + block_insert,
size boundaries, auto-inject behavior, 3350001 hint enrichment,
per-field type-assertion guards, whitespace-only --parts guard
(distinct from the `[]` "at least 1 item" path), replacement
without root element surfaces pre-flight instead of reaching the
backend, and a tight negative assertion that non-3350001 errors
get no slides-specific hint.
Docs (skills/lark-slides)
- SKILL.md: add +replace-slide to the Shortcuts table, register the
new xml_presentation.slide.get / .replace native endpoints,
update core rule 7 to prefer block-level replace over full-page
rebuild now that element-level editing exists, extend the error
table with 3350001 / 3350002 pointing at the replace-slide doc,
add "add image to existing slide via block_insert" as an explicit
Workflow step and symptom-table entry, and refresh the reference
index to include the three new docs below. The old "整页替换" 4-rule
checklist is retired — its one still-relevant guard (new <img>
avoiding overlap) is preserved in the symptom table.
- New references:
* lark-slides-replace-slide.md — flags, parts schema, auto-inject
notes, mixed-action support, 200-item cap, revision_id
semantics, error table, and a "合法根元素速查" cheatsheet for
the eight supported root elements (shape / line / polyline /
img / icon / table / td / chart) with minimal verified XML
snippets. Explicit unsupported list: video / audio / whiteboard
(these appear only as <undefined> export placeholders in SML 2.0).
* lark-slides-edit-workflows.md — recipe-style edit flows covering
the read → modify → write loop and the block_replace vs
block_insert decision tree.
* lark-slides-xml-presentation-slide-get.md — native read API with
block_id extraction examples.
- Fixes across existing references:
* replace / create / delete / presentations.get: add the .data
wrapper in return-value examples, correct jq paths.
* media-upload: fix jq path .file_token → .data.file_token.
* examples.md: annotate auto-inject behavior, replace the
incorrect failed_part_index example with the actual 3350001
error shape.
Empirical corrections (BOE-verified)
- revision_id: stale-but-existing values are accepted; only values
greater than current return 3350002.
- Wrong block_id returns 3350001, not a 200 with failed_part_index.
- Mixed block_replace + block_insert in one call is supported.
- Type-mismatched block_replace (e.g. shape id with a <td>
replacement) is silently accepted by the backend and may destroy
content; 3350001 specifically signals a missing block_id.
Unify lark-cli im +messages-search pagination flags to use int semantics consistently.
Previously, page-limit was registered as an int flag while page-size was still handled as a string flag and parsed manually. This led to inconsistent runtime behavior inside the same shortcut and allowed test helpers to drift from the real CLI flag registration.
Change-Id: Ic4876f4ca7f410a8fe3234e08e41b54ce26990d9
* feat: unify minute artifacts output to ./minutes/{minute_token}/
* fix: tighten path validation and batch-mode --output rejection
* style: translate comments to english and trim historical context
* style: translate leftover chinese comments in vc_notes
* refactor: address review findings across validate ordering, error types, JSON, tests
* fix: sanitize server-provided filename to prevent escape from artifact dir
* style: tighten flag help text for minutes/vc output flags
* docs: update minutes/vc skill docs for unified artifact layout
Add lark-cli wiki +delete-space to delete a knowledge space via
DELETE /open-apis/wiki/v2/spaces/:space_id. When the API returns an
async task_id, the shortcut polls /open-apis/wiki/v2/tasks/:task_id
with task_type=delete_space for a bounded window and emits a
next_command pointing to drive +task_result on timeout. A new
wiki_delete_space scenario is added to drive +task_result for resuming
timed-out deletes.
Change-Id: I75da52b617c206fb778a493ffaa200adf7920a27
* feat(doc): add --from-clipboard flag to docs +media-insert
Allow users to upload the current clipboard image directly to a Lark
document without saving to a local file first.
- New --from-clipboard bool flag (mutually exclusive with --file)
- shortcuts/doc/clipboard.go: readClipboardToTempFile() with per-OS impl
macOS — osascript (built-in, no extra deps)
Windows — PowerShell + System.Windows.Forms (built-in)
Linux — tries xclip / wl-paste / xsel in order; clear install hint
on failure
- No new Go dependencies, no Cgo
- Temp file is created before upload and removed via defer cleanup()
- --file changed from Required:true to optional; Validate enforces
exactly-one of --file / --from-clipboard
* fix(doc): fix clipboard image read on macOS for screenshots and browser-copied images
- Add TIFF fallback (macOS screenshots default to TIFF, not PNG)
- Add HTML base64 fallback (images copied from Feishu/browser embed data URI)
- Use current directory for temp file so FileIO path validation passes
* fix(doc): scan HTML/RTF/text clipboard formats for base64 image data URIs
Extend attempt-3 fallback to iterate all text-based clipboard formats
(HTML, RTF, UTF-8, plain text) rather than only HTML. Any format that
contains a "data:<mime>;base64,<data>" pattern is accepted, covering
images copied from Feishu, Chrome, Safari, and other apps that embed
base64 in non-HTML clipboard slots. Also handle URL-safe base64.
* test(doc): add unit tests for clipboard helpers to meet 60% coverage threshold
Cover decodeHex, hexVal, decodeOsascriptData, reBase64DataURI, and
extractBase64ImageFromClipboard (via fake osascript on PATH).
Package coverage: 57% → 61.2%.
* fix(doc): address CodeRabbit review comments on clipboard feature
- Extend reBase64DataURI regex to cover URL-safe base64 chars (-_) so
URL-safe payloads are matched before decoding is attempted
- Fix readClipboardLinux to continue to next tool when a found tool
returns empty output instead of failing immediately
- Guard fake-osascript test with runtime.GOOS == "darwin" skip
- Use os.PathListSeparator instead of hardcoded ":" in test PATH setup
* fix(doc): replace os.* temp-file clipboard path with in-memory streaming
Fixes forbidigo lint violations in shortcuts/doc: os.CreateTemp, os.Remove,
os.Stat, os.WriteFile are banned in shortcuts/; replaced with vfs.* equivalents
for sips TIFF→PNG conversion, and eliminated temp files entirely elsewhere by
having platform clipboard readers return []byte directly.
- readClipboardDarwin: osascript outputs hex literals decoded in Go (no file I/O)
- readClipboardWindows: PowerShell outputs base64 to stdout, decoded in Go
- readClipboardLinux: tool stdout bytes returned directly
- convertTIFFToPNGViaSips: still needs temp files — uses vfs.CreateTemp/Remove
- DriveMediaUploadAllConfig/DriveMediaMultipartUploadConfig: add Content io.Reader
field so in-memory clipboard bytes skip FileIO.Open() path
- Fix ineffassign in clipboard_test.go (scriptBody double-assignment)
- Update TestReadClipboardLinux_NoToolsReturnsError for new signature
* fix(doc): address CodeRabbit review comments on Linux clipboard path
- Update --from-clipboard flag description to list xclip, xsel and wl-paste
- Preserve last backend-specific error in readClipboardLinux so users see
a meaningful message when a tool is found but fails
- Validate PNG magic bytes for xsel output (xsel cannot negotiate MIME types)
- Add URL-safe base64 regression test for reBase64DataURI
* fix(doc): strip whitespace from base64 payload before decoding clipboard data URI
HTML and RTF clipboard content often line-wraps base64 at 76 characters.
FindSubmatch returns the raw wrapped token so direct decode would fail.
Normalize whitespace with strings.Fields before passing to base64.Decode.
* fix(doc): drop TIFF fallback and internal/vfs import on macOS clipboard
depguard rule shortcuts-no-vfs forbids shortcuts/ from importing
internal/vfs directly. The only caller was the sips TIFF→PNG
conversion, which was already a fragile best-effort fallback that
required temp files.
Remove the TIFF fallback entirely; the remaining two attempts cover
the real-world cases:
1. osascript → PNG hex literal — native screenshots and most apps
2. scan text clipboard formats for base64 data URI — Feishu/browsers
* test(doc): cover readClipboardLinux xsel PNG validation and dispatcher path
Added tests:
- TestReadClipboardLinux_XselRejectsNonPNG: fake xsel that returns plain
text is rejected by the PNG-magic check, preventing text from being
uploaded as an "image".
- TestHasPNGMagic: table-driven coverage of the PNG signature check.
- TestReadClipboardImageBytes_UnsupportedPlatform: exercises the shared
dispatcher post-processing and asserts the (nil, nil) invariant.
Raises clipboard.go diff coverage and brings the package from 61.6% to
63.8% overall.
* test: cover in-memory Content upload paths for clipboard feature
Adds unit tests for the new Content io.Reader branches introduced by
the clipboard feature:
- UploadDriveMediaAll with in-memory Content (drive_media_upload.go 87.5%)
- UploadDriveMediaMultipart with in-memory Content (84.6%)
- uploadDocMediaFile single-part and multipart with clipboard bytes
(doc_media_upload.go 0% -> 88.9%)
Adds TestNewRuntimeContextForAPI helper that wires Factory, context,
and bot identity so package tests can invoke DoAPI without mounting
the full cobra command tree.
* test: cover clipboard Validate/DryRun branches and testing helper
Adds unit tests for the clipboard-related Validate/DryRun paths that
Codecov patch-coverage was flagging as uncovered:
- Validate error when neither --file nor --from-clipboard is supplied
- Validate error when both are supplied (mutual exclusion)
- DryRun output contains <clipboard image> placeholder
- Self-test for TestNewRuntimeContextForAPI so shortcuts/common
sees coverage for the new helper (not just shortcuts/doc)
* test: cover Execute clipboard branch via injectable readClipboardImage
Makes readClipboardImageBytes swappable in tests by routing the call
through a package-level variable readClipboardImage. Tests inject a
synthetic PNG payload so the full Execute clipboard flow
(resolve → create block → upload in-memory bytes → bind) runs under
unit test without a real pasteboard.
Covers:
- TestDocMediaInsertExecuteFromClipboard: end-to-end happy path
- TestDocMediaInsertExecuteClipboardReadError: early-return on
readClipboardImage() failure
* ci: re-trigger pull_request workflow for PR #508
Previous push to 9dedb7a did not trigger the main CI workflow via
the pull_request event (only PR Labels ran). The workflow_dispatch
run I triggered manually lacks PR-scoped secrets so security and
e2e-live failed. An empty commit replays the pull_request event so
the full matrix (deadcode, license-header, security, e2e-live) runs
with proper context.
* test(doc): guard info.Size() behind err check to prevent nil-deref
CodeRabbit flagged that 't.Fatalf("... size=%d err=%v", info.Size(), err)'
evaluates info.Size() even when os.Stat returned (nil, err), which nil-derefs.
Split the check into two stages so the error-path t.Fatalf does not touch
info.
* fix(doc): address fangshuyu-768 review on clipboard PR
Seven code changes driven by review feedback:
1. clipboard.go: stop using CombinedOutput() on osascript / powershell.
Stdout is decoded, stderr is captured separately via cmd.Stderr and
surfaced in the terminal error message, so locale warnings or
AppleEvent permission prompts no longer pollute the hex/base64
payload or mask the real failure.
2. clipboard.go: validate decoded base64 data URI bytes against known
image magic headers (PNG/JPEG/GIF/WebP/BMP). A text clipboard that
happens to contain a literal 'data:image/...;base64,...' fragment
(documentation, tutorials, pasted HTML source) no longer silently
becomes an image upload.
3. clipboard.go: simplify the Linux 'no tool found' install hint to a
distro-agnostic phrasing instead of apt/yum only.
4. clipboard_test.go: delete the stale TestReadClipboardToTempFile_*
tests. They referenced a readClipboardToTempFile function that no
longer exists and only exercised os.CreateTemp/os.Remove. Replace
with TestReadClipboardImageBytes_EmptyResultReturnsError which
actually locks in the 'empty clipboard' → error contract of the
current API (Linux-only since mac/Windows need a real pasteboard).
5. doc_media_upload.go: introduce UploadDocMediaFileConfig struct so
uploadDocMediaFile takes a named config instead of 8 positional
params. Drops the //nolint:lll the old call site had to carry.
6. doc_media_insert.go: convert the clipboard upload call to the new
config struct and only set Config.Content when the clipboard branch
actually produced bytes — this also fixes a latent typed-nil bug
where a nil *bytes.Reader was being passed through an io.Reader
parameter, which tripped the 'if cfg.Content != nil' check in
UploadDriveMediaAll and crashed --file uploads.
7. shortcuts/common/testing.go: TestNewRuntimeContextForAPI now takes
the identity as an explicit core.Identity parameter instead of
hardcoding core.AsBot, and its self-test covers both AsBot and
AsUser. Existing call sites pass core.AsBot explicitly.
Also annotates DryRun output with an 'upload_size_note' when
--from-clipboard is set, since DryRun never reads the pasteboard and
can't predict whether the payload will take the single-part or
multipart path.
* fix(doc): capture line-wrapped base64 in clipboard data URI regex (#586)
HTML and RTF clipboard content commonly folds base64 payloads at
76 chars (standard MIME folding). The previous character class
[A-Za-z0-9+/\-_]+=* stopped at the first \n, so the downstream
strings.Fields normalisation was a no-op (nothing to strip) and
extractBase64ImageFromClipboard silently uploaded a truncated
payload whose 8-byte prefix happened to pass hasKnownImageMagic.
Extend the class to include \s so the Fields strip actually has
whitespace to remove before base64 decoding. Terminators (", <,
), ;) remain outside the class so the match still ends at the
URI boundary.
Add TestReBase64DataURI_LineWrapped covering \n, \r\n, and \t
folds, full round-trip byte-equality, and the terminator-boundary
invariant so any future regression trips a failing test.
* docs(skill): add clipboard-empty fallback guidance for +media-insert
When --from-clipboard returns 'no image data' (empty clipboard, non-image
content, or Linux without xclip/wl-paste/xsel), the agent must NOT silently
swallow the error. It should tell the user the clipboard had no image, ask
for a local file path, then retry the same insert command with --file.
Lists three anti-patterns (silent success, guessing a file path, pre-emptive
save-then-file workaround) that agents have been tempted into.
* docs(skill): user-stated source trumps clipboard/file heuristic
The heuristic table (prefer --from-clipboard when image is on the
clipboard) is a fallback for when the user is vague. If the user
explicitly says 'use the screenshot I just copied' → clipboard; if
they give a path → --file. Agent must not silently swap sources even
when the other looks 'better'.
---------
Co-authored-by: fangshuyu-768 <shuyufang768@outlook.com>
Allow drive +export to request bitable snapshots with --file-extension base and write them with a .base suffix.
Allow drive +import to accept .base files for bitable only, enforce the 20 MB size limit, and document the new examples and constraints.
Add unit tests for validation and size-limit coverage.
Change-Id: Ia13f5013913812df5fc600c43f90918de4ca6b39
Preserve fenced code blocks and balanced-parenthesis URLs when converting markdown to post elements. Add regression tests covering code-block URLs and wiki-style links.
Change-Id: I709a3daf3635402848c96b5122edfc67979ed1a4
When downloading message resources, the saved filename was always derived from
file_key (e.g. file_v2_abc123.xlsx), ignoring the original filename the
sender uploaded. This PR resolves filenames from the Content-Disposition
response header first, falling back to Content-Type-based extension inference
only when the header is absent.
Change-Id: I68b48cf428aa8aded4ad9d55fa042f9d68263c3a
Skill examples taught the pattern --markdown "## A\n\n- x\n- y",
which in bash double quotes is a literal backslash + n, not a
newline. lark-cli forwards the value byte-for-byte to MCP, so
the resulting Feishu doc renders "\n\n" as visible text. Agents
and users copy-pasting the examples reliably produced broken
docs.
Documentation-only fix (issue #580 Option 1, non-breaking):
- Replace 9 "...\n..." examples with multi-line quoted strings,
plus 1 single-quoted example that had the same bug inside
Markdown-block content
- Add a one-sentence warning callout at the top of each file
- Add a stdin/heredoc example in lark-doc-create.md for longer
content
- Leave existing $'...' ANSI-C examples untouched — those
already produce real newlines
No CLI behavior change. Byte-for-byte forwarding is standard
shell semantics; auto-interpreting \n (Option 2) would be a
breaking change and is intentionally not pursued.
Fixes#580
* feat(cmdutil): add X-Cli-Build header for CLI build classification
Adds X-Cli-Build (official / extended / unknown) so the gateway can distinguish official CLI from ISV-repackaged builds.
* test(cmdutil): lift coverage on build-kind classification
Extract classifyBuild as a pure helper so every branch (unknown / extended
main-path / extended credential / extended transport / extended fileio /
official) is reachable without mutating process-wide provider registries.
Also cover: isBuiltinProvider non-pointer values, BuildHeaderTransport
nil-Base fallback path, and fix the Name-spoof test so the test double
returns a value that actually mimics an ISV provider.
Coverage on PR-changed functions:
- classifyBuild: 100% (new)
- computeBuildKind: 61.5% -> 93.3%
- BuildHeaderTransport.RoundTrip: 80% -> 100%
Wrap the POST /drive/v1/permissions/:token/members/apply endpoint as a
user-only shortcut. --token accepts either a bare token or a document
URL, with type auto-inferred from the URL path (/docx/, /sheets/,
/base/, /bitable/, /file/, /wiki/, /doc/, /mindnote/, /minutes/,
/slides/); an explicit --type always wins. --perm is limited to view or
edit; full_access is rejected client-side to match the spec.
Classifier gains two domain-specific hints for the endpoint's newly
documented error codes: 1063006 (per-user-per-document quota of 5/day
reached) and 1063007 (document does not accept apply requests — covers
disallow-external-apply, already-has-access, and unsupported-type).
test(drive): add dry-run E2E for +apply-permission
Invoke the real CLI binary via clie2e.RunCmd under --dry-run and
parse the rendered request JSON with gjson to lock in method, URL
path (including the token segment), type query parameter (auto-inferred
for docx / sheet / slides URLs, taken from explicit --type for bare
tokens), perm body field, and remark presence/omission. A separate
test asserts --perm full_access is rejected by the enum validator
before reaching the server. Fake LARKSUITE_CLI_APP_ID / APP_SECRET /
BRAND are enough because dry-run short-circuits before any API call.
Update drive coverage.md to add a row and refresh metrics.
test(drive): isolate E2E dry-run subprocess from local CLI config
Set LARKSUITE_CLI_CONFIG_DIR to t.TempDir() in both +apply-permission
dry-run tests so the subprocess can't read a developer's real
credentials/profile instead of the fake env vars the tests inject.
test(drive): add E2E case that exercises URL inference override
Previous "bare token with explicit type wins over inference" row used a
bare token, which has no URL-derived type to override. Replace it with
a /docx/ URL + --type wiki combo that actually forces the explicit flag
to win over URL inference, and add a separate bare-token row to keep
the simpler path covered. Refresh coverage.md wording to match.
* fix(base): add default-table follow-up hint to base-create
* fix(base): route base-create hint to stderr
* fix(base): prefix base-create stderr tip
---------
Co-authored-by: kongenpei <kongenpei@users.noreply.github.com>
* fix: skip flag-completion registration outside completion path
Cobra keeps completion callbacks in a package-global map keyed by
*pflag.Flag with no removal path, so registrations made during Build()
outlive the command itself. Route all seven call sites through
cmdutil.RegisterFlagCompletion and enable registration only when the
invocation actually serves a __complete request.
Measured over 30 dropped Builds: ~202 KB / 2180 retained objects per
Build before, ~0 after.
Change-Id: I734d598a4c91a92c33b02e0f292f640cc0e224c6
The <<<<<<< HEAD marker was accidentally left in mail.md and SKILL.md
by commit cb301a3 (draft preview URL). Remove it.
Change-Id: I6e1d5c0c66761302a3c4ee1421a16961b666bd80
* feat(mail): add large attachment support via medias/upload API
When attachments would cause the EML to exceed the 25MB limit, they are
automatically uploaded to the mail attachment storage (medias/upload_all
with parent_type="email") and a download-link card is injected into the
HTML body, matching the desktop client's exportLargeFileArea style.
Key changes:
- Add classifyAttachments: EML-size-based splitting of normal vs oversized
- Add uploadLargeAttachments: upload via medias API with email MountPoint
- Add buildLargeAttachmentHTML: desktop-aligned card with CDN icons
- Add processLargeAttachments: unified entry point for all compose shortcuts
- Add LargeAttachmentHTML to emlbuilder.Builder for HTML block injection
- Fix 7bit line folding: use RFC 5322 limit (998) instead of incorrect 76
- Integrate into +draft-create, +forward, +reply, +reply-all
Known limitation: recipient access to large attachment links requires
backend support to register tokens with the draft (see progress doc).
Change-Id: If8d5938015cac8bc82de3ea3ff41022950f2571e
Co-Authored-By: AI
* refactor(mail): remove legacy size check, add 3GB limit, integrate +send
- Remove checkAttachmentSizeLimit (replaced by processLargeAttachments)
- Remove 25MB pre-check from validateComposeInlineAndAttachments so that
large files reach Execute where they are uploaded as large attachments
- Integrate processLargeAttachments into +send shortcut
- Add 3GB single file limit aligned with desktop client
- Clean up unused imports from helpers.go and helpers_test.go
Change-Id: Ie590ad2b58263c075f48b338959b8f5b3f912f85
Co-Authored-By: AI
* feat(mail): quote-aware HTML insertion, +draft-edit support, cleanup emlbuilder
- Add insertBeforeQuoteOrAppend: insert large attachment HTML before the
quote block (lark-mail-quote) instead of appending to body end, matching
desktop's exportLargeFileArea placement logic
- Add preprocessLargeAttachmentsForDraftEdit: intercept add_attachment
patch ops before draft.Apply, upload oversized files, inject HTML into
snapshot's HTML body Part directly. No changes to draft sub-package.
- Remove LargeAttachmentHTML field/setter/logic from emlbuilder — it was
business logic (quote-aware insertion) that doesn't belong in a generic
EML builder. processLargeAttachments now sets the full HTML body via
bld.HTMLBody() after merging the large attachment card at the right position.
- All compose shortcuts pass htmlBody to processLargeAttachments for
quote-aware insertion (composedHTMLBody for reply/forward, body for others).
Change-Id: If6e7ed7e77989ab9a8a41a93758f686d72ccf497
Co-Authored-By: AI
* fix(mail): align large attachment HTML IDs with desktop client
- Container ID: lark-mail-large-file-container → large-file-area (matching
desktop's MAIL_LARGE_FILE_CONTAINER constant)
- Item ID: lark-mail-large-file-item → large-file-item (matching
desktop's MAIL_LARGE_FILE_ITEM constant)
- Timestamp: truncate to 9 digits (matching TIMESTAMP_CUT_OUT_ID = 9)
- Refactor HTML generation to use template constants for readability
These IDs are used by the desktop client's BigAttachmentPlugin
([id^=large-file-area]) and the server's LargeFileRule to identify and
remove the HTML block when rendering the attachment card UI.
Change-Id: Ib5a77a1a3d60eeb3a05c585f2af0a5ddaacf887b
Co-Authored-By: AI
* docs(mail): document large attachment behavior in skill references
Update --attach parameter descriptions across all compose shortcuts
(+send, +reply, +reply-all, +forward, +draft-create, +draft-edit) to
describe automatic large attachment handling when EML exceeds 25 MB.
Change-Id: I8c30e390c127ea1119cb8c4b83ec636e41fbaf66
Co-Authored-By: AI
* fix(mail): pass signature-injected HTML to processLargeAttachments
When both --signature-id and large attachments are used, the htmlBody
passed to processLargeAttachments must include the already-injected
signature. Previously mail_send and mail_draft_create passed the
original body, causing processLargeAttachments to overwrite the
signature-injected HTML body when inserting the large attachment card.
Use composedHTMLBody variable (same pattern as reply/forward) to
capture the full processed HTML including signature.
Change-Id: I6be330776abca704b10cc3b8bfd5e20838e6e538
Co-Authored-By: AI
* fix(mail): skip draft.Apply when all ops consumed by large attachment preprocessing
When all patch ops are add_attachment targeting oversized files,
preprocessLargeAttachmentsForDraftEdit uploads them and removes the
ops from the patch. The resulting empty patch caused draft.Apply to
fail with "patch ops is required". Now skip Apply when no ops remain.
Change-Id: I8067a54b5f849fa519e8344a7eb10c48f58e54b8
Co-Authored-By: AI
* fix(mail): add X-Lms-Large-Attachment-Ids header in draft-edit large attachment flow
draft-edit's preprocessLargeAttachmentsForDraftEdit uploaded oversized files
and injected HTML cards but never wrote the X-Lms-Large-Attachment-Ids header
into the snapshot, so the mail server could not associate the attachments with
the draft. Merge new token IDs with any existing ones already in the snapshot.
Also extract the duplicated largeAttID struct and header name string into
package-level declarations.
Change-Id: Id256d948ec07e86296157436feefa3c2052af721
Co-Authored-By: AI
* fix(mail): i18n large attachment HTML text aligned with desktop client
Parameterize title and download text in large attachment HTML templates.
Chinese lang uses "来自Lark邮箱的超大附件"/"下载", others use
"Large file from Lark Mail"/"Download", matching desktop's i18n keys
Mail_Attachment_AttachmentFromFeishuMail and Mail_Attachment_Download.
Change-Id: I2aada8d52af41ae77dd7001d24d14e333f12066e
Co-Authored-By: AI
* fix(mail): insert large attachment card before quote wrapper, not inside nested quote
insertBeforeQuoteOrAppend matched id="lark-mail-quote" which can appear
deeply nested inside quoted content from previous replies in a thread.
This caused the card to be placed inside the quote area instead of before
it. Switch to matching the "history-quote-wrapper" class which is the
outermost quote container generated by the CLI.
Change-Id: I720b6d62d719613b411b7ed4b7820a1535bf14bd
Co-Authored-By: AI
* feat(mail): unify large attachment handling in +draft-edit with normal attachments
Extend +draft-edit so that large attachments behave like normal attachments
from the user's perspective: survive body edits, are listed in inspect
output, and are removed via the same remove_attachment op.
Code-wise:
- remove_attachment target now accepts token (for large attachments) in
addition to part_id / cid; priority part_id > cid > token.
- setBody / setReplyBody auto-preserve the large attachment card in the
HTML body, mirroring how normal attachments (MIME parts) survive body
edits. Detection checks only the user-authored region of the value so
cards inside an appended quote block (from the original quoted message)
are not mistaken for user-supplied cards.
- --inspect returns large_attachments_summary (token, filename, size) by
parsing the X-Lms-Large-Attachment-Ids header and the HTML card DOM.
- Well-known Lark HTML/header constants (LargeAttachmentIDsHeader,
LargeFileContainerIDPrefix, LargeFileItemID, LargeAttachmentTokenAttr)
moved to the draft package alongside QuoteWrapperClass; the mail package
consumes them.
- Shared helpers FindHTMLBodyPart and InsertBeforeQuoteOrAppend exported
from the draft package; mail package switched to consume them, removing
local duplicates.
Skill reference (lark-mail-draft-edit.md) updated: three locator fields by
attachment type, unified remove_attachment examples, set_body behavior.
Change-Id: Ic064d1a8df0edf1cef6069cd44ec2a7534cd2182
Co-Authored-By: AI
* fix(mail): place signature before large attachment card consistently
When inserting a signature into a draft that already has a large
attachment card, the signature was placed after the card, diverging from
the compose-time layout where the order is [user][sig][card][quote].
Root cause: insertSignatureOp split only at the quote block, so the
"user region" side inadvertently included the card.
Centralize signature placement in draft.PlaceSignatureBeforeSystemTail,
which splits at the earliest system-managed element (card or quote,
whichever comes first). Both edit-time insertSignatureOp and compose-time
injectSignatureIntoBody now share this single source of truth, removing
the duplicated HTML splicing logic.
Change-Id: I234bfebaaa31a32731ebbaa78c6596a72618b7c5
Co-Authored-By: AI
* fix(mail): auto-preserve signature in set_body and set_reply_body
Previously set_body / set_reply_body replaced the entire HTML body,
silently dropping the signature block. The "replace whole body" semantic
treated signature as user-authored content, which is inconsistent with
how attachments (normal + large) and quote blocks survive body edits —
signature is a system-managed element managed via insert_signature /
remove_signature ops.
Unify the mental model: body-edit ops replace user-authored content
only; signature, large attachment card, normal attachments, and (for
set_reply_body) quote block are all auto-preserved. Users can override
by including equivalents in value, or explicitly delete via dedicated
ops (remove_signature, remove_attachment).
- Add ExtractSignatureBlock helper (symmetric to RemoveSignatureHTML).
- Rename autoPreserveLargeAttachmentCard to
autoPreserveSystemManagedRegions; extract and inject both sig and card
from old body, respecting user-supplied equivalents in value's
user-authored region.
- Update skill doc and patch template notes to reflect the new
semantics consistently.
Change-Id: I96660d2ff06a6c9cdf1b86793c2d89cf9cb09ffe
Co-Authored-By: AI
* fix(mail): use brand-aware display name in large attachment card title
The title "Large file from Lark Mail" / "来自Lark邮箱的超大附件" hard-coded
"Lark" regardless of brand. The desktop client switches between
"Feishu"/"飞书" and "Lark" based on the APP_DISPLAY_NAME i18n
substitution.
Add brandDisplayName(brand, lang) helper:
- BrandLark → "Lark"
- BrandFeishu → "飞书" (zh) / "Feishu" (en)
Applied to title in buildLargeAttachmentHTML, aligning with the icon CDN
and download URL, which already branch on brand.
Change-Id: I06258b9982b6280a2230193d90a6a88884e10aa3
Co-Authored-By: AI
* style(mail): apply gofmt
CI fast-gate check flagged gofmt-unformatted files. Run gofmt -w on
touched mail files only.
Change-Id: Iec690dc63adfaa54b8f7c85ab5b3ca035476ddbd
* fix(mail): address review feedback on large attachment PR
- Strip <html><head><body> wrapper from xhtml.Render output in
removeLargeFileItemFromHTML to avoid polluting the HTML body
- Reject plain-text messages with oversized attachments instead of
silently losing the body content
- Fix attachment count limit in skill doc (100 → 250)
- Remove unused fio/attachFlag params from validateComposeInlineAndAttachments
- Add token escaping test for large attachment HTML builder
Change-Id: Ie589a1f1d204b0aeebc4486b16bb435041793ceb
Co-Authored-By: AI
* fix(mail): recognize server-format X-Lark-Large-Attachment header in draft-edit
When a draft with large attachments is created by the desktop client,
the server returns X-Lark-Large-Attachment (with file_key/file_name/
file_size fields) instead of the CLI-written X-Lms-Large-Attachment-Ids.
Previously CLI only recognized its own header, causing existing large
attachments to be silently dropped when the draft was edited.
- Parse both header formats via IsLargeAttachmentHeader and unified
largeAttHeaderEntry struct
- Convert server-format entries to CLI-format on save so the server
can process the update
- Fix inline attachment classification: require non-empty CID to
classify as inline image (large attachments may have is_inline=true
but no CID)
Change-Id: Ie7def4fc5923d2cf3446eedfbca4fd8cae44bfac
Co-Authored-By: AI
* fix(mail): skip large attachments in forward URL validation
Large attachments do not have download URLs since they are referenced
by token, not embedded in the EML. Validate only normal attachments
to avoid false "missing download URL" errors when forwarding messages
that contain expired or token-based large attachments.
Change-Id: Ibe3f45390cd3b3cbe6ddd15961dcda4f17aefe4f
Co-Authored-By: AI
* fix(mail): classify forwarded original attachments for large attachment upload
Previously, all original attachments were unconditionally embedded in
the EML before user attachments were processed for large attachment
upload. When original + user attachments together exceeded the 25 MB
EML limit, the build would fail.
Now all attachments (original + user-added) are classified together
via classifyAttachments. Original attachments that push the EML over
the limit are re-uploaded as large attachments with download cards,
matching the compose/reply flow behavior.
Also refactors uploadLargeAttachmentBytes to reuse the shared
common.UploadDriveMediaAll utility (via new Reader field on the
config struct) instead of duplicating the upload logic, and replaces
bare fmt.Errorf with output.ErrValidation for user input errors.
Change-Id: I98d4ad8960cd68e38765b05c94f7786d6a8444c8
Co-Authored-By: AI
* fix(mail): normalize large attachment header on draft edit to prevent loss
Server returns X-Lark-Large-Attachment header on draft readback, but only
recognizes X-Lms-Large-Attachment-Ids on write. Without normalization,
editing a draft with existing large attachments (e.g. adding a small
attachment) would send back the server-format header unchanged, causing
the server to drop the large attachment association.
Add normalizeLargeAttachmentHeader() at the entry of
preprocessLargeAttachmentsForDraftEdit to convert server-format headers
to CLI format before any processing or early return.
Change-Id: Id99a46f29015a32921bfb72a003f766c397787e1
Co-Authored-By: AI
* fix(mail): extract large attachment card from quote on forward
When forwarding a message that contains large attachments, the original
message's download card (large-file-area div) was left inside the
forward quote block. Extract it and place it in the main body area
(after signature, before quote), matching the desktop client behavior.
Change-Id: Iebede35cdf4ed0f65b72bce28ffb18af21ddf668
Co-Authored-By: AI
* fix(mail): use octet-stream for re-embedded attachments and file-based large upload on forward
- Use application/octet-stream instead of original content type when
re-embedding downloaded attachments in forward EML. Prevents the mail
server from treating image/* attachments as inline parts.
- Replace in-memory uploadLargeAttachmentBytes with temp-file-based
uploadLargeAttachments for oversized original attachments. This
enables multipart upload for files >20MB which the single-part API
does not support.
Change-Id: Ib02add5710e8b052e47b513ed3d9a688e0f98212
Co-Authored-By: AI
* fix(mail): address PR review — blocked extension bypass, index-based op filtering, plain-text draft guard
1. Move CheckBlockedExtension into statAttachmentFiles so oversized
attachments are validated before classification, covering compose,
draft-edit, and forward paths.
2. Replace path-based oversized op filtering with SourceIndex-based
filtering in preprocessLargeAttachmentsForDraftEdit to avoid
incorrectly removing duplicate-path normal ops.
3. Add HTML body preflight in preprocessLargeAttachmentsForDraftEdit
before uploading, so plain-text-only drafts fail early instead of
silently producing a draft with tokens but no download card.
Change-Id: Ib8771812f50a18f00a40e50149b028b8aaa101fe
Co-Authored-By: AI
* fix(mail): preserve original content type for normal forwarded attachments
The octet-stream override was only needed for the large attachment
upload path (to prevent image/* from being treated as inline by the
drive API). Normal attachments embedded in the EML should retain their
original MIME type so recipients can preview/open them correctly.
Change-Id: Ie40b7c362524a3b82255b58e9bcfd770eacfe911
Co-Authored-By: AI
* fix(mail): reconstruct missing large attachment HTML cards on draft edit
The server strips HTML download cards from the EML body when storing
drafts, so every draft read-back (regardless of creator) lacks them.
Add ensureLargeAttachmentCards which runs before header normalization,
compares server-format header tokens against existing HTML cards via
data-mail-token, and rebuilds only the missing ones. This ensures
external recipients see download links after draft-edit → send.
Also exports ParseLargeAttachmentSummariesFromHeader and
ParseLargeAttachmentItemsFromHTML from the draft package for
cross-package use.
Change-Id: I9cb0f47a9f4582909de24984d9a9f6e366521e62
Co-Authored-By: AI
* feat(mail): support large attachments in plain-text emails
Previously large attachments required an HTML body for the download card.
Now plain-text emails (--plain-text or text/plain-only drafts) get download
info appended as structured text (title + filename + size + URL), with
i18n and brand awareness matching the HTML card.
Changes:
- Add buildLargeAttachmentPlainText and injectLargeAttachmentTextIntoSnapshot
- Add FindTextBodyPart in draft/projection.go
- Update processLargeAttachments to accept textBody parameter
- Update ensureLargeAttachmentCards to handle text/plain body reconstruction
- Update preprocessLargeAttachmentsForDraftEdit to allow text/plain drafts
- Update all callers (send, draft-create, reply, reply-all, forward)
Change-Id: I3b375e2ff34697eeb73a3768ace6d577d1bead3e
Co-Authored-By: AI
* fix(mail): FindBodyPart skips attachment-disposition parts; update skill docs
FindHTMLBodyPart and FindTextBodyPart now skip parts with
Content-Disposition: attachment, preventing .txt/.html file attachments
from being mistakenly treated as the email body.
Also update all lark-mail skill reference docs to reflect that large
attachments now work in both HTML (download card) and plain-text
(download link text) modes.
Change-Id: I1e6da4fd614217dff61304212304b5fd80c8246c
Co-Authored-By: AI
* fix(mail): fix origIdx mismatch, predictable temp files, and attachment count on forward
- Use SourceIndex instead of linear origIdx counter so classifyAttachments
reordering does not cause content mismatch between normal/oversized loops
- Use os.CreateTemp for temp files instead of predictable names in CWD
- Include original large attachment count in totalCount limit check
Change-Id: Ide5dce14b1efc672687800d77c3853f15dfc191b
Co-Authored-By: AI
* fix(mail): use composed body size and source inline bytes in EML size estimation
estimateEMLBaseSize was using len(body) (raw --body flag) instead of the
actual composed body (which includes quotes, signatures, forward headers).
Source inline images downloaded from the original message were also not
counted. This could cause borderline attachments to be misclassified.
- Use len(composedHTMLBody) + len(composedTextBody) for body size
- Return total downloaded bytes from addInlineImagesToBuilder and pass
as extraBytes to estimateEMLBaseSize
- Fix applied to all compose shortcuts: send, draft-create, reply,
reply-all, forward
Change-Id: Ibe6c44e22d40ac51f0a4652d279e66bd92330723
Co-Authored-By: AI
* fix(mail): merge large attachment items into single container on draft edit
When draft-edit had both set_body and add_attachment (oversized), the
ensureLargeAttachmentCards and preprocessLargeAttachmentsForDraftEdit
each created independent large-file-area containers. The subsequent
set_body's autoPreserveSystemManagedRegions only captured the first
container via SplitAtLargeAttachment, discarding the second one.
Fix: injectLargeAttachmentHTMLIntoSnapshot now detects an existing
large-file-area container and appends new items inside it instead of
creating a new container, matching the desktop client's single-container
behavior.
Change-Id: I3d701683053842f1d7bdad34fc4b2ef26ede784e
Co-Authored-By: AI
* fix(mail): strip large attachment card from reply/reply-all quote
Reply and reply-all should not carry over the original email's large
attachment HTML card into the quoted block. Extract the shared
stripLargeAttachmentCard helper (also used by forward) that removes
the card from orig.bodyRaw before quote construction.
- Reply/reply-all: card is discarded (not re-inserted)
- Forward: card is moved to body area before the quote (unchanged)
Change-Id: I5399bb901c120206c7c045bed107f7d68be23bb1
Co-Authored-By: AI
* fix(mail): skip invalid attachments on forward instead of blocking
When forwarding a message with deleted/expired attachments, the forward
flow now automatically removes them instead of either blocking (normal
attachments) or silently including dead references (large attachments).
- Propagate failed_ids from fetchAttachmentURLs into composeSourceMessage
- Skip failed attachments in the forward download loop with a warning
- Remove corresponding large attachment HTML card items from the body
- Extend itemContainsToken to match server-generated href?token= format
Change-Id: I9c0096dcbe96f1d61caa0f6f0b2f8b738fdfa66b
Co-Authored-By: AI
* fix(mail): restore dry-run file preflight and reserve card overhead in classifier
1. Restore file existence and blocked-extension checks in
validateComposeInlineAndAttachments so --dry-run surfaces local
path errors before Execute.
2. Reserve 3KB per oversized file in classifyAttachments to account
for the HTML card / plain-text block injected after classification.
Change-Id: Ib48a75f86a50298413c1f9ab8226e583c0161a8c
Co-Authored-By: AI
* fix(mail): revert classifier overhead reserve for simplicity
The 3KB-per-oversized-file reserve in classifyAttachments addressed
a boundary case that is practically impossible to trigger (requires
Normal attachments to fill to within a few KB of 25MB). Remove it
to keep the classifier simple.
Change-Id: I5148f14ecca1a0dee677a1a2c60ec4efab160ea8
Co-Authored-By: AI
* style(mail): fix gofmt indentation in draft create tests
Change-Id: Ib41aa22f94144f2d47b12675d444aa43cb333a88
Co-Authored-By: AI
* fix(mail): remove temp files in forward, use in-memory upload instead
Replace os.CreateTemp/os.WriteFile/os.Remove with in-memory Data field
on attachmentFile, conforming to the project's forbidigo rule against
temp files in shortcuts. Also remove dead uploadLargeAttachmentBytes.
Change-Id: Ic26e4025eebfa1bac3948438ef185ff3e2f15abb
Co-Authored-By: AI
* test(mail): add tests for validateComposeInlineAndAttachments and fileTypeIcon
Covers all branches: inline+plain-text conflict, inline+non-HTML body,
missing file, blocked extension, valid pass-through, and all file type
icon mappings.
Change-Id: I8b81c1b34010a9ecb7153462a5524e3d7b171de2
Co-Authored-By: AI
* test(mail): improve coverage for large attachment and draft edit functions
Add tests for snapshotEMLBaseSize, flattenSnapshotParts, estimateEMLBaseSize,
normalizeLargeAttachmentHeader, processLargeAttachments error paths,
preprocessLargeAttachmentsForDraftEdit early-return paths, inject edge cases,
buildLargeAttachmentItems, statAttachmentFiles edge cases, and
prettyDraftAddresses.
Change-Id: Ie661e6ebea63512864d97e20135dd89cb9e9304e
Co-Authored-By: AI
* fix(docs): validate --selection-by-title format early
* fix(docs): reject multiline selection-by-title before prefix check
* chore: refresh CI against current main (no code change)
* test(doc): cover DocsUpdate.Validate integration for selection-by-title
codecov/patch was at 27.27% because the PR added three lines to the
Validate closure (the `if err := validateSelectionByTitle(selTitle); err
!= nil { return err }` block) but nothing in the test file exercised
that closure — only the helper function was tested directly.
TestDocsUpdateValidate now builds a bare RuntimeContext via
common.TestNewRuntimeContext, sets the relevant flags on a cobra
command, and calls DocsUpdate.Validate(ctx, rt) across five cases:
1. Heading-style selection-by-title passes — covers the happy path
through the new call site and the final `return nil`.
2. Plain-text title is rejected with heading-prefix guidance —
covers the new error branch.
3. Multi-line title is rejected as not a single heading line —
covers the other error branch inside the helper.
4. Invalid --mode is still rejected first — proves the new check
doesn't swallow pre-existing validation.
5. Conflicting --selection-with-ellipsis + --selection-by-title is
rejected at the mutual-exclusion check — same ordering contract.
Coverage profile confirms the three added production lines
(docs_update.go L65-67) are now hit: condition 3x, error branch 2x,
happy path via the closure's return nil 1x.
* refactor(cmd): split Execute into Build with IO/Keychain injection
Introduce a public cmd.Build entry point so external consumers (cli-server,
MCP server, other embedders) can assemble the full CLI command tree without
going through os.Args or the platform keychain. Build takes an
InvocationContext plus functional BuildOptions:
* WithIO(in, out, errOut) — inject custom streams; terminal detection
is derived from the input's underlying *os.File when present.
* WithKeychain(kc) — swap the credential store.
* HideProfile(bool) — registered later in cmd.HideProfile.
The existing Execute() keeps using the internal buildInternal (which
still returns the Factory so error handling can attribute exit codes),
and SetDefaultFS replaces the global VFS implementation at startup.
Hardening applied up front:
* cmdutil.NewIOStreams(in, out, errOut) centralizes terminal detection
so SystemIO() and WithIO share one path.
* cmdutil.NewDefault normalizes partial IOStreams — callers may pass
&IOStreams{Out: buf} without tripping nil-writer panics in the
RoundTripper warnings, Cobra, or the credential provider.
* Build guards against nil functional options.
* An API contract test (cmd/build_api_test.go) exercises Build +
WithIO + WithKeychain + HideProfile + SetDefaultFS so the public
surface is reachable by deadcode analysis.
Change-Id: I7c895e6019817401accbde2db3ef800da40ad319
* feat(schema): filter methods by strict mode in schema output
When strict mode is active, schema output now excludes methods that
are incompatible with the forced identity. This applies to both
pretty and JSON output formats at the resource and method levels.
Change-Id: I39647d5578466c3e23dc545bfb917ae075203ad7
* refactor: centralize strict-mode as flag registration
Change-Id: Iec11151c5002c2f58a8aa067d08747db2e4d2d8c
* fix(cmd): align strict-mode completion and build context; drop dead register shims
Thread a context.Context through RegisterShortcuts, RegisterServiceCommands,
and service.registerService/Resource/Method by introducing explicit
*WithContext variants. Pass that context into NewCmdServiceMethodWithContext
so shortcut and service command construction can honor cancellation and
strict-mode pruning consistently.
Also drop the context-less registerMethod and registerResource shims —
they became unreachable once the WithContext variants took over, and
were the source of new deadcode warnings. registerService is retained
because service_test.go still calls it directly.
Change-Id: I3fe5673aed663c7383bbbc5b0ae94d1f3491f22d
* refactor(cmd): hide --profile in single-app mode via build option
- GlobalOptions gains HideProfile; RegisterGlobalFlags stays pure and reads
the policy off the struct. No boolean-trap parameter, one call per site.
- buildConfig holds GlobalOptions inline so HideProfile(bool) BuildOption
mutates it directly. buildInternal stays a pure assembly function and
requires callers to supply WithIO — no implicit os.Std* fallback.
- Add WithIO BuildOption (wrapping raw io.Reader/Writer with automatic
*os.File TTY detection); Execute injects streams explicitly and decides
profile visibility via HideProfile(isSingleAppMode()).
- installTipsHelpFunc force-shows hidden root flags while rendering the
root command's own help, so single-app users still discover --profile
via lark-cli --help without it polluting subcommand helps.
Change-Id: I7755387e993992ca969e0a4a6f54441cc1993eef
* feat(transport): extension abort hook and shared base transport
Two transport-layer changes bundled because both reshape the base
round-tripper contract used by the HTTP client, the Lark SDK client,
and the in-process updater.
1. Extension abort hook (PreRoundTripE).
Extensions implementing exttransport.AbortableInterceptor can now
return an error from PreRoundTripE to skip the built-in chain. The
post hook still fires with (nil, reason) so extensions can unwind
resources. extensionMiddleware captures the provider name so the
returned *AbortError carries attribution.
2. Shared base transport to stop RPC leak.
util.NewBaseTransport cloned http.DefaultTransport on every call, so
each cmdutil.Factory produced a fresh *http.Transport whose
persistConn readLoop/writeLoop goroutines lingered until
IdleConnTimeout (~90s). Invisible in a single-process CLI, but the
fork is consumed by cli-server where each RPC request constructs a
new Factory, causing linear memory + goroutine growth under load.
Replace NewBaseTransport with SharedTransport — returns
http.DefaultTransport (the stdlib-wide singleton) by default, and
a cached proxy-disabled clone only when LARK_CLI_NO_PROXY is set.
Return type is http.RoundTripper to discourage in-place mutation of
the shared instance. FallbackTransport is kept as a thin
*http.Transport wrapper so existing callers in internal/auth and
internal/cmdutil transport decorators (which were already on the
singleton path) do not have to migrate.
Leak-site migrations: factory_default.go (HTTP + SDK base) and
update.go now call SharedTransport directly.
Change-Id: Ia82462134c5c5ee838be878b887860f41446a235
* fix: unblock Build() zero-opts path and sidecar demo build
Two regressions surfaced on refactor/build-execute-split:
1. cmd.Build(ctx, inv) without WithIO panicked at rootCmd.SetIn/Out/Err
because cfg.streams stayed nil — NewDefault normalized internally
but cmd/build.go never saw the normalized value. Default cfg.streams
to cmdutil.SystemIO() before the root command wires them, and add a
TestBuild_NoOptions regression guard.
2. sidecar/server-demo/main.go still called cmdutil.NewDefault(inv),
so `go build -tags authsidecar_demo ./sidecar/server-demo` failed
with "not enough arguments". Pass nil for the new streams parameter
to preserve the prior behavior (NewDefault substitutes SystemIO).
Change-Id: I20227b2355cde7d19e22eba3eb841c6d8611e8a7
* feat(doc): add pre-write semantic warnings to docs +update
Two static checks run before the MCP update-doc call:
1. replace_* + blank-line markdown: replace_range / replace_all only
swap text inside an existing block — a \n\n in the payload will
render as literal text, not a paragraph break. Hint to use
delete_range + insert_before instead.
2. Combined bold+italic emphases (***text***, **_text_**, _**text**_)
cannot round-trip through Lark and are silently downgraded to a
single emphasis. Hint to split into two separate emphases.
Both warnings go to stderr and never block the update — they inform,
not gate. Adds table-driven tests for each check plus an aggregation
test, and wires the checks into Execute right before CallMCPTool.
Closes the first batch of items from the docs +update pitfalls
review (Cases 1 and 5).
* fix(doc): exclude code regions and escaped markers from docs +update checks (#578)
* fix(doc): exclude code regions and escaped markers from docs +update checks
Addresses the three review comments on #569: the blank-line paragraph
check and the bold+italic emphasis check both operate on the raw
markdown string, so fenced code blocks / inline code spans / literal
escaped markers produce false-positive warnings on content users
expect to pass through verbatim.
Changes:
- Add proseHasBlankLine(): fence-aware detector that returns true only
when a blank line sits outside of ```...``` or ~~~...~~~ regions.
Replaces the raw strings.Contains("\n\n") check in
checkDocsUpdateReplaceMultilineMarkdown.
- Add stripMarkdownCodeRegions(): blanks out fenced code lines and
masks inline code spans (via scanInlineCodeSpans from markdown_fix.go)
with equal-length whitespace so byte offsets outside the stripped
regions are preserved.
- Add stripEscapedEmphasisMarkers(): removes "\*" and "\_" so literal
sequences like "\***text***" — which CommonMark renders as a literal
asterisk plus bold — don't match the combined bold+italic regex.
- Wire both helpers into checkDocsUpdateBoldItalic(): the regex now runs
on stripEscapedEmphasisMarkers(stripMarkdownCodeRegions(markdown)),
so code samples and escaped markers are sanitized away before
detection.
Shared fence-parsing helpers (codeFenceOpenMarker, isCodeFenceClose,
leadingRun) are kept local to this file to avoid touching files outside
the scope of the reviewed PR. If a future change wants to reuse them
across the doc package, they can be promoted then.
Tests:
- TestCheckDocsUpdateReplaceMultilineMarkdown: add 4 negative/positive
cases — blank line inside backtick and tilde fences (no flag), blank
line in prose while fence also has blanks (flag wins), fenced code
with no blank lines (no flag).
- TestCheckDocsUpdateBoldItalic: add 9 cases — ***text*** / **_text_** /
_**text**_ inside fenced code (backtick and tilde), inside inline
code spans, and escaped \***text*** / \*\*_text_\*\* (none flagged);
plus two positive cases to verify the strip doesn't over-sanitize
(real emphasis in prose still fires when inline/fenced code is nearby).
* fix(doc): close CommonMark gaps and add three more combined-emphasis shapes
Self-review of the first commit turned up three issues:
- isCodeFenceClose was strict on exact marker length. Per CommonMark
§4.5, a closing fence must be at least as long as the opener, not
exactly the same length. A 3-backtick open legitimately closed by a
4-backtick closer (used to embed triple-backticks inside the code
sample) was left open-ended, causing the rest of the document to be
treated as code and both checks to silently skip it.
- Both fence helpers accepted any amount of leading whitespace because
they ran on strings.TrimSpace(line). CommonMark allows 0..3 leading
spaces before a fence marker; 4+ spaces (or any tab in leading
position, which expands to 4 columns) makes the line indented code
block content, not a fence open/close. Indented fence-like lines now
correctly remain prose and blank lines around them are detected.
- The bold/italic check only covered three of the six documented
combined-emphasis shapes. Added ___text___, __*text*__, and
*__text__* so parity with the asterisk variants is complete. The
regex set is now table-driven (combinedEmphasisPatterns) to make
adding future shapes a one-line change.
Implementation changes:
- New fenceIndentOK(line) helper: returns (body, true) for 0..3 leading
spaces with no tabs, else (_, false). Used by both codeFenceOpenMarker
and isCodeFenceClose.
- isCodeFenceClose now counts the fence-char run and accepts any run
length >= len(marker), with trailing whitespace only.
- checkDocsUpdateBoldItalic replaced three named var regexes with a
table of six {shape, re} entries and a single early-exit loop.
- Updated docsUpdateWarnings top docstring to list all six shapes.
- Noted the known limitation of stripEscapedEmphasisMarkers around
doubled backslash escapes ("\\***text***"), which is a false negative
we accept in exchange for keeping this a simple string replace.
Test additions (docs_update_check_test.go):
- Fence close: longer-marker close correctly ends fence; real prose
blank after a longer-close fence is still detected.
- Indentation: 4-space indented fence-like line is not a fence open,
so a surrounding blank line still flags; tab-indented variant same;
3-space indented fence is still a real fence.
- New shapes: ___text___ positive + all three negative-guards (fenced
code, inline code, escaped); __*text*__ and *__text__* positive +
fenced/inline negative-guards; plus two composition tests to ensure
the strip does not over-sanitize across the six-regex alternative set.
All 53 sub-tests in this file pass; go vet and gofmt are clean.
---------
Co-authored-by: fangshuyu-768 <shuyufang768@outlook.com>
* fix(doc): address CodeRabbit review on docs +update warnings (#581)
Two CodeRabbit nits from #569:
1. Unit test hint assertion only checked for `delete_range` in the
remediation message; the companion `insert_before` half of the
guidance could regress undetected. Broaden the assertion to require
both tokens so a future edit that drops half the remediation
produces an immediate test failure.
2. No E2E coverage proved the dry-run contract in the PR description
("Not emitted in dry-run mode — kept quiet during planning"). The
helper itself is unit-tested, but nothing caught a regression where
a later refactor wired docsUpdateWarnings into the DryRun path.
Add tests/cli_e2e/docs/docs_update_dryrun_test.go:
TestDocs_UpdateDryRunSuppressesSemanticWarnings invokes
`docs +update --dry-run --mode=replace_range --markdown "***x***\n\nb"`
— an input crafted to trip BOTH pre-write warnings — and asserts
neither the "warning:" prefix, the blank-line message, nor the
combined-emphasis message appears on stdout or stderr.
Note: the file needs -f to add because .gitignore has a bare
`docs/` rule that accidentally matches tests/cli_e2e/docs/. The
existing tracked files under that directory predate the rule; new
additions have to be force-added until the ignore pattern is
narrowed. Not worth rewriting .gitignore for one file.
Verified manually that the new E2E fails cleanly when warnings are
injected into DryRun and passes again after reverting — the test has
real regression-detection power, not just a sticker.
Co-authored-by: fangshuyu-768 <shuyufang768@outlook.com>
- Register DocMediaUpload in doc/shortcuts.go (was defined but never
registered, so lark-cli docs +media-upload was unavailable)
- Rename MediaUpload to DocMediaUpload for consistency with
DocMediaInsert/DocMediaPreview/DocMediaDownload
- Add whiteboard to --parent-type flag description
- Update --parent-node description to mention board_token for whiteboard
Drive +upload (parent_type=explorer) produces file tokens that the
whiteboard API does not recognize (500 error). The correct approach
is docs +media-upload with parent_type=whiteboard.
* feat(doc): add --after-keyword/--before-keyword flags to +media-insert
Allows inserting images/files at a position relative to the first block
whose plain text matches a keyword (case-insensitive substring match).
- Add --after-keyword: insert after the matched root-level block
- Add --before-keyword: insert before the matched root-level block
- Flags are mutually exclusive; default behavior (append to end) unchanged
- fetchAllBlocks: paginated block listing (up to 50 pages × 200 blocks)
- extractBlockPlainText: covers text, heading1-9, bullet, ordered, todo, code, quote
- findInsertIndexByKeyword: walks parent_id chain to resolve nested blocks to their root-level ancestor
- DryRun updated to show block-listing step when keyword flag is set
* test(doc): add fetchAllBlocks pagination and keyword dry-run coverage
- TestFetchAllBlocksPaginationViaExecute: exercises fetchAllBlocks via a
full Execute flow with --after-keyword, covering multi-page block listing
(fetchAllBlocks was previously at 0% coverage)
- TestDocMediaInsertDryRunWithAfterKeyword: verifies that the dry-run output
includes a block-listing step and mentions "search blocks" in the
description when --after-keyword is provided
fetchAllBlocks coverage: 0% → 76.2%
* refactor(doc): use MCP locate-doc for keyword-based block positioning
Replace fetchAllBlocks + keyword scan with MCP locate-doc tool,
consistent with DriveAddComment. Flags changed from --after-keyword /
--before-keyword to --selection-with-ellipsis + --before.
* fix(doc): show <locate_index> in dry-run create-block when selection is set
When --selection-with-ellipsis is provided, the create-block step in dry-run
now shows index: "<locate_index>" instead of "<children_len>" to accurately
reflect that the insertion position is computed from MCP locate-doc, not
appended to end.
* fix(doc): address CodeRabbit review on +media-insert selection feature
- Validate: reject blank/whitespace --selection-with-ellipsis unconditionally
so a mis-typed empty value cannot silently fall back to append-mode.
- Redact the raw selection string when logging to stderr and when emitting
error messages. --selection-with-ellipsis is copied verbatim from document
content and may contain confidential text; the new redactSelection helper
keeps a short prefix and rune count so operators can still identify the
failing selection.
- Harden the after/before mode tests: root children now have three entries
so the two modes land on different indices, and the tests decode the
create-block request body to assert the computed `index` actually reaches
the /children API. A regression that ignored --before would now fail.
- Harden the nested-block test so it exercises the fallback parent-walk:
the anchor is now two levels deep (blk_grandchild under blk_section_child
under blk_section), which forces the walk to fetch the intermediate block
via GET /blocks/{id} to discover the root-level ancestor.
* fix(doc): harden +media-insert selection UX on top of #335 (#577)
Follow-up to #335 review: closes a handful of UX and robustness gaps in
the new --selection-with-ellipsis flow.
- Flag description rewritten to make the "insert at the top-level
ancestor" semantics explicit — when the selection is inside a callout,
table cell, or nested list, media lands outside that container, not
inside. Also calls out the 'start...end' disambiguator.
- locate-doc is now called with limit=2 so an ambiguous selection
(same phrase in more than one block) surfaces a stderr warning
pointing at 'start...end', instead of silently picking the first
match. The first-match return behaviour is unchanged.
- When the anchor is nested below the root, locateInsertIndex now
logs a note to stderr naming the walk depth and the root-level
ancestor's insert index. Users don't have to guess why the image
landed outside the callout they were editing.
- maxDepth bumped 8 → 32 with a comment explaining the invariants:
`visited` is the real cycle guard, `maxDepth` is belt-and-suspenders.
32 comfortably exceeds real docx nesting depth so a deeply-nested
but well-formed anchor is no longer silently rejected.
- Comment added before the parent-walk loop noting why the API calls
are serial (each level's parent_id is only known after the previous
GET returns; can't be batched or parallelised).
Tests:
- TestLocateInsertIndexWarnsOnMultipleMatches: stubs two matches,
asserts the stderr warning names the ambiguity and mentions
'start...end', and that the first-match insert index is unchanged.
- TestLocateInsertIndexLogsNestedAnchor: anchor two levels below root,
asserts stderr carries the "nested … top-level ancestor" note.
- TestLocateInsertIndexCycleDetection: malformed parent chain with
blk_x.parent = blk_y and blk_y.parent = blk_x, neither reachable
from root. Registering a single GET /blocks/blk_y stub also bounds
the call count — a regression that broke `visited` tracking would
either hang or fail via httpmock's extra-call guard.
Co-authored-by: fangshuyu-768 <shuyufang768@outlook.com>
Adds 5 invariant-level tests on top of #469's transforms:
- TestFixExportedMarkdownIdempotent — f(f(x)) == f(x) across rich
fixtures (kitchen sink, CJK, nested containers). Protects the core
round-trip promise from future transform interactions that rewrite
their own output.
- TestFixExportedMarkdownPreservesFencedCodeByteForByte — packs every
pipeline-touching shape into a fence and asserts byte-identical output.
Code samples must never be silently rewritten by a formatting pass.
- TestFixExportedMarkdownPreservesCRLF — CRLF input preserves line
endings AND still triggers transforms. Windows-authored markdown
should not be silently LF-normalized.
- TestFixExportedMarkdownTransformInteractions — composition regressions:
nested-list + trailing-space bold, text→list transition, callout
containing list with emphasis, heading vs paragraph bold.
- TestNormalizeNestedListIndentationDocumentedSkips — locks in the
deliberate no-op branches (odd-space indent, blank-line loose-list
sibling, 4-space indented code block, parentless two-space) as an
explicit spec so future heuristic tweaks surface in the test diff.
All transforms, fixtures, and expectations are derived from the head of
PR #469. No production code changes.
Co-authored-by: fangshuyu-768 <shuyufang768@outlook.com>
* fix(doc): preserve round-trip formatting in fetch output
- trim leading spaces inside bold and italic emphasis exported by docs +fetch
- normalize nested list indentation to avoid flattening and literal text on re-import
- add regression tests for emphasis spacing and nested list indentation
* fix(doc): avoid false positives in markdown spacing fixes
- keep literal * x * and ** x ** text unchanged
- only normalize indented nested list markers when a parent list item exists
- add regression coverage for both CodeRabbit findings
* fix(doc): 修正嵌套列表缩进的空行误判
- 遇到空行时停止向上查找父级列表项,避免把 loose list sibling 误改成嵌套列表
- 避免把列表项中的四空格缩进代码块误改成 tab 缩进列表项
- 补充两个回归测试,并更新 fixBoldSpacing 注释使其与当前实现一致
* fix(doc): 修复 Markdown emphasis 空格回写
- 将 fixBoldSpacingLine 改为按星号 run 扫描,修复 ** hello **、* hello * 和同一行多个 italic span 的空格清理
- 保留 inline code、heading 和 *** hello** 这类近邻字面量,避免误改 emphasis nesting
fix: address coderabbit review comments on role-config docs
- Update `allow_edit` field description to reflect conditional default:
`true` when table perm is `edit`, `false` for `read_only` or explicit restriction
- Move `record_operations.delete` out of "默认关闭项" into new "默认开启项(条件性)"
section to accurately reflect it is default-included when `perm = edit`
- Add `view_rule.allow_edit` to "默认开启项(条件性)" section with same logic
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat(sidecar): add sidecar proxy for sandbox credential isolation
Keep real secrets (app_secret, access_token) out of sandbox environments.
CLI instances inside sandboxes connect to a trusted sidecar process via
HTTP; the sidecar verifies HMAC-signed requests and injects real tokens
before forwarding to the Lark API.
Key components:
- `auth proxy` subcommand to start the sidecar server (build tag: authsidecar)
- Noop credential provider returns sentinel tokens in sidecar mode
- Transport interceptor rewrites requests to sidecar with HMAC signature
- Env provider yields to sidecar provider when AUTH_PROXY is set
- Supports both feishu and lark brand endpoints
* feat(sidecar): implement priority ordering for credential providers
* feat(sidecar): strip client-supplied auth headers and improve shutdown logging
* feat(sidecar): buffer request body to prevent HMAC mismatches on read errors
* feat(sidecar): fix CI
* refactor(sidecar): publish protocol package and move server to reference demo
The sidecar server is no longer shipped as a `lark-cli auth proxy`
subcommand. Instead, the CLI provides only the standard sidecar *client*
(via `-tags authsidecar`), while the wire-protocol utilities are exposed
as a public package for integrators to implement their own server.
Changes:
- Move `internal/sidecar/` → `sidecar/` so external integrators can
import HMAC signing, headers, sentinels and address validators.
- Remove `cmd/auth/proxy.go`, `proxy_stub.go`, `proxy_test.go` and the
conditional registration in `cmd/auth/auth.go`.
- Add `sidecar/server-demo/` — a reference server implementation behind
the `authsidecar_demo` build tag. It reuses the lark-cli credential
pipeline for local development; production integrators are expected
to replace the credential layer with their own secrets source.
- Update all internal imports from `internal/sidecar` to `sidecar`.
Rationale:
- Each integrator has different secrets management / HA / multi-tenant
requirements, so a one-size-fits-all server doesn't belong in the
shipped CLI.
- Keeping the client in-tree guarantees all sandbox-side code stays
protocol-compatible without a second repo to sync.
- The public `sidecar/` package pins the wire protocol as a stable
contract third-party servers must conform to.
Build matrix after this change:
- `go build` → standard CLI, no sidecar code
- `go build -tags authsidecar` → CLI + sidecar client
- `go build -tags authsidecar_demo \
./sidecar/server-demo/` → reference server binary
No production users are affected today because the server was not yet
released; existing sidecar-client users are unchanged.
* feat(sidecar): close 5 pre-release security gaps
- Server: enforce https-only target (no path/query/userinfo), pin
forwardURL to https:// — blocks cleartext token leak
- Protocol v1: canonical now covers version/identity/auth-header,
blocks identity-flip replay within drift window
- Client: ValidateProxyAddr requires loopback or same-host alias,
rejects userinfo and https (interceptor is http-only); cross-machine
is out of scope
- Build: non-authsidecar builds exit(2) when AUTH_PROXY is set,
preventing silent fallback to env credentials
- Demo: whitelist auth-header to Authorization / X-Lark-MCP-{UAT,TAT},
blocks token injection into Cookie / UA / X-Forwarded-For exfil paths
/style and /styles_batch_update require full "A1:A1" form and reject
single-cell shorthand "A1". +set-style was using normalizeSheetRange
(prefix-only) and +batch-set-style passed --data through unchanged,
so both failed with `wrong range` when callers supplied a single cell.
Switch +set-style to normalizePointRange, and walk each ranges[]
entry in +batch-set-style through normalizePointRange before sending.
Multi-cell spans pass through unchanged.
Implement +create-float-image, +update-float-image, +get-float-image,
+list-float-images, and +delete-float-image shortcuts wrapping the v3
spreadsheet float_image API. The create reference doc includes the
prerequisite media upload step with the correct parent_type
(sheet_image) to avoid common token mismatch errors.
The POST /contact/v3/users/basic_batch endpoint caps user_ids at 1~10
per request, but batchResolveByBasicContact was chunking by 50. When
user identity needed to resolve >10 unresolved sender names, the
single oversized request was rejected, causing the batch resolver to
bail out and leave sender names empty for the rest.
Lower batchSize to 10 and add a unit test that exercises 25 missing
IDs and asserts they are sent as 10 / 10 / 5.
* feat(mail): add email priority support for compose and read
Write: all compose shortcuts (+send, +reply, +reply-all, +forward,
+draft-create) accept --priority (high/normal/low) which sets the
X-Cli-Priority EML header. +draft-edit accepts --set-priority.
Read: normalizeMessage now infers priority from label_ids
(HIGH_PRIORITY/LOW_PRIORITY), with priority_type as fallback.
Change-Id: Ib5bc4e99331c6ce0d3850865825fcd1ff2183f0c
Co-Authored-By: AI
* docs(mail): add --priority and --set-priority to skill references
Update 6 skill reference docs: +send, +reply, +reply-all, +forward,
+draft-create add --priority param; +draft-edit adds --set-priority.
Change-Id: I75d13fbf6a5ca4dfbf76e84fe39e4ee55b689751
Co-Authored-By: AI
* test(mail): add unit and integration tests for --priority
- helpers_test.go: cover parsePriority (valid/invalid/case/whitespace)
and applyPriority (empty vs non-empty) end-to-end via EML builder
- mail_draft_create_test.go: verify --priority propagates to X-Cli-Priority
header in the built EML, and no header when priority is empty
Change-Id: I62ca96b3e296b5898798cfa681f5efd4f101cb40
Co-Authored-By: AI
* test(mail): cover buildDraftEditPatch --set-priority and label-based priority
- helpers_test.go: TestBuildMessageOutput_PriorityFromLabels verifies
HIGH_PRIORITY/LOW_PRIORITY labels map to priority_type_text, and that
label values override the priority_type fallback field
- mail_draft_edit_test.go (new): cover --set-priority high/low/normal
(set_header vs remove_header), invalid value rejection, and absence
of priority op when the flag is unused
Change-Id: Idd5ace2fb812cf3eb329c79eeab3c8b9808fcf0b
Co-Authored-By: AI
* fix(mail): write priority_type to output when inferred from label_ids
buildMessageOutput only wrote priority_type_text but not priority_type
when priority was inferred from HIGH_PRIORITY/LOW_PRIORITY labels.
Also covers the case where label overrides an explicit priority_type field.
Change-Id: I7879976d21235b8006b5c8ebe6a413e2815354e1
* fix(mail): validate --priority in Validate so invalid values fail before dry-run/Execute
Change-Id: Ic277ab683967c47f28c892d3512b0ab745bd86f6
* test(mail): add TestValidatePriorityFlag to cover invalid --priority rejected in Validate
Change-Id: I7f12c0a0b0d15c491c28fdcb8729f2f648ba0244
Extend +add-comment to accept sheet URLs and wiki URLs that resolve
to sheets. Reuse --block-id with <sheetId>!<cell> format (e.g.
a281f9!D6) for sheet cell positioning.
Wiki links resolving to sheet type are handled by first calling
get_node, then redirecting to the sheet comment path with proper
parameter validation.
* feat(doc): add --file-view flag to +media-insert for file block rendering
The docx File block supports three render modes via view_type
(1=card, 2=preview inline player, 3=inline), but --type=file today
always creates with the default card view. Because view_type can only
be set at creation time (PATCH replace_file ignores it), callers
wanting an inline audio/video player had to abandon the shortcut and
reimplement the full 4-step orchestration manually.
Add --file-view card|preview|inline that threads into file.view_type
on block creation. Omitting the flag preserves the exact request body
that the shortcut sends today, so existing users are unaffected.
--file-view is rejected when combined with --type=image (images have
their own rendering) and when an unknown value is passed.
* refactor(doc): narrow view_type gate and relax file-view test
Address review feedback from automated reviewers on #419:
- Replace `fileViewType > 0` with an explicit 1|2|3 whitelist inside
buildCreateBlockData so a stray positive int cannot escape into the
request payload if a future caller bypasses Validate.
- Relax TestFileViewMapCoversDocumentedValues to assert only the
documented keys rather than full-map equality, so future aliases
(e.g. a "player" synonym for preview) do not falsely break the test.
No behaviour change for any existing --file-view input.
* test(doc): cover --file-view Validate contract and explicit card path
Pins down the two CLI guard branches (unknown --file-view value and
--file-view passed with --type!=file) that were previously only covered
indirectly through buildCreateBlockData. Also adds the --file-view card
case so the explicit view_type=1 payload (different from the legacy
file: {} shape when the flag is omitted) is locked in as part of the
public flag contract.
* fix: repair unit tests
Change-Id: I8c6bb69bfa22c9455a2cbb0f46b401e2cbe87762
---------
Co-authored-by: Nick Zhang <nickzhangcomes@users.noreply.github.com>
Co-authored-by: wangweiming <wangweiming@bytedance.com>
- Reorder sections, fix formatting and indentation in SKILL.md
- Add spaces.create method and its scope to API resources and permission table
- Add wiki domain template for skill-template
Change-Id: Ib03dacc02cf2b42f807615c2adedbf79694b5dc0
* feat(base): auto grant current user for bot create and copy
* fix(base): declare auto-grant permission scope
* Apply suggestion from @kongenpei
Co-authored-by: kongenpei <kongenpei.jojo@bytedance.com>
* Apply suggestion from @kongenpei
Co-authored-by: kongenpei <kongenpei.jojo@bytedance.com>
* style(base): format auth-specific scope declarations
* fix(base): use bitable permission target for auto-grant
---------
Co-authored-by: kongenpei <kongenpei.jojo@bytedance.com>
* feat(base): add identity priority strategy and 91403 error handling
Establish user-first identity selection with graceful degradation to bot,
and add no-retry rule for error code 91403 (permission denied on Base).
* fix(base): add 91403 early-exit before identity fallback logic
Move non-retryable error code check (e.g. 91403) to a dedicated step
before the user/bot fallback decision, resolving conflicting instructions
between the error table and the execution rules.
* Update SKILL.md
* Update SKILL.md
---------
* feat(auth): improve login scope handling and messages
- Add AuthorizedUser message to display current authorized account
- Update scope mismatch message wording to be more accurate
- Reorganize login success output to show scope issues first
- Remove redundant success message when scope issues exist
* fix(auth): update login success message wording from "login" to "authorization"
Update both Chinese and English login success messages to use "authorization" instead of "login" for consistency with the authentication flow. Also update corresponding test cases to match the new wording.
* test(auth): update login test for missing scope case
Update test assertions to verify correct error messages when requested scopes are not granted. Remove checks for success message in this scenario.
mediaBuffer.FileName() returned a hardcoded "media"+ext, so IM file
messages sent via URL displayed generic names like "media.pdf" instead
of the filename parsed from the URL. This regressed the pre-refactor
tempfile path which at least carried a unique basename.
Store fileNameFromURL(rawURL) on the buffer and return it from
FileName(). Split newMediaBuffer so the URL-to-filename wiring is
reachable from tests without going through the hardened download
transport.
Also lock in that the local upload branch keeps filepath.Base(filePath)
as file_name, so the URL fix cannot silently regress the local branch
later.
Change-Id: I729b217e9dc9237aeb89c2b89df86a37ad64a840
The /open-apis/im/v1/images and /open-apis/im/v1/files APIs now support User Access Token (UAT) in addition to Tenant Access Token (TAT). Previously the upload helpers forced bot identity unconditionally; this PR aligns them with the surrounding shortcut's --as flag so uploads and sends share the same identity.
Change-Id: I3d7fd528dd30fef9aea2d88100ceb03db4c7c3ac
This release prep captures the version bump and changelog entry for v1.0.12 without pulling unrelated workspace edits into the release branch.
Change-Id: Ib343337c4851b7cc15a52dd0068795a92092b781
Constraint: Keep the release PR scoped to package version and changelog only
Rejected: Include .gitignore and local workspace files | unrelated to this release PR
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep release notes aligned with shipped changes only; exclude reverted work from summaries
Tested: make unit-test
Tested: go mod tidy
Tested: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev=origin/main
Not-tested: Manual tag/release publishing flow
* feat(mail): add signature foundation, draft exports, and +signature shortcut
- Add signature data model, API provider, and template variable
interpolation with tests (shortcuts/mail/signature/)
- Export signature-related symbols from draft package (SignatureWrapperClass,
BuildSignatureHTML, FindMatchingCloseDiv, SplitAtQuote, RemoveSignatureHTML,
SignatureSpacing, SignatureImage) for use by compose shortcuts
- Add +signature shortcut for listing and viewing email signatures
- Add signature reference documentation
Change-Id: I62525e7b475692ada9ec8590b6d0252cf5afcdbc
Co-Authored-By: AI
* feat(mail): add --signature-id to all compose shortcuts
- Add --signature-id flag to +draft-create, +send, +reply, +reply-all,
+forward for inserting a signature into the email body
- Add signature image download with SSRF protection (https enforcement,
no token leak, context timeout, size limit)
- Add signature HTML insertion with quote-aware placement
- Update compose shortcut reference docs
Change-Id: Ic5606bab7826a20364084898ad1714778e5a8bd0
Co-Authored-By: AI
* feat(mail): add signature insert/remove ops for +draft-edit
- Add insert_signature and remove_signature patch operations with
old-signature MIME cleanup and case-insensitive CID matching
- Expose signature ops in supported_ops flat list
- Update SKILL.md and draft-edit reference docs
Change-Id: I74affbf555e32351520f610ef42195f399a265d9
Co-Authored-By: AI
* test(mail): add unit tests for signature patch operations
Test insert_signature and remove_signature ops:
- Insert into basic HTML body
- Insert before quote block (reply/forward)
- Replace existing signature
- Error on plain-text-only draft
- Remove existing signature
- Error when no signature present
Change-Id: Icd713552b130d6eb461ef1cabca61e82327f4f0b
Co-Authored-By: AI
* fix(mail): address reviewer findings on signature PR
- Remove --device flag and device field from docs (not exposed in CLI)
- Fix signature interpolation to match --from alias address in send_as
list, instead of always using the primary mailbox address
- Update lark-mail-signature.md reference doc
Change-Id: I65f41a029cd33b17785e2355a99d042063962d23
Co-Authored-By: AI
* fix(mail): resolve lint issues — remove unused code, fix gofmt
- Remove unused cidSrcRe, collectSignatureCIDs, isCIDReferencedInHTML
from signature_html.go (CID logic lives in draft/patch.go)
- Remove unused strings import
- Run gofmt on all affected files
Change-Id: Ie142744a7ab17acf440dc69a5a78cefb3ce6c341
Co-Authored-By: AI
* fix(mail): use draft From address for signature interpolation in +draft-edit
Moved signature resolution after draft fetch+parse so insert_signature
reads the From header from the existing draft. This ensures alias and
shared-mailbox senders get correct template variable values (B-NAME,
B-ENTERPRISE-EMAIL) instead of falling back to the primary address.
Change-Id: I917016b17176090124814f30e8e15c67f1604de0
Co-Authored-By: AI
* feat(mail): add contact search workflow and multi_entity search API
- Add recipient search workflow to mail skill template (search by name,
email keyword, or group name with rich result display)
- Regenerate SKILL.md with multi_entity.search command
Change-Id: Ie307af16a5ee38dac99c1d8d0df528730bf847d0
Co-Authored-By: AI
* fix: require user confirmation for all contact search results
multi_entity.search is a fuzzy keyword search — a single result does
not guarantee an exact match (e.g. searching "张三" may only return
"张三丰"). Always show candidates for user confirmation before using
the email address in compose parameters.
Change-Id: I447c54cd59b06a88c5d6806bfe76f0adfdceb1ce
Co-Authored-By: AI
- Add buildSendResult helper that includes recall_available/recall_tip
when backend returns recall_status in send response
- Update +send, +reply, +reply-all, +forward to use buildSendResult
- Add "Recall Email" section to mail skill template with recall and
get_recall_detail command examples
- Regenerate SKILL.md
Change-Id: I44317ead8f8a65db81e874cfc3529ffeb21e1384
Co-Authored-By: AI
- New `slides +media-upload` shortcut: upload a local image to a slides
presentation and return the file_token for use in <img src="...">.
- `slides +create --slides` now supports `@./path.png` placeholders that
are auto-uploaded and replaced with file_tokens.
- Reject images >20 MB (multipart upload not supported for slide_file).
- Support wiki URL resolution for --presentation flag.
Explicitly mention historical dates in the description of lark-vc skill to improve query matching for past meetings.
Change-Id: I796382793bb5d910924fac450e5315645ce543d4
Update the package version and changelog entry so the release branch matches the v1.0.11 changes already queued after v1.0.10.
This keeps the published package version and human-readable release notes aligned without pulling unrelated local workspace changes into the release PR.
Change-Id: Ia937651001e0057df4fe82bd11705c52d343f9a9
Implement +set-dropdown, +update-dropdown, +get-dropdown, and
+delete-dropdown shortcuts wrapping the v2 dataValidation API.
This resolves the issue where multipleValue writes silently
became plain text because the prerequisite dropdown configuration
step was not exposed as a CLI command.
Also add lark-sheets-formula.md reference for Lark-specific formula
rules (ARRAYFORMULA, native array functions, date diff, etc.) and
update the dropdown limitation note in SKILL.md to link to the new
+set-dropdown shortcut.
The secondary confirmation step in the interactive login process has been removed (Phase 2: After the user selects the complete domain name, permission level, and scope, they no longer need to confirm "authorize" again and can directly proceed to the authorization process).
* docs(readme): add lark-attendance to Agent Skills table and update counts
- Add lark-attendance to Agent Skills table in both EN and ZH READMEs
- Add Attendance to ZH Features table
- Update skill count 21 → 22 and domain count 13 → 14
Document the correct object format for writing formulas, URLs with
text, mentions, and dropdown lists via --values parameter. Add
examples contrasting correct object format vs incorrect plain string.
Add range download support for IM OAPI resources so lark-cli can reliably download large files. This improves stability for large payloads and network interruptions.
Change-Id: I38e6f6f9cf8b8711dc40650d19c77503f4e44989
The interactive `config init` flow showed a QR code and verification
link without indicating their relationship, leaving users unsure
which to act on first and whether the link was still needed after
scanning.
Split the message strings on TTY vs non-TTY:
- TTY: header above QR ("使用飞书 / Lark 扫码配置应用"), "或打开链接"
framing to mark the link as an alternative, and an active waiting
indicator.
- Non-TTY (AI / piped callers via --new): keep the original copy
verbatim so existing parsers and prompts are unaffected.
QR is still rendered in both branches.
Change-Id: I9b753f044ebefaedbb4b095cabf7beff4669eb2e
The chat_p2p/batch_query endpoint that resolves a user's p2p chat_id
requires user identity. Calling +chat-messages-list with --user-id
under bot identity previously failed silently or returned wrong
results.
- Validate: reject --user-id when runtime.IsBot(), with a hint to
pass --as user or use --chat-id instead
- resolveP2PChatID: add defensive guard for the same condition in
case the helper is reached via another path
- Update --user-id flag description and the lark-im skill reference
to note the user-identity requirement
- Tests: add bot-rejection cases for Validate and resolveP2PChatID,
switch p2p happy-path tests to a user-identity runtime helper
* fix(mail): add missing event scope for mail watch
The mail +watch shortcut requires scope
mail:user_mailbox.event.mail_address:read to receive the mail_address
field in WebSocket event payloads, but this scope was neither declared
in the shortcut's Scopes list nor included in the auto-approve
(recommend.allow) set.
Without this scope, +watch events arrive without the mail_address field,
which breaks mailbox filtering and fetch-mailbox resolution.
- Add scope to mail +watch Scopes declaration
- Add scope to scope_overrides.json recommend.allow list so that
auth login --recommend requests it automatically
* fix(mail): add missing mailbox profile scope for mail watch
The +watch shortcut calls fetchMailboxPrimaryEmail (GET
user_mailboxes/me/profile) to resolve the mailbox address for event
filtering, which requires scope mail:user_mailbox:readonly. All other
mail shortcuts that call this API (send, reply, forward, draft-create,
draft-edit) already declare this scope, but +watch did not.
* fix(mail): remove event scope from scope_overrides.json
The mail:user_mailbox.event.mail_address:read scope only needs to be
declared in the +watch shortcut's Scopes list, not in the global
recommend.allow set.
* fix(mail): restrict --output-dir to current working directory
Previously, mail +watch --output-dir accepted absolute paths (e.g.
/etc, /tmp) and home directory paths (~/), allowing writes to arbitrary
locations. Since mail content is sender-controlled, this posed a risk
of writing attacker-influenced data to sensitive system directories.
Now all --output-dir values go through validate.SafeOutputPath which:
- Rejects absolute paths and ~ expansion
- Resolves .. and symlinks
- Enforces the result stays under CWD
* fix(mail): reject tilde paths in --output-dir explicitly
SafeOutputPath treats ~/x as a literal relative path, silently creating
a directory named "~" under CWD. Reject ~ prefixed paths with a clear
error message instead.
* fix(mail): reject all tilde-prefixed paths and use ErrValidation
- Broaden ~ check from "~ || ~/" to "~" prefix, covering ~user/path forms
- Use output.ErrValidation for consistent error type (exit code 2)
* fix(mail): add post-mkdir EvalSymlinks + CWD re-verification (TOCTOU)
SafeOutputPath validates before MkdirAll, but an attacker could replace
the newly created directory with a symlink between mkdir and the first
write. Add EvalSymlinks after MkdirAll and re-verify the resolved path
is still under CWD.
Also broaden ~ rejection to all tilde-prefixed paths (~user/path) and
use output.ErrValidation for consistent error types.
* fix(mail): use validate.SafeOutputPath for post-mkdir TOCTOU check
Replace direct os.Getwd and filepath.EvalSymlinks calls with a second
SafeOutputPath call after MkdirAll. This satisfies the forbidigo lint
rule (no direct os/filepath calls in shortcuts/) while maintaining the
same TOCTOU protection.
* fix(mail): use original relative path for post-mkdir re-validation
SafeOutputPath rejects absolute paths, but after the first call
outputDir was already resolved to an absolute path. Pass the original
relative path to the second SafeOutputPath call so it can properly
re-validate after MkdirAll.
* fix(mail): remove redundant post-mkdir SafeOutputPath call
The second SafeOutputPath call after MkdirAll provided no real TOCTOU
protection: mail +watch is long-running, so the directory could be
replaced at any point during the session, not just between mkdir and
the check. The first SafeOutputPath already validates and resolves
the path; one call is sufficient.
* docs(task): document sections API resources and add URL parsing reminder
* feat(task): support --section-guid flag in tasklist-task-add shortcut
* docs(task): document sections API resources, permissions, and URL parsing
After creating the presentation, call drive batch_query (with_url=true)
to fetch the document URL and include it in the output. The fetch is
best-effort so it won't break creation if the API call fails.
Also update the skill reference doc to document the new optional url
return field.
Add 5 new sheet shortcuts for row/column management:
- +add-dimension: append rows/columns at the end
- +insert-dimension: insert rows/columns at a position
- +update-dimension: update visibility and size
- +move-dimension: move rows/columns to a new position
- +delete-dimension: delete rows/columns
Includes unit tests (89-100% coverage) and skill reference docs.
Add BotInfo() method on RuntimeContext that lazily fetches the current
app's bot open_id and display name from /bot/v3/info on first call,
cached via sync.OnceValues for the lifetime of the process.
- BotInfo struct (OpenID, AppName) in Identity section of runner.go
- fetchBotInfo() uses DoAPIAsBot for consistent header injection
- CanBot() on CliConfig gates the call when bot identity is unavailable
- Nil guard prevents panic in test contexts
- Full test coverage via httpmock.Registry + mounted shortcuts
Change-Id: I40ac710fb52d13939853f71827a5cbdbddd4f80f
* docs(base): document Base attachment download via docs +media-download
Base attachment files must be downloaded via 'lark-cli docs +media-download',
not 'lark-cli drive +download' (which returns HTTP 403). The existing
lark-doc reference already documents the command thoroughly, so this PR
just adds entries to the lark-base skill that reference it.
- SKILL.md: add download row to field classification, routing, and record
commands tables, referencing lark-doc-media-download.md
- references/lark-base-record.md: add download entry to the command
navigation table and notes, referencing lark-doc-media-download.md
* docs: add output flag to base attachment download examples
---------
Co-authored-by: kongenpei <kongenpei@users.noreply.github.com>
- Add `+dashboard-arrange` command that triggers server-side smart layout optimization via POST /open-apis/base/v3/bases/{token}/dashboards/{id}/arrange
- Add `text` block type support for dashboard blocks with Markdown syntax (headers, bold, italic, strikethrough, lists)
- Update `validateBlockDataConfig()` to handle text-specific validation rules
- Update documentation (SKILL.md, lark-base-dashboard.md, dashboard-block-data-config.md, lark-base-dashboard-arrange.md)
- Add comprehensive unit tests for new commands and block type
- [x] Unit tests pass (`go test ./shortcuts/base/...`)
- [x] All dashboard-related tests pass including new `TestBaseDashboardExecuteArrange`
- [x] Text block type validation tests pass
- None
* feat(cmdutil): add shared file upload helpers
Add ParseFileFlag, ValidateFileFlag, and BuildFormdata to support
multipart file upload via --file flag across raw API and meta API commands.
Change-Id: Ib724cf8b055b0b314af11d8d830f38559dac60eb
* feat(api): add --file flag for multipart/form-data file uploads
Add --file flag to `lark-cli api` command enabling file upload via
multipart/form-data. The flag accepts [field=]path format and supports
stdin (-). Includes mutual exclusion validation with --output,
--page-all, and GET method. Dry-run mode shows file metadata instead
of building actual formdata.
Change-Id: Icf34aba5da3a558219a97a583e8f6aa951ded199
* feat(service): add --file flag with auto-detection from metadata
Add file upload support to meta API service method commands. The --file
flag is conditionally registered only for methods whose metadata declares
file-type fields (POST/PUT/PATCH/DELETE). The default field name is
auto-detected from metadata when exactly one file field exists.
Change-Id: Ibbf04eb42341ba11bb1fd9750e63bc1d0eacd08d
* feat(schema): show file upload indicators in method detail display
Add hasFileFields helper to detect file-type fields in requestBody
metadata. Modify printMethodDetail to display [file upload] tag on
--data line, --file flag description with default field name, and
--file <path> in CLI example for methods that accept file uploads.
Change-Id: Iae3bc14fe07e16a8b5f6a50a2b3592d6d8490ed9
* fix: address code review findings for file upload feature
- ParseFileFlag: change idx >= 0 to idx > 0 to prevent empty field name
when input like "=photo.jpg" is passed
- BuildFormdata: read file into bytes.Reader with defer Close to prevent
file handle leak on later errors
- BuildFormdata: remove unused ctx parameter from signature and callers
- Eliminate duplicated dry-run logic by having buildAPIRequest and
buildServiceRequest return FileUploadMeta when in dry-run mode,
removing ~60 lines of copy-pasted URL building and validation code
Change-Id: I27b9534fd0eaefce40390f6e723dd0c04a2cdf80
* fix: address PR review findings
- Remove opts.File=="" guard on dual-stdin check so --file photo.jpg
--params - --data - correctly reports an error instead of silently
dropping --data content (P1 bug in both api.go and service.go)
- Extract shared DetectFileFields into cmdutil, deduplicate
detectFileFields (service.go) and hasFileFields (schema.go)
- Show "<stdin>" instead of empty path in dry-run output for --file -
Change-Id: Iccc5d879165ea6a3d04f0425ec6a5018a10e72e1
* fix: reject non-object --data with --file and improve multi-file schema
- --data with --file now requires a JSON object; arrays/strings/numbers
are rejected with a clear error instead of being silently dropped
- Schema display for multi-file methods shows explicit field=path syntax
and lists valid field names instead of advertising a false default
Change-Id: I0facdb3ad86f68cb125c7ea109a33714fd91dba0
* feat(base): add record batch add/set shortcuts
* docs: clarify record batch add/set input guidance
* docs: mark base shortcut references as required before calling
* fix(base): remove stale token stub calls in batch record tests
* feat(base): rename record batch add/set to create/update
* refactor(base): remove noop record json validators
* test(base): align record validate test with nil hooks
* fix: align base record batch shortcuts with openapi routes
* fix(base): pass parse context for record batch JSON parsing
* docs: move base record batch JSON guidance to tips
* refactor: remove noop record validate
* docs: remove has_more from batch update guide
---------
Co-authored-by: kongenpei <kongenpei@users.noreply.github.com>
* feat(base): add +record-search json passthrough shortcut
* docs(base): refine record-search wording and field constraints
* docs(base): prefer record-list unless keyword is explicit
* refactor(base): inline record-search parsing and align tests
* refactor(base): remove noop record validate hook
* docs(base): unify record example token placeholders
* fix: align record search JSON parsing with parse context
* feat: add help tips for base record search
* docs: refine base record search reference
---------
Co-authored-by: kongenpei <kongenpei@users.noreply.github.com>
* feat(base): add record field filters
* fix(base): align record field filter flags with OpenAPI params
* fix: scope record dry-run field filters and align docs
* docs(base): clarify record-list field_scope priority
* refactor(base): remove field-id from record-get
---------
Co-authored-by: zgz2048 <zhonggangzhi.tim@bytedance.com>
Co-authored-by: kongenpei <kongenpei@users.noreply.github.com>
* fix(keychain): improve error hint for keychain initialization
Clarify the error message for uninitialized keychain by combining both possible scenarios (sandbox/CI environment and normal usage) into a single hint to avoid confusion.
* docs(keychain): improve error message hints for sandbox environments
Add suggestion to try running outside sandbox when keychain access fails. Also update hint for uninitialized keychain case to include same suggestion.
* docs(keychain): fix grammar in error message hints
* docs(keychain): fix typo in error message hint
- Add `+dashboard-arrange` command that triggers server-side smart layout optimization via POST /open-apis/base/v3/bases/{token}/dashboards/{id}/arrange
- Add `text` block type support for dashboard blocks with Markdown syntax (headers, bold, italic, strikethrough, lists)
- Update `validateBlockDataConfig()` to handle text-specific validation rules
- Update documentation (SKILL.md, lark-base-dashboard.md, dashboard-block-data-config.md, lark-base-dashboard-arrange.md)
- Add comprehensive unit tests for new commands and block type
- [x] Unit tests pass (`go test ./shortcuts/base/...`)
- [x] All dashboard-related tests pass including new `TestBaseDashboardExecuteArrange`
- [x] Text block type validation tests pass
- None
Add cmd/diagnose_scope_test.go that exports a JSON snapshot of all API
methods and shortcuts with their minimum-privilege scopes, identity
support, auto-approve status, and scope_priorities coverage. Consumed
by scripts/scope_audit.py for diff and reporting.
* fix(mail): replace os.Exit with graceful shutdown in mail watch
The signal handler in mail +watch called os.Exit(0), which bypassed all
deferred cleanup functions, made the code path untestable, and did not
follow Go's idiomatic context cancellation pattern.
Key changes:
- Remove os.Exit(0) and use context.WithCancel to propagate shutdown
- Run cli.Start in a separate goroutine so the main goroutine can return
immediately on signal receipt (the Lark WebSocket SDK does not return
promptly after context cancellation)
- Extract handleMailWatchSignal as a testable standalone function
- Use sync.Once + defer for idempotent cleanup on all exit paths
- Fix eventCount data race with atomic.Int64
- Add signal.Reset to support forced termination via a second Ctrl+C
Closes#268
* docs: add docstrings to handleMailWatchSignal test functions
* fix(mail): cancel watch context on signal handler panic
If handleMailWatchSignal panics, the recover block now calls
cancelWatch() to unblock the main select. Without this, a panic
would leave shutdownBySignal unclosed and watchCtx uncancelled,
causing the process to hang.
* fix(mail): use triggerShutdown to unblock main select on signal handler panic
The previous panic recovery only called cancelWatch(), but since the
WebSocket SDK does not return promptly after context cancellation,
the main select could still hang waiting on startErrCh.
Introduce triggerShutdown() that closes shutdownBySignal (via
sync.Once) and cancels the watch context, used by both the normal
signal path and the panic recovery path. This ensures the main
select unblocks immediately regardless of how the signal goroutine
exits.
Add regression test that forces a panic and asserts shutdownBySignal
is closed promptly.
* feat(mail): add --page-token and --page-size pagination support to mail +triage
Support external pagination for mail +triage with two new flags:
- --page-token: resume from a previous response's page token
- --page-size: alias for --max
Token carries a "search:" or "list:" prefix to identify the API path,
with strict validation: conflicting parameters (e.g. list: token with
--query) fail fast, and bare tokens without prefix are rejected.
JSON/data output now returns an object with messages, total, has_more,
and page_token fields. Table output shows next-page hint on stderr.
* fix(mail): address PR review — keep data format as array, fix whitespace query edge case
- --format data preserves backward-compatible flat array output
- --format json returns the new envelope object with pagination fields
- Align search: prefix guard with TrimSpace(query) to match usesTriageSearchPath
* fix(mail): simplify page-token format and fix page-size change data loss
- Remove page_size encoding from token (search:abc → not search:5:abc)
The search API token is a session cursor; page_size only controls how
many items to return, not the cursor position. Encoding page_size
caused data loss when users changed --page-size between requests.
- Token format is now simply "search:<raw>" / "list:<raw>"
- Add parseTriagePageToken/encodeTriagePageToken helpers for clean
token handling with proper validation
- next page hint in table output now includes --query and --filter
for easy copy-paste continuation
* docs(mail): update triage skill doc for json/data format split and search pagination note
- Separate --format json (object with pagination) and --format data (array) examples
- Update table next-page hint example to show --query/--filter inclusion
- Add search pagination caveat about cross-session result ordering
* fix(mail): make --format data include pagination fields same as json
* fix(mail): address remaining PR review comments
- Reject empty prefixed tokens (search: / list:) in parseTriagePageToken
- Shell-escape query/filter in next-page hint to handle single quotes
- Fix doc caption mismatch (data → json/data) and add language tag to code block
- Fix test comment for TestResolveTriagePageSizeDefaultMax
* fix(mail): rename total to count in triage pagination output
total was misleading — it represented the current page count, not the
global total. Renamed to count to match len(messages) semantics.
* fix(mail): improve dry-run desc when using --page-token
* fix(base): improve --json help examples and group guide
* fix(base): unify --json help tips format
* docs(base): fix view-set-group schema with group_config
* fix(base): remove array wording from view-set-group json help
---------
Co-authored-by: kongenpei <kongenpei@users.noreply.github.com>
* fix(api): add stdin and single-quote support for --params/--data on Windows (#64)
Windows PowerShell 5.x mangles JSON double-quotes when passing arguments
to native executables, causing --params and --data to fail with
"invalid JSON format". This commit adds two mitigations at the framework
level:
- stdin piping: `echo '{"k":"v"}' | lark-cli --params -` bypasses
shell argument parsing entirely and works on all platforms/shells.
- single-quote stripping: cmd.exe passes literal single quotes which
are now transparently removed before JSON parsing.
Implementation:
- New `cmdutil.ResolveInput(raw, stdin)` handles `-` (stdin), strip
surrounding `'...'`, and plain passthrough.
- `ParseJSONMap` and `ParseOptionalBody` now accept an `io.Reader` and
delegate to `ResolveInput` before JSON unmarshalling.
- `cmd/api` and `cmd/service` pass `IOStreams.In` and guard against
simultaneous stdin usage by --params and --data.
- Empty stdin is rejected with a clear error message.
Closes#64
Change-Id: If21e735d0aed5c6a2d6674c1e6c898186fca3aba
* test: add stdin e2e regression coverage
Change-Id: I4e00bf1c6b6f3259f503e3414cae10fa4b34ba75
New capabilities:
1. Alias (send_as) sending for all compose shortcuts (+send, +reply, +reply-all,
+forward, +draft-create, +draft-edit):
- New --mailbox flag separates mailbox routing from sender identity, enabling
alias sending where --mailbox specifies the owning mailbox and --from
specifies the alias address in the From header.
- Example: --mailbox me --from alias@example.com --to bob@example.com
- --mailbox priority: --mailbox > --from > "me"
- --from priority: --from > --mailbox > profile("me")
2. Discovery APIs for available mailboxes and sender addresses:
- accessible_mailboxes: lists all mailboxes the user can access (primary + shared)
- send_as: lists available sender addresses for a mailbox (primary, aliases, mailing lists)
3. Mail rules API:
- user_mailbox.rules resource: create, delete, list, reorder, update
4. Reply-all self-exclusion improvement:
fetchSelfEmailSet now also excludes the --from alias address, preventing the
sender from appearing in the recipient list when replying via an alias.
No breaking changes — omitting --mailbox preserves existing behavior.
When config.json is hand-edited, the appId field can become out of sync
with the appSecret keychain reference (e.g. appId changed but
appSecret.id still points to the old app). This causes silent auth
failures at API call time. Add a pre-flight check in
ResolveConfigFromMulti that compares the two before any keychain lookup
or OAPI request, failing fast with actionable guidance.
Change-Id: I74b9ab640642dde3df1ad70890b93b91ee422022
- Add depguard linter to block shortcuts/ from importing internal/vfs
directly (must use runtime.FileIO() instead)
- Add forbidigo rules for os.* filesystem ops, IO streams, os.Exit,
and filepath.* functions that bypass vfs
- Split os.Remove / os.RemoveAll into separate patterns with accurate
guidance (RemoveAll not yet in vfs)
- Use compact regex groups for maintainability, no duplicate or
shadowed patterns
Change-Id: I9e45ab07ca58a61b86bdcea9f1f2cc6181c974bc
* feat(auth): improve scope handling and output in login flow
- Add scope validation to check for missing requested scopes
- Implement detailed scope breakdown in login success output
- Add new message strings for scope-related output
- Refactor login success output to handle both JSON and text formats
- Add tests for scope validation and output scenarios
* feat(auth): add requested scope caching for device code login
Implement caching of requested scopes during device code login flow to ensure proper scope validation after authorization. The cache is stored in JSON files under config directory and automatically cleaned up after successful or failed authorization.
Add tests for scope caching functionality and verify proper integration with existing login flow.
* docs(auth): add function comments for login scope handling
Add detailed doc comments to all functions in login scope cache and result handling files to improve code documentation and maintainability.
* refactor(auth): remove pending scopes and improve json output stability
- Remove PendingScopes field and related logic as it's no longer needed
- Add emptyIfNil helper to ensure nil slices are normalized to empty slices in JSON output
- Update tests to verify JSON output stability and fix expected text outputs
* refactor(auth): extract device token polling function for testability
Move device token polling to a package-level variable to enable mocking in tests
Add test case for scope cleanup when token is nil
* fix(auth): return JSON write errors instead of ignoring them
Previously, JSON write errors were only logged to stderr but not returned, causing tests to pass when they should fail. Now properly propagate these errors to callers and update tests to verify error handling.
* refactor(auth): simplify scope handling and improve user messaging
remove redundant scope display and consolidate hint messages to focus on actionable guidance
* refactor(auth): improve scope handling and messaging in login flow
remove ShortHint field and simplify scope hint messages
always display missing scopes section with consistent formatting
add StatusHint for successful login with no missing scopes
update tests to reflect new message structure and content
* refactor: migrate vc/minutes shortcuts to FileIO
- vc_notes: replace vfs.Stat + validate.SafeOutputPath + validate.AtomicWrite
with FileIO.Stat/Save for transcript download
- minutes_download: replace validate.SafeOutputPath + validate.AtomicWriteFromReader
with FileIO.Save, use FileIO.Stat for overwrite checks
- Use WrapSaveError to preserve original error messages
* fix: resolve concurrency races in RuntimeContext
- getAPIClient: replace check-then-act with sync.OnceValues, matching
the factory_default.go convention; use NewAPIClientWithConfig to avoid
post-construction config override; fall back to direct construction
for test contexts that bypass newRuntimeContext.
- outputErr: guard first-error capture with sync.Once to prevent data
races if Out() is ever called from concurrent goroutines.
Change-Id: I99c94c3dcb7663fa61571c9720163e41a5fc0e36
* fix: use tenant token for auth scopes
Change-Id: I83bb677e9a33e906e207679b2ba8d0364bc20fe3
* refactor: migrate common/client/im to FileIO and add localfileio tests
- runner resolveInputFlags: replace validate.SafeInputPath + vfs.ReadFile
with FileIO.Open + io.ReadAll
- SaveResponse: delegate to FileIO.Save + ResolvePath
- cmd/api, cmd/service: pass FileIO to ResponseOptions
- im: replace validate.SafeLocalFlagPath with RuntimeContext.ValidatePath,
migrate download/upload to FileIO.Save/Open/Stat
- Add path_test.go and atomicwrite_test.go for localfileio
- Add validate_media_test.go for im media flag validation
- Adapt test mocks to fileio.FileInfo interface
* fix: reject positional arguments in shortcuts with clear error
Shortcuts silently ignored positional arguments (e.g. `lark-cli docs
+search "hello"`), causing empty results. Add Args validator to all
declarative shortcuts so cobra prints usage and a clear error message
telling users to pass values via flags instead.
Change-Id: I7579f9c871138cf91dd5f5d8c1d51bda3f77a1db
* fix: address PR review comments
- Remove unused *Shortcut parameter from rejectPositionalArgs
- Show all positional args in error message instead of only the first
- Add test case for multiple positional arguments
Change-Id: Ifea92d09ddabcd35fbf2db98d9888d18af59b894
All draft-related shortcuts now support <img src="./local.png"> in --body,automatically resolving relative paths into cid: inline MIME parts. Only relative paths are supported; absolute paths are rejected. Previously only +draft-edit supported this; now extended to +draft-create, +send, +reply, +reply-all, and +forward.
- Add internal/client/api_errors.go with WrapDoAPIError and WrapJSONResponseParseError to classify JSON decode issues vs generic network errors
- Route cmd/api DoAPI errors and HandleResponse JSON parse errors through the new helpers
- Add regression tests in cmd/api and internal/client
Related: https://github.com/larksuite/cli/issues/215
* feat: add FileIO extension for file transfer abstraction
Introduce extension/fileio package with Provider/FileIO/File interfaces
and a global registry, following the same pattern as extension/credential.
- Add LocalFileIO default implementation with path validation and atomic writes
- Wire FileIOProvider into Factory and resolve at runtime via RuntimeContext.FileIO()
- Factory holds Provider (not resolved instance), deferring resolution to execution time
* feat: linux support custom data dir via environment variable
* feat(keychain): support custom log directory via LARKSUITE_CLI_LOG_DIR
* feat(security): validate env dir paths for security
Add validation for environment variable directory paths to ensure they are absolute and safe. This prevents potential security issues from malformed paths. Also add corresponding tests to verify the validation behavior.
* docs(validate): add function and test documentation comments
Add missing documentation comments for SafeEnvDirPath function and related test cases to improve code clarity and maintainability
* refactor(keychain): remove warning logs for invalid env vars
* feat(vc): add +recording shortcut for meeting_id to minute_token conversion
* fix(vc): address PR review feedback for +recording shortcut
* docs(vc): merge Recording and Minutes in resource diagram as they share minute_token
* docs(vc): simplify resource diagram to use Minutes only
* test(vc): add integration eval for +recording execute paths
* docs(vc): fix +recording description to include both input modes
* fix(vc): address review findings for +recording docs and code consistency
+update already calls normalizeDocsUpdateResult to surface board_tokens when
markdown contains mermaid/plantuml/whiteboard blocks. +create was missing the
same call, so callers could not know how many whiteboards were created or
retrieve their tokens. One-line fix: call normalizeDocsUpdateResult after
CallMCPTool in DocsCreate.Execute.
* docs: add v1.0.5 changelog
Change-Id: Ia2c5e8f3d3e5fb95b4509e2f5d62a1ee253cd679
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: bump version to v1.0.5
Change-Id: I8d19ec44311f9bf0e700152beab1fd8d261c3f73
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: (MacOS) add fallback file-based master key storage
* refactor(keychain): improve master key file handling and corruption checks
- Replace temporary file approach with direct file creation
- Add explicit corruption checks for existing keys
- Ensure atomic operations and proper cleanup on failure
* docs(keychain): add comments to clarify constants and variables
Add descriptive comments to explain the purpose of timeout, crypto parameters, and test variables in the macOS keychain implementation.
* fix(keychain): use atomic write for master key initialization
* fix(keychain): add retry logic for reading master key file
Add retry mechanism when reading existing master key file to handle potential race conditions. Return early if read error occurs instead of waiting for all retries.
* refactor(keychain): simplify master key validation logic
Restructure the key validation flow to reduce redundant checks and improve readability. The corrupted key check is moved after the error handling block for better logical flow.
* refactor(keychain): replace os package with vfs for file operations
Use vfs package instead of os for file operations to improve testability and
abstract filesystem access. This change makes it easier to mock filesystem
operations in tests and provides a consistent interface for file handling.
* feat: add transport extension with interceptor pre/post hooks
Add extension/transport package following the same Provider pattern as
credential and fileio extensions. The Interceptor interface uses a
PreRoundTrip/post-closure design that guarantees built-in transport
decorators (SecurityHeader, SecurityPolicy, Retry) cannot be skipped,
overridden, or tampered with by extensions. The original request context
is restored after PreRoundTrip to prevent context tampering.
Change-Id: I2e51ff67a0e2d8d32944a0565c2a6781110f281f
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reset registry test globals more completely, tighten the overlay pollution regressions, and ensure tenant scope coverage tests rebuild a fresh isolated registry before asserting.
* fix(issue-labels): reduce mislabeling and handle missing labels
Make type classification more conservative to avoid incorrect labels, and avoid skipping entire issues when some managed labels are missing.
* test(issue-labels): add more real-world issue samples
Add labeled/unlabeled issue examples to cover question/bug/enhancement and domain inference.
* test(issue-labels): avoid duplicate issue samples
Keep one sample per source_url to reduce confusion and maintain stable regression coverage.
* fix(issue-labels): include missing-label-only items in JSON output
Keep stderr and JSON output consistent under --only-missing when desired labels are missing from the repo.
* feat: add strict mode identity filter, profile management and credential extension
Port changes from feat/strict-mode-identity-filter_3 branch:
- Add strict mode for identity filtering and configuration
- Add profile management commands (add/list/remove/rename/use)
- Add credential extension framework (registry, env provider)
- Add VFS abstraction layer
- Refactor factory default and client options
- Update shortcuts to use new credential and validation patterns
Change-Id: I8c104c6b147e1901d94aefcefe35a174932c742b
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: go mod tidy
Change-Id: I0f610ccea6bc874248e84c24770944a3071dcc57
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: fix test failures from credential provider migration
- Remove unused TAT stub registrations in api and service tests
(CredentialProvider manages tokens, SDK no longer calls TAT endpoint)
- Update strict mode integration test: +chat-create now supports user
identity, so it should succeed under strict mode user
Change-Id: Iab51c2e12a97995e0b95dcd71df212d2d1f76570
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: migrate remaining os calls to internal/vfs
Replace direct os.Stat/Open/MkdirAll/OpenFile/Remove/ReadDir/UserHomeDir
with vfs equivalents in shortcuts/minutes, shortcuts/drive, and
internal/keychain. Add ReadDir to the vfs interface and OsFs implementation.
Change-Id: I8f97e5fb3e1731b4684d276644fcb10fae823067
* fix: resolve gofmt and goimports formatting issues
Change-Id: If61578631f5698f7ca2d9a946ca59753651463fb
* feat: add Flag.Input support for @file and stdin input sources
Add framework-level support for reading flag values from files (@path)
or stdin (-), solving the fundamental problem of passing complex text
(markdown, multi-line content) via CLI arguments where shell escaping
breaks content. Closes#239, fixes#163.
- Add File/Stdin constants and Input field to Flag struct
- Add resolveInputFlags() in runner pipeline (pre-Validate)
- Support @@ escape for literal @ prefix
- Guard against multiple stdin consumers
- Auto-append "(supports @file, - for stdin)" to help text
- Apply to: docs +create/+update --markdown, im +messages-send/+reply
--text/--markdown/--content, task +comment --content,
drive +add-comment --content
Change-Id: I305a326d972417542aeadd70f37b74ea456461ef
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: fix pre-existing test failures in task, minutes, and registry
- task/minutes: remove unused tenant_access_token httpmock stubs
(TestFactory's testDefaultToken provides tokens directly, so the
HTTP stub was never consumed and failed verification)
- registry: fix hasEmbeddedData() to check for actual services instead
of just byte length (meta_data_default.json has empty services array)
Change-Id: Ic7b5fc7f9de09137a7254fe1ddf47d24ade40587
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: suppress nilerr lint for intentional nil returns
Both cases intentionally return nil on error for graceful degradation:
- profile list: show friendly message when config is not initialized
- service: skip scope check when token resolution fails
Change-Id: I7285c37277c9b0361a421ab00359244c2cd150b3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CodeRabbit review feedback
- runner.go: fail fast when Input is used on non-string flags
- remote_test.go: rename hasEmbeddedData → hasEmbeddedServices
- profile/list.go: add omitempty to optional JSON fields
- service.go: surface context cancellation errors in scope check
Change-Id: I7072d41f8c711b4b37c542e32dfd8150f42b13c0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: tighten credential resolution and profile flows
Change-Id: I83f6d424540eab9b1708944b9b6e26e8477cc60d
* refactor: centralize identity hint resolution
Change-Id: I38d5f98160b92adb62dc929ae73697ae5b3d64f8
* fix: surface unverified extension identities
Change-Id: Ia86d9bd19add9010176339ec4cc89deb033f5b4f
* fix: honor runtime credential sources in config views
Change-Id: I40b2ffedc5c1db5e08e86b9472ea2b84fa02bb29
* fix: prefer runtime values in config show commands
Change-Id: I5663a53e147577f0f1f533f67d12bea504e6b839
* Revert "fix: prefer runtime values in config show commands"
This reverts commit 4f9db3a227.
* Revert "fix: honor runtime credential sources in config views"
This reverts commit b3bfd526c5.
* fix: harden profile flows and credential boundaries
Change-Id: Ica61cd2730a639f71516cb1b237a639cb6511f7a
* fix: optimize profile and config inspection for agents
Change-Id: I19c368102f19654952638180ab947788a6971563
* refactor: unify credential env contracts
Change-Id: I0ff2c0a650ea53589a0626333e8f6e628ef10a54
* docs: expand AGENTS guidance
Change-Id: I289027dfd364c92205012feef6f05037066c035b
* fix: resolve regression bugs found during PR #252 review
- im: fix double SafeInputPath in resolveLocalMedia → uploadImageToIM/
uploadFileToIM chain that rejected all local image/file uploads
- credential: stop writing plain-text warnings to stderr, preserving
JSON envelope contract for AI agent consumers
- profile add: reject duplicate app-id to prevent keychain credential
collisions across profiles
- profile rename: exclude self when checking name uniqueness so renaming
to own appId works correctly
- config: replace bare fmt.Errorf with output.Errorf in save-failure
paths (default_as, strict_mode ×2, profile add)
- factory: remove unused resolveDefaultAs method (lint)
Change-Id: I6aa0d064414016f367f1edb08dd0604adf7bf13d
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove flaky TestColdStart_UsesEmbedded (race in registry)
The test triggers a data race: resetInit() writes package globals while
a background goroutine from a previous test may still be reading them.
The embedded-data path is covered by other tests.
Change-Id: I7a0c3bf85a9fb337b9279c9053697f40a0c0a0d4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: type-strengthen Brand and DefaultAs across credential chain
Replace raw string fields with typed enums for compile-time safety:
- extension/credential: add Brand and Identity named types
- internal/core: AppConfig.DefaultAs and CliConfig.DefaultAs → Identity
- internal/credential: Account.DefaultAs and IdentityHint.DefaultAs → core.Identity
The full data flow is now typed end-to-end:
extcred.Brand → core.LarkBrand (named-type cast)
extcred.Identity → core.Identity (named-type cast)
No string intermediaries, no implicit conversions.
Change-Id: I715b3b3f033fcb624010f1af9619e3562740ef08
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: fix gofmt alignment in extension/credential/types.go
Change-Id: Ibfac0703a5a28f3c6ba4a47bf40696028d0f3b90
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove file/stdin input support from task comment content flag
Change-Id: If49704ca4612465a23bd30b755d6e72a35fc2349
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(cmdutil): remove dead code autoDetectIdentity
autoDetectIdentity() is only called from tests, never from production
code. Remove it along with its 3 test cases to reduce surface area
before the upcoming ctx propagation refactor.
Change-Id: I35a188860f17656f3e1fe9874f87f284985ae196
* refactor(cmdutil): add ctx parameter to resolveIdentityHint
Private method resolveIdentityHint now accepts context.Context and
passes it to CredentialProvider.ResolveIdentityHint instead of using
context.Background(). The caller (ResolveAs) still uses
context.Background() temporarily until its own signature is updated.
Change-Id: I14634a4e0dc1d657d56936ba61a7b7a206da8ac4
* refactor(cmdutil): add ctx parameter to ResolveStrictMode
ResolveStrictMode now accepts context.Context and passes it to
CredentialProvider.ResolveAccount instead of using context.Background().
Callers in cobra RunE pass cmd.Context(); callers outside RunE
(cmd/root.go startup, tests) use context.Background() explicitly.
Change-Id: I31be48e548ac5ac5640a65f3bfdde4a53ed1dc7e
* refactor(cmdutil): add ctx parameter to CheckStrictMode
CheckStrictMode now accepts context.Context and forwards it to
ResolveStrictMode. Callers pass cmd.Context() (cobra RunE) or
opts.Ctx (APIOptions/ServiceMethodOptions).
Change-Id: I47888519d4cae8c94054771c32aff075565a8cdc
* refactor(cmdutil): add ctx parameter to ResolveAs
ResolveAs now accepts context.Context as first parameter and forwards
it to ResolveStrictMode and resolveIdentityHint. This completes the
ctx propagation chain: all Factory methods that call
CredentialProvider now receive ctx from cobra cmd.Context().
No more context.Background() calls remain in factory.go for
credential provider operations.
Change-Id: I6d10b6350e3b149470660de3e7855614314e8b29
* test: fix gofmt in cmdutil factory tests
Change-Id: I4a87d5a815b959f14cc4371b73dee4aae106932f
* fix: remove file/stdin input support from im send/reply and drive comment
The Input (file/stdin) feature is not yet ready for these flags:
- im send/reply: --content, --text, --markdown
- drive add-comment: --content
Retained only in doc create/update where markdown from file is essential.
Change-Id: I582b6349528fccb639ad9edc84650cca3b68535c
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: liushiyao <liushiyao.1206@bytedance.com>
* fix(mail): restore CID validation and stale PartID lookup lost in revert (#199)
The revert of PR #81 (eda2b9c) also removed two independent bugfixes:
1. CID character validation in newInlinePart — reject spaces, tabs,
angle brackets, and parentheses to prevent malformed MIME output.
2. Stale PartID lookup in validateInlineCIDAfterApply and
validateOrphanedInlineCIDAfterApply — use findPrimaryBodyPart by
media type instead of findPart by PrimaryHTMLPartID, which can go
stale when ops restructure the MIME tree.
* test(mail): add tests for CID character validation and stale PartID lookup
- TestAddInlineRejectsInvalidCharactersInCID: verify spaces, tabs,
embedded angle brackets, and parentheses in CID are rejected.
- TestValidateInlineCIDAfterSetBody: verify inline CID validation
works correctly after set_body restructures the MIME tree (covers
the findPrimaryBodyPart fix for stale PartID).
* fix(mail): add CID character validation to replaceInline and strengthen test assertions
Address CR feedback:
1. Add the same CID character validation (spaces, tabs, angle brackets,
parentheses) to replaceInline, matching the check in newInlinePart.
Previously replace_inline could bypass the restriction.
2. Strengthen orphaned CID test assertion to check for specific
"orphaned cids" error message, not just non-nil error.
3. Add TestReplaceInlineRejectsInvalidCharactersInCID to cover the
new validation in replace_inline.
* ci: add issue labeler workflow
Add a manual GitHub Actions workflow and script to poll issues and apply type/domain labels.
* feat(issue-labels): refine heuristics and add docs
Improve domain detection and add safeguards to avoid overriding manual type triage by default. Refresh regression samples from real issues and document usage.
* ci(issue-labels): enable hourly scheduled labeling
Run hourly on schedule with write mode by default while keeping manual dispatch dry-run by default.
* ci(issue-labels): shorten lookback window to 6h
Reduce scheduled scan window while keeping overlap for missed runs.
* ci(issue-labels): opt into Node 24 actions runtime
Set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 and use Node 24 for the script runtime to avoid upcoming Node 20 deprecation warnings.
* ci(issue-labels): restore lookback input for manual runs
Allow workflow_dispatch to override lookback_hours while keeping hourly schedule fixed.
* ci(issue-labels): upgrade checkout/setup-node to v6
Use actions/checkout@v6 and actions/setup-node@v6 to align with Node 24 runtime and avoid Node 20 deprecation warnings.
* fix(ci): label only unlabeled issues via search api
* fix(ci): refine issue labeling heuristics from live issues
* fix(ci): address remaining issue label review comments
* fix(ci): fix issue label arg parsing regression
* docs(issue-labels): clarify one-shot unlabeled triage scope
* feat(drive): support multipart upload for files larger than 20MB
Previously, `drive +upload` rejected files exceeding 20MB with a
validation error. Now files > 20MB automatically use the three-step
chunked upload API (upload_prepare → upload_part × N → upload_finish),
removing the size ceiling for Drive uploads.
Tested with a 189MB file (48 blocks × 4MB) against a live Feishu tenant.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(drive): add upload error-path tests to improve coverage
Cover small-file upload (upload_all) success + error paths and
multipart upload error paths (invalid prepare, part API error,
part invalid JSON, finish missing token, custom name flag).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: cli e2e test framework and demo
* feat: add cli-e2e-testcase-writer skill and task case
* feat: add cli e2e config and fix test resource prefix
All HTTP clients previously used http.DefaultTransport which silently respects
HTTP_PROXY/HTTPS_PROXY env vars, allowing credentials to transit through
untrusted proxies. This adds a proxy detection warning and an opt-out switch
(LARK_CLI_NO_PROXY=1) so security-sensitive users can disable proxy entirely.
- Redact proxy credentials in warning output (handles both scheme-prefixed and bare URL formats)
- Suppress warning when LARK_CLI_NO_PROXY is already set
- Use FallbackTransport singleton for nil-Base fallback paths to preserve connection pooling
- Emit proxy warning on both HTTP client and Lark SDK client paths
Change-Id: Ibed7d0470409c73fbd42bccac6673f9fc5e87a83
- Add --as user support to +chat-create
- Add UserScopes (im:chat:create_by_user) / BotScopes (im:chat:create)
- Update skill docs and reference files to reflect user/bot support
- Default identity remains bot (first element of AuthTypes)
Change-Id: I6be0a160567a0d87a92f176ae12297a11d06dcb1
* feat(auth): add response logging and centralize path constants
* refactor(auth): improve response logging and error handling
* fix(auth): ensure log cleanup runs only once per process
Add flag to track if cleanup has run and prevent duplicate executions
Add test to verify cleanup only runs once
* refactor(auth): simplify log writer and cleanup logic
* docs(auth): add comments to auth paths and logging functions
* style(auth): fix indentation in path constants
* docs(auth): add missing function comments across auth package
* docs(tests): add descriptive comments to auth test functions
* test(auth): rename test case and cleanup unused params
* fix(auth): handle file close error in auth response logging
* fix(auth): ensure log cleanup runs only once
* refactor(auth): replace custom log writer with standard logger
* feat(auth): add structured logging for keychain errors
* fix(auth): remove goroutine from auth log cleanup to prevent race condition
* fix(auth): remove goroutine from auth log cleanup to prevent race condition
* refactor(auth): move auth logging logic to keychain package
* docs(mail): add identity guidance to prefer user over bot for mail APIs
Add an identity selection section to the mail skill documentation,
guiding AI agents to default to --as user when operating on mailboxes.
Bot identity requires the app to have tenant-level mail scopes enabled
in the developer console, which most apps do not.
* docs(mail): clarify identity selection wording and bot scope limits
- Replace ambiguous "默认使用" with "策略上应优先显式使用" to
distinguish policy recommendation from CLI default (auto)
- Note that bot identity only supports read operations; all write
operations (send, reply, forward, draft edit) require user identity
- Rewrite decision rules by read/write classification
Users reported that AI agents sometimes wrote shell scripts to manually
extract and re-decode JSON string fields (e.g. unicode_escape), causing
Chinese character corruption. Add notes to mail skill docs clarifying
that JSON output can be read directly without additional encoding
conversion.
Mail scope tests (TestConfirmSendMissingScope*) were calling
auth.SetStoredToken/RemoveStoredToken which accessed the real macOS
keychain via go-keyring, causing persistent popup dialogs when the
master key was missing. Add keyring.MockInit() to swap in an in-memory
backend during tests.
Node.js https.get() does not honor https_proxy/HTTP_PROXY env vars,
causing silent download failures behind firewalls. Switch to curl which
natively supports proxy settings, and add npmmirror.com as a fallback
mirror for regions where GitHub is slow or blocked.
Change-Id: If9ace1e467e46f2a3009610a808bce8d78259e78
* feat: add --jq flag for filtering JSON output across all command types
Add jq expression filtering (--jq / -q) to api, service, and shortcut
commands using gojq. Includes early expression validation, mutual
exclusion checks with --output and non-json --format, pagination+jq
aggregation path, and comprehensive test coverage.
* fix: correct gofmt alignment in jq_test.go struct literal
* fix: downgrade gojq to v0.12.17 to keep Go 1.23 compatibility
gojq v0.12.18 requires Go 1.24, which unnecessarily bumped the project
minimum version. v0.12.17 requires only Go 1.21 and provides the same
jq functionality needed.
* refactor: consolidate jq validation and pagination logic
Extract ValidateJqFlags() and PaginateWithJq() shared functions to
eliminate duplicated jq logic across api, service, and shortcut commands.
* fix: reject --jq for non-JSON responses and propagate shortcut jq errors
- HandleResponse now returns a validation error when --jq is used with
a non-JSON Content-Type instead of silently falling through to binary save.
- Shortcut runtime jq errors are captured in RuntimeContext.outputErr
and propagated as the command exit code, matching api/service behavior.
Accept escaped and full-width sheet/range separators in sheets shortcuts.
Normalize range parsing in the shared sheets helper so read, find, write,
and append handle \!, \!, and ! consistently.
Add regression tests for separator normalization in dry-run paths.
- Add --as user support to +messages-send and +messages-reply
- Add UserScopes (im:message.send_as_user) / BotScopes (im:message:send_as_bot)
- Add DoAPIAsBot to RuntimeContext so file/image uploads always use bot
identity even when the surrounding command runs as user
- Update skill docs and reference files to reflect user/bot support
- Default identity remains bot (first element of AuthTypes)
* fix(mail): on-demand scope checks, event filtering, and watch lifecycle
- Remove mail:user_mailbox.folder:read from watch's static Scopes; add
validateFolderReadScope and validateLabelReadScope that check
permissions on-demand when listMailboxFolders/listMailboxLabels is
called (same pattern as validateConfirmSendScope).
- Resolve --mailbox me to real email address via profile API for event
filtering, preventing other users' mail events from being processed.
Block startup if resolution fails, with proper error type distinction.
- Add unsubscribe cleanup (guarded by sync.Once) on all exit paths:
SIGINT/SIGTERM, profile resolution failure, and WebSocket failure.
- Remove bot from AuthTypes since bot tokens cannot subscribe.
- Include profile lookup in dry-run output and update tests.
- Update fetchMailboxPrimaryEmail to return error for diagnostics.
- Update documentation for on-demand scope requirements.
* fix(mail): preserve original error in enhanceProfileError fallback
Return the original error directly for non-permission failures instead
of wrapping with fmt.Errorf, so structured exit codes (ExitNetwork,
ExitAPI) are preserved for scripting.
* Revert "fix(mail): clarify that file path flags only accept relative paths (#141)"
This reverts commit 1ffe870dc8.
* Revert "feat(mail): auto-resolve local image paths in draft body HTML (#81) (#139)"
This reverts commit 70c72a2c02.
* Reapply "fix(mail): clarify that file path flags only accept relative paths (#141)"
This reverts commit d465e085b1.
* feat: add TestGenerateShortcutsJSON for registry shortcut export
Add a test that exports all shortcuts as JSON when SHORTCUTS_OUTPUT
env var is set, enabling the registry repo to extract shortcut
metadata without depending on a dump-shortcuts CLI command.
* refactor(keychain): improve error handling and consistency across platforms
- Change platformGet to return error instead of empty string
- Add proper error wrapping for keychain operations
- Make master key creation conditional in getMasterKey
- Improve error messages and handling for keychain access
- Update dependent code to handle new error returns
* docs(keychain): improve function documentation and error message
Add detailed doc comments for all platform-specific keychain functions to clarify their purpose and behavior. Also enhance the error hint message to include a suggestion for reconfiguring the CLI when keychain access fails.
* refactor(keychain): reorder operations in platformGet for better error handling
Check for file existence before attempting to read and get master key
* fix(keychain): improve error handling and consistency across platforms.
* fix(keychain): handle corrupted master key case
* fix(keychain): handle I/O errors when reading master key
* feat(ci): add PR size label pipeline
* chore(ci): make PR label sync non-blocking
* feat(ci): add dry-run mode for PR label sync
* feat(ci): add PR label dry-run samples
* test(ci): update PR label samples with real historical merged PRs
Replaced synthetic or open PR samples with actual merged/closed PRs from the
repository to provide a more accurate reflection of the size label categorization.
Added 4 samples each for sizes S, M, and L covering docs, fixes, ci, and features.
* feat(ci): add high-level area tags for PRs
Based on user feedback, fine-grained domain labels (like `domain/base`) are too detailed for the early stages.
This change adds support for applying `area/*` tags to indicate which important top-level modules a PR touches.
Currently tracked areas:
- `area/shortcuts`
- `area/skills`
- `area/cmd`
Minor modules like docs, ci, and tests are intentionally excluded to keep tags focused on critical architectural components.
* refactor(ci): extract pr-label-sync logic to a dedicated directory
To avoid polluting the root `scripts/` directory, moved `sync_pr_labels.js` and
`sync_pr_labels.samples.json` into a new `scripts/sync-pr-labels/` folder.
Added a dedicated README to document its usage and behavior.
Updated `.github/workflows/pr-labels.yml` to reflect the new path.
* refactor(ci): rename pr label script directory for simplicity
Renamed `scripts/sync-pr-labels/` to `scripts/pr-labels/` to keep directory
names concise. Updated internal references and GitHub workflow files to point
to the new path.
* ci: add GitHub Actions workflow to check skill format
* test(ci): update sample json to include expected_areas
Added `expected_areas` lists to each sample in `samples.json` to reflect
the newly added `area/*` high-level module tagging logic. Allows testing
to accurately check both `size/*` and `area/*` outputs.
* refactor(scripts): move skill format check to isolated directory and add README
* test(scripts): add positive and negative tests for skill format check
* fix(scripts): revert skill changes and downgrade version/metadata checks to warnings
* fix(scripts): completely remove version check and skip lark-shared
* refactor(ci): improve pr-labels script readability and maintainability
- Reorganized code into logical sections with clear comments
- Encapsulated GitHub API interactions into a reusable `GitHubClient` class
- Extracted and centralized classification logic into a pure `evaluateRules` function
- Replaced magic numbers with named constants (`THRESHOLD_L`, `THRESHOLD_XL`)
- Fixed `ROOT` path resolution logic
- Simplified conditional statements and control flow
* ci: fix setup-node version in pr-labels workflow
* tmp
* refactor(ci): replace generic area labels with business-specific ones
- Add PATH_TO_AREA_MAP to map shortcuts/skills paths to business areas (im, vc, ccm, base, mail, calendar, task, contact)
- Replace importantAreas with businessAreas throughout the codebase
- Remove area/shortcuts, area/skills, area/cmd generic labels
- Now generates specific labels like area/im, area/vc, area/ccm, etc.
- Update samples.json expected_areas to match new behavior
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ci): address PR review feedback for label scripts and workflows
- Add `edited` event to PR labels workflow to trigger on title changes
- Add security warning comment in pr-labels.yml workflow
- Update pr-labels README with latest business area labels
- Exclude `skills/lark-*` paths from low risk doc classification
- Handle renamed files properly in PR path classification
- Fix YAML frontmatter extraction to handle CRLF line endings
- Use precise regex for YAML key validation instead of substring match
- Fix exit code checking logic in skill-format-check test script
- Translate Chinese comments in skill-format-check to English
* fix(skill-format-check): address CodeRabbit review feedback
- Fix frontmatter closing delimiter detection to strictly match '---' using regex, preventing invalid closing tags like '----' from passing.
- Improve test fixture reliability by failing tests immediately if fixture preparation fails, avoiding false positives.
* fix: address review comments from PR 148
- ci: warn when PR label sync fails in job summary
- test(skill-format-check): capture validator output for negative tests
- fix(skill-format-check): catch errors when reading SKILL.md to avoid hard crashes
* fix: add error handling for directory enumeration in skill-format-check
- refactor: use `fs.readdirSync` with `{ withFileTypes: true }` to avoid extra stat calls
- fix: catch and report errors gracefully during skills directory enumeration instead of crashing
* docs(skill-format-check): clarify `metadata` requirement in README
test(pr-labels): add edge case samples for skills paths, CCM multi-paths, and renames
* test(pr-labels): add real PR edge case samples
- use PR #134 to test skill path behaviors
- use PR #57 to test multi-path CCM resolution
- use PR #11 to test track renames cross domains
* refactor(ci): migrate pr labels from area to domain prefix
- Replaced `area/` prefix with `domain/` for PR labeling to align with existing GitHub labels
- Renamed internal constants and variables from `area` to `domain` (e.g. `PATH_TO_AREA_MAP` to `PATH_TO_DOMAIN_MAP`)
- Updated `samples.json` test data to use new `domain/` format and `expected_domains` key
- Added `scripts/pr-labels/test.js` runner script for continuous validation of labeling logic against PR samples
- Corrected expected size label for PR #134 test sample
* test: use execFileSync instead of execSync in pr-labels test script
* fix: resolve target path against process.cwd() instead of __dirname in skill-format-check
* docs: correct label prefix in PR label workflow README
- Updated README.md to reflect the new `domain/` label prefix instead of `area/`
* fix(ci): fix dry-run console output formatting and enforce auth in tests
- Removed duplicate domain array interpolation in printDryRunResult
- Added process.env.GITHUB_TOKEN guard in test.js to prevent ambiguous failures from API rate limits
* fix(ci): ensure PR labels can be applied reliably
- Added `issues: write` permission to pr-labels workflow, which is strictly required by the GitHub REST API to modify labels on pull requests
- Reordered script execution in `index.js` to apply/remove labels on the PR *before* attempting to sync repository-level label definitions (colors/descriptions). The definition sync is now a trailing best-effort step with error catching so transient repo-level API failures don't abort the critical path.
* fix(ci): fix edge cases in pr-label index script
- Added missing `skills/lark-task/` to `PATH_TO_DOMAIN_MAP` to properly detect task domain modifications
- Updated GitHub REST API error checking in `syncLabelDefinition` to reliably match `error.status === 422` rather than loosely checking substring
- Moved token presence check in `main()` to happen before `resolveContext` to avoid triggering unauthenticated 401 API limits when GITHUB_TOKEN is omitted locally
* test(ci): clean up PR label test samples
- Removed duplicate PR entries (#11 and #57) to reduce redundant API calls during testing
- Renamed sample test cases to correctly reflect their expected labels (e.g. `size-l-skill-format-check` -> `size-m-skill-format-check`)
* fix(ci): bootstrap new labels before applying to PRs
- Prior changes correctly made full label sync best-effort, but broke the flow for brand new domains
- GitHub API returns a 422 error if you attempt to attach a label to an Issue/PR that does not exist in the repository
- Added a targeted bootstrap loop to create/sync specifically the labels in `toAdd` before attempting `client.addLabels()`
- Left the remaining global label synchronization as a best-effort trailing action
* test(ci): automate PR label regression testing
- Added a dedicated GitHub Actions workflow (`pr-labels-test.yml`) to automatically run `test.js` against `samples.json` whenever the labeling logic is updated
- Documented local testing instructions in `scripts/pr-labels/README.md`
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(mail): auto-resolve local image paths in draft body HTML (#81)
Allow <img src="./local/path.png" /> in set_body/set_reply_body HTML.
Local file paths are automatically resolved into inline MIME parts with
generated CIDs, eliminating the need to manually pair add_inline with
set_body. Removing or replacing an <img> tag in the body automatically
cleans up or replaces the corresponding MIME inline part.
- Add postProcessInlineImages to unify resolve, validate, and orphan
cleanup into a single post-processing step
- Extract loadAndAttachInline shared helper to deduplicate addInline
and resolveLocalImgSrc logic
- Cache resolved paths so the same file is only attached once
- Use whitelist URI scheme detection instead of blacklist
- Remove dead validateInlineCIDAfterApply and
validateOrphanedInlineCIDAfterApply functions
Closes#81
* fix(mail): harden inline image CID handling
1. Fix imgSrcRegexp to skip attribute names like data-src/x-src that
contain "src" as a suffix — only match the real src attribute.
2. Sanitize cidFromFileName to replace whitespace with hyphens,
producing RFC-safe CID tokens (e.g. "my logo.png" → "my-logo").
3. Add CID validation in newInlinePart to reject spaces, tabs, angle
brackets, and parentheses — fail fast instead of silently producing
broken inline images in the sent email.
* refactor(mail): use UUID for auto-generated inline CIDs
Replace filename-derived CID generation (cidFromFileName + uniqueCID)
with UUID-based generation. UUIDs contain only [0-9a-f-] characters,
eliminating all RFC compliance risks from special characters, Unicode,
or filename collisions. Same-file deduplication via pathToCID cache
is preserved — multiple <img> tags referencing the same file still
share one MIME part and one CID.
* fix(mail): avoid panic in generateCID by using uuid.NewRandom
uuid.New() calls Must(NewRandom()) which panics if the random source
fails. Replace with uuid.NewRandom() and propagate the error through
resolveLocalImgSrc, so the CLI returns a clear error instead of
crashing in extreme environments.
* fix(mail): restore quote block hint in set_reply_body template description
The auto-resolve PR accidentally dropped "the quote block is
re-appended automatically" from the set_reply_body shape description.
Restore it alongside the new local-path support note.
* fix(mail): add orphan invariant comment and expand regex test coverage
- Add comment in postProcessInlineImages explaining that partially
attached inline parts on error are cleaned up by the next Apply.
- Add regex test cases: single-quoted src, multiple spaces before src,
and newline before src.
* fix(mail): use consistent inline predicate and safer HTML part lookup
1. removeOrphanedInlineParts: change condition from
ContentDisposition=="inline" && ContentID!="" to
isInlinePart(child) && ContentID!="", matching the predicate used
elsewhere — parts with only a ContentID (no Content-Disposition)
are now correctly cleaned up.
2. postProcessInlineImages: use findPrimaryBodyPart instead of
findPart(snapshot.Body, PrimaryHTMLPartID) to avoid stale PartID
after ops restructure the MIME tree.
* fix(mail): revert orphan cleanup to ContentDisposition check to protect HTML body
The previous change (d3d1982) broadened the orphan cleanup predicate to
isInlinePart(), which treats any part with a ContentID as inline. This
deletes the primary HTML body when it carries a Content-ID header
(valid in multipart/related), even on metadata-only edits like
set_subject.
Revert to the original ContentDisposition=="inline" && ContentID!=""
condition — only parts explicitly marked as inline attachments are
candidates for orphan removal. Add regression test covering
multipart/related with a Content-ID-bearing HTML body.
* fix: Fix the issue where the URL returned by the "lark-cli auth login --no-wait" command contains \u0026
* style: fix indentation and whitespace in error handling code
* fix(auth): handle JSON encoding errors in login output
* docs(cmd/auth): add comment for authLoginRun function
Add non-blocking update check that queries the npm registry for the
latest @larksuite/cli version. Results are cached locally (24h TTL)
to avoid repeated network requests.
When a newer version is detected, a `_notice.update` field is injected
into all JSON output envelopes (success, error, and shortcut responses),
enabling AI agents and scripts to surface upgrade prompts.
Key changes:
- New `internal/update` package: registry fetch, semver compare, cache
- Async check in root command (cache-first, then background refresh)
- `_notice` field added to Envelope/ErrorEnvelope structs
- `PrintJson` injects notice into map-based envelopes with "ok" key
- `doctor` command gains cli_version and cli_update checks
- Suppressed for CI, DEV builds, shell completion, and git-describe versions
* feat: add npm publish job to release workflow
Change-Id: Ibfae2af6bd2aabf09936c96d21964af98b77c127
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: bump package version to 1.0.1
Change-Id: Ifb58789be5621ab4979b5fe60e0e30042e07fea8
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(base): clarify field description usage in json
* chore(base docs): clarify field description usage
* docs(base): add description to field schema
Add workflows, forms, roles & permissions to the Base feature description
across READMEs and service registry to accurately reflect full coverage.
Co-authored-by: dengfanxin <dengfanxin.dfx@bytedance.com>
* docs: add official badge and maintainer attribution to README
Many third-party Feishu/Lark CLI tools exist, causing confusion
about which is the official one. Add an "Official" badge and a
blockquote clearly stating this repo is maintained by the
Lark/Feishu Open Platform team at larksuite.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: use 飞书官方 CLI 工具 in Chinese README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: merge official attribution into description, drop Open Platform wording
Remove the separate blockquote and fold official status into the
main description line. Also remove "Open Platform" — this is
the Lark/Feishu CLI tool, not an "Open Platform CLI tool".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: remove self-made Official badge
No major official repo (OpenAI, Anthropic, Stripe, AWS, HashiCorp)
uses a custom shields.io "Official" badge — anyone can make one, so
it signals nothing. The org namespace (larksuite/) and the "official"
wording in the description are sufficient.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove sensitive send scope from reply and forward shortcuts
Remove mail:user_mailbox.message:send from the required scopes of
+reply, +reply-all, and +forward shortcuts. This scope is sensitive
and may not be granted, while these shortcuts default to saving
drafts and do not strictly require it.
* fix: validate send scope dynamically when --confirm-send is set
Add validateConfirmSendScope() to check mail:user_mailbox.message:send
in the Validate phase when --confirm-send is used, preventing the
"draft created but send failed" scenario. Add regression tests for
+reply, +reply-all, and +forward.
MarkRaw previously skipped both enrichPermissionError and
WriteErrorEnvelope, causing api command to exit 1 with no output
on API errors. Now MarkRaw only skips enrichPermissionError while
WriteErrorEnvelope always runs, ensuring stderr error envelope is
always written.
- Simplify apiRun MarkRaw logic (remove unnecessary IsJSONContentType check)
- Update existing tests to match new behavior
- Add 5 e2e tests covering api/service/shortcut error output
* ci: improve CI workflows and add golangci-lint config
- Add path filters to avoid unnecessary CI runs on non-Go changes
- Use go-version-file instead of hardcoded Go version
- Unify runners to ubuntu-latest
- Consolidate staticcheck/vet into golangci-lint with curated linter set
- Add go mod tidy check, govulncheck, and dependency license check
- Enable race detector in coverage, increase test timeout to 5m
- Add build verification step to tests workflow
- Add .codecov.yml with patch coverage target (60%)
- Add .golangci.yml (v2) with security and correctness linters
Change-Id: I409beb21cc1f1568ff47739c0a00f6214c10a0dd
* ci: replace Codecov upload with GitHub Job Summary coverage report
- Remove Codecov action dependency and CODECOV_TOKEN usage
- Generate coverage report using go tool cover and display in Job Summary
- Rename job from 'codecov' to 'coverage'
- Remove .codecov.yml from paths filter
Change-Id: Ib65dab6c4d7117c3300a9ea31eb1550537c72f88
* ci: trigger lint workflow
Change-Id: Ic1c492dd339f5460d2be2971ac65ea8f99e524eb
* ci: replace golangci-lint action with go run to avoid action whitelist restriction
Change-Id: I87274abf9780eb8b6350e98a27302ec5acc2a2e5
* ci: replace golangci-lint action with go run, keep incremental lint via --new-from-rev
Change-Id: I3d4a13cfd7b6c02e4098b04b8533a7248185c077
* ci: add fetch-depth 0 to lint checkout for incremental lint to work
Change-Id: I112279c5ec06dc0aa3aa7e01d564ea27fbd20533
* ci: disable errcheck linter due to high volume of existing violations
Change-Id: Iec57e8fbe42699f687d931d9dde2f879f2ae5b02
* ci: align golangci-lint config with GitHub CLI, make govulncheck non-blocking
- Add exptostd, gocheckcompilerdirectives, gochecksumtype, gomoddirectives linters
- Move gosec, staticcheck, errname, errorlint, misspell to TODO for later enablement
- Remove G104 exclusion (errcheck is disabled)
- Make govulncheck continue-on-error until Go version is upgraded
Change-Id: I330ece4f202229aee1e2f50790f6b22738704c05
* ci: fix go-licenses module path for v2
Change-Id: Ifd018ebe79cd18402171417b1b73313af2d23c6d
Combine the separate "Install CLI" and "Install AI Agent Skills" sections
into a single "Install" section for both human and AI Agent quick start
guides. Renumber the AI Agent steps accordingly.
Change-Id: I4ac10538d912a8889f52ea5a85e757d3e8bad21e
Add npm version badge to both README files for better package
discoverability. Reword the Quick Start tip to directly address
AI assistants instead of human users.
Change-Id: I9fb4252e4a7bde4ab6644c6ca6e63dc5d34b6f0c
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: emphasize Skills installation as required step for AI Agents
Split the AI Agent quick start from a single code block into separate
labeled steps, and add a prominent note explaining that without Skills
the Agent cannot discover available commands or parameters.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: position Skills installation as the core step of the setup
Reword the Skills step to convey that it is the most important part of
the entire setup — the CLI is just a binary, Skills are what give the
Agent the knowledge to operate Lark.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: tone down Skills step wording, keep it concise
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both notices recommend the same fix command: `lark-cli update`. The skills notice's `current` field is `""` when skills have never been synced (cold start) and a version string when synced for an older binary (drift).
## Pre-PR Checks (match CI gates)
1.`make unit-test`
2.`go vet ./...`
3.`gofmt -l .` — must produce no output
4.`go mod tidy` — must not change `go.mod`/`go.sum`
5.`go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev=origin/main`
6. If dependencies changed: `go run github.com/google/go-licenses/v2@v2.0.1 check ./... --disallowed_types=forbidden,restricted,reciprocal,unknown`
This CLI's primary consumers include AI agents (Claude Code, Cursor, Gemini CLI). Your code is read by machines — error messages, output format, and flag design all directly affect agent success rates.
The one rule to internalize: **every error message you write will be parsed by an AI to decide its next action.** Make errors structured, actionable, and specific.
## Code Conventions
### Structured errors in commands
`RunE` functions must return `output.Errorf` / `output.ErrWithHint` — never bare `fmt.Errorf`. AI agents parse stderr as JSON; bare errors break this contract.
### stdout is data, stderr is everything else
Program output (JSON envelopes) goes to stdout. Progress, warnings, hints go to stderr. Mixing them corrupts pipe chains.
### Use `vfs.*` instead of `os.*`
All filesystem access goes through `internal/vfs`. This enables test mocking.
### Validate paths before reading
CLI arguments are untrusted (they come from AI agents). Call `validate.SafeInputPath` before any file I/O.
### Tests
- Every behavior change needs a test alongside the change.
-`cmdutil.TestFactory(t, config)` for test factories.
-`t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())` to isolate config state.
### E2E Testing
**Dry-run E2E (required for every shortcut change)**
- Validates request structure without calling real APIs
- Place in `tests/cli_e2e/dryrun/` or the corresponding domain directory
- Set env vars `LARKSUITE_CLI_APP_ID`/`APP_SECRET`/`BRAND`, use `--dry-run`, assert method/URL/params
- No secrets needed — runs on fork PRs
- Explore correct params with `lark-cli <domain> --help` and `lark-cli schema` first
**Live E2E (required for new flows or behavior changes)**
- Validates real API round-trips
- Place in `tests/cli_e2e/<domain>/`
- Must be self-contained: create -> use -> cleanup
- Needs bot credentials (CI secrets, skipped on fork PRs)
> Thank you for your interest in open source projects hosted or managed by ByteDance Ltd. and/or its Affiliates ("**ByteDance**") . In order to clarify the intellectual property license granted with Contributions from any person or entity, ByteDance must have a Contributor License Agreement ("**CLA**") on file that has been signed by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of ByteDance and its users; it does not change your rights to use your own Contributions for any other purpose.
> If you are an individual making a submission on your own behalf, you should accept the Individual Contributor License Agreement. If you are making a submission on behalf of a legal entity (the “**Corporation**”), you should sign the separation Corporate Contributor License Agreement.
By clicking “Accept” on this page, You accept and agree to the following terms and conditions for Your present and future Contributions submitted to ByteDance. Except for the license granted herein to ByteDance and recipients of software distributed by ByteDance, You reserve all right, title, and interest in and to Your Contributions.
1.Definitions.
"Affiliate" shall mean an entity that Controls, is Controlled by, or is under common Control with You or ByteDance, respectively (but only as long as such Control exists).
"Control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to ByteDance for inclusion in, or documentation of, any of the products owned or managed by ByteDance (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to ByteDance or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, ByteDance for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with ByteDance. For the avoidance of doubt, the Corporation making a Contribution and all of its Affiliates are considered to be a single Contributor and this CLA shall apply to Contributions Submitted by the Corporation or any of its Affiliates.
2.Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to ByteDance and to recipients of software distributed by ByteDance a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
3.Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to ByteDance and to recipients of software distributed by ByteDance a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
4.You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to ByteDance, or that your employer has executed a separate Corporate CLA with ByteDance.
5.You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.
6.You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
7.Should You wish to submit work that is not Your original creation, You may submit it to ByteDance separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
8.You agree to notify ByteDance of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
9.You agree that contributions to Projects and information about contributions may be maintained indefinitely and disclosed publicly, including Your name and other information that You submit with your submission.
10.This Agreement is the entire agreement and understanding between the parties, and supersedes any and all prior agreements, understandings or communications, written or oral, between the parties relating to the subject matter hereof. This Agreement may be assigned by ByteDance.
A person authorized to sign legal documents on behalf of your employer (usually a VP or higher) must sign the Contributor Agreement on behalf of the employer.
If you have not already signed this agreement, please complete and sign, then scan and email a pdf file of this Agreement to opensource-cla@bytedance.com. Please read this document carefully before signing and keep a copy for your records.
If you need to update your CLA, please email<opensource-cla@bytedance.com>from the email address associated with your individual or corporate information.
A command-line tool for [Lark/Feishu](https://www.larksuite.com/) Open Platform — built for humans and AI Agents. Covers core business domains including Messenger, Docs, Base, Sheets, Calendar, Mail, Tasks, Meetings, and more, with 200+ commands and 19 AI Agent [Skills](./skills/).
The official [Lark/Feishu](https://www.larksuite.com/) CLI tool, maintained by the [larksuite](https://github.com/larksuite) team — built for humans and AI Agents. Covers core business domains including Messenger, Docs, Base, Sheets, Slides, Calendar, Mail, Tasks, Meetings, Markdown, and more, with 200+ commands and 24 AI Agent [Skills](./skills/).
- **Agent-Native Design** — 19 structured [Skills](./skills/) out of the box, compatible with popular AI tools — Agents can operate Lark with zero extra setup
- **Wide Coverage** — 11 business domains, 200+ curated commands, 19 AI Agent [Skills](./skills/)
- **Agent-Native Design** — 24 structured [Skills](./skills/) out of the box, compatible with popular AI tools — Agents can operate Lark with zero extra setup
- **Wide Coverage** — 17 business domains, 200+ curated commands, 24 AI Agent [Skills](./skills/)
- **AI-Friendly & Optimized** — Every command is tested with real Agents, featuring concise parameters, smart defaults, and structured output to maximize Agent call success rates
- **Open Source, Zero Barriers** — MIT license, ready to use, just `npm install`
- **Up and Running in 3 Minutes** — One-click app creation, interactive login, from install to first API call in just 3 steps
@@ -21,19 +22,25 @@ A command-line tool for [Lark/Feishu](https://www.larksuite.com/) Open Platform
| 📋 Project | Meegle — manage work items, schedules, and data via the standalone [meegle-cli](https://github.com/larksuite/meegle-cli) (install separately) |
## Installation & Quick Start
@@ -46,31 +53,32 @@ Before you start, make sure you have:
### Quick Start (Human Users)
> **Tip:** If you have an AI Agent, you can hand this README to it and let the AI handle installation and setup — jump to [Quick Start (AI Agent)](#quick-start-ai-agent).
> **Note for AI assistants:** If you are an AI Agent helping the user with installation, jump directly to [Quick Start (AI Agent)](#quick-start-ai-agent), which contains all the steps you need to complete.
#### Install CLI
#### Install
**From npm (recommended):**
Choose **one** of the following methods:
**Option 1 — From npm (recommended):**
```bash
# Install CLI
npm install -g @larksuite/cli
# Install CLI SKILL (required)
npx skills add larksuite/cli -y -g
```
**From source:**
**Option 2 — From source:**
Requires Go `v1.23`+ and Python 3.
```bash
git clone https://github.com/larksuite/cli.git
cd cli
make install
```
#### Install AI Agent Skills
[Skills](./skills/) are structured instruction documents that enable AI Agents to use this CLI:
```bash
# Install all skills to current directory
npx skills add larksuite/cli -y
# Install all skills globally
# Install CLI SKILL (required)
npx skills add larksuite/cli -y -g
```
@@ -91,48 +99,66 @@ lark-cli calendar +agenda
> The following steps are for AI Agents. Some steps require the user to complete actions in a browser.
**Step 1 — Install**
```bash
# 1. Install CLI
# Install CLI
npm install -g @larksuite/cli
# 2. Install Skills (enables AI Agent to use this CLI)
npx skills add larksuite/cli --all -y
# Install CLI SKILL (required)
npx skills add larksuite/cli -y -g
```
# 3. Configure app credentials
# Important: run this command in the background. It will output an authorization URL — extract it and send it to the user. The command exits automatically after the user completes the setup in browser.
**Step 2 — Configure app credentials**
> Run this command in the background. It will output an authorization URL — extract it and send it to the user. The command exits automatically after the user completes the setup in the browser.
```bash
lark-cli config init --new
```
# 4. Login
# Same as above: run in the background, extract the authorization URL and send it to the user.
**Step 3 — Login**
> Same as above: run in the background, extract the authorization URL and send it to the user.
Run `lark-cli <service> --help` to see all shortcut commands.
@@ -199,7 +225,7 @@ Call any Lark Open Platform endpoint directly, covering 2500+ APIs.
```bash
lark-cli api GET /open-apis/calendar/v4/calendars
lark-cli api POST /open-apis/im/v1/messages --params '{"receive_id_type":"chat_id"}' --body'{"receive_id":"oc_xxx","msg_type":"text","content":"{\"text\":\"Hello\"}"}'
lark-cli api POST /open-apis/im/v1/messages --params '{"receive_id_type":"chat_id"}' --data'{"receive_id":"oc_xxx","msg_type":"text","content":"{\"text\":\"Hello\"}"}'
```
## Advanced Usage
@@ -250,6 +276,10 @@ We recommend using the Lark/Feishu bot integrated with this tool as a private co
Please fully understand all usage risks. By using this tool, you are deemed to voluntarily assume all related responsibilities.
## Star History
[](https://star-history.com/#larksuite/cli&Date)
## Contributing
Community contributions are welcome! If you find a bug or have feature suggestions, please submit an [Issue](https://github.com/larksuite/cli/issues) or [Pull Request](https://github.com/larksuite/cli/pulls).
lark-cli api POST /open-apis/im/v1/messages --params '{"receive_id_type":"chat_id"}' --body'{"receive_id":"oc_xxx","msg_type":"text","content":"{\"text\":\"Hello\"}"}'
lark-cli api POST /open-apis/im/v1/messages --params '{"receive_id_type":"chat_id"}' --data'{"receive_id":"oc_xxx","msg_type":"text","content":"{\"text\":\"Hello\"}"}'
fmt.Sprintf("strict mode is %q, user login is disabled in this profile",mode),
"if the user explicitly wants to switch to user identity, see `lark-cli config strict-mode --help` (confirm with the user before switching; switching does NOT require re-bind)")
}
opts.Ctx=cmd.Context()
ifrunF!=nil{
returnrunF(opts)
@@ -53,6 +60,7 @@ browser. Run it in the background and retrieve the verification URL from its out
"hint":fmt.Sprintf("Show verification_url to user, then immediately execute: lark-cli auth login --device-code %s (blocks until authorized or timeout). Do not instruct the user to run this command themselves.",authResp.DeviceCode),
})
fmt.Fprintln(f.IOStreams.Out,string(b))
}
encoder:=json.NewEncoder(f.IOStreams.Out)
encoder.SetEscapeHTML(false)
iferr:=encoder.Encode(data);err!=nil{
returnoutput.Errorf(output.ExitInternal,"internal","failed to write JSON output: %v",err)
}
returnnil
}
// Step 2: Show user code and verification URL
// Step 2: Show user code and verification URL.
// Both branches surface AgentTimeoutHint, but on different channels:
// JSON mode embeds it as a structured field (so an agent that captures
// stdout into a JSON parser sees it without stream-mixing surprises),
// text mode prints to stderr (alongside the URL prompt).
OpenURL:"Open this URL in your browser to authenticate:\n\n",
WaitingAuth:"Waiting for user authorization...",
AuthSuccess:"Authorization successful, fetching user info...",
LoginSuccess:"Login successful! User: %s (%s)",
GrantedScopes:" Granted scopes: %s\n",
OpenURL:"Open this URL in your browser to authenticate:\n\n",
WaitingAuth:"Waiting for user authorization...",
AgentTimeoutHint:"[AI agent] This command blocks for up to ~10 minutes while waiting for the user to authorize in their browser. Make sure your runner's timeout is ≥ 600s. If long timeouts are not supported, use `lark-cli auth login --no-wait --json` to get a device_code, then `lark-cli auth login --device-code <code>` to resume polling. **Do NOT retry with a short timeout** — each restart invalidates the previous device code, so any URL the user already authorized becomes useless.",
AuthSuccess:"Authorization confirmed, fetching user info and validating granted scopes...",
ScopeMismatch:"authorization result is abnormal: these requested scopes were not granted: %s",
ScopeHint:"The result above is the user's final confirmation for this authorization request. Do not retry continuously. Scopes may be not granted for various reasons, such as a scope being disabled. The specific reason has already been shown to the user on the authorization page. Run `lark-cli auth status` to inspect all scopes currently granted to the account.",
RequestedScopes:" Requested scopes: %s\n",
NewlyGrantedScopes:" Newly granted scopes: %s\n",
NoScopes:"(none)",
StatusHint:"Run `lark-cli auth status` to inspect all scopes currently granted to the account.",
HintHeader:"Please specify the scopes to authorize:\n",
# Interactive (terminal user) — TUI prompts for everything:
lark-cli config bind`,
RunE:func(cmd*cobra.Command,args[]string)error{
opts.langExplicit=cmd.Flags().Changed("lang")
ifrunF!=nil{
returnrunF(opts)
}
returnconfigBindRun(opts)
},
}
cmd.Flags().StringVar(&opts.Source,"source","","Agent source to bind from (openclaw|hermes); auto-detected from env signals when omitted")
cmd.Flags().StringVar(&opts.AppID,"app-id","","App ID to bind (required for OpenClaw multi-account)")
cmd.Flags().StringVar(&opts.Identity,"identity","","identity preset (bot-only|user-default); defaults to bot-only in flag mode (safer: no impersonation)")
cmd.Flags().BoolVar(&opts.Force,"force",false,"confirm a risky transition (currently: bot-only → user-default identity change in flag mode)")
cmd.Flags().StringVar(&opts.Lang,"lang","zh","language for interactive prompts (zh|en)")
returncmd
}
// configBindRun is the top-level orchestrator. Each step delegates to a named
// helper whose signature declares its contract; the body reads as the shape of
// the bind flow itself, not its mechanics.
funcconfigBindRun(opts*BindOptions)error{
iferr:=validateBindFlags(opts);err!=nil{
returnerr
}
// Decide TUI-vs-flag mode exactly once; every downstream branch reads
// opts.IsTUI instead of re-checking IOStreams.IsTerminal.
SelectSourceDesc:"lark-cli will read your %s app credentials from the selected Agent and apply them automatically.",
SourceOpenClaw:"OpenClaw — config: %s",
SourceHermes:"Hermes — config: %s",
// Args order (source, brand) matches the Chinese template; %[N]s lets the
// English reading order differ while the caller passes args in one order.
SelectAccount:"Multiple %[2]s apps configured in %[1]s — select one to continue.",
ConflictTitle:"Existing configuration found",
ConflictDesc:"lark-cli is already set up for %q:\n App ID: %s\n Brand: %s\n Config: %s",
ConflictForce:"Update config",
ConflictCancel:"Keep current config",
ConflictCancelled:"Current config kept. No changes made.",
MessageBotOnly:"Bound app %s to %s. The %s app (bot) identity is ready — you can now continue with the user's request.",
MessageUserDefault:"Bound app %s to %s. Next, in this %s chat, run `lark-cli auth login --recommend`. The command prints the verification URL to stderr and then blocks until the user authorizes it; relay the URL to the user so they can approve it in their own browser (do not call browser_navigate or any tool that opens a browser yourself — your browser is sandboxed and cannot complete the authorization). The command returns automatically once authorization completes.",
SelectIdentity:"How should the AI work with you?",
IdentityBotOnly:"As bot",
IdentityUserDefault:"As you",
IdentityBotOnlyDesc:"Works under its own identity in %s. Best for group chats, team notifications, and shared documents.",
IdentityUserDefaultDesc:"Works under your identity in %s, managing docs, messages, calendar, and more on your behalf. Personal use only.\n"+
"⚠️ Don't share this bot with others or add it to group chats. It has access to your personal %s data.",
BindSuccessHeader:"All set! lark-cli is now ready to use in %s.",
BindSuccessNotice:"Note: This is a one-time sync. To re-sync future changes, run `lark-cli config bind`",
IdentityEscalationMessage:"you are switching from bot-only to user-default — the AI will then act under your Feishu identity for all operations (docs, messages, calendar, etc.). ⚠️ Don't share this bot with others or add it to group chats. It has access to your personal Feishu data.",
IdentityEscalationHint:"if the user confirms the switch, re-run with --force: `lark-cli config bind --identity user-default --force`",
}
funcgetBindMsg(langstring)*bindMsg{
iflang=="en"{
returnbindMsgEn
}
returnbindMsgZh
}
// brandDisplay returns the UI-friendly product name for the given brand
// identifier and display language. "lark" maps to "Lark" in both zh and en.
// "feishu" (or empty / unknown) maps to "飞书" in zh and "Feishu" in en —
// this is the safe default when the brand hasn't been resolved yet (for
// example, on the pre-binding source-selection screen).
For AI agents: use --new to create a new app. The command blocks until the user
completes setup in the browser. Run it in the background and retrieve the
verification URL from its output.`,
verification URL from its output.
Inside an Agent context (OPENCLAW_HOME / HERMES_HOME set) this command
refuses by default — use 'lark-cli config bind' to bind to the Agent's
existing app instead of creating a parallel one. Pass --force-init only
if the user explicitly wants a separate app inside the Agent workspace.`,
RunE:func(cmd*cobra.Command,args[]string)error{
opts.Ctx=cmd.Context()
opts.langExplicit=cmd.Flags().Changed("lang")
iferr:=guardAgentWorkspace(opts);err!=nil{
returnerr
}
ifrunF!=nil{
returnrunF(opts)
}
@@ -59,10 +78,34 @@ verification URL from its output.`,
cmd.Flags().BoolVar(&opts.AppSecretStdin,"app-secret-stdin",false,"Read App Secret from stdin to avoid process list exposure")
cmd.Flags().StringVar(&opts.Brand,"brand","feishu","feishu or lark (non-interactive, default feishu)")
cmd.Flags().StringVar(&opts.Lang,"lang","zh","language for interactive prompts (zh or en)")
cmd.Flags().StringVar(&opts.ProfileName,"name","","create or update a named profile (append instead of replace)")
cmd.Flags().BoolVar(&opts.ForceInit,"force-init",false,"allow init inside an Agent workspace (OPENCLAW_HOME / HERMES_HOME); use config bind instead unless you really want a separate app")
returncmd
}
// guardAgentWorkspace refuses 'config init' when run inside an OpenClaw or
// Hermes Agent context, because the Agent has already provisioned an app
// and 'config bind' is the right tool for hooking lark-cli into it.
// Running init here would create a parallel app under the agent's workspace
// dir, breaking the binding the user actually wants. --force-init lets a
// human user override when they really do want a separate app.
Message:fmt.Sprintf("config init is refused inside %s context (would create a parallel app and shadow the existing %s binding)",ws.Display(),ws.Display()),
Hint:"see `lark-cli config bind --help` to bind lark-cli to the Agent's existing app instead. Pass --force-init only if the user explicitly wants a separate app in this workspace.",
}
}
// hasAnyNonInteractiveFlag returns true if any non-interactive flag is set.
constrealisticPermError=`API GET /open-apis/application/v6/applications/cli_XXXXXXXXXXXXXXXX/app_versions?lang=zh_cn&page_size=2 returned 400: {"code":99991672,"msg":"Access denied. One of the following scopes is required: [application:application:self_manage, application:application.app_version:readonly].应用尚未开通所需的应用身份权限:[application:application:self_manage, application:application.app_version:readonly],点击链接申请并开通任一权限即可:https://open.feishu.cn/app/cli_XXXXXXXXXXXXXXXX/auth?q=application:application:self_manage,application:application.app_version:readonly&op_from=openapi&token_type=tenant","error":{"message":"Refer to the documentation...","log_id":"20260421101203E2A5F141245B6F43B3A6"}}`
cmd.Flags().StringVar(&o.jqExpr,"jq","","JQ expression to filter output")
cmd.Flags().BoolVar(&o.quiet,"quiet",false,"Suppress informational messages on stderr")
cmd.Flags().StringVar(&o.outputDir,"output-dir","","Write each event as a file in this directory (relative paths only; absolute paths and ~ are rejected to prevent path traversal)")
cmd.Flags().IntVar(&o.maxEvents,"max-events",0,"Exit after N successful emits (0 = unlimited). Multi-worker EventKeys may emit up to workers-1 past N before all workers stop.")
cmd.Flags().DurationVar(&o.timeout,"timeout",0,"Exit after DURATION (e.g. 30s, 2m). 0 = no timeout. Timeout is a normal exit (code 0; stderr 'reason: timeout').")
cmd.Flags().String("as","auto","identity type: user | bot | auto (must match EventKey's declared AuthTypes)")
"grant these scopes and publish a new app version at: %s",
consoleScopeGrantURL(brand,appID,missing),
)
}
returnfmt.Sprintf(
"run `lark-cli auth login --scope \"%s\"` in the background. It blocks and outputs a verification URL — retrieve the URL and open it in a browser to complete login.",
strings.Join(missing," "),
)
}
// preflightEventTypes verifies every RequiredConsoleEvents entry is subscribed in the app's current published version.
// resolveSchemaJSON returns the final JSON Schema for an EventKey (reflected base, V2-wrapped for Native, overlay applied); orphans lists unresolved FieldOverrides pointers.
Long:"Display detailed information about an EventKey including type, events, parameters, and response schema. Use --json for machine-readable output.",
Args:cobra.ExactArgs(1),
RunE:func(cmd*cobra.Command,args[]string)error{
returnrunSchema(f,args[0],asJSON)
},
}
cmd.Flags().BoolVar(&asJSON,"json",false,"Emit the EventKey definition + resolved schema as JSON (for AI / scripts)")
Short:"Show event bus daemon status for all discovered apps",
Long:"Connect to each bus daemon under the config-dir/events/ tree and show PID, uptime, and active consumers. Use --current for only the current profile's app. Use --json for machine-readable output. Use --fail-on-orphan to exit 2 when any orphan bus is detected (for health checks).",
RunE:func(cmd*cobra.Command,args[]string)error{
returnrunStatus(f,current,asJSON,failOnOrphan)
},
}
cmd.Flags().BoolVar(&asJSON,"json",false,"Emit status as JSON (for AI / scripts)")
cmd.Flags().BoolVar(¤t,"current",false,"Only show status for the current profile's app")
cmd.Flags().BoolVar(&failOnOrphan,"fail-on-orphan",false,"Exit 2 when any orphan bus is detected (default: always exit 0)")
returncmd
}
typebusStateint
const(
stateNotRunningbusState=iota
stateRunning
stateOrphan
)
func(sbusState)String()string{
switchs{
casestateRunning:
return"running"
casestateOrphan:
return"orphan"
default:
return"not_running"
}
}
// appStatus bundles one AppID's derived status; State picks which fields are meaningful.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.