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:.