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