@lark-project/meegle 1.0.20 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,69 @@ versioned section on each npm release.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [v1.0.22] - 2026-08-27
12
+
13
+ ### Fixed
14
+
15
+ - Restored the Facade command-string Go SDK to its MCP-only contract: local CLI API commands such as `ai-handoff` and `preference handoff` remain available in the npm-distributed `meegle` CLI but are no longer registered or routed by the remote RPC SDK.
16
+
17
+ ## [v1.0.21] - 2026-08-26
18
+
19
+ ### Added
20
+
21
+ - Added an interactive startup update notifier for npm-distributed CLI installs. At most once every 24 hours it checks the latest npm version, summarizes released `Added` and `Changed` entries from the GitHub CHANGELOG, and offers an arrow-key choice between immediate upgrade (recommended and selected by default) or a 24-hour deferral. Immediate upgrade installs the latest CLI first, then best-effort upgrades the Meegle Agent Skill through the existing install-wizard path; an unavailable Skill installer or failed Skill upgrade cannot fail the completed CLI upgrade. Non-interactive/CI, piped-output, install, and shell-completion invocations are skipped, and `MEEGLE_NO_UPDATE_CHECK=1` disables the check explicitly.
22
+ - Added non-MCP `ai-handoff availability` and `ai-handoff create-link` commands to both the CLI binary and programmatic command-string SDK. Availability reads the Handoff section of generic CLI config discovery; expected business rejections use `reject_code`/`reject_msg`, while dependency and transport failures use the standard error model. Successful config snapshots are cached per profile for up to 1 hour. Link creation accepts typed Project/WorkItemType/WorkItem/View/MeasureChart entities, always re-validates server-side, and returns an AI assistant URL.
23
+ - Handoff Config, Preference, and Create Link failures now surface the gateway `x-tt-logid` response header as `meta.logid` in the structured error envelope, so production-package failures can be traced without enabling or exporting debug logs.
24
+ - Successful Handoff Config, Preference, and Create Link calls now retain the same response-header LogID in result metadata, exposing it as `meta.logid` only when `--envelope` is requested while keeping default output unchanged.
25
+ - Create-link responses always include `available`: success returns HTTP 200 with `available=true` and the generated `url`; expected business rejection returns HTTP 200 with `available=false`, `reject_code`, and `reject_msg` and invalidates the local config cache. Unexpected failures continue to use the standard API error model.
26
+ - Added `preference handoff auto|ask|off`, backed by the server-side unified user preference service. The generic batch write API uses `type=handoff_suggestions` with a type-owned `{"mode":"off|ask|auto"}` JSON-string payload and reports only write success; current values are read through CLI config discovery. A successful write invalidates the local CLI config cache.
27
+ - Added the local `MEEGLE_AI_HANDOFF=disabled` hard-disable. It makes `availability` and valid `create-link` invocations return `available=false` with `reject_code=LOCAL_DISABLED` without requiring authentication, reading cached availability, or calling the Handoff API.
28
+ - Added compile-time enterprise CLI extensions without requiring a repository fork: public `cmd.Execute` / `cmd.ExecuteWithVersion` entry points, Credential providers, a single Transport interceptor, and Platform plugins for command observation, wrapping, lifecycle hooks, and restrictions.
29
+ - Added `meegle extension doctor|credentials|transport|plugins|policy|discovery` diagnostics that expose non-secret registration, compatibility, selector, rule, transport-baseline, and isolated dynamic-tool metadata.
30
+ - Added standalone no-extension and enterprise binaries under `examples/`, including public-module build and end-to-end MCP/OAuth/governance coverage.
31
+ - Added wire-level `tools/list` metadata parsing so previously unknown MCP tools with `metadata.resource` and `metadata.method` become executable dynamic commands in both CLI and SDK registries.
32
+ - Dynamic discovery now isolates malformed, oversized, duplicate, flag-conflicting, and static-command-shadowing tools per entry; known fallback paths remain immutable, help text is sanitized, Registry rebuild state is published coherently, and SDK callers can inspect skipped entries through `Client.DiscoveryIssues()`.
33
+ - Unknown tools without metadata now report a stable `missing_mapping` discovery issue in both CLI and SDK diagnostics instead of being silently omitted.
34
+ - Nullable JSON Schema parameter types such as `["string", "null"]` now remain available in CLI and SDK discovery; unions with multiple non-null types report `unsupported_schema_union` without affecting valid sibling tools.
35
+
36
+ ### Changed
37
+
38
+ - Renamed the Handoff commands to `ai-handoff availability` and `ai-handoff create-link`; the earlier plus-prefixed forms are no longer registered.
39
+ - `ai-handoff create-link` keeps the CLI/facade `user_query/related_context` contract and the CLI flags `--query`/`--related-context`; facade converts it to the AI `query/entities` contract internally. Public context payloads use business identifiers (`project_key`, `work_item_type_key`, `work_item_id`, `view_id`, `chart_id`) instead of exposing AI's generic `key`; View keeps `work_item_type_key` optional.
40
+ - Successful `ai-handoff create-link` responses now replace the returned URL host with the active CLI login host while preserving the scheme, path, query, and fragment.
41
+ - Expanded local-command help with full `meegle` usage paths, required-flag markers, behavior/parameter details, and examples; `meegle inspect` now includes local and nested commands such as `ai-handoff create-link` and `preference handoff auto`.
42
+
43
+ ### Security
44
+
45
+ - Transport extensions apply a 30-second timeout to provider and hook callbacks without shortening the caller-owned MCP, OAuth, or attachment request lifetime. They retain a 10-redirect limit and HTTPS downgrade protection, including when inherited redirect callbacks mutate both the destination and redirect history. Requests rejected before reaching the base transport now close their bodies, and extension callback panics are converted to controlled failures.
46
+ - MCP requests using a custom token header remove stale static credential headers and reject cross-origin redirects against an immutable source snapshot, so credentials cannot be duplicated or copied to another HTTPS origin.
47
+ - Hand-written Platform plugins that declare command restrictions with a fail-open policy now fail CLI startup before `Install` runs instead of silently disabling all of that plugin's policy rules.
48
+ - Platform metadata/Install and Startup callbacks now have a two-second safety boundary. Fail-open timeouts do not block later plugins, fail-closed timeouts stop execution, and timed-out Install callbacks cannot commit late registrations.
49
+ - Transport pre-hooks that replace a request Body now release both the original and replacement streams; post-hook TLS snapshots deep-clone certificate objects so extensions cannot mutate live response metadata.
50
+ - Extension startup and runtime failures retain safe `errors.Is` / `errors.As` matching without reflecting callback causes or panic values into public output; callback errors whose custom `Is`, `As`, `Unwrap`, `Error`, or payload methods panic are contained at Credential, Transport, Platform, formatter, and process-entry boundaries, while runtime and explicit abort failures expose stable structured error codes.
51
+
52
+ ### Compatibility
53
+
54
+ - Broken profile variables now produce `CONFIG_ENV_UNRESOLVED` for dynamic business commands with or without a discovery cache, `inspect --profile` consistently uses the selected profile, and extension diagnostics report frozen resolution states instead of re-running providers.
55
+
56
+ - The no-extension wrapper is regression-tested against the official binary, and the official binary is checked against a pinned `main@6326b7d` contract for legacy help, version, completion, authentication status, output, and exit-code behavior. CLI extensions remain isolated from SDK clients.
57
+ - Restrict policy denials honor explicit structured output modes and expose the stable `CLIENT_COMMAND_DENIED` error envelope.
58
+ - The legacy `--version` flag is routed through the governed `version` command, and destructive WBS publish/reset operations are classified as `high-risk-write` for enterprise policy enforcement.
59
+ - Generated open-source trees are required to pass both `go build ./...` and `go test ./...`; source-only sync tooling and its tests are excluded from the published repository together.
60
+
61
+ ### Fixed
62
+
63
+ - Tool discovery and server-side CLI configuration caches now share one profile-aware JSON file cache with atomic replacement, preventing concurrent CLI processes from exposing partially written cache files.
64
+ - Remote catalog resources can no longer shadow locally owned root commands such as `ai-handoff` and `preference` and abort the complete CLI startup; conflicting tools are isolated as `reserved_path` discovery issues while unrelated dynamic commands remain available.
65
+ - Map Facade invalid-parameter envelopes, including handoff query/context limit violations, to non-retryable `HANDOFF_API_INVALID_PARAM` errors and replace internal biz error IDs/causes/chains with a concise user-safe message and availability hint.
66
+ - Credential and Platform failures raised before CLI App construction now honor explicit JSON/NDJSON output and retain their stable error codes; the published readonly enterprise policy keeps `extension/**` diagnostics available for troubleshooting.
67
+ - Broken `${VAR}` profile placeholders no longer lock users out of help, version, login help, or configuration repair commands; credential-dependent business commands still fail with `CONFIG_ENV_UNRESOLVED`. Known local/recovery commands also defer Credential Provider resolution, so slow or unavailable OIDC providers no longer block them, while dynamic business commands remain fail-closed on ordinary errors, timeouts, and `BlockError`. A literal `--version` consumed as another flag's value is no longer rewritten as the version command. `dev` builds still reject non-empty `RequireCLI` constraints but now point enterprise developers to `ExecuteWithVersion` in the compatibility error chain.
68
+ - Transport extensions no longer cancel successful HTTP requests when `RoundTrip` returns: delayed `tools/list` and `tools/call` bodies remain readable until closed, invalid URLs produced by pre-hooks fail closed instead of panicking, and post-hooks receive body-free metadata snapshots so timeouts release the live response immediately without a response-body race or connection leak.
69
+ - Credential-bearing MCP requests now freeze the exact original origin for both default Bearer and custom token headers, preventing HTTPS downgrade, port-change, cross-domain forwarding, and multi-hop redirect-history mutation from leaking credentials.
70
+ - Credential redirect guards now retain the standard 10-hop limit even when they install a custom redirect callback; all JSON-RPC responses are bounded before decoding (`tools/list` at 8 MiB, other calls at 32 MiB); Transport post-hooks receive a cloned TLS connection state instead of a pointer into the live response.
71
+ - Multiple Restrict rules now compose as cumulative constraints; wrappers cannot return success after skipping, delaying, or duplicating `next`; a token-only Credential provider cannot hide a broken built-in configuration; first-run setup uses the Profile selected by the Credential provider; first-run completion is treated as success by Shutdown hooks and process entries without hiding fail-closed Shutdown failures; and spaced `RequireCLI` comparators such as `>= 1.2.0` are accepted.
72
+ - Wrappers can no longer turn a non-nil downstream command error into exit 0 by ignoring the result of synchronous or awaited asynchronous `next` calls.
73
+
11
74
  ## [v1.0.20] - 2026-08-20
12
75
 
13
76
  ### Changed
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  Command-line tool for [Meegle](https://meegle.com?utm_source=github&utm_medium=readme&utm_campaign=meegle_cli) ([Lark Project](https://project.feishu.cn?utm_source=github&utm_medium=readme&utm_campaign=meegle_cli)). Manage work items, schedules, and data from your terminal — no browser needed.
10
10
 
11
- [Install](#installation) · [Quick Start](#quick-start-human-users) · [Agent Skill](#ai-agent-skill) · [Commands](#commands) · [Auth](#authentication) · [Config](#configuration) · [Security](#security--risk-warnings) · [Contributing](#contributing)
11
+ [Install](#installation) · [Quick Start](#quick-start-human-users) · [Agent Skill](#ai-agent-skill) · [Commands](#commands) · [Enterprise Extensions](#enterprise-cli-extensions) · [Auth](#authentication) · [Config](#configuration) · [Security](#security--risk-warnings) · [Contributing](#contributing)
12
12
 
13
13
  ## Why Meegle CLI?
14
14
 
@@ -17,6 +17,7 @@ Command-line tool for [Meegle](https://meegle.com?utm_source=github&utm_medium=r
17
17
  - **Two-Layer Parameters** — Ergonomic `--flag-name` for everyday use, fallback `--params <json>` for complex payloads like `fields[]` — pick the right granularity per call
18
18
  - **Flexible Output** — `json` / `table` / `ndjson` / `raw`, with `--select` dot-path projection for piping to other tools
19
19
  - **Secure by Default** — OS keychain credential storage, `${VAR}` env-var templating so secrets never land in config files, multi-profile switching for staging / prod
20
+ - **Enterprise-Extensible** — Build a company distribution without forking this repository by importing the public `cmd`, Credential, Transport, and Platform packages at compile time
20
21
 
21
22
  ## Features
22
23
 
@@ -55,6 +56,26 @@ npx @lark-project/meegle@latest install
55
56
 
56
57
  The wizard installs or upgrades the CLI globally, installs the AI Agent Skill, configures the Meegle host, and starts login.
57
58
 
59
+ ### Automatic update prompt
60
+
61
+ The npm-distributed CLI checks npm for a newer version at most once every 24 hours when it starts in an interactive terminal. When an update is available, it reads the released `Added` and `Changed` entries between the installed and latest versions from the [CHANGELOG](https://github.com/larksuite/meegle-cli/blob/main/CHANGELOG.md#changed), then shows an interactive prompt:
62
+
63
+ ```text
64
+ ✨ Meegle CLI update available: v<current> → v<latest>
65
+
66
+ What's new:
67
+ v<version>
68
+ • Added: <feature summary>
69
+
70
+ ❯ Update now (recommended)
71
+ Remind me later
72
+ Use ↑/↓ to select, then press Enter
73
+ ```
74
+
75
+ Pressing Enter first upgrades the CLI with `npm install -g @lark-project/meegle@latest`, then makes a best-effort attempt to upgrade the Meegle Agent Skill through the same installer used by `meegle install`, and finally continues the original command. A missing or incompatible Skill installer, or a Skill download failure, never changes a successful CLI upgrade into a failure. Choosing **Remind me later** defers the next check for 24 hours. This lightweight update does not reconfigure the host or trigger login; use `meegle install` when you want the full setup wizard.
76
+
77
+ The check is skipped for non-interactive/CI execution, piped stdout, `install`, and shell-completion commands, so structured command output remains unchanged. Set `MEEGLE_NO_UPDATE_CHECK=1` to disable it explicitly.
78
+
58
79
  ## Quick Start (Human Users)
59
80
 
60
81
  > **Note for AI assistants:** if you are an AI Agent helping the user set this up, jump directly to [Quick Start (AI Agent)](#quick-start-ai-agent--ci--headless) — it contains the non-interactive command you need.
@@ -244,6 +265,69 @@ The agent consults the skill, picks the right `meegle` commands, and runs them f
244
265
  | `wbs get-draft-progress` | Get the execution progress of a WBS draft operation (create / edit / publish) |
245
266
  | `wbs list-element-templates` | List element templates (resource nodes and tasks) from the flow resource library |
246
267
 
268
+ ### ai-handoff — AI Assistant Handoff
269
+
270
+ These commands are registered locally in the CLI and are not MCP tools. `availability` is an optional preflight before preparing `query` or related context; an expected business rejection is a successful probe and includes stable `reject_code` and display-only `reject_msg` fields. Dependency and transport failures use the standard CLI error model.
271
+
272
+ `availability` reads the Handoff section of the generic `GET /goapi/v5/meeglecli/config` discovery response, including its `mode` (`off`, `ask`, or `auto`). The complete successful config snapshot is cached locally per profile for up to 1 hour; dependency or transport errors are never cached. The config cache is invalidated immediately on a successful `preference handoff auto|ask|off`, on `auth login`, and whenever `create-link` is rejected server-side. `create-link` never reads the cache: it always re-validates server-side and creates the link directly.
273
+
274
+ The create-link HTTP response always includes `available`: success returns HTTP 200 with `available=true` and `url`; the CLI then replaces only that URL's host (including the configured port) with the active login host, preserving its scheme, path, query, and fragment. This keeps links in the currently selected Meegle or Lark Project environment. An expected business rejection returns HTTP 200 with `available=false`, `reject_code`, and `reject_msg`, and clears the local config cache. Unexpected failures use the standard API error response.
275
+
276
+ Set `MEEGLE_AI_HANDOFF=disabled` to hard-disable Handoff in the local installation. Both `availability` and a valid `create-link` invocation then return `available=false` with `reject_code=LOCAL_DISABLED` and a display-only `reject_msg`; they do not require authentication, read the CLI configuration cache, or call the Handoff API. Unset values and values other than `disabled` preserve the normal server-controlled behavior. This local gate can only disable the feature—it cannot override the server business switch, entitlement, or personal preference to enable it.
277
+
278
+ | Command | Description |
279
+ |---------|-------------|
280
+ | `ai-handoff availability` | Check the business switch, rollout, AI entitlement, personal preference, and link-service readiness |
281
+ | `ai-handoff create-link` | Create an AI assistant link from required `--query` and optional, repeatable typed `--related-context` JSON objects |
282
+
283
+ ```bash
284
+ meegle ai-handoff availability --format json
285
+
286
+ MEEGLE_AI_HANDOFF=disabled meegle ai-handoff availability --format json
287
+
288
+ meegle ai-handoff create-link --params '{
289
+ "query": "Summarize the risks and propose next actions",
290
+ "related_context": [{
291
+ "type": 3,
292
+ "work_item": {
293
+ "project_key": "PROJ",
294
+ "work_item_type_key": "story",
295
+ "work_item_id": "123"
296
+ }
297
+ }]
298
+ }' --format json
299
+
300
+ meegle ai-handoff create-link --help
301
+ meegle inspect ai-handoff create-link
302
+ ```
303
+
304
+ Each `related_context` item must set `type` and exactly one matching payload. The facade contract uses business identifiers and never exposes the AI service's generic `key`; facade fills that field only while converting to the internal `query + entities` contract:
305
+
306
+ | Type | Payload | Required fields | Optional fields |
307
+ |------|---------|-----------------|-----------------|
308
+ | `1` Project | `project` | `project_key` | — |
309
+ | `3` WorkItem | `work_item` | `project_key`, `work_item_type_key`, `work_item_id` | — |
310
+ | `4` View | `view` | `project_key`, `view_id` | `work_item_type_key` |
311
+ | `5` MeasureChart | `measure_chart` | `project_key`, `chart_id` | — |
312
+
313
+ Context type `2` is reserved by the IDL for the currently unsupported WorkItemType context and must not be used.
314
+
315
+ Each attempt is bounded by a per-attempt timeout, and transient transport failures (timeout, HTTP 5xx/429, network errors) are retried up to 3 times with exponential backoff and jitter. `create-link` sends a stable idempotency key that is reused across those retries, so a retried request never creates a second link. Policy, validation, and 4xx errors fail fast without retrying. Facade invalid-parameter envelopes are reported as `HANDOFF_API_INVALID_PARAM` with `retryable=false`, including payloads that exceed the negotiated query or context limits. Internal Facade biz error IDs, causes, and chains are not exposed; the CLI returns a concise message plus a suggestion to inspect the current limits with `ai-handoff availability`.
316
+
317
+ ### preference — Personal Preferences
318
+
319
+ Handoff suggestion mode is stored in the server-side unified user preference service. It has no project or tenant argument and defaults to `auto` when no override exists. The generic write request carries a `preferences` list with `type=handoff_suggestions`; its payload is `{"mode":"off|ask|auto"}`. A successful mode update invalidates the local `ai-handoff availability` cache.
320
+
321
+ | Command | Description |
322
+ |---------|-------------|
323
+ | `preference handoff auto` | Automatically show AI handoff recommendations |
324
+ | `preference handoff ask` | Ask before showing an AI handoff recommendation |
325
+ | `preference handoff off` | Disable AI handoff recommendations |
326
+
327
+ Use `meegle preference handoff --help` for mode behavior and `meegle inspect preference handoff auto` (or `ask` / `off`) for the command-level parameter view.
328
+
329
+ `reset` is intentionally not exposed until the preference service provides an atomic unset operation.
330
+
247
331
  ### auth — Authentication
248
332
 
249
333
  | Command | Description |
@@ -658,6 +742,8 @@ meegle workflow get-node --work-item-id 12345 --need-sub-task
658
742
  | `--auto-paginate` | | Automatically fetch and merge all pages when the response contains pagination signals (`next_page_token` or `pagination.has_more`); merged list arrays are concatenated, and a 200-page safety cap plus a 3-empty-page streak guard prevent runaway loops |
659
743
  | `--version` | | Print the CLI version and exit (alias of `meegle version`) |
660
744
 
745
+ `--version` is treated as the version alias only when it is a standalone flag. If a preceding string flag is waiting for a value, the literal `--version` remains that flag's value.
746
+
661
747
  ## Advanced Usage
662
748
 
663
749
  ### Output Formats
@@ -726,6 +812,11 @@ carries the backend trace id (when the server returns one). Hand that id
726
812
  to oncall to look up the request in argos. Without `--envelope` the id is
727
813
  suppressed so the default output stays clean for piping.
728
814
 
815
+ AI handoff Config, Preference, and Create Link responses also copy the gateway
816
+ `x-tt-logid` response header to `meta.logid`. Successful calls expose it only
817
+ with `--envelope`; failures include it in their structured error envelope
818
+ automatically. No debug logging is required.
819
+
729
820
  ### Dry Run
730
821
 
731
822
  For commands with side effects, preview the rendered request with `--dry-run` before executing:
@@ -758,6 +849,10 @@ escape sequences retain their backslash.
758
849
  This decoding only applies to programmatic command-string entry points such as
759
850
  `CommandClient.Execute` and `ExecuteCommandString`. The `meegle` binary receives
760
851
  an argument array from the shell, so normal shell quoting rules apply there.
852
+ The command-string Go SDK used by Facade for remote RPC execution registers only
853
+ MCP-discovered commands. Local CLI API commands such as `ai-handoff` and
854
+ `preference handoff` are available only in the npm-distributed `meegle` CLI;
855
+ direct `CallTool` also continues to address MCP tools only.
761
856
 
762
857
  ## Authentication
763
858
 
@@ -840,16 +935,17 @@ Main config options:
840
935
 
841
936
  ### Sandbox / CI: Direct Environment-Variable Injection
842
937
 
843
- Two well-known environment variables are read directly at CLI startup and override the matching profile fields without requiring any `config set`:
938
+ The following well-known environment variables are read directly by the CLI without requiring any `config set`:
844
939
 
845
940
  ```bash
846
941
  export MEEGLE_HOST=project.feishu.cn
847
942
  export MEEGLE_USER_ACCESS_TOKEN=<your-user-token>
848
943
  export MEEGLE_USER_AGENT=ci-runner # optional; appended to User-Agent, highest priority over config.user_agent
944
+ export MEEGLE_AI_HANDOFF=disabled # optional; locally hard-disable AI Handoff
849
945
  meegle workitem get --work-item-id 123
850
946
  ```
851
947
 
852
- Either variable may be set independently. When `MEEGLE_USER_ACCESS_TOKEN` is set, the CLI bypasses the keychain and does not attempt to refresh on 401 — the caller is responsible for rotating the env value. Setting only `MEEGLE_HOST` (without a token) still uses the keychain-stored credentials.
948
+ These variables may be set independently. When `MEEGLE_USER_ACCESS_TOKEN` is set, the CLI bypasses the keychain and does not attempt to refresh on 401 — the caller is responsible for rotating the env value. Setting only `MEEGLE_HOST` (without a token) still uses the keychain-stored credentials. `MEEGLE_AI_HANDOFF=disabled` is a disable-only local gate and does not alter profile configuration.
853
949
 
854
950
  ### Custom Auth Header
855
951
 
@@ -883,7 +979,7 @@ If your runtime exposes a variable with a name other than `MEEGLE_*`, bind it th
883
979
 
884
980
  Rules:
885
981
  - Only whole-string placeholders are recognized. `"${X}"` is expanded; `"Bearer ${X}"` is treated as a literal.
886
- - When a referenced variable is unset or empty, the CLI fails fast and reports the field path and variable name.
982
+ - When a referenced variable is unset or empty, commands that require credentials fail fast and report the field path and variable name. Recovery entry points such as `--help`, `version`, `auth login --help`, and `config set` remain available so the profile can be repaired.
887
983
  - When `user_access_token` is configured, it takes precedence over any token stored locally by `meegle auth login`. Because this mode has no refresh path, rotate the environment value yourself when the server returns 401.
888
984
 
889
985
  ### Multi-Environment Profiles
@@ -910,6 +1006,22 @@ meegle mywork todo --action this_week --page-num 1 --profile staging
910
1006
  meegle config profile delete staging
911
1007
  ```
912
1008
 
1009
+ ## Enterprise CLI Extensions
1010
+
1011
+ Companies can depend on this Go module, register trusted in-process adapters, and build their own `meegle` binary without modifying the official repository. V1 supports three extension seams:
1012
+
1013
+ - `extension/credential` selects an account and supplies an existing Meegle user token. Known local/recovery commands bootstrap without invoking Credential providers, so a slow OIDC provider cannot block help, version, configuration repair, completion, URL parsing, or extension diagnostics; commands that need identity still resolve the provider and fail closed on any provider error.
1014
+ - `extension/transport` observes or blocks CLI HTTP requests while retaining redirect and TLS-downgrade protections. Provider and hook callbacks have a 30-second safety timeout, but the real MCP, OAuth, and attachment request keeps the caller's Context and original HTTP client timeout, so enabling an extension does not shorten large uploads, downloads, or slow server operations. Its trusted in-process pre-hook sees the credential-injected live request and can technically change authentication headers; the CLI does not provide in-process isolation or freeze header values. Its post-hook receives an isolated metadata snapshot with `http.NoBody` and a cloned TLS state, so a stalled hook cannot consume, retain, or mutate the live response stream. Credential-bearing MCP requests—default Bearer or custom token header—retain a 10-redirect limit and are never redirected away from their exact original origin.
1015
+ - `extension/platform` observes, wraps, or restricts both static commands and MCP-discovered dynamic commands. Plugin metadata/Install and each Startup hook have a two-second safety boundary; a timed-out fail-open plugin is skipped, while fail-closed stops the CLI, and late registration is ignored. Restrict plugins must be fail-closed; a hand-written plugin that declares `Restricts=true` with `FailurePolicy=FailOpen` fails startup instead of being silently skipped.
1016
+
1017
+ Use `cmd.ExecuteWithVersion(version)` for an enterprise binary whose plugins declare `RequireCLI`; `cmd.Execute()` remains the compatible default entry point. A `dev` build deliberately cannot satisfy a version constraint and its compatibility error points to `ExecuteWithVersion`; this fail-closed check is not bypassed. Extensions are linked at build time—there is no runtime plugin download—and the SDK does not load CLI extension registries. See [all extension examples](./examples/README.md) and the [architecture contract](./docs/design/CLI-EXTENSION-ARCHITECTURE.md).
1018
+
1019
+ An extension-enabled binary exposes non-secret diagnostics under `meegle extension doctor|credentials|transport|plugins|policy|discovery`. Credential and transport diagnostics distinguish `not-evaluated`, `active`, and `failed` instead of re-running providers during an offline diagnostic command. Restrict rules also govern these commands, so a readonly allow-list should include `extension/**` when operators need troubleshooting access.
1020
+ Policy denials honor explicit structured output modes and use the stable `CLIENT_COMMAND_DENIED` error code. Credential and Platform failures that happen before CLI App construction also honor explicit JSON/NDJSON output and expose `CLIENT_CREDENTIAL_RESOLUTION_FAILED` or `CLIENT_EXTENSION_INSTALL_FAILED` instead of plain text.
1021
+ Errors and panic values returned by extension callbacks stay behind a guarded Go error-chain boundary: custom `Is`, `As`, `Unwrap`, `Error`, or payload methods cannot crash the CLI, and panic details cannot enter public output. Extension code should still return ordinary, context-aware errors because it runs as trusted code in the CLI process.
1022
+ Every JSON-RPC response is bounded before decoding: `tools/list` uses an 8 MiB discovery limit and all other calls use a 32 MiB per-response limit.
1023
+ Dynamic discovery accepts nullable JSON Schema types such as `"type": ["string", "null"]` and exposes them as the underlying CLI/SDK parameter type. Unions containing multiple non-null types are isolated with the stable `unsupported_schema_union` diagnostic instead of silently removing unrelated tools.
1024
+
913
1025
  ## FAQ
914
1026
 
915
1027
  ### Empty Command List
package/README.zh-CN.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  飞书项目([Meegle](https://meegle.com?utm_source=github&utm_medium=readme&utm_campaign=meegle_cli) / [Lark Project](https://project.feishu.cn?utm_source=github&utm_medium=readme&utm_campaign=meegle_cli))命令行工具。在终端中管理工作项、查看排期、搜索数据,无需打开浏览器。
10
10
 
11
- [安装](#安装) · [快速开始](#快速开始人类用户) · [Agent Skill](#ai-agent-skill) · [命令](#命令一览) · [认证](#认证) · [配置](#配置) · [安全](#安全与风险提示) · [贡献](#贡献)
11
+ [安装](#安装) · [快速开始](#快速开始人类用户) · [Agent Skill](#ai-agent-skill) · [命令](#命令一览) · [企业扩展](#企业-cli-扩展) · [认证](#认证) · [配置](#配置) · [安全](#安全与风险提示) · [贡献](#贡献)
12
12
 
13
13
  ## 为什么选择 Meegle CLI?
14
14
 
@@ -17,6 +17,7 @@
17
17
  - **两层参数模型** — 日常用 `--flag-name` 轻便直接,复杂载荷(如 `fields[]`)用 `--params <json>` 兜底 —— 按场景选择合适粒度
18
18
  - **输出格式灵活** — 支持 `json` / `table` / `ndjson` / `raw`,配合 `--select` 点路径投影可直接 pipe 给其他工具
19
19
  - **默认安全** — 凭证存进系统 keychain、`${VAR}` 环境变量模板让 secret 不落地到 config 文件、多 profile 分离 staging / prod
20
+ - **支持企业扩展** — 企业通过编译期引用公开的 `cmd`、Credential、Transport、Platform 包构建自己的发行版,无需 Fork 本仓库
20
21
 
21
22
  ## 功能概览
22
23
 
@@ -55,6 +56,26 @@ npx @lark-project/meegle@latest install
55
56
 
56
57
  向导会全局安装或升级 CLI、安装 AI Agent Skill、配置 Meegle host,并启动登录。
57
58
 
59
+ ### 自动更新提示
60
+
61
+ 通过 npm 分发的 CLI 在交互式终端启动时,最多每 24 小时检查一次 npm 最新版本。发现新版本后,会从 [CHANGELOG](https://github.com/larksuite/meegle-cli/blob/main/CHANGELOG.md#changed) 提取当前版本到最新版本之间已经发布的 `Added` 和 `Changed` 条目,并展示交互式功能上新提示:
62
+
63
+ ```text
64
+ ✨ Meegle CLI 有新版本: v<当前版本> → v<最新版本>
65
+
66
+ 功能上新:
67
+ v<版本号>
68
+ • 新增: <功能摘要>
69
+
70
+ ❯ 立即更新(推荐)
71
+ 稍后提醒
72
+ 使用 ↑/↓ 选择,按 Enter 确认
73
+ ```
74
+
75
+ 直接按 Enter 会先执行 `npm install -g @lark-project/meegle@latest` 更新 CLI,再通过 `meegle install` 使用的同一套安装方式 best-effort 更新 Meegle Agent Skill,最后继续原命令。当前环境缺少合适的 Skill 安装器、安装器不兼容或 Skill 下载失败时,只会提示已跳过,不会把成功的 CLI 更新改判为失败。选择**稍后提醒**后,24 小时内不再检查。这个轻量更新流程不会重新配置 host 或触发登录;需要完整的一站式设置时仍可运行 `meegle install`。
76
+
77
+ 非交互/CI 环境、stdout 被管道接走、执行 `install` 或 Shell 补全命令时会跳过检查,保证结构化命令输出不变。也可以设置 `MEEGLE_NO_UPDATE_CHECK=1` 显式关闭。
78
+
58
79
  ## 快速开始(人类用户)
59
80
 
60
81
  > **给 AI Agent 的提示:** 如果你是在替用户完成这套安装的 AI Agent,请直接跳到 [快速开始(AI Agent)](#快速开始ai-agent--ci--无头环境) —— 那里有你需要的非交互命令。
@@ -246,6 +267,69 @@ Agent 会参考 skill,自动选择合适的 `meegle` 命令执行。配合 `--
246
267
  | `wbs get-draft-progress` | 查询计划表草稿操作(创建 / 编辑 / 发布)的执行进度 |
247
268
  | `wbs list-element-templates` | 列出流程资源库中的资源节点与资源任务模板 |
248
269
 
270
+ ### ai-handoff — AI 助手接力
271
+
272
+ 这组命令由 CLI 本地注册,不属于 MCP tool。`availability` 是准备 `query` 或关联上下文前的可选预检;预期内的业务拒绝属于成功探测,返回稳定的 `reject_code` 和仅用于展示的 `reject_msg`。依赖或传输异常走 CLI 通用错误模型。
273
+
274
+ `availability` 读取通用 `GET /goapi/v5/meeglecli/config` 探测结果中的 Handoff 配置及其 `mode`(`off`、`ask`、`auto`)。CLI 按 profile 缓存成功返回的完整配置快照至多 1 小时,依赖或传输异常不缓存。`preference handoff auto|ask|off` 成功、`auth login`、以及 `create-link` 被服务端拒绝时会立即失效配置缓存。`create-link` 不读缓存:它始终在服务端二次校验并直接创建链接。
275
+
276
+ create-link HTTP 响应始终包含 `available`:正常情况返回 HTTP 200 和 `available=true + url`;CLI 随后只把该 URL 的 host(含配置的端口)替换为当前登录域名,保留 scheme、path、query 和 fragment,使链接始终落在当前选择的 Meegle 或飞书项目环境。业务预期内的不可用返回 HTTP 200 和 `available=false + reject_code + reject_msg`,同时清理本地配置缓存;非预期异常走接口通用错误响应。
277
+
278
+ 设置 `MEEGLE_AI_HANDOFF=disabled` 可在当前安装环境中硬禁用 Handoff。此时 `availability` 和参数合法的 `create-link` 都会返回 `available=false`、`reject_code=LOCAL_DISABLED` 及仅用于展示的 `reject_msg`,不要求登录、不读取 availability 缓存,也不调用 Handoff API。未设置或值不是 `disabled` 时保持原有的服务端控制行为。本地门禁只能关闭功能,不能绕过服务端业务开关、权益或个人偏好将功能强制打开。
279
+
280
+ | 命令 | 说明 |
281
+ |------|------|
282
+ | `ai-handoff availability` | 检查业务开关、灰度、AI 助手权益、个人偏好和链接服务可用性 |
283
+ | `ai-handoff create-link` | 根据必填的 `--query` 和可选、可重复的类型化 `--related-context` JSON 对象生成 AI 助手链接 |
284
+
285
+ ```bash
286
+ meegle ai-handoff availability --format json
287
+
288
+ MEEGLE_AI_HANDOFF=disabled meegle ai-handoff availability --format json
289
+
290
+ meegle ai-handoff create-link --params '{
291
+ "query": "总结风险并给出下一步行动建议",
292
+ "related_context": [{
293
+ "type": 3,
294
+ "work_item": {
295
+ "project_key": "PROJ",
296
+ "work_item_type_key": "story",
297
+ "work_item_id": "123"
298
+ }
299
+ }]
300
+ }' --format json
301
+
302
+ meegle ai-handoff create-link --help
303
+ meegle inspect ai-handoff create-link
304
+ ```
305
+
306
+ 每个 `related_context` 元素必须设置 `type`,并且只设置一个与类型匹配的 payload。Facade 契约只使用业务标识,不对外暴露 AI 服务的通用 `key`;只有转换为内部 `query + entities` 契约时才补齐该字段:
307
+
308
+ | Type | Payload | 必填字段 | 可选字段 |
309
+ |------|---------|----------|----------|
310
+ | `1` Project | `project` | `project_key` | — |
311
+ | `3` WorkItem | `work_item` | `project_key`、`work_item_type_key`、`work_item_id` | — |
312
+ | `4` View | `view` | `project_key`、`view_id` | `work_item_type_key` |
313
+ | `5` MeasureChart | `measure_chart` | `project_key`、`chart_id` | — |
314
+
315
+ 上下文类型 `2` 是 IDL 为当前暂不支持的 WorkItemType 上下文保留的枚举值,请勿使用或复用。
316
+
317
+ 每次尝试都有独立的超时保护,可重试的传输失败(超时、HTTP 5xx/429、网络错误)会以指数退避加抖动的方式最多重试 3 次。`create-link` 会携带一个稳定的幂等键,并在这些重试中保持不变,因此重试不会创建第二条链接。策略、参数校验和 4xx 错误会快速失败,不做重试。Facade 返回的参数错误信封会映射为 `HANDOFF_API_INVALID_PARAM` 且 `retryable=false`,包括 query 或 context 数量超过协商上限的情况。CLI 不暴露 Facade 内部的 biz error id、cause 或 chain,只返回简洁消息,并建议通过 `ai-handoff availability` 查看当前限制。
318
+
319
+ ### preference — 个人偏好
320
+
321
+ Handoff Suggestions 偏好存储在服务端统一用户偏好服务中,不接收项目或租户参数;不存在用户覆盖值时默认 `auto`。通用偏好写接口使用 `preferences` 列表,当前 item 的 `type=handoff_suggestions`,payload 为 `{"mode":"off|ask|auto"}`;成功仅表示写入完成。读取当前配置统一走 CLI config 探测接口。模式写入成功后会失效本地配置缓存,使变更在下次探测时生效。
322
+
323
+ | 命令 | 说明 |
324
+ |------|------|
325
+ | `preference handoff auto` | 自动展示 AI Handoff 推荐 |
326
+ | `preference handoff ask` | 展示 AI Handoff 推荐前先询问 |
327
+ | `preference handoff off` | 关闭 AI Handoff 推荐 |
328
+
329
+ 使用 `meegle preference handoff --help` 查看模式行为,使用 `meegle inspect preference handoff auto`(或 `ask` / `off`)查看命令级参数说明。
330
+
331
+ 在偏好服务提供原子 unset 能力前,MVP 不开放 `reset`。
332
+
249
333
  ### auth — 认证域
250
334
 
251
335
  | 命令 | 说明 |
@@ -640,6 +724,8 @@ meegle workflow get-node --work-item-id 12345 --need-sub-task
640
724
  | `--auto-paginate` | | 当响应包含分页信号(`next_page_token` 或 `pagination.has_more`)时自动拉取并合并所有页;列表数组会拼接,200 页安全上限与连续 3 空页保护防止失控循环 |
641
725
  | `--version` | | 输出 CLI 版本号并退出(`meegle version` 的别名) |
642
726
 
727
+ 只有作为独立 Flag 时,`--version` 才表示版本别名;如果前面的字符串 Flag 正在等待参数值,字面量 `--version` 会保留为该 Flag 的值。
728
+
643
729
  ## 进阶用法
644
730
 
645
731
  ### 输出格式
@@ -698,6 +784,10 @@ meegle workflow update-node --work-item-id 12345 \
698
784
  `meta.logid` 里——把这个 id 交给 oncall 就能在 argos 定位到这次请求。
699
785
  不加 `--envelope` 时 logid 会被抑制,保持默认输出干净便于管道处理。
700
786
 
787
+ AI Handoff 的 Config、Preference、Create Link 响应也会把网关响应头 `x-tt-logid`
788
+ 写入 `meta.logid`:成功时仅在指定 `--envelope` 后展示,失败时自动进入结构化错误
789
+ envelope,无需开启 debug 日志。
790
+
701
791
  ### Dry Run
702
792
 
703
793
  有副作用的命令先用 `--dry-run` 预览请求再执行:
@@ -728,6 +818,9 @@ meegle inspect workitem.create
728
818
  该解码只作用于 `CommandClient.Execute`、`ExecuteCommandString` 等程序化
729
819
  命令字符串入口。`meegle` 二进制直接接收 shell 解析后的参数数组,因此仍遵循
730
820
  对应 shell 的引号和转义规则。
821
+ 供 Facade 远程 RPC 调用的命令字符串 Go SDK 只注册 MCP 动态发现命令。
822
+ `ai-handoff`、`preference handoff` 等本地 CLI API 命令只在 npm 分发的
823
+ `meegle` CLI 中提供;直接调用 `CallTool` 时也仍然只访问 MCP tool。
731
824
 
732
825
  ## 认证
733
826
 
@@ -809,16 +902,17 @@ meegle config get host
809
902
 
810
903
  ### 沙盒 / CI:直接注入环境变量
811
904
 
812
- `MEEGLE_HOST` 和 `MEEGLE_USER_ACCESS_TOKEN` 两个约定名环境变量会在 CLI 启动时被直接读取,覆盖 profile 中的同名字段,无需任何 `config set`:
905
+ CLI 会直接读取以下约定名环境变量,无需执行任何 `config set`:
813
906
 
814
907
  ```bash
815
908
  export MEEGLE_HOST=project.feishu.cn
816
909
  export MEEGLE_USER_ACCESS_TOKEN=<your-user-token>
817
910
  export MEEGLE_USER_AGENT=ci-runner # 可选;追加到 User-Agent,优先级高于 config.user_agent
911
+ export MEEGLE_AI_HANDOFF=disabled # 可选;在本地硬禁用 AI Handoff
818
912
  meegle workitem get --work-item-id 123
819
913
  ```
820
914
 
821
- 任一变量可以单独设置。当 `MEEGLE_USER_ACCESS_TOKEN` 设置时,CLI 不访问 keychain,401 错误不会自动 refresh,由调用方自行轮转。仅设置 `MEEGLE_HOST`(不带 token)时仍走 keychain 中存储的凭证。
915
+ 各变量可以单独设置。当 `MEEGLE_USER_ACCESS_TOKEN` 设置时,CLI 不访问 keychain,401 错误不会自动 refresh,由调用方自行轮转。仅设置 `MEEGLE_HOST`(不带 token)时仍走 keychain 中存储的凭证。`MEEGLE_AI_HANDOFF=disabled` 只是本地禁用门禁,不修改 profile 配置。
822
916
 
823
917
  ### 自定义 Auth Header
824
918
 
@@ -852,7 +946,7 @@ export MEEGLE_ACCESS_TOKEN_HEADER=x-meegle-auth
852
946
 
853
947
  规则:
854
948
  - 仅识别**整串形态**的占位符。`"${X}"` 会被展开;`"Bearer ${X}"` 按字面量处理,不展开。
855
- - 引用的环境变量未设置或为空时 CLI **fail fast**,错误信息会带上字段路径和变量名。
949
+ - 引用的环境变量未设置或为空时,需要凭证的业务命令会 **fail fast**,错误信息会带上字段路径和变量名;`--help`、`version`、`auth login --help`、`config set` 等自救入口仍可运行,方便修复 profile。
856
950
  - 当配置了 `user_access_token` 时,它会覆盖 `meegle auth login` 在本地 keychain 里写入的令牌。由于这种模式下没有本地 refresh 能力,服务端返回 401 时需要自行轮转环境变量值。
857
951
 
858
952
  ### 多环境 Profile
@@ -879,6 +973,22 @@ meegle mywork todo --action this_week --page-num 1 --profile staging
879
973
  meegle config profile delete staging
880
974
  ```
881
975
 
976
+ ## 企业 CLI 扩展
977
+
978
+ 企业可以直接依赖本 Go Module,注册可信的进程内适配器并构建自己的 `meegle` 二进制,无需修改官方仓库。V1 提供三个扩展点:
979
+
980
+ - `extension/credential` 选择账号,并提供现有的 Meegle 用户 Token。已知的本地/自救命令在启动时不会调用 Credential Provider,因此慢 OIDC Provider 不会阻塞帮助、版本、配置修复、补全、URL 解析或扩展诊断;需要身份的命令仍会解析 Provider,任何 Provider 错误都会 fail-closed。
981
+ - `extension/transport` 观察或阻断 CLI HTTP 请求,同时保留重定向和 TLS 防降级约束。Provider 和 Hook 回调有 30 秒安全超时,但真实 MCP、OAuth 和附件请求继续使用调用方 Context 与原 HTTP Client timeout,因此启用扩展不会截断大文件上传下载或慢服务端操作;可信的进程内前置 Hook 会看到注入凭证后的真实请求,技术上可以修改认证 Header,CLI 不提供进程内隔离也不会冻结 Header 值;后置 Hook 只接收 `Body=http.NoBody`、TLS 状态也已复制的隔离元数据快照,阻塞的 Hook 不会消费、占住或修改真实响应流;携带默认 Bearer 或自定义 Token Header 的 MCP 请求都保留 10 跳上限,且不会被重定向到原始精确 origin 之外。
982
+ - `extension/platform` 观察、包装或限制静态命令和 MCP 动态发现命令。插件元数据/Install 和每个 Startup Hook 都有两秒安全边界;fail-open 插件超时后跳过,fail-closed 会终止 CLI,迟到注册会被忽略。Restrict 插件必须 fail-closed;手写插件如果同时声明 `Restricts=true` 和 `FailurePolicy=FailOpen`,CLI 会启动失败,不会静默跳过策略。
983
+
984
+ 企业插件声明 `RequireCLI` 时,入口应调用 `cmd.ExecuteWithVersion(version)`;`cmd.Execute()` 仍是兼容的默认入口。`dev` 构建会按设计拒绝版本约束,兼容性错误会提示改用 `ExecuteWithVersion`,不会绕过 fail-closed 校验。扩展在编译时链接,不支持运行时下载插件;SDK 也不会加载 CLI 的全局扩展注册表。完整用法见[扩展示例](./examples/README.md)和[架构契约](./docs/design/CLI-EXTENSION-ARCHITECTURE.md)。
985
+
986
+ 扩展版二进制可通过 `meegle extension doctor|credentials|transport|plugins|policy|discovery` 查看脱敏后的诊断信息。Credential 与 Transport 诊断会区分 `not-evaluated`、`active`、`failed`,离线诊断不会为了显示状态而重新调用 Provider。Restrict 同样治理这些命令;需要保留排障入口时,readonly Allow 列表应包含 `extension/**`。
987
+ Policy 拒绝遵循显式结构化输出格式,并使用稳定错误码 `CLIENT_COMMAND_DENIED`。CLI App 构造前发生的 Credential 与 Platform 失败也遵循显式 JSON/NDJSON 输出,分别暴露 `CLIENT_CREDENTIAL_RESOLUTION_FAILED` 或 `CLIENT_EXTENSION_INSTALL_FAILED`,不会退化成纯文本。
988
+ 扩展回调返回的 error 和 panic 值会留在受保护的 Go 错误链边界内;自定义 `Is`、`As`、`Unwrap`、`Error` 或错误载荷方法不能让 CLI 崩溃,panic 细节也不会进入公开输出。扩展仍属于进程内可信代码,实现方应返回普通且遵守 Context 的错误。
989
+ 所有 JSON-RPC 响应都会在解码前限制大小:`tools/list` 使用 8 MiB 的发现上限,其他调用使用每次响应 32 MiB 的通用上限。
990
+ 动态发现支持 `"type": ["string", "null"]` 这类 nullable JSON Schema,并在 CLI/SDK 中按其非 null 类型注册参数。包含多个非 null 类型的 union 会被单项隔离,并输出稳定的 `unsupported_schema_union` 诊断,不影响其他工具。
991
+
882
992
  ## 常见问题
883
993
 
884
994
  ### 命令列表为空
@@ -157,6 +157,20 @@ function runCapture(cmd, args, opts = {}) {
157
157
  });
158
158
  }
159
159
 
160
+ // installAgentSkill is shared by the full install wizard and the lightweight
161
+ // startup updater. The wizard keeps strict failure semantics, while the
162
+ // updater passes bestEffort=true so an unavailable Skill installer never
163
+ // changes a successful CLI upgrade into a failure.
164
+ function installAgentSkill({ bestEffort = false, runFn = run } = {}) {
165
+ try {
166
+ runFn("npm", ["exec", "--yes", "--package=skills", "--", "skills", "add", SKILLS_REPO, "-y", "-g"], { timeout: 120000 });
167
+ return true;
168
+ } catch (err) {
169
+ if (!bestEffort) throw err;
170
+ return false;
171
+ }
172
+ }
173
+
160
174
  function question(prompt) {
161
175
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
162
176
  return new Promise((resolve) => {
@@ -274,7 +288,7 @@ async function stepInstallSkills(msg) {
274
288
  // skill just upgrades / no-ops. We always run it instead of trying to detect
275
289
  // prior installs — name-substring detection mis-fired on sibling skills like
276
290
  // `meegle-plugin` and silently skipped the core skill.
277
- run("npm", ["exec", "--yes", "--package=skills", "--", "skills", "add", SKILLS_REPO, "-y", "-g"], { timeout: 120000 });
291
+ installAgentSkill();
278
292
  console.log(msg.skillsDone);
279
293
  }
280
294
 
@@ -375,4 +389,4 @@ if (require.main === module) {
375
389
  });
376
390
  }
377
391
 
378
- module.exports = { main };
392
+ module.exports = { installAgentSkill, main };
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/bin/meegle.js CHANGED
@@ -3,6 +3,8 @@ const { spawnSync } = require("child_process");
3
3
  const fs = require("fs");
4
4
  const os = require("os");
5
5
  const path = require("path");
6
+ const { maybeCheckForUpdates } = require("./update-notifier.js");
7
+ const packageJSON = require("../package.json");
6
8
 
7
9
  const SUPPORTED = [
8
10
  "darwin-arm64", "darwin-x64",
@@ -15,14 +17,13 @@ const arch = os.arch();
15
17
  const ext = platform === "win32" ? ".exe" : "";
16
18
  const binName = `meegle-${platform}-${arch}${ext}`;
17
19
  const binPath = path.join(__dirname, binName);
18
- const args = process.argv.slice(2);
19
20
 
20
- if (args[0] === "install") {
21
- Promise.resolve(require("./install-wizard.js").main(args.slice(1))).catch((err) => {
22
- console.error("Unexpected install error:", err && err.message ? err.message : err);
23
- process.exit(1);
24
- });
25
- } else {
21
+ async function main(args = process.argv.slice(2)) {
22
+ if (args[0] === "install") {
23
+ await require("./install-wizard.js").main(args.slice(1));
24
+ return 0;
25
+ }
26
+
26
27
  try {
27
28
  fs.accessSync(binPath, fs.constants.X_OK);
28
29
  } catch {
@@ -38,15 +39,43 @@ if (args[0] === "install") {
38
39
  : `Unsupported platform: ${detected}\n` +
39
40
  `Supported platforms: ${SUPPORTED.join(", ")}`
40
41
  );
41
- process.exit(1);
42
+ return 1;
42
43
  }
43
44
 
44
- const result = spawnSync(binPath, args, { stdio: "inherit" });
45
+ let commandBinPath = binPath;
46
+ try {
47
+ const update = await maybeCheckForUpdates({
48
+ args,
49
+ currentVersion: packageJSON.version,
50
+ binName,
51
+ });
52
+ if (update.updated && update.binaryPath) commandBinPath = update.binaryPath;
53
+ } catch (err) {
54
+ if (err && err.code === "UPDATE_PROMPT_INTERRUPTED") {
55
+ process.kill(process.pid, "SIGINT");
56
+ return 130;
57
+ }
58
+ // The update notifier is best-effort. A registry, GitHub, cache, or prompt
59
+ // failure must never prevent the user's actual CLI command from running.
60
+ }
61
+
62
+ const result = spawnSync(commandBinPath, args, { stdio: "inherit" });
45
63
 
46
64
  // Re-raise the signal so parent shells see the real cause (e.g. 130 for SIGINT)
47
65
  // instead of a generic exit 1.
48
66
  if (result.signal) {
49
67
  process.kill(process.pid, result.signal);
50
68
  }
51
- process.exit(result.status ?? 1);
69
+ return result.status ?? 1;
70
+ }
71
+
72
+ if (require.main === module) {
73
+ main().then((status) => {
74
+ process.exit(status);
75
+ }).catch((err) => {
76
+ console.error("Unexpected meegle error:", err && err.message ? err.message : err);
77
+ process.exit(1);
78
+ });
52
79
  }
80
+
81
+ module.exports = { main };
@@ -0,0 +1,544 @@
1
+ #!/usr/bin/env node
2
+ const { execFileSync, spawnSync } = require("child_process");
3
+ const fs = require("fs");
4
+ const https = require("https");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const readline = require("readline");
8
+
9
+ const PKG = "@lark-project/meegle";
10
+ const CHANGELOG_URL = "https://github.com/larksuite/meegle-cli/blob/main/CHANGELOG.md#changed";
11
+ const CHANGELOG_RAW_URL = "https://raw.githubusercontent.com/larksuite/meegle-cli/main/CHANGELOG.md";
12
+ const CACHE_SCHEMA_VERSION = 1;
13
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
14
+ const FAILURE_RETRY_MS = 60 * 60 * 1000;
15
+ const NETWORK_TIMEOUT_MS = 5000;
16
+ const MAX_CHANGELOG_BYTES = 1024 * 1024;
17
+ const MAX_FEATURE_ITEMS = 8;
18
+
19
+ const messages = {
20
+ en: {
21
+ title: "Meegle CLI update available",
22
+ whatsNew: "What's new",
23
+ added: "Added",
24
+ changed: "Changed",
25
+ maintenance: "This update mainly contains fixes and maintenance changes.",
26
+ omitted: "... and %s more feature changes",
27
+ changelog: "Full changelog",
28
+ updateNow: "Update now (recommended)",
29
+ later: "Remind me later",
30
+ hint: "Use ↑/↓ to select, then press Enter",
31
+ updating: "Updating @lark-project/meegle to v%s...\n",
32
+ updated: "✓ Meegle CLI updated to v%s.\n",
33
+ skillUpdating: "Updating the Meegle Agent Skill (best effort)...\n",
34
+ skillUpdated: "✓ Meegle Agent Skill updated.\n",
35
+ skillSkipped: "! Meegle Agent Skill update was unavailable; the CLI update is still complete.\n",
36
+ continuing: "Continuing with your command...\n\n",
37
+ updateFailed: "Could not update automatically. Continue with the current version and retry later:\n npm install -g @lark-project/meegle@latest\n\n",
38
+ },
39
+ zh: {
40
+ title: "Meegle CLI 有新版本",
41
+ whatsNew: "功能上新",
42
+ added: "新增",
43
+ changed: "变化",
44
+ maintenance: "本次更新以问题修复和维护性改进为主。",
45
+ omitted: "……另有 %s 项功能变化",
46
+ changelog: "完整更新日志",
47
+ updateNow: "立即更新(推荐)",
48
+ later: "稍后提醒",
49
+ hint: "使用 ↑/↓ 选择,按 Enter 确认",
50
+ updating: "正在将 @lark-project/meegle 更新到 v%s...\n",
51
+ updated: "✓ Meegle CLI 已更新到 v%s。\n",
52
+ skillUpdating: "正在尝试更新 Meegle Agent Skill(失败不影响 CLI 更新)...\n",
53
+ skillUpdated: "✓ Meegle Agent Skill 已更新。\n",
54
+ skillSkipped: "! 当前环境无法更新 Meegle Agent Skill;CLI 已成功更新。\n",
55
+ continuing: "将继续执行当前命令。\n\n",
56
+ updateFailed: "自动更新失败,将继续使用当前版本。你可以稍后手动执行:\n npm install -g @lark-project/meegle@latest\n\n",
57
+ },
58
+ };
59
+
60
+ function fmt(template, ...values) {
61
+ let index = 0;
62
+ return template.replace(/%s/g, () => String(values[index++] ?? ""));
63
+ }
64
+
65
+ function localeFromEnv(env = process.env) {
66
+ const locale = [env.LC_ALL, env.LC_MESSAGES, env.LANG, env.LANGUAGE]
67
+ .find((value) => typeof value === "string" && value.trim() !== "") || "";
68
+ return locale.toLowerCase().startsWith("zh") ? "zh" : "en";
69
+ }
70
+
71
+ function parseSemver(raw) {
72
+ const match = String(raw || "").trim().match(
73
+ /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/,
74
+ );
75
+ if (!match) return null;
76
+ return {
77
+ major: Number(match[1]),
78
+ minor: Number(match[2]),
79
+ patch: Number(match[3]),
80
+ prerelease: match[4] ? match[4].split(".") : [],
81
+ };
82
+ }
83
+
84
+ function comparePrerelease(left, right) {
85
+ if (left.length === 0 && right.length === 0) return 0;
86
+ if (left.length === 0) return 1;
87
+ if (right.length === 0) return -1;
88
+ const count = Math.max(left.length, right.length);
89
+ for (let index = 0; index < count; index += 1) {
90
+ if (left[index] === undefined) return -1;
91
+ if (right[index] === undefined) return 1;
92
+ if (left[index] === right[index]) continue;
93
+ const leftNumber = /^\d+$/.test(left[index]) ? Number(left[index]) : null;
94
+ const rightNumber = /^\d+$/.test(right[index]) ? Number(right[index]) : null;
95
+ if (leftNumber !== null && rightNumber !== null) return leftNumber < rightNumber ? -1 : 1;
96
+ if (leftNumber !== null) return -1;
97
+ if (rightNumber !== null) return 1;
98
+ return left[index].localeCompare(right[index]) < 0 ? -1 : 1;
99
+ }
100
+ return 0;
101
+ }
102
+
103
+ function compareVersions(leftRaw, rightRaw) {
104
+ const left = parseSemver(leftRaw);
105
+ const right = parseSemver(rightRaw);
106
+ if (!left || !right) return null;
107
+ for (const field of ["major", "minor", "patch"]) {
108
+ if (left[field] !== right[field]) return left[field] < right[field] ? -1 : 1;
109
+ }
110
+ return comparePrerelease(left.prerelease, right.prerelease);
111
+ }
112
+
113
+ function isNewerVersion(candidate, current) {
114
+ return compareVersions(candidate, current) === 1;
115
+ }
116
+
117
+ function parseFeatureItems(section) {
118
+ const items = [];
119
+ let category = "";
120
+ let activeItem = null;
121
+ for (const line of section.split(/\r?\n/)) {
122
+ const heading = line.match(/^###\s+(Added|Changed)\s*$/i);
123
+ if (heading) {
124
+ category = heading[1].toLowerCase();
125
+ activeItem = null;
126
+ continue;
127
+ }
128
+ if (/^###\s+/.test(line)) {
129
+ category = "";
130
+ activeItem = null;
131
+ continue;
132
+ }
133
+ if (!category) continue;
134
+ const bullet = line.match(/^-\s+(.+)/);
135
+ if (bullet) {
136
+ activeItem = { category, text: bullet[1].trim() };
137
+ items.push(activeItem);
138
+ continue;
139
+ }
140
+ if (activeItem && /^\s{2,}\S/.test(line)) {
141
+ activeItem.text += ` ${line.trim()}`;
142
+ }
143
+ }
144
+ return items;
145
+ }
146
+
147
+ function parseReleaseNotes(markdown, currentVersion, latestVersion) {
148
+ const source = String(markdown || "");
149
+ const headingPattern = /^## \[v?([^\]]+)\](?:\s+-\s+.*)?\s*$/gm;
150
+ const headings = Array.from(source.matchAll(headingPattern));
151
+ const releases = [];
152
+ for (let index = 0; index < headings.length; index += 1) {
153
+ const version = headings[index][1].trim();
154
+ const newerThanCurrent = compareVersions(version, currentVersion);
155
+ const noNewerThanLatest = compareVersions(version, latestVersion);
156
+ if (newerThanCurrent !== 1 || noNewerThanLatest === null || noNewerThanLatest > 0) continue;
157
+ const start = headings[index].index + headings[index][0].length;
158
+ const end = index + 1 < headings.length ? headings[index + 1].index : source.length;
159
+ releases.push({ version, items: parseFeatureItems(source.slice(start, end)) });
160
+ }
161
+ releases.sort((left, right) => compareVersions(right.version, left.version) || 0);
162
+ return releases;
163
+ }
164
+
165
+ function sanitizeMarkdown(text) {
166
+ return String(text || "")
167
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
168
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
169
+ .replace(/<[^>]+>/g, "")
170
+ .replace(/[`*_~]/g, "")
171
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "")
172
+ .replace(/\s+/g, " ")
173
+ .trim();
174
+ }
175
+
176
+ function truncate(text, maxLength) {
177
+ const chars = Array.from(text);
178
+ if (chars.length <= maxLength) return text;
179
+ return `${chars.slice(0, Math.max(1, maxLength - 1)).join("")}…`;
180
+ }
181
+
182
+ function renderUpdateNotice({ currentVersion, latestVersion, releases, locale = "en", columns = 100 }) {
183
+ const msg = messages[locale] || messages.en;
184
+ const lines = [
185
+ "",
186
+ `✨ ${msg.title}: v${currentVersion} → v${latestVersion}`,
187
+ "",
188
+ `${msg.whatsNew}:`,
189
+ ];
190
+ const allItems = releases.flatMap((release) =>
191
+ release.items.map((item) => ({ ...item, version: release.version })),
192
+ );
193
+ const maxTextLength = Math.max(60, Math.min(180, Number(columns || 100) - 18));
194
+ let displayed = 0;
195
+ let previousVersion = "";
196
+ for (const item of allItems.slice(0, MAX_FEATURE_ITEMS)) {
197
+ if (item.version !== previousVersion) {
198
+ lines.push(` v${item.version}`);
199
+ previousVersion = item.version;
200
+ }
201
+ const category = item.category === "added" ? msg.added : msg.changed;
202
+ lines.push(` • ${category}: ${truncate(sanitizeMarkdown(item.text), maxTextLength)}`);
203
+ displayed += 1;
204
+ }
205
+ if (displayed === 0) lines.push(` • ${msg.maintenance}`);
206
+ if (allItems.length > displayed) lines.push(` ${fmt(msg.omitted, allItems.length - displayed)}`);
207
+ lines.push("", `${msg.changelog}: ${CHANGELOG_URL}`, "");
208
+ return `${lines.join("\n")}\n`;
209
+ }
210
+
211
+ function renderChoices(output, msg, selected, rerender) {
212
+ if (rerender) {
213
+ readline.cursorTo(output, 0);
214
+ readline.moveCursor(output, 0, -2);
215
+ readline.clearScreenDown(output);
216
+ }
217
+ const marker = (index) => (selected === index ? "❯" : " ");
218
+ output.write([
219
+ `${marker(0)} ${msg.updateNow}`,
220
+ `${marker(1)} ${msg.later}`,
221
+ ` ${msg.hint}`,
222
+ ].join("\n"));
223
+ }
224
+
225
+ function selectUpdateAction({ input = process.stdin, output = process.stderr, locale = "en" } = {}) {
226
+ const msg = messages[locale] || messages.en;
227
+ if (!input.isTTY || !output.isTTY) return Promise.resolve("later");
228
+ readline.emitKeypressEvents(input);
229
+ const wasRaw = Boolean(input.isRaw);
230
+ const wasPaused = typeof input.isPaused === "function" ? input.isPaused() : true;
231
+ if (typeof input.setRawMode === "function") input.setRawMode(true);
232
+ input.resume();
233
+ let selected = 0;
234
+ let rendered = false;
235
+
236
+ return new Promise((resolve, reject) => {
237
+ function cleanup() {
238
+ input.removeListener("keypress", onKeypress);
239
+ if (typeof input.setRawMode === "function" && !wasRaw) input.setRawMode(false);
240
+ if (wasPaused && typeof input.pause === "function") input.pause();
241
+ output.write("\n");
242
+ }
243
+
244
+ function onKeypress(_value, key = {}) {
245
+ if (key.ctrl && key.name === "c") {
246
+ cleanup();
247
+ const err = new Error("update prompt interrupted");
248
+ err.code = "UPDATE_PROMPT_INTERRUPTED";
249
+ reject(err);
250
+ return;
251
+ }
252
+ if (key.name === "up" || key.name === "down") {
253
+ selected = selected === 0 ? 1 : 0;
254
+ renderChoices(output, msg, selected, rendered);
255
+ rendered = true;
256
+ return;
257
+ }
258
+ if (key.name === "return" || key.name === "enter") {
259
+ cleanup();
260
+ resolve(selected === 0 ? "update" : "later");
261
+ }
262
+ }
263
+
264
+ input.on("keypress", onKeypress);
265
+ renderChoices(output, msg, selected, rendered);
266
+ rendered = true;
267
+ });
268
+ }
269
+
270
+ function npmInvocation(args, env = process.env) {
271
+ if (process.platform !== "win32") return { command: "npm", args };
272
+ return {
273
+ command: env.ComSpec || env.COMSPEC || "cmd.exe",
274
+ args: ["/d", "/s", "/c", "npm", ...args],
275
+ };
276
+ }
277
+
278
+ function getLatestVersion({ env = process.env } = {}) {
279
+ const invocation = npmInvocation(["view", PKG, "version"], env);
280
+ const value = execFileSync(invocation.command, invocation.args, {
281
+ encoding: "utf8",
282
+ env,
283
+ stdio: ["ignore", "pipe", "pipe"],
284
+ timeout: NETWORK_TIMEOUT_MS,
285
+ }).trim();
286
+ return parseSemver(value) ? value.replace(/^v/, "") : null;
287
+ }
288
+
289
+ function fetchText(url, redirectsLeft = 3) {
290
+ return new Promise((resolve, reject) => {
291
+ const request = https.get(url, {
292
+ headers: {
293
+ Accept: "text/plain",
294
+ "User-Agent": "meegle-cli-update-notifier",
295
+ },
296
+ }, (response) => {
297
+ const status = response.statusCode || 0;
298
+ if (status >= 300 && status < 400 && response.headers.location && redirectsLeft > 0) {
299
+ response.resume();
300
+ const redirectURL = new URL(response.headers.location, url);
301
+ if (redirectURL.protocol !== "https:") {
302
+ reject(new Error("refusing non-HTTPS changelog redirect"));
303
+ return;
304
+ }
305
+ fetchText(redirectURL.toString(), redirectsLeft - 1).then(resolve, reject);
306
+ return;
307
+ }
308
+ if (status !== 200) {
309
+ response.resume();
310
+ reject(new Error(`changelog request failed with HTTP ${status}`));
311
+ return;
312
+ }
313
+ const chunks = [];
314
+ let size = 0;
315
+ response.on("data", (chunk) => {
316
+ size += chunk.length;
317
+ if (size > MAX_CHANGELOG_BYTES) {
318
+ request.destroy(new Error("changelog response is too large"));
319
+ return;
320
+ }
321
+ chunks.push(chunk);
322
+ });
323
+ response.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
324
+ response.on("error", reject);
325
+ });
326
+ request.setTimeout(NETWORK_TIMEOUT_MS, () => request.destroy(new Error("changelog request timed out")));
327
+ request.on("error", reject);
328
+ });
329
+ }
330
+
331
+ function fetchChangelog() {
332
+ return fetchText(CHANGELOG_RAW_URL);
333
+ }
334
+
335
+ function defaultCacheFile(env = process.env) {
336
+ if (env.MEEGLE_UPDATE_CACHE_FILE) return env.MEEGLE_UPDATE_CACHE_FILE;
337
+ return path.join(os.homedir(), ".meegle", "cache", "update-notifier.json");
338
+ }
339
+
340
+ function readCache(cacheFile) {
341
+ try {
342
+ const parsed = JSON.parse(fs.readFileSync(cacheFile, "utf8"));
343
+ return parsed && parsed.schema_version === CACHE_SCHEMA_VERSION ? parsed : {};
344
+ } catch (_) {
345
+ return {};
346
+ }
347
+ }
348
+
349
+ function writeCache(cacheFile, value) {
350
+ try {
351
+ fs.mkdirSync(path.dirname(cacheFile), { recursive: true, mode: 0o700 });
352
+ fs.writeFileSync(cacheFile, `${JSON.stringify({ schema_version: CACHE_SCHEMA_VERSION, ...value }, null, 2)}\n`, {
353
+ mode: 0o600,
354
+ });
355
+ } catch (_) {
356
+ // Update checks are best-effort and must never block a CLI command.
357
+ }
358
+ }
359
+
360
+ function truthyEnv(value) {
361
+ const normalized = String(value || "").trim().toLowerCase();
362
+ return normalized !== "" && normalized !== "0" && normalized !== "false" && normalized !== "no";
363
+ }
364
+
365
+ function shouldRunUpdateCheck({
366
+ args = [],
367
+ input = process.stdin,
368
+ output = process.stderr,
369
+ stdout = process.stdout,
370
+ env = process.env,
371
+ } = {}) {
372
+ if (!input.isTTY || !output.isTTY || !stdout.isTTY) return false;
373
+ if (truthyEnv(env.CI) || truthyEnv(env.MEEGLE_NO_UPDATE_CHECK)) return false;
374
+ const command = args[0] || "";
375
+ return command !== "install" && command !== "completion" && !command.startsWith("__complete");
376
+ }
377
+
378
+ function getGlobalBinaryPath(binName, env = process.env) {
379
+ try {
380
+ const invocation = npmInvocation(["root", "-g"], env);
381
+ const root = execFileSync(invocation.command, invocation.args, {
382
+ encoding: "utf8",
383
+ env,
384
+ stdio: ["ignore", "pipe", "pipe"],
385
+ timeout: NETWORK_TIMEOUT_MS,
386
+ }).trim();
387
+ const candidate = path.join(root, ...PKG.split("/"), "bin", binName);
388
+ fs.accessSync(candidate, process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
389
+ return candidate;
390
+ } catch (_) {
391
+ return "";
392
+ }
393
+ }
394
+
395
+ function updateAgentSkillBestEffort({ output = process.stderr, locale = "en", installerFn } = {}) {
396
+ const msg = messages[locale] || messages.en;
397
+ output.write(msg.skillUpdating);
398
+ let updated = false;
399
+ try {
400
+ const install = installerFn || require("./install-wizard.js").installAgentSkill;
401
+ updated = install({ bestEffort: true }) !== false;
402
+ } catch (_) {
403
+ updated = false;
404
+ }
405
+ output.write(updated ? msg.skillUpdated : msg.skillSkipped);
406
+ return updated;
407
+ }
408
+
409
+ function installLatest({
410
+ latestVersion,
411
+ binName,
412
+ env = process.env,
413
+ output = process.stderr,
414
+ locale = "en",
415
+ skillInstallerFn,
416
+ spawnFn = spawnSync,
417
+ globalBinaryPathFn = getGlobalBinaryPath,
418
+ }) {
419
+ const msg = messages[locale] || messages.en;
420
+ output.write(fmt(msg.updating, latestVersion));
421
+ const invocation = npmInvocation(["install", "-g", `${PKG}@latest`], env);
422
+ const result = spawnFn(invocation.command, invocation.args, {
423
+ env,
424
+ stdio: "inherit",
425
+ timeout: 120000,
426
+ });
427
+ if (result.signal) process.kill(process.pid, result.signal);
428
+ if (result.error || result.status !== 0) {
429
+ output.write(msg.updateFailed);
430
+ return { ok: false, binaryPath: "" };
431
+ }
432
+ output.write(fmt(msg.updated, latestVersion));
433
+ const skillUpdated = updateAgentSkillBestEffort({ output, locale, installerFn: skillInstallerFn });
434
+ output.write(msg.continuing);
435
+ return { ok: true, binaryPath: globalBinaryPathFn(binName, env), skillUpdated };
436
+ }
437
+
438
+ async function maybeCheckForUpdates(options = {}) {
439
+ const args = options.args || [];
440
+ const input = options.input || process.stdin;
441
+ const output = options.output || process.stderr;
442
+ const stdout = options.stdout || process.stdout;
443
+ const env = options.env || process.env;
444
+ const currentVersion = options.currentVersion;
445
+ const binName = options.binName || "";
446
+ const now = options.now === undefined ? Date.now() : options.now;
447
+ const locale = options.locale || localeFromEnv(env);
448
+ const cacheFile = options.cacheFile || defaultCacheFile(env);
449
+ if (!shouldRunUpdateCheck({ args, input, output, stdout, env }) || !parseSemver(currentVersion)) {
450
+ return { status: "skipped", updated: false, binaryPath: "" };
451
+ }
452
+
453
+ const cache = readCache(cacheFile);
454
+ if (cache.current_version === currentVersion && Number(cache.next_check_at || 0) > now) {
455
+ return { status: "cached", updated: false, binaryPath: "" };
456
+ }
457
+
458
+ const latestVersionFn = options.getLatestVersionFn || getLatestVersion;
459
+ let latestVersion;
460
+ try {
461
+ latestVersion = await latestVersionFn({ env });
462
+ } catch (_) {
463
+ latestVersion = null;
464
+ }
465
+ if (!latestVersion || !parseSemver(latestVersion)) {
466
+ writeCache(cacheFile, {
467
+ current_version: currentVersion,
468
+ checked_at: now,
469
+ next_check_at: now + FAILURE_RETRY_MS,
470
+ });
471
+ return { status: "unavailable", updated: false, binaryPath: "" };
472
+ }
473
+ if (!isNewerVersion(latestVersion, currentVersion)) {
474
+ writeCache(cacheFile, {
475
+ current_version: currentVersion,
476
+ latest_version: latestVersion,
477
+ checked_at: now,
478
+ next_check_at: now + CHECK_INTERVAL_MS,
479
+ });
480
+ return { status: "current", updated: false, binaryPath: "" };
481
+ }
482
+
483
+ const fetchChangelogFn = options.fetchChangelogFn || fetchChangelog;
484
+ let changelog = "";
485
+ try {
486
+ changelog = await fetchChangelogFn();
487
+ } catch (_) {
488
+ // Version discovery still succeeded, so show the update with a link even
489
+ // when GitHub is temporarily unreachable.
490
+ }
491
+ const releases = parseReleaseNotes(changelog, currentVersion, latestVersion);
492
+ output.write(renderUpdateNotice({
493
+ currentVersion,
494
+ latestVersion,
495
+ releases,
496
+ locale,
497
+ columns: output.columns,
498
+ }));
499
+
500
+ const selectActionFn = options.selectActionFn || selectUpdateAction;
501
+ const action = await selectActionFn({ input, output, locale });
502
+ if (action !== "update") {
503
+ writeCache(cacheFile, {
504
+ current_version: currentVersion,
505
+ latest_version: latestVersion,
506
+ checked_at: now,
507
+ next_check_at: now + CHECK_INTERVAL_MS,
508
+ deferred_at: now,
509
+ });
510
+ return { status: "deferred", updated: false, binaryPath: "" };
511
+ }
512
+
513
+ const installFn = options.installFn || installLatest;
514
+ const installResult = await installFn({ latestVersion, binName, env, output, locale });
515
+ const ok = installResult === true || Boolean(installResult && installResult.ok);
516
+ writeCache(cacheFile, {
517
+ current_version: ok ? latestVersion : currentVersion,
518
+ latest_version: latestVersion,
519
+ checked_at: now,
520
+ next_check_at: now + (ok ? CHECK_INTERVAL_MS : FAILURE_RETRY_MS),
521
+ });
522
+ return {
523
+ status: ok ? "updated" : "update-failed",
524
+ updated: ok,
525
+ binaryPath: ok && installResult.binaryPath ? installResult.binaryPath : "",
526
+ };
527
+ }
528
+
529
+ module.exports = {
530
+ CHANGELOG_URL,
531
+ CHECK_INTERVAL_MS,
532
+ compareVersions,
533
+ defaultCacheFile,
534
+ getLatestVersion,
535
+ installLatest,
536
+ isNewerVersion,
537
+ localeFromEnv,
538
+ maybeCheckForUpdates,
539
+ parseReleaseNotes,
540
+ renderUpdateNotice,
541
+ selectUpdateAction,
542
+ shouldRunUpdateCheck,
543
+ updateAgentSkillBestEffort,
544
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-project/meegle",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "description": "Agent-First CLI for Meegle (Lark Project)",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/larksuite/meegle-cli#readme",
@@ -29,6 +29,7 @@
29
29
  "files": [
30
30
  "bin/meegle.js",
31
31
  "bin/install-wizard.js",
32
+ "bin/update-notifier.js",
32
33
  "bin/meegle-*",
33
34
  "README.md",
34
35
  "README.zh-CN.md",
@@ -38,7 +39,8 @@
38
39
  "third_party_licenses/**"
39
40
  ],
40
41
  "scripts": {
41
- "prepublishOnly": "cp ../../README.zh-CN.md ./README.zh-CN.md && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md && cp ../../THIRD_PARTY_NOTICES.md ./THIRD_PARTY_NOTICES.md && rm -rf ./third_party_licenses && cp -R ../../third_party_licenses ./third_party_licenses && cp ../../README.md ./README.md"
42
+ "prepublishOnly": "cp ../../README.zh-CN.md ./README.zh-CN.md && cp ../../LICENSE ./LICENSE && cp ../../CHANGELOG.md ./CHANGELOG.md && cp ../../THIRD_PARTY_NOTICES.md ./THIRD_PARTY_NOTICES.md && rm -rf ./third_party_licenses && cp -R ../../third_party_licenses ./third_party_licenses && cp ../../README.md ./README.md",
43
+ "test": "node --test test/*.test.js"
42
44
  },
43
45
  "keywords": [
44
46
  "meegle",