mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 09:11:44 +08:00
feat/sidecar-remote-https
254 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7c54f9b023 |
feat(drive): switch markdown export to V2 docs_ai fetch API (#948)
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
|
||
|
|
e6bc292575 |
fix(identitydiag): harden verify path and tighten status semantics (#961)
* fix(identitydiag): harden verify path and tighten status semantics Follow-ups to #957: - bound bot/user verify calls with a 10s timeout (mirrors the doctor endpoint probe) so a hanging server cannot wedge `auth status --verify` or `doctor` - return StatusNotConfigured (not StatusMissing) when the user-identity path is blocked by missing app config, matching the bot side - surface the `{code, msg}` envelope on bot-info HTTP 4xx responses so callers see why bot auth was rejected, not just the bare HTTP code - introduce identity{User,Bot,None} constants in cmd/auth/status.go and use the exported StatusMessage() in the human-readable note instead of raw status codes like "not_configured" - collapse the duplicated verify-failed identity construction in the user path into a local helper - cover the new failure paths with unit tests (HTTP 4xx with envelope, business error code, user server-rejected, expired user token, strict-mode user-only, missing app config for user) Change-Id: I581348a65f15b1452a6f48a3e3245d09257314ac * fix(identitydiag): decode bot/v3/info from "bot" field, not "data" `/open-apis/bot/v3/info` returns `{code, msg, bot: {...}}` — the bot payload is under `bot`, not `data` as the newer Lark API convention would suggest. The decoder was reading from a non-existent `data` field, so `envelope.Data.OpenID` was always empty and every successful verify was reported as `Bot identity: verify failed: open_id is empty`. The pre-existing test mocks used `{"data": {...}}` matching the buggy decoder, so unit tests passed while production reads of every Lark account failed verification. Fix: - change the JSON tag on the envelope from `json:"data"` to `json:"bot"` - update mocks in identitydiag and cmd/auth/status tests to emit `bot` Verified locally: `lark-cli doctor` now reports `bot_identity: pass` for both a normal account and a bot-only profile, restoring the behavior that #957 set out to deliver. Change-Id: Ib26dfdd5a0cc37d2d62537ae2bf5e854e67cb83c * fix(shortcuts/common): decode bot/v3/info from "bot" field, not "data" Same schema bug as the one fixed in identitydiag — `RuntimeContext. fetchBotInfo` reads from a non-existent "data" key, so every successful call would report "open_id is empty" once a caller starts depending on it. There are no production callers of `RuntimeContext.BotInfo()` yet (only tests + the `TestNewRuntimeContextWithBotInfo` helper), so this bug is dormant — but the pre-existing tests pass with the same wrong schema in their mocks, so the first real consumer would silently break. Fix: tag `json:"data"` → `json:"bot"` plus aligning the four mock fixtures in runner_botinfo_test.go. The Go field name `Data` is kept to minimize the diff; only the JSON contract is corrected. Change-Id: I11e1e871603e5349f8df29b1d58e35d07b628dfd |
||
|
|
4aa61db8b2 |
feat(drive): add +inspect shortcut for document URL inspection with wiki unwrapping (#947)
* 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. |
||
|
|
28c66be199 |
fix(wiki): surface real node url for +node-create / +node-copy (#960)
* fix(wiki): surface real node url for +node-create / +node-copy The create-node and copy-node OpenAPI responses carry a real `url` field (present in practice though absent from the documented schema). Both shortcuts ignored it: +node-create synthesized a link via BuildResourceURL, and +node-copy emitted no URL at all. Parse `url` into the shared wikiNodeRecord and add a wikiNodeURL helper that prefers the response url, falling back to BuildResourceURL only when it is blank. Wire +node-create and +node-copy to the helper so both surface the canonical link when available. Change-Id: I0ca5f91b02c24e81d083793e6a8e4f8c966aeec3 * refactor(wiki): move wikiNodeURL to shared wiki_helpers.go The helper is consumed by both +node-create and +node-copy, so its placement should reflect the broader usage rather than living in the create command's file. Pure move; no behavior change. Change-Id: I9990c12da042f631fe2519911c6a9d663fd5c22b |
||
|
|
0e70b056f8 |
feat(mail): bot+mailbox=me validation and dynamic --as help tests (#895)
* feat(mail): bot+mailbox=me validation and dynamic --as help tests Add validateBotMailboxNotMe helper to shortcuts/mail/helpers.go and wire it as a Validate callback into +message, +messages, +thread and +triage, so bot identity combined with the default --mailbox me is rejected early with a clear fixup hint instead of a late opaque API error. The --as help text was already dynamic via AddShortcutIdentityFlag; add TC-10/TC-11 tests in internal/cmdutil/identity_flag_test.go to pin that behaviour, and TC-1 through TC-9 in shortcuts/mail/mail_shortcut_validation_test.go to cover the new Validate callbacks. +watch is excluded: its AuthTypes is ["user"], so bot is never valid. sprint: S2 * test(cmdutil): add Hidden and DefValue assertions to identity flag tests * fix(mail): add bot+mailbox=me validation to +template-create and +template-update * fix(mail): add bot+mailbox=me validation to +template-update * fix(mail): gofmt mail_template_create.go * fix(mail): gofmt mail_template_update.go * fix(mail): skip bot+mailbox=me check for print-patch-template local path |
||
|
|
e511404065 |
feat(mail): expose draft priority in --inspect projection and document --set-priority (#779)
Add a Priority field to DraftProjection populated from the EML header pair X-Cli-Priority (CLI/OAPI primary) → X-Priority (RFC fallback for IMAP-回灌 historical drafts), with case-insensitive lookup via the existing headerValue helper and a local mapping table aligned with the backend gopkg/mail_priority.PriorityValueToType vocabulary. When neither header is present (the symmetric read of --set-priority normal=remove_header) the projection emits "unknown" so agents have a stable read-side surface. Append one notes entry to buildDraftEditPatchTemplate documenting the --set-priority flag and the X-Cli-Priority translation contract. The write-side (--set-priority flag, parsePriority helper, translation branch in mail_draft_edit.go, EML header target) is unchanged — already shipped on master. sprint: S4 |
||
|
|
3354494579 | fix: address Base attachment review follow-ups (#958) | ||
|
|
2bb69d1942 |
feat: support Base attachment APIs (#887)
* feat: support base attachment APIs * fix: handle duplicate base attachment downloads * fix: remove unused attachment token helper |
||
|
|
c4fb7006d2 |
feat(wiki): add +node-get / +node-delete / +space-create shortcuts (#904)
- +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 |
||
|
|
583349e572 | fix(docs): clarify replace_all selection errors (#954) | ||
|
|
de00343063 |
feat: add markdown +patch shortcut (#857)
* feat: add markdown +patch shortcut Change-Id: I8159941ff9dec4e5cbf0c757ec19ee172b302224 * fix: align markdown patch validation and dry-run Change-Id: I98079901e980b74998938afc4917b91a79689948 |
||
|
|
df4b657737 |
feat(drive): add +sync workflow for Drive directories (#873)
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). |
||
|
|
4b721c0410 |
fix(sheets): explicitly document safe JSON unmarshal ignore in DryRun (#935)
Two DryRun functions in the sheets shortcuts called json.Unmarshal without checking the return value. This looks like a bug, but Validate already parses and validates the same --style / --data JSON before DryRun runs, so the error is structurally impossible at this point. Use _ = assignment + comment to silence the unchecked-error lint warning and make the safety invariant explicit to future readers. Co-authored-by: KhanCold <KhanCold@users.noreply.github.com> |
||
|
|
241952459d |
feat: add drive version shortcut (#841)
Change-Id: I87bb32c86e3c3362f541ccc6320c656eb795ec9b |
||
|
|
33c292c05e |
feat(extension): Plugin / Hook framework with command pruning (#910)
* feat(extension): introduce Plugin / Hook framework with command pruning
Add a single public extension contract under extension/platform: integrators
implement the Plugin interface and register Observers, Wrappers, Lifecycle
handlers, and pruning Rules through the Registrar in one Install call.
Command pruning:
- Rule (Allow / Deny / MaxRisk / Identities) with doublestar globs
- 4-axis AND evaluation, parent-group aggregation, unknown-risk allow
- Sources: Plugin.Restrict (single-rule) and ~/.lark-cli/policy.yml
- Plugin path is fail-closed (envelope on rule error / multiple Restrict);
yaml path is fail-open (warning, CLI continues)
- strict-mode stubs now also write the denial annotation so the hook
layer's denial guard physically isolates Wrap chains on them
- HOME path never leaked through policy_source label
Hook framework:
- Observer (panic-safe, Before/After), Wrapper (middleware, may short-circuit
via AbortError), Lifecycle (Startup + Shutdown only)
- Recover guards every plugin entry point: Capabilities(), Install(),
Wrapper factory composition AND inner Handler, Lifecycle handlers
- namespacedWrap copies AbortError so a plugin's package-level sentinel
is never mutated across concurrent invocations
- Selector unknown-risk uniform: ByExactRisk / ByWrite / ByReadOnly never
match unannotated commands; safety-side hooks opt in via
ByWrite().Or(ByUnknownRisk())
Bootstrap orchestration (cmd/build.go + cmd/policy.go):
- InstallAll uses a staging Registrar + atomic commit
- FailClosed plugin install / Plugin.Restrict conflict / Startup handler
failure each install a structured envelope guard at every dispatch path
- walkGuard neutralises every cobra bypass we know of (PersistentPreRunE
first-wins, ValidateArgs, ParseFlags, legacyArgs, __complete /
__completeNoDesc, non-runnable groups, required-arg subcommands)
- cmd/root.go::Execute calls hook.Emit(Shutdown, runErr) after
rootCmd.Execute; isCompletionCommand skips both __complete and
__completeNoDesc so Tab completion never triggers Shutdown handlers
Capabilities consistency:
- Restricts=true must declare FailurePolicy=FailClosed
- RequiredCLIVersion (semver constraint) is validated against build.Version;
a malformed constraint is treated as untrusted-config and aborts
unconditionally, regardless of FailurePolicy (DEV builds included)
JSON envelope contract:
- error.type closed enum: pruning / strict_mode / hook / plugin_install /
plugin_conflict / plugin_lifecycle
- reason_code closed enums per type, all referenced by structured tests
Bootstrap surfaces (new user commands):
- lark-cli config policy show -- JSON view of the active Rule + source
- lark-cli config policy validate -- parse + schema + glob check, no apply
Coverage:
- extension/platform: every public type has a unit test
- internal/{pruning,hook,platformhost,policydecision,cmdmeta}: full coverage
of denial guard isolation, AbortError sentinel safety, observer panic
safety, lifecycle error/panic typing, staging atomic rollback
- cmd/plugin_integration_test.go: end-to-end through buildInternal with
synthetic and real command trees
- cmd/install_guard_test.go: walkGuard covers auth / config / __complete /
__completeNoDesc / non-runnable parents
* fix(pruning): deny stub must override Args + PersistentPreRunE
The pruning denyStub and the strict-mode stub previously only swapped
RunE plus Hidden + DisableFlagParsing. Cobra's dispatch order means
several pre-RunE gates can fire BEFORE the stub's RunE ever runs:
1. Args validator: shortcut commands often declare cobra.NoArgs.
With DisableFlagParsing=true the user's `--doc xxx --mode append`
looks like positional args, so ValidateArgs surfaces a usage
error instead of the pruning / strict_mode envelope. Observer
hooks also miss the dispatch entirely.
2. Parent PersistentPreRunE: cmd/auth/auth.go declares a
PersistentPreRunE that returns external_provider when env
credentials are set. Cobra's "first PersistentPreRunE wins
walking up from the leaf" then short-circuits with
external_provider instead of the leaf's denial envelope.
Both stubs now also set:
- Args = cobra.ArbitraryArgs (bypass gate 1)
- PersistentPreRunE = no-op leaf hook (bypass gate 2)
- PreRunE / PreRun / PersistentPreRun = nil (defensive)
Effect: dispatch reaches the wrapped RunE, observers fire, the real
pruning / strict_mode envelope is emitted regardless of credential
provider or flag count.
Adds regression tests covering both gates on both stub paths.
* fix(config): policy subcommand bypasses parent's credential check
cmd/config/config.go::NewCmdConfig declares a PersistentPreRunE that
calls f.RequireBuiltinCredentialProvider; with env credentials set,
it returns external_provider for every config subcommand.
`config policy show` and `config policy validate` are READ-ONLY
diagnostic commands -- they inspect or parse the user-layer rule
without touching credentials. They MUST work regardless of which
credential provider is active, otherwise users on env-credential
deployments cannot debug their policy.
Same shape as the codex C11/C13 fix: install a no-op leaf-level
PersistentPreRunE on the `policy` group so cobra's "first walking up
from leaf" rule picks ours over the config parent's.
Regression caught by divergent e2e (F1-F6 all returned external_provider
before this fix; all pass after). Adds a unit test pinning the
PersistentPreRunE override.
* feat(shortcuts): tag service groups with cmdmeta.Domain
RegisterShortcutsWithContext now calls cmdmeta.SetDomain on each
service-level cobra.Command (im, docs, drive, calendar, ...) so the
business-domain axis is actually populated on every shortcut leaf via
parent-chain inheritance.
Before this change, platform.ByDomain("docs") never matched any
command: the domain annotation was unset across the entire shortcut
tree, so the selector's d != "" guard always failed and risk-style
selectors silently degraded to no-op.
The SetDomain call is placed AFTER the create-or-reuse branch so it
fires whether the service command was freshly created here or had
already been added by cmd/service/service.go's OpenAPI auto-
registration (which runs first and creates im, drive, calendar, etc.).
Without this placement only pure-shortcut services like docs would
have been tagged.
Adds a regression test asserting:
- service-group cobra.Command carries the cmdmeta.domain annotation
- leaf shortcuts inherit the domain via parent-chain walk
* feat(diagnostic): add unconditionally allowed command paths for introspection
* feat(plugins): add diagnostic command to inspect installed plugins and their contributions
* fix(cli): surface unknown_subcommand error instead of silent help fallback
When a user passed an unknown subcommand or shortcut (e.g. `lark-cli drive
+bogus`), cobra returned `flag.ErrHelp` for the non-runnable group command,
printed the parent help, and exited 0. AI agents couldn't distinguish a
typo from an intentional help request.
Install a tree-wide guard that attaches a RunE to every group command
without its own Run/RunE. The RunE forwards no-args invocations to help
(preserving prior behavior) and emits a structured unknown_subcommand
ExitError (exit 2) listing available subcommands when args are present.
* refactor(envelope): rename error.type pruning/strict_mode to command_denied
The envelope's `type` field was leaking implementation terms ("pruning",
"strict_mode") that describe enforcement mechanism rather than the user-
facing semantic. It also duplicated `detail.layer`, and forced consumers
to branch on two values for the same conceptual error ("a command was
denied by policy").
Collapse both into a single semantic type "command_denied". The
enforcement layer ("pruning" / "strict_mode") is preserved in
`detail.layer` so debugging and per-layer diagnostics still work.
* feat(platform): fail closed on unannotated/invalid risk when a Rule is active
The pruning engine used to treat any command without a risk annotation as
ALLOW even when a Rule with MaxRisk was set, and would silently skip the
MaxRisk comparison whenever the command's risk string was outside the
closed taxonomy. Both gaps let an unannotated or typo'd write command
slip past an "agent read-only" pruning rule.
Engine now denies before any other axis when a Rule is registered:
- reason_code "risk_not_annotated" for commands with no risk
- reason_code "risk_invalid" for commands whose risk is outside
the read | write | high-risk-write
taxonomy (e.g. typo "wrtie")
Main-flow is preserved: a nil Rule still returns Allowed=true
unconditionally, so a CLI with no pruning plugin behaves identically to
before. ByUnknownRisk() is removed from the public surface since the
Unknown state is no longer reachable through risk-based selectors when
any Rule is active; safety-side widening composition is no longer needed.
* chore(config): hide diagnostic policy/plugins commands from --help
`config policy show`, `config policy validate`, and `config plugins show`
are local-introspection-only commands kept behind the pruning
diagnostic whitelist so operators can always inspect why a command was
denied. They do not need to surface in `--help` for AI agents and were
contributing to help noise.
Hide the `policy` and `plugins` parent groups and both `show` /
`validate` leaves. Commands remain callable by exact name and continue
to bypass user-layer pruning via diagnosticPaths.
* style: gofmt
* fix(platform): nil Selector honours None contract; reject multi-doc policy yaml
- selector.go: And/Or/Not now treat nil Selector as None() per godoc,
preventing runtime panic when composed selectors are invoked.
- schema.go: Parse rejects multi-document YAML input so a stray '---'
separator can't silently drop trailing policy constraints.
* chore: go mod tidy
* feat(extension/platform): plugin SDK with policy engine, hooks, and Builder
Introduces extension/platform — the in-process plugin SDK external
Go forks of lark-cli use to extend or restrict the command surface.
Plugins compile in via blank import; there is no dynamic loading
and no RPC isolation.
Public SDK (extension/platform):
- Plugin interface (Name / Version / Capabilities / Install).
- Registrar verbs: Observe, Wrap, On, Restrict.
- Hook types: Observer (side-effect, panic-safe, fires Before/After
RunE), Wrapper (middleware, may short-circuit via AbortError),
LifecycleHandler (Startup / Shutdown), Selector with nil-safe
And/Or/Not composition.
- Risk / Identity are defined string types with closed taxonomies;
ParseRisk / ParseIdentity convert raw strings with the
absent-vs-invalid distinction the engine relies on.
- Builder ergonomic constructor (NewPlugin().Observer().Wrap()
...MustBuild()) that enforces name/hookName grammar, hookName
uniqueness, and the Restrict ↔ FailClosed pairing regardless of
call order.
- Invocation is a read-only interface; the framework's concrete
invocation type lives in internal/hook so plugins cannot
fabricate denial / strict-mode / identity state. Args() returns
a defensive copy on every call so hook mutation cannot leak
into the original RunE.
- CommandDeniedError + AbortError carry structured fields for the
closed `command_denied` / `hook` envelope contract.
- ResetForTesting gated behind //go:build testing.
- README + godoc examples (Observer / Wrapper / Restrict) + two
runnable example forks (audit-observer, readonly-policy).
Host (internal/platform, internal/hook, internal/cmdpolicy):
- InstallAll: staged plugin registration with atomic commit, panic
isolation, FailOpen / FailClosed semantics, RequiredCLIVersion
semver check, single-Restrict invariant, duplicate-plugin-name
detection.
- hook.Install wraps every runnable cmd.RunE with:
Before observers (panic-safe) → denial guard → composed Wrap
chain → original RunE → After observers (always fire, even on
err). Denied commands physically bypass the Wrap chain so a
plugin Wrapper cannot suppress or rewrite a denial; observers
still see the attempt for audit.
- Recover shim around plugin Wrappers converts panics (including
the factory call) into a structured `hook` envelope with
reason_code=panic; namespacing shim attributes AbortError to
the namespaced hook name.
- cmdpolicy (renamed from internal/pruning) is the user-layer
command policy engine: walks the cobra tree, evaluates each
runnable command against a Rule's four-axis filter (Allow /
Deny / MaxRisk / Identities), produces parent-group aggregate
denials, and installs denyStubs. Rule.AllowUnannotated opts out
of the unannotated-deny gate for gradual adoption; risk_invalid
typos always deny with an edit-distance "did you mean"
suggestion.
- Strict-mode stub in cmd/prune.go composes the shared
detail.* / wrapped CommandDeniedError shape via cmdpolicy
helpers (BuildDenialError / CommandDeniedFromDenial /
DenialDetailMap), so command_denied envelopes from strict-mode
and user-layer policy carry the same closed-enum fields
(detail.layer / reason_code / policy_source). The historical
short Message + independent Hint are preserved unchanged.
- cmdpolicy/yaml: structural parsing of ~/.lark-cli/policy.yml
with KnownFields strict mode, including allow_unannotated.
- `config policy show` / `config policy validate` and the plugin
inventory diagnostic surface the resolved Rule (allow,
deny, max_risk, identities, allow_unannotated) and the hook
contributions per plugin.
Envelope contract (docs/extension/reason-codes.md):
- error.type is a closed set: command_denied, hook, plugin_install,
plugin_conflict, plugin_lifecycle.
- reason_code is a closed enum per error.type, dispatched on by
external agents and CI integrations.
- detail.layer = "policy" | "strict_mode" attributes the rejection.
Build / CI:
- Makefile unit-test / vet / coverage and ci.yml fast-gate +
unit-test + coverage now pass -tags testing so register_testing.go
is visible; ./extension/... is in the package list so the SDK's
own tests actually run.
- fmt-check and examples-build Makefile targets.
- bmatcuk/doublestar/v4 added as a direct dependency for `**` glob
matching in Rule.Allow / Rule.Deny.
Author-facing material:
- docs/extension/ (quickstart, plugin-author-guide, reason-codes)
is provided in the working tree but kept out of git tracking
per repo convention (.gitignore covers docs/).
Change-Id: I3b8ecc2923bd54c2dff19e5dce8a0855a6f9e703
* feat(extension/platform): plugin SDK with policy engine, hooks, and Builder
Introduces extension/platform — the in-process plugin SDK external
Go forks of lark-cli use to extend or restrict the command surface.
Plugins compile in via blank import; there is no dynamic loading
and no RPC isolation.
Public SDK (extension/platform):
- Plugin interface (Name / Version / Capabilities / Install).
- Registrar verbs: Observe, Wrap, On, Restrict.
- Hook types: Observer (side-effect, panic-safe, fires Before/After
RunE), Wrapper (middleware, may short-circuit via AbortError),
LifecycleHandler (Startup / Shutdown), Selector with nil-safe
And/Or/Not composition.
- Risk / Identity are defined string types with closed taxonomies;
ParseRisk / ParseIdentity convert raw strings with the
absent-vs-invalid distinction the engine relies on.
- Builder ergonomic constructor (NewPlugin().Observer().Wrap()
...MustBuild()) that enforces name/hookName grammar, hookName
uniqueness, and the Restrict ↔ FailClosed pairing regardless of
call order.
- Invocation is a read-only interface; the framework's concrete
invocation type lives in internal/hook so plugins cannot
fabricate denial / strict-mode / identity state. Args() returns
a defensive copy on every call so hook mutation cannot leak
into the original RunE.
- CommandDeniedError + AbortError carry structured fields for the
closed `command_denied` / `hook` envelope contract.
- ResetForTesting gated behind //go:build testing.
- README + godoc examples (Observer / Wrapper / Restrict) + two
runnable example forks (audit-observer, readonly-policy).
Host (internal/platform, internal/hook, internal/cmdpolicy):
- InstallAll: staged plugin registration with atomic commit, panic
isolation, FailOpen / FailClosed semantics, RequiredCLIVersion
semver check, single-Restrict invariant, duplicate-plugin-name
detection.
- hook.Install wraps every runnable cmd.RunE with:
Before observers (panic-safe) → denial guard → composed Wrap
chain → original RunE → After observers (always fire, even on
err). Denied commands physically bypass the Wrap chain so a
plugin Wrapper cannot suppress or rewrite a denial; observers
still see the attempt for audit.
- Recover shim around plugin Wrappers converts panics (including
the factory call) into a structured `hook` envelope with
reason_code=panic; namespacing shim attributes AbortError to
the namespaced hook name.
- cmdpolicy (renamed from internal/pruning) is the user-layer
command policy engine: walks the cobra tree, evaluates each
runnable command against a Rule's four-axis filter (Allow /
Deny / MaxRisk / Identities), produces parent-group aggregate
denials, and installs denyStubs. Rule.AllowUnannotated opts out
of the unannotated-deny gate for gradual adoption; risk_invalid
typos always deny with an edit-distance "did you mean"
suggestion.
- Strict-mode stub in cmd/prune.go composes the shared
detail.* / wrapped CommandDeniedError shape via cmdpolicy
helpers (BuildDenialError / CommandDeniedFromDenial /
DenialDetailMap), so command_denied envelopes from strict-mode
and user-layer policy carry the same closed-enum fields
(detail.layer / reason_code / policy_source). The historical
short Message + independent Hint are preserved unchanged.
- cmdpolicy/yaml: structural parsing of ~/.lark-cli/policy.yml
with KnownFields strict mode, including allow_unannotated.
- `config policy show` / `config policy validate` and the plugin
inventory diagnostic surface the resolved Rule (allow,
deny, max_risk, identities, allow_unannotated) and the hook
contributions per plugin.
Envelope contract (docs/extension/reason-codes.md):
- error.type is a closed set: command_denied, hook, plugin_install,
plugin_conflict, plugin_lifecycle.
- reason_code is a closed enum per error.type, dispatched on by
external agents and CI integrations.
- detail.layer = "policy" | "strict_mode" attributes the rejection.
Build / CI:
- Makefile unit-test / vet / coverage and ci.yml fast-gate +
unit-test + coverage now pass -tags testing so register_testing.go
is visible; ./extension/... is in the package list so the SDK's
own tests actually run.
- fmt-check and examples-build Makefile targets.
- bmatcuk/doublestar/v4 added as a direct dependency for `**` glob
matching in Rule.Allow / Rule.Deny.
Author-facing material:
- docs/extension/ (quickstart, plugin-author-guide, reason-codes)
is provided in the working tree but kept out of git tracking
per repo convention (.gitignore covers docs/).
Change-Id: I3b8ecc2923bd54c2dff19e5dce8a0855a6f9e703
* refactor(policy): remove validate command and update diagnostics
* fix(extension/platform): address PR review must-fix items
- cmdpolicy: skip AnnotationPureGroup commands in EvaluateAll,
aggregateParents, and hasRunnableDescendant so user-layer policy
no longer blocks `<group> --help` after the unknown-subcommand
guard attaches RunE to every parent
- cmd/root: tag guarded parent groups with AnnotationPureGroup
- extension/platform: drop `//go:build testing` from register_testing.go
so `go test ./...` works without an extra build tag
- extension/platform/README: inline reason_code reference, fix plugin
lifecycle diagram order (init/Register precede RegisteredPlugins)
- cmd/platform_bootstrap: route userPolicyPath through
core.GetBaseConfigDir so LARKSUITE_CLI_CONFIG_DIR is honoured
- cmdpolicy: add RedactHomeDir helper, fold base config dir and
$HOME prefixes for config policy show + resolver errors
- internal/platform: reject unrecognised FailurePolicy values with
invalid_capability instead of silently fail-open
- cmd/config: surface diagnostic policy/plugins commands in
`config --help` Long text
- CHANGELOG: document command_denied error.type rename and
unknown_subcommand exit-2 behavior change
* fix(extension/platform): address CodeRabbit review comments + CI gofmt
- hook/install: propagate wrapper-injected ctx to invokeOriginal so
RunE/Run see context values added by upstream Wrappers
- hook/testing: SetStderrForTesting returns a restore func; tests now
defer it via t.Cleanup to avoid cross-test sink leakage
- cmdpolicy/active: deep-copy ActivePolicy.Rule on SetActive/GetActive
so callers can't mutate the stored global through shared slices
- platform/inventory: deep-copy Inventory + nested Plugins / HookEntry
/ RuleView slices on SetActiveInventory / GetActiveInventory
- platform/staging: Restrict clones the plugin-supplied Rule before
retaining it so the plugin can't mutate it after Install returns
- platform/version: reject RequiredCLIVersion with more than three
numeric components instead of silently truncating 1.2.3.4 to 1.2.3
- cmd/platform_bootstrap: clear cmdpolicy.SetActive on yaml resolver
error so config policy show doesn't surface a stale rule
- cmd/platform_bootstrap_test: tmpHome pins LARKSUITE_CLI_CONFIG_DIR
so host env can't bleed into the policy test fixtures
- cmdpolicy/apply: installDenyStub returns bool; Apply count no longer
over-reports when strict-mode short-circuits the install
- cmdpolicy/engine: aggregateParents now returns the runnable hybrid's
own denial status when all children are placeholder branches
- cmdpolicy/resolver_test: use t.TempDir()-rooted missing path instead
of hardcoded /nonexistent for hermetic missing-file assertion
- cmd/config/plugins: empty-inventory branch emits total: 0 so the
JSON schema stays stable across populated/empty cases
- cmd/platform_guards_test: select leaf by RunE != nil (not Runnable)
so the test doesn't nil-deref on Run-only commands
- gofmt run on previously committed cmdpolicy/path*.go (CI fast-gate)
* fix(cmdpolicy): replace filepath.Abs with filepath.Clean for lint policy
The depguard / forbidigo rule blocks filepath.Abs in internal/ on the
grounds that it accesses the filesystem (Getwd) directly. Switch
RedactHomeDir + foldPrefix to operate on filepath.Clean strings; real
callers pass already-absolute paths (resolver builds yamlPath via
filepath.Join on the absolute config root), so the redaction outcome
is unchanged for production inputs. Relative inputs fall through to
the unchanged branch — filepath.Rel rejects the mixed-absoluteness
case with an error, which the foldPrefix helper already treats as
"not a hit".
* refactor(cmdpolicy): pure Resolve + drop path redaction & verbose comments
- Resolve becomes a pure function; I/O moves to LoadYAMLPolicy so
precedence selection can be unit-tested without vfs mocks
- ActivePolicy drops YAMLPath; config policy show JSON loses yaml_path
and yaml_shadowed (and the TOCTOU stat that surfaced them)
- RedactHomeDir and path_test.go removed: the home-dir folding was only
earning its keep through the now-deleted yaml_path field
- cmd/build.go bootstrap block trimmed from 71 to 39 lines by cutting
PR-rationale comments; one note kept for the fail-CLOSED-vs-fail-OPEN
business rule
- cmd/config/config.go: parent Long no longer hard-codes hidden command
hints, matching their Hidden:true intent
Change-Id: Icfbb818ce3ef523c63286bfbed34c49be08ed6a2
* refactor(platform): drop StrictMode/Identity from Invocation interface
These two accessors were documented in the public SDK as "After observers
always see ok=true" but the framework never plumbed values to them, so they
always returned ("", false). Zero internal/example/test callers; a plugin
author trusting the doc would silently get wrong behaviour.
Identity is also fundamentally unsuited for Before observers (per-command
identity resolves inside RunE via f.AuthFor, after Before fires). StrictMode
is a global value better placed on a Framework/Environment interface than
per-Invocation. Removing is non-breaking now (no callers); adding later is
non-breaking too.
Change-Id: Ice200543e9bca3bda759ad98a6e34a56df69e915
* fix(prune): preserve original metadata on strict-mode denial stubs
strictModeStubFrom built a fresh *cobra.Command from scratch, dropping
the original command's annotations (risk_level, lark:supportedIdentities,
cmdmeta.domain) and help text. cobraCommandView is a live proxy walking
parent annotations, so after the Remove+Add replacement, audit observers
firing on a strict-mode-denied command saw Cmd().Risk()=("",false) and
Cmd().Identities()=nil -- breaking the first-class use case for
audit/compliance plugins.
Copy child.Annotations into the stub (stamping the denial annotations on
top) and propagate Short/Long for help-text parity with
cmdpolicy/apply.go::installDenyStub, which preserves these by virtue of
mutating in place.
Regression test asserts risk_level / supportedIdentities / Short / Long
all survive replacement, alongside the denial annotations.
Change-Id: I19810a34575996344b63e839066888c154d69335
* chore(platform): align docs with implementation; fold home in yaml warnings
Followup cleanup to the previous three refactor commits, addressing review
fallout where public docs / examples / contract notes still pointed at
deleted symbols or unimplemented designs:
- cmd/build.go: Build() docstring now mentions the plugin install + Startup
emit side effects; Shutdown only fires on Execute path
- extension/platform/doc.go, lifecycle.go, invocation.go: drop references
to the deleted StrictMode/Identity methods, restore minimal Godoc on
Cmd/Args/Started
- extension/platform/view.go, cmd/platform_bootstrap.go,
internal/hook/install.go: rewrite "snapshot before pruning" promise to
match the actual contract (live view + strict-mode stub metadata
preservation)
- cmd/platform_guards_test.go: stubInvocation drops the two old methods
- cmd/platform_bootstrap.go: redactHome() last-mile folds $HOME -> ~ in
warnPolicyError so an os.PathError carrying the absolute policy path
does not leak the user's home dir to stderr / agent / CI logs
- examples/readonly-policy/README.md: drop yaml_path from the sample
`config policy show` envelope (the field was removed in
|
||
|
|
ca6c6c3e29 | fix: mark base field update high risk (#936) | ||
|
|
5778adfefa |
fix(drive): preserve parent token on nested overwrite (#908)
* 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. |
||
|
|
7400226e34 |
feat(doc): add --width/--height flags to docs +media-insert (#832)
* feat(doc): add width/height params to buildBatchUpdateData Extend buildBatchUpdateData signature with width and height int params. When mediaType is "image" and either dimension is positive, the value is included in the replace_image payload. Existing call sites pass 0, 0. * feat(doc): add --width/--height flags with validation to docs +media-insert * feat(doc): add aspect-ratio auto-calculation helpers Add computeMissingDimension (pure ratio math) and detectImageDimensions (header-only image.DecodeConfig) with PNG/JPEG/GIF blank-import decoders, plus imageDimensions struct; drive with two new TDD tests. * feat(doc): wire --width/--height into Execute with aspect-ratio calculation * feat(doc): add best-effort dimension computation to DryRun * docs: add --width/--height to docs +media-insert SKILL.md * fix: add SafeInputPath validation to detectImageDimensionsFromPath * fix: guard computeMissingDimension against division by zero and add rounding * fix: add dimension upper bound, fix err variable reuse in Execute * refactor: use early-return guard for zero native dimensions per review * fix: add pixels unit to dimension validation error messages * fix: surface dimension detection failures in dry-run to match Execute behavior * fix: move dimension detection before upload to fail fast * fix: restore withRollbackWarning on dimension detection errors in Execute Dimension detection runs after the placeholder block is created (Step 2), so failures must clean up the block to avoid leaving an empty placeholder in the document. |
||
|
|
f03138b9f0 |
feat(wiki): add +space-list / +node-list / +node-copy shortcuts (#392)
Introduce three new wiki shortcuts that wrap the corresponding raw APIs
with structured flags, formatted output, my_library alias handling, and
unified envelope shape, replacing the bare `lark-cli wiki spaces list`
/ `wiki nodes list` / `wiki nodes copy` flows for the common cases.
Shortcuts
- wiki +space-list (read, scopes: wiki:space:retrieve):
lists wiki spaces. Default fetches a single page; --page-all walks
every page capped by --page-limit (default 10, 0 = unlimited).
Supports --page-size / --page-token / --format json|pretty|table|csv|ndjson.
Output: {spaces, has_more, page_token} + Meta.Count. Pretty mode
distinguishes "no spaces" from "empty page with has_more" and hints
the caller to resume.
- wiki +node-list (read, scopes: wiki:node:retrieve):
lists nodes in a space or under a parent. Same pagination + format
story as +space-list. Accepts the my_library alias for --space-id
with --as user (resolved via a shared resolveMyLibrarySpaceID helper
extracted from +node-create); rejects my_library upfront for --as bot.
- wiki +node-copy (high-risk-write, scopes: wiki:node:copy):
copies a node into a target space or parent. --target-space-id and
--target-parent-node-token are mutually exclusive. Risk is marked
high-risk-write to match the upstream API's danger: true flag, so the
framework requires --yes. Source is preserved; subtree is copied.
Both list shortcuts pick the narrowest scope the upstream API accepts.
The framework's preflight (internal/auth/scope.go MissingScopes) does
exact-string scope matching, so declaring the broader wiki:wiki:readonly
form would wrongly reject tokens that carry only the per-API scope —
which the API itself accepts — and emit a misleading missing-scope hint.
Shared changes
- shortcuts/wiki/wiki_node_create.go: factor out resolveMyLibrarySpaceID
so +node-list and +node-create share one my_library resolution path.
- shortcuts/wiki/shortcuts.go: register the three new shortcuts.
- skills/lark-wiki/SKILL.md and references/lark-wiki-{space,node-list,
node-copy}.md: documentation for the new shortcuts.
Tooling
- scripts/check-doc-tokens.sh + Makefile gitleaks target:
pre-commit check that scans skill reference docs for realistic-looking
Lark token values without the _EXAMPLE_TOKEN placeholder convention,
preventing gitleaks false positives.
- .gitleaks.toml: allowlist tuning.
- .gitignore: ignore .tmp/.
Tests
- shortcuts/wiki/wiki_list_copy_test.go: unit tests covering registry
membership, declared-narrow-scope pinning, flag validation (page-size
range, page-limit >= 0, target flag exclusivity, my_library + bot
rejection), auto-pagination merging, --page-limit truncation
surfacing next cursor, --page-token single-page mode, empty-slice
serialisation, has_more hint pretty rendering, my_library user-path
resolution, +node-copy copy-to-space / copy-to-parent + body shape,
pretty rendering, and the high-risk-write --yes gate.
- tests/cli_e2e/wiki/wiki_shortcut_workflow_test.go: live end-to-end
workflow exercising the shortcut layer against a real tenant.
Reuses an existing my_library node as a host so the test never adds
to the top-layer quota; the copy is placed under the same host node.
- tests/cli_e2e/wiki/coverage.md: shortcut coverage entries added.
Minor cleanups
- skills/lark-doc/references/lark-doc-search.md and
skills/lark-minutes/references/lark-minutes-search.md: replace
realistic-looking example ou_ tokens with _EXAMPLE_ placeholders so
scripts/check-doc-tokens.sh passes.
Change-Id: I9efb0557f477d369d7f26a09c1e154d4ab15b253
Co-authored-by: liujinkun <liujinkun@bytedance.com>
|
||
|
|
52e0129078 | feat(drive): add quick mode to status diff (#870) | ||
|
|
1c2d3d7679 |
docs: update lark-doc skill description (#890)
Change-Id: I77e2ae690b8976e37f69ae5d581fccc13917ec5e |
||
|
|
0d20f88453 |
feat: support file-token overwrite and version output for drive +upload (#885)
Change-Id: I76c334578fc2fa5cfd2eedb4525b0d9d735f610e |
||
|
|
a54a879330 |
feat(im): add --exclude-muted to +chat-search and new +chat-list (#820)
Add im +chat-list shortcut wrapping GET /open-apis/im/v1/chats (previously not exposed via lark-cli). Add --exclude-muted to both +chat-search and +chat-list: client-side filter that calls POST /open-apis/im/v1/chat_user_setting/batch_get_mute_status after each page and drops is_muted=true chats. Introduce shortcuts/im/mute_filter.go with pure helpers and an orchestrator (MaybeApplyMuteFilter) shared by both shortcuts. Change-Id: I22221ac5835667f58cbd40b34de75825d2445d1c |
||
|
|
f1aa7d8f42 | feat(drive): add modified-time smart sync mode (#859) | ||
|
|
5e0ac02f08 |
feat(im): add --chat-mode topic to +chat-create (#790)
Adds --chat-mode group|topic to lark-cli im +chat-create so users and AI agents can create 话题群 (topic chats) directly via the CLI. Without this, requests to create a topic chat silently fall back to a normal conversation group. Default remains group; chat_mode is now always emitted in the POST /open-apis/im/v1/chats request body. Change-Id: I79385e2e8606f84e3f27de240d1b41037bf51261 |
||
|
|
1180baac61 |
feat(mail): add unknown-flag fuzzy-match for lark-cli mail domain (#806)
Adds shortcuts/mail/flag_suggest.go (~120 LOC) implementing a cobra
FlagErrorFunc hook for the mail subcommand tree. On 'unknown flag: --X'
or 'unknown shorthand flag: "X" in -X', it collects flags from the
current command via cmd.Flags().VisitAll, runs bidirectional prefix
match + Levenshtein DP (threshold=max(1,len/3+1), cap 4), and returns
top-5 candidates inside the existing ErrorEnvelope JSON:
error.type = "unknown_flag"
error.detail.{unknown, command_path, candidates}
error.detail.candidates[*] = {flag, shorthand, distance, reason}
Exit code stays 1 (ExitAPI), not ExitValidation - no breaking change for
CI/agent scripts that check non-zero exit. stderr switches from plain
'Error: unknown flag: --X' to JSON envelope, aligning with the existing
'errors = JSON envelope on stderr' convention; mail unknown-flag was the
last gap.
Scope is strictly the mail subcommand tree: shortcuts/register.go gains
a single 'if service == "mail" { mail.InstallOnMail(svc) }' branch
after the existing Mount loop. Other domains (calendar / im / api /
auth / ...) keep cobra's default FlagErrorFunc and unchanged plain-text
stderr behavior.
Covers:
- shortcuts/mail/flag_suggest.go (new, ~120 LOC)
- shortcuts/mail/flag_suggest_test.go (new, 12 table-driven tests)
- shortcuts/register.go (+3 lines after mail Mount loop)
No changes to cmd/root.go or internal/output/* - ErrDetail.Detail is
already interface{}, handleRootError already routes *ExitError via
WriteErrorEnvelope.
|
||
|
|
db1a3fc0a6 |
feat(vc): add agent meeting join, leave, and events shortcuts (#824)
* feat(vc): agent join meeting basic shortcuts structure Change-Id: Ic5d64067eb48670fa6636841cd00cbfa9b0bf3e7 * docs: add skill references for vc +meeting-join and +meeting-leave * feat(vc): add meeting events shortcut Add vc +meeting-events for bot meeting activity queries with page-all pagination support and tested pretty/json output. * feat(vc): refine meeting events pagination and output * test: add unit tests for vc +meeting-join and +meeting-leave shortcuts * feat(vc): improve meeting events pretty timeline * feat(vc): refine meeting events pretty output * docs(skill): add vc meeting events shortcut guide * docs(skill): clarify vc meeting events output guidance * docs: clarify participant-snapshot vs meeting-events routing * refactor: split lark-vc-agent from lark-vc * docs: drop nonexistent workflow skill reference and fix identity * docs: fix cross-links in lark-vc-agent references after split * fix(vc): send meeting join password at top level * docs: rewrite lark-vc-agent description in user-facing language * docs: tighten lark-vc-agent description to descriptive neutral tone * fix: use Chinese quotes in vc/vc-agent description YAML frontmatter * docs: downgrade dry-run from mandatory to optional for vc-agent writes * docs: clarify pretty vs json format choice by processing depth * docs: systematic review of lark-vc-agent SKILL for clarity and precision * feat(vc): print meeting event page token in pretty output * docs(skill): refine vc agent meeting guidance * revert: restore CRITICAL banner in lark-vc-agent to match repo convention * docs: replace inaccurate no-replay warning with real social-cost risk * docs: tighten meeting-join risk warning to single sentence * docs: tighten vc-agent references - remove redundancy and fix vague wording * Revert "docs: tighten vc-agent references - remove redundancy and fix vague wording" This reverts commit 9845fc40622c65b0811da1c9ae4902434377f33e. * docs(skill): refine vc meeting events paging guidance * fix(vc): keep meeting event count aligned with events list * docs(skill): tighten vc agent meeting events workflow * refactor(vc): simplify meeting events pagination * docs(skill): tighten vc agent meeting guidance * docs(skill): require reading shared docs for meeting summaries * chore(env): switch default feishu endpoints to pre * fix(env): use feishu accounts host * docs(vc): use explicit date in recording example * revert(env): remove default ppe request header * chore(env): switch default feishu endpoints to pre * docs(skill): guide users to early-bird group on agent meeting gray miss Teach the lark-vc-agent skill to recognize OAPI's new gray-miss signal for the three agent meeting commands (`+meeting-join`, `+meeting-leave`, `+meeting-events`) and route the user to the early-bird group instead of treating it as a permission error. When CLI stderr JSON returns `error.code=20017 / ErrNotInGray`, the agent renders the fixed early-bird invite link `https://go.larkoffice.com/join-chat/2f4nb0e1-fe00-4f67-bed7-25beaf533fbd`. The user manual is intentionally not surfaced yet. Scope-related errors still follow the existing `auth login --scope` flow with no early-bird copy mixed in. lark-shared and other skills are not touched, so the guidance stays scoped to the agent meeting commands only. * chore(env): switch endpoints to boe for agent meeting gray testing * chore(vc-agent): update gray guide and boe endpoints * docs(vc-agent): refine gray guidance flow * docs(vc-agent): centralize gray guidance * fix(ci): stabilize vc output and skill frontmatter * fix(vc): address review feedback --------- Co-authored-by: zhaolei.vc <zhaolei.vc@bytedance.com> Co-authored-by: renaocheng <renaocheng@bytedance.com> |
||
|
|
4ba39ef392 | fix(drive): handle duplicate remote sync paths (#803) | ||
|
|
0ed63b02e4 |
chore(doc): inject docs scene into v2 requests (#808)
Change-Id: I4f23880e24164c8b229a5403942bfa1b7ddb0ce6 |
||
|
|
16f1a0f320 |
feat: add flag shortcuts for im (#770)
Add IM flag shortcut commands to lark-cli, enabling users to create, list, and cancel bookmarks on messages and threads via +flag-create, +flag-list, and +flag-cancel. Change-Id: I8f87f0eadf83fb59b024a3b9fe67b23d363abe0a |
||
|
|
a81d07ca4f | fix: clean base error detail output (#783) | ||
|
|
e754b3bc1b |
feat(im): add message_app_link to IM message outputs (#668)
- 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 |
||
|
|
90757887b2 |
whiteboard-update as "write" risk (#775)
Change-Id: Iacc4d349b44337813392d75f4f0ec67718074efc |
||
|
|
b65147f208 | fix: migrate task shortcut errors from bare fmt.Errorf to structured output.Errorf/ErrValidation (#740) | ||
|
|
d317493e49 |
fix: add url to markdown +create output (#753)
Change-Id: I4fa870415bbad76f721f8aa170180e83fd20281b |
||
|
|
a8f078478e |
docs: refine field update conversion guidance (#748)
* docs: refine field update conversion guidance * docs: refine field update conversion rules * docs: adjust field update conversion allowlist |
||
|
|
06275415b1 |
feat(task): add upload task attachment shortcut (#736)
* feat(task): add upload task attachment shortcut Change-Id: I668bf3d856baa6e35ed982a33c4bf4d03b924f4b * feat(task): update SKILL.md adding resource_type description Change-Id: I3ef1aba33ee22e8b03e6f59bc2fb64f55a742270 |
||
|
|
b4c9c09de0 |
feat(base): support batch record get and delete (#630)
* 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 |
||
|
|
7fb71c6947 |
feat(sheets): add sheet management shortcuts (#722)
* feat(sheets): add sheet management shortcuts - add +create-sheet, +copy-sheet, +delete-sheet, and +update-sheet - cover request-shape dry-run and sheet workflow tests - document new sheet management shortcuts in lark-sheets skill * docs(sheets): consolidate lark-sheets reference docs |
||
|
|
020aeb87ad |
feat(drive): pre-flight 10000-rune total cap for +add-comment reply_elements (#605)
* 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>
|
||
|
|
cfd89e0e28 |
feat(doc): warn when callout uses type= without background-color (#467)
* 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. |
||
|
|
ac4c34f2ad |
feat: support file-name for drive export (#685)
* feat: support file-name for drive export * test: cover drive export file-name metadata |
||
|
|
3ed691b25c |
feat(base): add markdown output for record reads (#726)
* 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 |
||
|
|
30ad38d4b6 |
feat(drive): add +pull shortcut for one-way Drive → local mirror (#696)
* 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. |
||
|
|
f27b8fdf40 |
feat: add markdown shortcuts and skill docs (#704)
Change-Id: Iced88525deb10b014b755ec68bd9a8ae6a935143 |
||
|
|
c100ca049e |
feat(cmdutil): support @file for params and data (#724)
* 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 |
||
|
|
4d68e09537 |
feat(drive): add +push shortcut for one-way local → Drive mirror (#709)
* 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.
|
||
|
|
a3bbe00ee0 |
feat(drive): add +status shortcut for content-hash diff (#692)
* 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. |
||
|
|
0250054a90 |
feat(minutes): add media upload shortcut (#725)
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> |
||
|
|
d7ee5b5769 |
feat: guide lark-doc v2 usage (#710)
## 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
|