Every failure on the authentication, authorization, and configuration
path now surfaces as a typed structured error instead of an ad-hoc
envelope. Users and scripts that consume CLI output get:
- a fixed nine-category taxonomy on the wire, each mapped to a
stable shell exit code (authentication/authorization/config = 3,
network = 4, internal = 5, policy = 6, confirmation = 10)
- identity-aware detail fields (missing_scopes, requested_scopes,
granted_scopes, console_url, log_id, retryable, hint) carried
uniformly on the envelope
- a single canonical policy envelope at exit 6; the legacy
auth_error carve-out is retired
- per-subtype canonical message + hint that preserves Lark's
diagnostic phrasing and routes recovery to the right actor:
app developer (app_scope_not_applied), user (missing_scope,
token_scope_insufficient, user_unauthorized), or tenant admin
(app_unavailable, app_disabled)
- wrong app credentials classify as config/invalid_client whether
surfaced by the Open API endpoint (99991543) or the tenant
access-token mint endpoint (10003 / 10014), instead of
collapsing to a transport error or api/unknown
- local shortcut scope preflight emits the same
authorization/missing_scope envelope (identity + deterministic
missing-scope set) used by the post-call permission path, so AI
consumers read the same structured shape from precheck and from
server-returned permission denial
- streaming download/upload failures keep the same network subtype
split (timeout / TLS / DNS / transport) as the non-stream path
instead of collapsing every cause to a generic transport failure
- console_url is carried only on the bot-perspective
app_scope_not_applied envelope (where the recovery action is
"developer applies the scope at the developer console"); the
user-perspective missing_scope envelope drops the field, since
the only actionable user recovery is `lark-cli auth login --scope`
and pointing an end user at a console they cannot modify is
misleading
- bind workflows (Hermes / OpenClaw / lark-channel) flatten dynamic
Type tags to wire 'config' with the original module name kept
as a metric label
All 10 typed errors are cause-bearing, nil-safe on .Error() and
.Unwrap(), and defensively clone slice setter inputs. Four lint
rules (CheckNilSafeError / CheckBuilderImmutable / CheckUnwrapSymmetry
/ CheckBuildAPIErrorArms) lock these invariants on migrated paths.
The six TestDriveInspectExecute_* tests set
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) but build the CLI via
cmdutil.TestFactory(t, cfg), which provides an in-memory config closure
(func() (*core.CliConfig, error) { return config, nil }) and never reads the
filesystem. Per the repo learning from PR #343, this env var should only be
set for tests exercising the real NewDefault() factory path. None of these
tests use NewDefault(), so the calls are dead and removed.
No behavior change; all TestDriveInspect* tests still pass.
Co-authored-by: kyalpha313 <kyalpha313@users.noreply.github.com>
Introduce a typed error contract framework for lark-cli so in-process
Go callers can branch via errors.As(&errs.XxxError{}) and shell scripts,
AI agents, and protocol adapters can branch on stable JSON type/subtype
fields instead of regex-parsing free-form messages.
Adds:
- Canonical taxonomy under errs/ (9 categories + typed Error structs
embedding a shared Problem, RFC 7807-aligned)
- Centralized Lark code metadata + identity-aware BuildAPIError dispatch
- Typed JSON envelope writer alongside the legacy envelope writer
- MCP / OAuth (RFC 6750 Bearer) projection adapters
- Five CI lint guards preventing ad-hoc taxonomy drift
Backward compatibility: legacy *output.ExitError producers (ErrAPI,
ErrWithHint, Errorf, ErrBare) and business shortcuts that use them
continue to render the legacy envelope unchanged. SecurityPolicyError
wire format and exit code are preserved via a carve-out; taxonomy
migration is deferred to PR 2. Domain-specific business migration is
staged across PR 3+.
Framework-direct paths now return typed *errs.*Error: ErrAuth /
ErrValidation / ErrNetwork emit category literals on the wire
(authentication / validation / network), *core.ConfigError is promoted
at the cmd/root boundary with exit code aligned from 2 to 3, and Lark
API permission denials classified by BuildAPIError exit 3.
At the SDK boundary, WrapDoAPIError preserves any already-classified
error (legacy *output.ExitError or typed *errs.*) so output.ErrAuth
from missing credentials surfaces with the auth category and exit 3
intact instead of being downgraded to a network error. Policy responses
classified by BuildAPIError (codes 21000 / 21001) extract challenge_url
and the canonical hint from the response body, matching what the
auth transport already surfaces at the HTTP layer; non-https
challenge URLs are dropped.
First PR in the feat/error-contract-* series.
When a resource is created with bot identity, the CLI attempts to
auto-grant full_access to the current user. If the user open_id is
missing or the grant API call fails, the result was only written to
the JSON permission_grant field and easily overlooked.
Changes:
- Add stderr warnings when auto-grant is skipped or fails
- Add 'hint' field to permission_grant JSON output with failure reason
and actionable next step (e.g. auth login, check scope, retry)
- Add end-to-end skipped/failed tests across all affected shortcuts
(doc, drive, sheets, slides, wiki, markdown, base)
Closes#963
docs +search is in maintenance and will be removed; cloud-space resource
discovery is consolidated onto drive +search. Two related doc/help fixes:
1. Redirect guidance: docs +search -> drive +search
- skill-template/domains/{doc,sheets}.md
- lark-base/SKILL.md: --filter '{"doc_types":["BITABLE"]}' -> --doc-types bitable
- lark-sheets/SKILL.md: body + frontmatter description, add drive-search ref link
Same server API, equivalent capability; only flattens the entry from
nested --filter JSON to flags. reference links repointed to lark-drive.
2. Fix creator_ids/--mine semantic: creator -> owner
The server matches creator_ids (incl. --mine / --creator-ids) by owner
(document owner), not original creator, despite the OpenAPI field name.
- shortcuts/drive/drive_search.go: --help Desc and Tip
- lark-drive/references/lark-drive-search.md: identity section, params, rules, examples
- lark-drive/SKILL.md: top-level guidance
- lark-doc/references/lark-doc-search.md: creator_ids usage note (now self-consistent)
Wire field name creator_ids kept (aligned with the server).
Docs/help strings only, no logic change; gofmt / go vet / package build pass.
Change-Id: If3ebf5a247b7e38b58050c677dc888a310f1c6b6
Switch `drive +export --file-extension markdown` from the legacy V1
GET /open-apis/docs/v1/content API to the V2
POST /open-apis/docs_ai/v1/documents/{token}/fetch API for
higher-quality Lark-flavored Markdown output.
- Update DryRun and Execute paths to use V2 endpoint with JSON body
- Add docx:document:readonly scope for the new API
- Validate V2 response structure (fail fast on missing document/content)
- Encode token in URL path via validate.EncodePathSegment
- Update unit tests and add V2 response validation error path tests
- Add E2E dry-run test for markdown export path
- Update skill documentation
* feat(drive): add +inspect shortcut for document URL inspection with wiki unwrapping
Implements #662: `lark-cli drive +inspect --url <url>` inspects any
Lark/Feishu document URL to get its type, title, and canonical token,
with automatic wiki URL unwrapping via get_node API.
- Add ParseResourceURL (inverse of BuildResourceURL) in common
- Extract FetchDriveMetaTitle as public shared helper
- Add drive +inspect shortcut with wiki unwrapping support
- Add skill reference docs and update SKILL.md
- Dry-run E2E tests for docx URL, wiki URL, and bare token
* refactor: move host validation from ParseResourceURL to +inspect
ParseResourceURL is a general-purpose URL parser that should not
hardcode domain lists — future Lark domains would silently break.
Move isLarkHost/larkHostSuffixes to drive_inspect.go where host
validation is a business decision of the +inspect command.
Add E2E test for non-Lark host with Lark-like path.
* refactor: remove host validation from +inspect
Lark supports custom enterprise domains, so a hardcoded suffix list
can never be exhaustive and would falsely reject valid URLs.
Path-based matching in ParseResourceURL is sufficient; invalid URLs
will fail naturally at the API call stage.
- +node-get: wrap wiki.spaces.get_node; accepts node_token, obj_token,
or a Lark URL (URL path auto-infers obj_type); formatted output with
creator / updated_at. No synthesized url — get_node returns none and a
BuildResourceURL fallback is a non-canonical link that misleads in a
read/confirm command (sibling read shortcuts omit it too)
- +node-delete: wrap space.node delete; high-risk-write (--yes gated),
async delete-node task polling, auto-resolves space_id via get_node
when --space-id omitted, actionable hints for codes 131011 / 131003.
The delete-node task result lives under the gateway's generic
`simple_task_result` key (NOT `delete_node_result`)
- +space-create: wrap spaces.create; user-only identity, --name
required (no empty-name spaces), flattened space output, no url
- factor the shared wiki async-task poll loop into wiki_async_task.go;
preserve upstream Lark Detail.Code on poll exhaustion (no longer
rebuilt via lossy ErrWithHint)
- drive +task_result: add wiki_delete_node scenario so +node-delete's
async-timeout next_command actually resolves
- skill docs: reference pages for the 3 new shortcuts + SKILL.md
shortcuts table (no raw nodes.delete API exists — it's shortcut-only,
so it is intentionally absent from API Resources / permission table);
drop the circular TestWikiShortcutsIncludeAllCommands change-detector
Change-Id: I316f78290cec5bc50f80d629173e3bf2a35dd005
Bidirectional sync between a local directory and a Drive folder with
diff detection (new_local, new_remote, modified, unchanged) and
conflict resolution strategies (--on-conflict: remote-wins, local-wins,
keep-both, ask).
Key behaviors:
- Type conflict detection: hard-fail when local file vs remote non-file
or local directory vs remote file
- Keep-both: rename local with __lark_<hash> suffix, then pull remote;
occupied map includes localDirs to prevent suffix collision
- Local-wins partial-success: prefer returned file_token on upload failure
- Empty directory mirroring: pre-create local dirs on Drive via
drivePushWalkLocal before scope preflight
- Structured errors throughout (output.Errorf / output.ErrWithHint)
Includes unit tests and E2E tests (dry-run + live workflow).
* fix(drive): preserve parent token on nested overwrite
Ensure drive +push overwrite requests for nested files keep parent_node aligned with the actual remote parent folder and report parent resolution failures explicitly.
* test(drive): cover nested overwrite push workflow
Add a live drive +push workflow case for overwriting a nested remote file so the PR parent-token fix is exercised against the real backend and verified to converge via +status.
* 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(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(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.
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.
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
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
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
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.
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: 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>
* 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>