@qilin-agent/cli 0.0.0-stage → 3.1.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kkutysllb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,22 @@
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
3
+ # After editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write apps/cli/README.md
5
+ /qilin-agent-cli:
6
+ en: 063f13813a8fa190
7
+ zh: 682d157e19163ff0
8
+ /qilin-agent-cli/entry-modes:
9
+ en: cebfeaa9a9e60c00
10
+ zh: 652208729fa392df
11
+ /qilin-agent-cli/app-arguments:
12
+ en: 979c1c9bedaf1b3e
13
+ zh: a77b6d9db2cfc191
14
+ /qilin-agent-cli/profiles:
15
+ en: 7d717e3a016726f8
16
+ zh: 9ffeeb5933153c80
17
+ /qilin-agent-cli/optional-overlays:
18
+ en: 55972a91e0d1d24d
19
+ zh: 920ae26285d8e1b6
20
+ /qilin-agent-cli/development:
21
+ en: fa8d7705c54df6f5
22
+ zh: 8fe3f713cfe31dbf
package/README.md CHANGED
@@ -1,3 +1,79 @@
1
- # Temporary Holding Version
1
+ # `@qilin-agent/cli`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ English | [中文](README.zh.md)
4
+
5
+ The `qilin` command is the sole supported Node application launcher: profiles are ordered stacks of plugin-bundle patch layers under the user's own overrides. SDK and ACP are profiles, not separate public bins. The Python runtime wheel packages this same command; the SDK defaults to `sdk`, and the minimal example selects `sdk-minimal`. [`src/args.ts`](src/args.ts) owns the command grammar, and [`src/bin.ts`](src/bin.ts) loads only the selected runner. Invalid commands, options from another mode, and fatal configuration or boot failures exit nonzero.
6
+
7
+ ## Entry modes
8
+
9
+ | Command | Purpose |
10
+ |---|---|
11
+ | `qilin <name>` / `qilin --profile <name>` | Boot the named profile under `$QILIN_HOME/profiles/<name>`. |
12
+ | `qilin --profile <name> --from-default-profile <template>` | Create a new custom profile from a shipped template, then boot it. |
13
+ | `qilin --profile acp` | Serve automation clients over ACP stdio until disconnect. |
14
+ | `qilin --profile headless "job"` | Run one fresh persisted session, print the final answer, and exit. |
15
+ | `qilin --profile sdk` | Serve SDK clients over JSON-RPC stdio until shutdown or disconnect. |
16
+ | `qilin --profile sdk-minimal` | Serve SDK clients with the standalone minimal agent tree. |
17
+ | `qilin`, `qilin web` | Serve the deployment at one address: one isolated Web runtime per account, each serving the application's own landing page and sign-in form. The profile you already start is the deployment. |
18
+ | `qilin tenants accounts` | List this Harness home's accounts and the tenant directory each owns. |
19
+ | `qilin tenants add <username>` | Create an account, prompting for its password or reading `--password-stdin`. |
20
+ | `qilin tenants passwd <username>` | Replace an account's password and retire the sessions it issued. |
21
+ | `qilin tenants remove <username>` | Retire an account, keeping its tenant directory unless `--purge-data` is given. |
22
+ | `qilin plugin --profile <name> <pnpm args>` | Manage a profile's plugins by forwarding to pnpm in the profile directory. |
23
+ | `qilin plugin list` | List a profile's bundle layers in activation order; omit `--profile` for the product profile. |
24
+ | `qilin plugin doctor <package\|directory>` | Report one plugin package's DSH-era compatibility without installing or running anything. |
25
+
26
+ The browser-serving profiles — the product surface `qilin` and `qilin web` — are the deployment: one command, one address, with no second entry. The entry draws no interface of its own: it starts one Web runtime per account on demand under `$QILIN_HOME/tenants/<account id>/`, and forwards every request the browser makes — the application document, the `/api` channel, the mux upgrade, and ranged media reads — to the runtime its routing cookie names, with the runtime's own authority in place of the entry's so its session cookie stays bound to the runtime that minted it. A runtime's harness home is its own: sessions, tasks, workspaces, attachments, plugins, model settings, credentials, skills, and presets resolve there, and only the roster at `$QILIN_HOME/auth/accounts.json` is shared. Credentials are verified by the named account's own runtime: the entry forwards the sign-in submission there and relays the session cookie it issues, and the runtime admits only its own account, so a forged routing cookie reaches a runtime that does not recognize the caller. `--idle-ms` reclaims a runtime that has served no traffic for that long, and `0` disables reclamation; `--max-tenants` bounds how many runtimes live at once, reclaiming the least recently used one past that bound; `--restart-backoff-ms` sets the first wait before a runtime that failed to start is retried, doubling per consecutive failure. `--patch` composes every runtime with an extra overlay layer; `--tls-cert` and `--tls-key` serve the entry's address over HTTPS and terminate TLS in front of the loopback runtimes, so the pair is supplied together and is refused with `--per-account-ports`. The entry reads only the flags it owns: every other argument reaches each runtime's own command line, and `--host` refuses a wildcard address. One browser drives one account at a time. The application inside a runtime owns the landing page, the sign-in form, and registration. With accounts in the roster the bare address redirects to the first account's path, `/t/<account>/`, and every other account is reachable at its own. With no accounts yet, every path goes to a first-run runtime, whose layer admits whoever the roster holds, so the product's own form creates the first account and the deployment never describes what to type. `--registration open|closed` decides whether the product's form may create accounts: the default is `open` on a loopback address and `closed` on every other, so an address other machines reach creates an account only where the operator says so, with `qilin tenants add`; a name the roster does not hold, a routing cookie naming an account it no longer holds, and every other request that names no account are answered by that same runtime, so the deployment never speaks for the application. `/healthz` and `/status` stay the entry's own reads.
27
+
28
+ The roster subcommands maintain the accounts that shared file holds. A tenant runtime admits exactly the account it serves and closes registration, so its own Web surface cannot create a second account; the first-run runtime carries the deployment's policy instead, which is where a deployment that accepts accounts grows through the product's own form. `add` creates an account without a browser, `passwd` re-keys one and retires its issued sessions, and `remove` retires one while keeping its Sessions on disk unless `--purge-data` is given. A runtime started for a removed account fails loud, because its overlay admits an account the file no longer holds.
29
+
30
+ The invoking directory is the default workspace root. The `web`, `headless`, `sdk`, `sdk-minimal`, and `acp` profiles auto-initialize on first use from shipped templates. Create another profile at an unused, non-shipped name with `--from-default-profile`, or initialize a base-backed profile through `qilin plugin`. The `desktop` name is reserved for the Electron-owned profile, so the CLI rejects boot, config-dump, and plugin-management requests for it. After a successful package operation the command reconciles the profile's bundle list, and it refuses a profile whose installed dependencies include an upstream DSH-era engine package: the diagnostic names each colliding package, the QiLin package it maps onto, and the `remove` command that clears it, and the command exits 1 without changing the bundle list.
31
+
32
+ `list` and `doctor` read the profile and never initialize it or run pnpm. `doctor` accepts an installed package name or a package directory and checks the four rules a plugin must satisfy to load here: whether the package builds the DSH-era home itself instead of reading `DSH_HOME`/`QILIN_HOME`, installs an engine package the harness supplies, imports an engine name it never declared as a peer, and injects client module names the compatibility layer cannot map. It prints one finding per line and exits 1 only when a finding blocks activation.
33
+
34
+ `list` marks a shipped template layer with `(shipped)`: it moves with the running installation and cannot be dropped from the profile. Every other listed layer belongs to the profile — for example the retired `dsh-animations` animations skill pack installed back through `qilin plugin add` — and upgrades or removes in place without a QiLin release.
35
+
36
+ Install the command itself from the published package (`npm install -g @qilin-agent/cli`) to get `qilin` on `PATH`; the manifest declares `lib/bin.js` as the `qilin` bin and ships only that bundle.
37
+
38
+ ## App arguments
39
+
40
+ The launcher parses only its own flags and hands everything after them to the booted profile, where any injected app plugin may parse the shared immutable snapshot ([`qilin-cmdline`](../../packages/boot/cmdline/README.md)). The first token the launcher does not recognize starts the app's arguments:
41
+
42
+ ```sh
43
+ qilin --profile web --port 8080 # --port is the deployment's address
44
+ qilin --profile tui --resume <id> # example, assuming the tui profile is installed; --resume belongs to the terminal app
45
+ qilin --profile headless "run the tests"
46
+ qilin --profile web --patch ./extra.cordis.yml # the overlay every account's runtime composes with
47
+ qilin --profile web --help # the deployment's flags, then the app's
48
+ qilin --help # the launcher's own help
49
+ ```
50
+
51
+ <a id="profiles"></a>
52
+ ## Profiles
53
+
54
+ A profile directory holds a `package.json` (out-of-tree plugin dependencies plus the profile manifest `qilin.profile` with its ordered `bundles` list) and a `cordis.patch.yml` (the user's own patch layer). `qilin-hmr`, when enabled in YAML, watches the profile manifest and both profile and home patch files, then recomposes all layers through one serialized reload. Without HMR, changes apply on restart. Edits arriving during watcher registration use the same nonfatal reload reporting as later edits. [Plugin Manager](../../packages/boot/plugin-manager/README.md) shares package operations and the profile write lock with `qilin plugin`; package updates retain disabled bundle selections. CLI package commands inherit authentication variables and terminal descriptors, including interactive build approval; service calls retain their scrubbed environment and captured diagnostics.
55
+
56
+ Installation and profile startup enforce declared qilin peer ranges against the same runtime version `qilin --version` prints. Incompatible plugins require an explicitly acknowledged exact-version exemption, which `qilin plugin allow-version`, `revoke-version`, and `version-exemptions` manage for one profile. The [plugin manager's compatibility reference](../../packages/boot/plugin-manager/README.md#version-compatibility-and-exemptions) documents the commands, persistence, and risks.
57
+
58
+ The tree composes over an empty root:
59
+ - each bundle's patch in `qilin.profile.bundles` order
60
+ - then the profile's `cordis.patch.yml`, then the home-level `$QILIN_HOME/cordis.patch.yml`
61
+ - then `--patch` overlays
62
+
63
+ Bundles named in `qilin.profile.bundles` resolve from the qilin installation first (`@qilin-agent/base`, `@qilin-agent/web-app`, `@qilin-agent/headless`, `@qilin-agent/sdk-app`, `@qilin-agent/sdk-minimal`, `@qilin-agent/acp-app`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins.
64
+
65
+ Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it.
66
+
67
+ The [CLI behavior reference](reference/README.md) owns exact layer precedence, flags, shutdown behavior, deployment defaults, and source execution. The [startup and reload failure table](../../packages/boot/app-boot/README.md#startup-and-reload-failures) compares optional and required plugin failures with configuration HMR.
68
+
69
+ ## Optional overlays
70
+
71
+ `config/examples/` ships opt-in overlays for GitHub review webhooks, session-local Schedule, memory MCP servers, and runtime Kylin tools. They are never part of a default profile; the [user guides](../../docs/user/guide/index.md) and [developer practice guides](../../docs/user/develop/practice/index.md) own setup and safety instructions.
72
+
73
+ ## Development
74
+
75
+ Production runs require built package and frontend artifacts. From the repository root, run `pnpm run build` separately, then use `pnpm qilin <args...>` to run the TypeScript entry and forward every argument; the [source-execution reference](reference/README.md#source-execution) owns the module-resolution contract.
76
+
77
+ The `@qilin-agent/cli/profile-boot` export provides the shared profile lifecycle to the Desktop host. A resolved application profile supplies its own installation anchor for runtime package resolution while retaining the Harness home patch, proxy environment, telemetry switch, patch reload, and bounded shutdown.
78
+
79
+ The [Web failure matrix](tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts) runs the built CLI through startup failures and native configuration HMR with `awaitWriteFinish` enabled in `test:expected`. It verifies authenticated HTTP responses, diagnostics, recovery, process exits, and disposal without model API calls; the [startup acceptance](tests/profiles/web/tests/web-best-effort-startup.expected.e2e.ts) also covers the shipped required Web dependencies and port conflicts.
package/README.zh.md ADDED
@@ -0,0 +1,79 @@
1
+ # `@qilin-agent/cli`
2
+
3
+ [English](README.md) | 中文
4
+
5
+ `qilin` 是唯一受支持的 Node 应用启动器;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。SDK 与 ACP(Agent Client Protocol)都是 profile,而不是独立的公开可执行命令。Python 运行时 wheel 包中也包含同一个命令;SDK 默认使用 `sdk`,极简示例选择 `sdk-minimal`。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项,以及致命的配置或启动错误都会以非零状态退出。
6
+
7
+ ## 入口模式
8
+
9
+ | 命令 | 用途 |
10
+ |---|---|
11
+ | `qilin <name>` / `qilin --profile <name>` | 启动位于 `$QILIN_HOME/profiles/<name>` 的指定 profile。 |
12
+ | `qilin --profile <name> --from-default-profile <template>` | 从随附模板创建新的自定义 profile,然后启动它。 |
13
+ | `qilin --profile acp` | 通过 ACP stdio 为自动化客户端提供服务,直至断开连接。 |
14
+ | `qilin --profile headless "job"` | 运行一个全新的持久化会话,打印最终答案并退出。 |
15
+ | `qilin --profile sdk` | 通过 JSON-RPC stdio 为 SDK 客户端提供服务,直至关闭或断开连接。 |
16
+ | `qilin --profile sdk-minimal` | 以独立极简 agent(智能体)配置树为 SDK 客户端提供服务。 |
17
+ | `qilin`、`qilin web` | 在一个地址上服务整套部署:每个账户一个隔离的 Web 运行时,各自服务应用自己的落地页与登录表单。你原本就在启动的那个 profile 就是整套部署。 |
18
+ | `qilin tenants accounts` | 列出本 harness home 的账户,以及每个账户拥有的租户目录。 |
19
+ | `qilin tenants add <用户名>` | 创建一个账户,交互式输入密码,或读取 `--password-stdin`。 |
20
+ | `qilin tenants passwd <用户名>` | 替换某个账户的密码,并使其已签发的会话失效。 |
21
+ | `qilin tenants remove <用户名>` | 注销某个账户,默认保留其租户目录,除非给出 `--purge-data`。 |
22
+ | `qilin plugin --profile <name> <pnpm args>` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 |
23
+ | `qilin plugin list` | 按激活顺序列出某个 profile 的 bundle 层;省略 `--profile` 时使用产品 profile。 |
24
+ | `qilin plugin doctor <包名\|目录>` | 报告某个插件包的 DSH 时代兼容性,不安装也不运行任何东西。 |
25
+
26
+ 会服务浏览器的 profile——产品面 `qilin` 与 `qilin web`——就是整套部署:一条命令,一个地址,没有第二个入口。入口自己不作画:它按需在 `$QILIN_HOME/tenants/<账户 id>/` 下为每个账户启动一个 Web 运行时,并把浏览器发出的每个请求——应用文档、`/api` 通道、mux 升级与带 Range 的媒体读取——转发到它的路由 cookie 所命中的那个运行时,同时把运行时自己的 authority 顶替掉入口的,从而让它的会话 cookie 始终绑定在签发它的那个运行时上。运行时的 harness home 归它自己:会话、任务、工作区、附件、插件、模型设置、凭据、技能与预设都在其中解析,只有 `$QILIN_HOME/auth/accounts.json` 这份名册是共享的。凭据由被指名账户自己的运行时校验:入口把登录提交转发到那里,并中继它签发的会话 cookie;运行时只接纳自己的账户,因此伪造的路由 cookie 只会到达一个不认可调用者的运行时。`--idle-ms` 会回收在该时长内没有服务过任何流量的运行时,`0` 表示不回收;`--max-tenants` 限定同时存活多少个运行时,超出上限时回收最久未使用的那个;`--restart-backoff-ms` 设定启动失败的运行时第一次重试前的等待,每次连续失败翻倍。`--patch` 让每个运行时都多叠一层 overlay;`--tls-cert` 与 `--tls-key` 让入口地址以 HTTPS 服务,并在 loopback 运行时之前终止 TLS,因此两者必须成对给出,且与 `--per-account-ports` 互斥。入口只读自己拥有的 flag:其余参数一律交给每个运行时自己的命令行,而 `--host` 拒绝通配地址。同一个浏览器一次只服务一个账户。落地页、登录表单与注册都归运行时内部的应用所有。名册里有账户时,裸地址会重定向到第一个账户的路径 `/t/<账户>/`,其余账户各自在自己的路径上可达。名册为空时,每个路径都交给首启运行时,它那一层的接纳判据就是名册里有什么就接纳什么,于是由产品自己的表单创建第一个账户,入口从不说明该输入什么。`--registration open|closed` 决定产品自己的表单能否创建账户:loopback 地址上默认为 `open`,其他地址上默认为 `closed`,因此别的机器能到达的地址只在运维明说时才创建账户(用 `qilin tenants add`);名册里没有的名字、指向一个名册已不再持有的账户的路由 cookie,以及其余任何没有指名账户的请求,都由同一个运行时回答,因此入口从不替应用说话。`/healthz` 与 `/status` 仍归入口自己读取。
27
+
28
+ roster 子命令维护该共享文件中的账户。租户运行时只接纳自己服务的那个账户且关闭注册,因此它自己的 Web 界面无法创建第二个账户;首启运行时承载的则是部署自己的策略,接受新账户的部署由此通过产品自己的表单扩充账户。`add` 无需浏览器即可创建账户,`passwd` 为一个账户重新设置密码并使其已签发的会话失效,`remove` 注销一个账户,默认把它的 Session 留在磁盘上,除非给出 `--purge-data`。为已注销账户启动的运行时会明确失败,因为它的覆盖层接纳的是一个文件里已不存在的账户。
29
+
30
+ 运行命令时所在的目录将作为默认 workspace 根目录。`web`、`headless`、`sdk`、`sdk-minimal` 和 `acp` profile 在首次使用时会从随附模板自动初始化。使用 `--from-default-profile` 可以基于这些模板之一,在尚未使用的非内置名称处创建其他 profile;通过 `qilin plugin` 则可以初始化一个以 base 为基础的 profile。`desktop` 名称保留给 Electron 持有的 profile,因此 CLI(命令行界面)会拒绝针对它的启动、配置 dump 和插件管理请求。包操作成功后命令会协调 profile 的 bundle 列表;若 profile 的已安装依赖中出现上游 DSH 时代的引擎包,命令会拒绝:诊断会指明每个冲突包、它映射到的 QiLin 包,以及清除它的 `remove` 命令,命令以退出码 1 结束且不改变 bundle 列表。
31
+
32
+ `list` 与 `doctor` 只读取 profile,既不初始化它也不运行 pnpm。`doctor` 接受已安装的包名或包目录,检查插件要在此加载必须满足的四条规则:包是否自己拼出 DSH 时代的主目录而不是读取 `DSH_HOME`/`QILIN_HOME`、是否安装了 harness 已提供的引擎包、是否导入了未声明为 peer 的引擎名、以及是否注入了兼容层无法映射的客户端模块名。它每行输出一条发现,仅当某条发现会阻断激活时才以退出码 1 结束。
33
+
34
+ `list` 把随附模板层标为 `(shipped)`:它随运行中的安装实例走,无法从 profile 移除。列表里其余每层都归 profile 所有——例如通过 `qilin plugin add` 装回的已退役 `dsh-animations` 动效技能包——可原地升级或移除,无需等待 QiLin 发版。
35
+
36
+ 命令本身从已发布的包安装(`npm install -g @qilin-agent/cli`)即可把 `qilin` 放到 `PATH`;manifest 把 `lib/bin.js` 声明为 `qilin` bin,并且只随包发布该 bundle。
37
+
38
+ ## 应用参数
39
+
40
+ 启动器只解析自身的 flag,并将其后的所有内容交给已启动的 profile;注入该 profile 的任意应用插件都可以解析这份共享的不可变快照([`qilin-cmdline`](../../packages/boot/cmdline/README.zh.md))。启动器无法识别的第一个 token 标志着应用参数的开始:
41
+
42
+ ```sh
43
+ qilin --profile web --port 8080 # --port is the deployment's address
44
+ qilin --profile tui --resume <id> # example, assuming the tui profile is installed; --resume belongs to the terminal app
45
+ qilin --profile headless "run the tests"
46
+ qilin --profile web --patch ./extra.cordis.yml # the overlay every account's runtime composes with
47
+ qilin --profile web --help # the deployment's flags, then the app's
48
+ qilin --help # the launcher's own help
49
+ ```
50
+
51
+ <a id="profiles"></a>
52
+ ## Profile
53
+
54
+ profile 目录包含一个 `package.json`,其中记录树外插件依赖,以及 profile manifest(元数据清单)`qilin.profile`、其中按顺序排列的 `bundles` 列表;还包含一个 `cordis.patch.yml`,其中保存用户自己的 patch 层。在 YAML 中启用的 `qilin-hmr` 监视 profile manifest、profile 与 home 级 patch 文件,再通过统一串行重载重新组合所有层。未启用 HMR 时,更改在重启后生效。监听器注册期间发生的编辑与后续编辑使用相同的非致命重载错误报告。[插件管理器](../../packages/boot/plugin-manager/README.zh.md) 与 `qilin plugin` 共享包操作和 profile 写锁;更新依赖会保留已停用的组合包选择。CLI 包操作继承认证环境和终端描述符,支持交互式构建批准;service 调用保留清理后的环境并捕获诊断。
55
+
56
+ 安装与 profile 启动会用 `qilin --version` 打印的同一个运行时版本校验已声明的 qilin peer 范围。不兼容的插件必须显式确认精确版本豁免,`qilin plugin allow-version`、`revoke-version` 与 `version-exemptions` 按 profile 管理这些豁免。[插件管理器兼容性参考](../../packages/boot/plugin-manager/README.zh.md#version-compatibility-and-exemptions)记录了命令、持久化方式与风险。
57
+
58
+ 配置树以空根为起点,依次叠加以下配置层:
59
+ - `qilin.profile.bundles` 中各组合包的 patch
60
+ - profile 自身的 `cordis.patch.yml`,然后是 home 级的 `$QILIN_HOME/cordis.patch.yml`
61
+ - `--patch` 指定的覆盖层
62
+
63
+ `qilin.profile.bundles` 中列出的组合包先从 qilin 安装目录解析(`@qilin-agent/base`、`@qilin-agent/web-app`、`@qilin-agent/headless`、`@qilin-agent/sdk-app`、`@qilin-agent/sdk-minimal`、`@qilin-agent/acp-app`),再从 profile 自身的 `node_modules` 解析;pnpm 会将树外插件安装到该目录。
64
+
65
+ 使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。
66
+
67
+ 层的确切优先级、flag、关闭行为、部署默认值和源码执行方式,以 [CLI 行为参考](reference/README.zh.md)为准。[启动与重载失败表](../../packages/boot/app-boot/README.zh.md#startup-and-reload-failures)对比 optional、required 插件启动失败与配置 HMR 的行为。
68
+
69
+ ## 可选覆盖层
70
+
71
+ `config/examples/` 交付 GitHub 评审 webhook、会话内 Schedule、记忆 MCP 服务器与运行时 Kylin 工具的可选覆盖层。它们绝不属于默认 profile;设置与安全说明由[用户指南](../../docs/user/guide/index.zh.md)和[开发实战指南](../../docs/user/develop/practice/index.zh.md)负责。
72
+
73
+ ## 开发
74
+
75
+ 生产运行需要已构建的包与前端产物。请在仓库根目录单独运行 `pnpm run build`,然后使用 `pnpm qilin <args...>` 运行 TypeScript 入口并转发所有参数;模块解析约定以[源码执行参考](reference/README.zh.md#source-execution)为准。
76
+
77
+ `@qilin-agent/cli/profile-boot` 导出向 Desktop Host 提供共享 profile 生命周期。已解析的应用 profile 为运行时包解析指定自己的安装锚点,同时沿用 Harness home patch、代理环境、遥测开关、patch 热重载和有界关闭。
78
+
79
+ [Web 失败矩阵](tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts)在 `test:expected` 中通过构建后的 CLI 验证启动失败与启用 `awaitWriteFinish` 的原生配置 HMR。它不调用模型 API,而是检查经过认证的 HTTP 响应、诊断、恢复、进程退出与 dispose;[启动验收测试](tests/profiles/web/tests/web-best-effort-startup.expected.e2e.ts)还覆盖随附 Web 的必需依赖与端口冲突。
package/lib/bin.js ADDED
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env node
2
+ import { StartupError, getQilinRuntimeVersion, loadLayeredEnv } from "@qilin-agent/app-boot";
3
+ import { resolveQilinHome } from "@qilin-agent/home-paths";
4
+ import { Command, CommanderError, InvalidArgumentError } from "commander";
5
+ import { randomUUID } from "node:crypto";
6
+ import { mkdir, writeFile } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ import { inspect } from "node:util";
9
+ //#region lib/types/args.js
10
+ /**
11
+ * Commander adapter for the `qilin` command line.
12
+ *
13
+ * The launcher parses only what it owns — which profile to boot, which extra
14
+ * patch overlays to apply, and the config dumps — and hands **everything after
15
+ * its own flags** to the booted tree verbatim, where injected app plugins parse
16
+ * their own flag families and print their own `--help` (see
17
+ * `@qilin-agent/cmdline`). Launcher flags therefore come first: the first
18
+ * token this parser does not recognize starts the inner arguments, so
19
+ * `qilin --profile tui --resume abc` boots the tui profile with `--resume abc`,
20
+ * and `qilin --profile web -h` prints the web app's help, not this one's.
21
+ *
22
+ * `qilin <name>` abbreviates `qilin --profile <name>`; `plugin` manages a profile's
23
+ * plugin dependencies by forwarding to pnpm.
24
+ * @module @qilin-agent/cli/args
25
+ */
26
+ /**
27
+ * Repeatable single-value collector: `--patch a.yml --patch b.yml`. Never
28
+ * variadic — a variadic `--patch` would swallow the inner arguments.
29
+ */
30
+ const collect = (value, previous = []) => [...previous, value];
31
+ function selectProfile(value, previous) {
32
+ if (previous !== void 0) throw new InvalidArgumentError("select a profile only once");
33
+ return value;
34
+ }
35
+ function rejectReservedDesktopProfile(program, profile) {
36
+ if (profile.toLowerCase() === "desktop") program.error("error: profile \"desktop\" belongs to the separate QiLin desktop application, not this repository");
37
+ }
38
+ /**
39
+ * Profile a bare `qilin` boots: the QiLin product surface. It is the only
40
+ * profile a user has to know, so naming it again on the command line would
41
+ * only repeat the product's own name.
42
+ */
43
+ const PRODUCT_PROFILE = "qilin";
44
+ /** The launcher's own help text; each app prints its own. */
45
+ const HELP_EXAMPLES = `
46
+ Examples:
47
+ qilin boot the QiLin product surface, then serve it
48
+ qilin --no-open the same, without opening a browser
49
+ qilin web boot the unbranded web surface (same as: qilin --profile web)
50
+ qilin rescue --from-default-profile web
51
+ create rescue from the shipped web template, then boot it
52
+ qilin headless "run the tests" answer one task, print the result, and exit
53
+ qilin tui --patch ./extra.yml boot a custom profile with one extra overlay
54
+ qilin tui --resume <session> arguments after the launcher flags reach the app
55
+ qilin web --help the web app's own flags and help
56
+ qilin plugin list list the product profile's plugin layers
57
+ qilin plugin doctor <package|directory> report one plugin's DSH-era compatibility
58
+ qilin plugin add <package> install a plugin into the product profile
59
+ qilin plugin --profile tui add <package> install a plugin into the tui profile
60
+ qilin plugin version-exemptions list the profile's exact-version exemptions
61
+ qilin plugin allow-version <pkg@ver> --qilin-version <exact> --accept-risk
62
+ allow one rejected plugin/runtime pair
63
+ qilin plugin revoke-version <pkg@ver> --qilin-version <exact>
64
+ withdraw one exemption
65
+ qilin web --port 0 serve the deployment; each account gets its own runtime
66
+ qilin tenants accounts list this home's accounts and their tenant directories
67
+ qilin tenants add alice create an account, prompting for its password
68
+ echo "$PASSWORD" | qilin tenants passwd alice --password-stdin
69
+ replace a password and retire its sessions
70
+ qilin tenants remove alice retire an account, keeping its Sessions on disk
71
+ `;
72
+ /**
73
+ * Resolve a boot or dump invocation from the launcher flags and the leftover
74
+ * inner arguments.
75
+ * @param program - the command whose options were parsed.
76
+ * @param profile - the profile these flags boot.
77
+ * @param options - the launcher flags commander collected.
78
+ * @param args - the leftover arguments, in argv order.
79
+ * @returns the resolved invocation.
80
+ */
81
+ function resolveBoot(program, profile, options, args) {
82
+ const patches = options.patch ?? [];
83
+ if (patches.includes("")) program.error("error: --patch needs a path");
84
+ if (options.fromDefaultProfile === "") program.error("error: --from-default-profile needs a name");
85
+ if (options.dumpConfig !== true && options.dumpDefaultConfig !== true) return {
86
+ mode: "profile",
87
+ profile,
88
+ fromDefaultProfile: options.fromDefaultProfile,
89
+ patches,
90
+ args
91
+ };
92
+ if (options.dumpConfig === true && options.dumpDefaultConfig === true) program.error("error: --dump-config and --dump-default-config are mutually exclusive");
93
+ if (args.length > 0) program.error(`error: config dumps take no app arguments, got ${args.map((argument) => JSON.stringify(argument)).join(" ")}`);
94
+ const defaultOnly = options.dumpDefaultConfig === true;
95
+ if (defaultOnly && patches.length > 0) program.error("error: --dump-default-config prints the bundle layers and takes no --patch");
96
+ return {
97
+ mode: "dump-config",
98
+ profile,
99
+ fromDefaultProfile: options.fromDefaultProfile,
100
+ defaultOnly,
101
+ patches
102
+ };
103
+ }
104
+ /**
105
+ * Resolve argv into one invocation, or print and exit for help, version, or an
106
+ * error.
107
+ * @param argv - arguments after the Node binary and script.
108
+ * @param version - version string printed by `--version`.
109
+ * @returns the resolved invocation.
110
+ */
111
+ function parseQilinArgs(argv, version) {
112
+ const first = argv[0];
113
+ let resolved;
114
+ const program = new Command();
115
+ program.name("qilin").version(version, "-V, --version", "output the version number").usage("[--profile] <name> [options] [app-args...]\n qilin plugin --profile <name> <pnpm-args...>\n qilin web [options]").description("qilin: boot the QiLin product surface, or any profile — an ordered stack of plugin-bundle patch layers under your own overrides.").addHelpText("after", HELP_EXAMPLES).exitOverride().helpOption(false).helpCommand(false).allowUnknownOption().passThroughOptions().enablePositionalOptions().argument("[args...]", "arguments for the booted profile's app (see: qilin --profile <name> --help)").option("--profile <name>", "the profile under $QILIN_HOME/profiles to boot", selectProfile).option("--from-default-profile <name>", "initialize a new custom profile from a shipped profile template").option("--patch <path>", "extra patch-list overlay applied after the profile layer (repeatable)", collect).option("--dump-config", "print the composed profile tree and exit").option("--dump-default-config", "print the profile tree without its user layer or --patch overlays and exit").action((args, options) => {
116
+ if (options.profile === void 0 && args.some((argument) => argument === "-h" || argument === "--help")) program.help();
117
+ const profile = options.profile ?? PRODUCT_PROFILE;
118
+ if (profile === "") program.error("error: --profile needs a name");
119
+ rejectReservedDesktopProfile(program, profile);
120
+ resolved = resolveBoot(program, profile, options, args);
121
+ });
122
+ if (first === "plugin") {
123
+ const plugin = program.command("plugin").description("manage a profile's plugins: list them, forward pnpm arguments, or grant an exact-version compatibility exemption");
124
+ plugin.option("--profile <name>", "the profile whose plugins to manage (defaults to the product profile; initialized on first use)", selectProfile).allowUnknownOption().argument("[args...]", "one of: list, doctor <name|path>, version-exemptions, allow-version <package@version> --qilin-version <exact> --accept-risk, revoke-version <package@version> --qilin-version <exact>; otherwise pnpm arguments forwarded verbatim (add <pkg>, remove <pkg>, why <pkg>, ...)").action((args, options) => {
125
+ const profile = options.profile ?? PRODUCT_PROFILE;
126
+ if (profile === "") program.error("error: --profile needs a name");
127
+ rejectReservedDesktopProfile(plugin, profile);
128
+ if (args.length === 0) program.error("error: plugin needs a subcommand (list, doctor <name|path>, version-exemptions, allow-version <package@version>, revoke-version <package@version>) or pnpm arguments to forward (e.g. add <package>)");
129
+ if (args[0] === "list" && args.length > 1) program.error("error: plugin list takes no further arguments");
130
+ if (args[0] === "doctor" && args.length !== 2) program.error("error: plugin doctor takes exactly one installed package name or package directory");
131
+ resolved = {
132
+ mode: "plugin",
133
+ profile,
134
+ args
135
+ };
136
+ });
137
+ }
138
+ if (first === "tenants") {
139
+ const tenants = program.command("tenants").description("maintain the accounts every runtime under this Harness home shares; qilin web serves the deployment, one isolated runtime per account");
140
+ tenants.action(() => {
141
+ program.error("error: tenants needs a subcommand (accounts, add, passwd, remove)");
142
+ });
143
+ tenants.command("accounts").description("list the accounts of this Harness home and the tenant directory each owns").action(() => {
144
+ resolved = {
145
+ mode: "tenants",
146
+ request: { command: "accounts" }
147
+ };
148
+ });
149
+ tenants.command("add").description("create one account in this Harness home").argument("<username>", "login name: 3-32 characters of a-z, 0-9, _, ., or -, starting with a letter or digit").option("--email <address>", "address stored with the account").option("--admin", "make this the deployment's administrator: the one account that reads every account's usage ledger").option("--password-stdin", "read the password from standard input instead of prompting").action((username, options) => {
150
+ resolved = {
151
+ mode: "tenants",
152
+ request: {
153
+ command: "add",
154
+ username,
155
+ email: options.email,
156
+ admin: options.admin === true,
157
+ password: options.passwordStdin === true ? { kind: "stdin" } : { kind: "prompt" }
158
+ }
159
+ };
160
+ });
161
+ tenants.command("passwd").description("replace one account's password, retiring the sessions it issued").argument("<username>", "login name of the account to re-key").option("--password-stdin", "read the password from standard input instead of prompting").action((username, options) => {
162
+ resolved = {
163
+ mode: "tenants",
164
+ request: {
165
+ command: "passwd",
166
+ username,
167
+ password: options.passwordStdin === true ? { kind: "stdin" } : { kind: "prompt" }
168
+ }
169
+ };
170
+ });
171
+ tenants.command("remove").description("retire one account from this Harness home").argument("<username>", "login name of the account to retire").option("--purge-data", "also remove the account's tenant directory, Sessions included").action((username, options) => {
172
+ resolved = {
173
+ mode: "tenants",
174
+ request: {
175
+ command: "remove",
176
+ username,
177
+ purgeData: options.purgeData === true
178
+ }
179
+ };
180
+ });
181
+ }
182
+ try {
183
+ const expanded = first !== void 0 && !first.startsWith("-") && first !== "plugin" && first !== "tenants" ? ["--profile", ...argv] : argv;
184
+ program.parse(expanded, { from: "user" });
185
+ } catch (error) {
186
+ return process.exit(error instanceof CommanderError ? error.exitCode : 1);
187
+ }
188
+ /* v8 ignore next -- an action resolves or Commander throws */
189
+ if (resolved === void 0) throw new Error("qilin: no invocation resolved");
190
+ return resolved;
191
+ }
192
+ //#endregion
193
+ //#region lib/types/scheduler-yield.js
194
+ /**
195
+ * Host fallback for the Web Scheduler API global.
196
+ * @module @qilin-agent/cli/scheduler-yield
197
+ */
198
+ /**
199
+ * Install `globalThis.scheduler.yield` unless the host already defines a
200
+ * `scheduler`.
201
+ *
202
+ * The fallback resolves on the next `setImmediate` — the API's "yield to the
203
+ * event loop once, then continue" semantic — and a host-provided `scheduler`
204
+ * is left untouched. Call it before importing anything that may read the
205
+ * global; `bin.ts` calls it at the top of the entry, ahead of every mode
206
+ * module. Rationale: [the host-shim Agent
207
+ * Note](../../../.agents/notes/implemented/architecture/2026-10-07-web-scheduler-yield-host-shim.md).
208
+ *
209
+ * @returns whether this call installed the fallback.
210
+ */
211
+ function installSchedulerYield() {
212
+ if (globalThis.scheduler !== void 0) return false;
213
+ Object.defineProperty(globalThis, "scheduler", {
214
+ value: { yield: () => new Promise((resolve) => setImmediate(resolve)) },
215
+ writable: true,
216
+ configurable: true
217
+ });
218
+ return true;
219
+ }
220
+ //#endregion
221
+ //#region lib/types/startup-diagnostics.js
222
+ /** Save original startup diagnostics while keeping the terminal report concise. */
223
+ /** Wait for stderr to finish the write before the failed process exits. */
224
+ function writeStderr(text) {
225
+ return new Promise((resolve, reject) => {
226
+ process.stderr.write(text, (error) => {
227
+ if (error) reject(error);
228
+ else resolve();
229
+ });
230
+ });
231
+ }
232
+ /**
233
+ * Print the startup summary and save a private, uniquely named report under QILIN_HOME/logs.
234
+ * Failed writes print the complete report to stderr instead of claiming a saved path.
235
+ * @param error - startup audit failure retaining plugin metadata and original errors.
236
+ * @param context - resolved Harness home, application version, and selected profile.
237
+ * @param write - terminal output sink; awaited before returning, defaults to stderr.
238
+ * @returns after saving or printing the report and completing terminal writes.
239
+ */
240
+ async function reportStartupFailure(error, context, write = writeStderr) {
241
+ const now = (/* @__PURE__ */ new Date()).toISOString();
242
+ const report = "WARNING: Raw diagnostics may contain configuration or credential values from plugin errors. Review before sharing.\n\n" + inspect({
243
+ timestamp: now,
244
+ qilinVersion: context.version,
245
+ nodeVersion: process.version,
246
+ platform: process.platform,
247
+ arch: process.arch,
248
+ profile: context.profile,
249
+ error
250
+ }, {
251
+ depth: null,
252
+ maxArrayLength: null,
253
+ maxStringLength: null,
254
+ showHidden: true,
255
+ customInspect: false,
256
+ getters: false,
257
+ colors: false
258
+ }) + "\n";
259
+ await write(`${error.message}\n`);
260
+ const logDir = join(context.home, "logs");
261
+ const logPath = join(logDir, `startup-${now.replaceAll(":", "-")}-${randomUUID()}.log`);
262
+ try {
263
+ await mkdir(logDir, {
264
+ recursive: true,
265
+ mode: 448
266
+ });
267
+ await writeFile(logPath, report, {
268
+ flag: "wx",
269
+ mode: 384
270
+ });
271
+ } catch (writeError) {
272
+ await write(`\nqilin: warning: could not write startup diagnostics: ${String(writeError)}\nFull diagnostics:\n${report}`);
273
+ return;
274
+ }
275
+ await write(`\nFull diagnostics: ${logPath}\n`);
276
+ }
277
+ //#endregion
278
+ //#region lib/types/bin.js
279
+ /**
280
+ * Command-line entry for qilin.
281
+ * @module @qilin-agent/cli/bin
282
+ */
283
+ /* v8 ignore file -- built-bin acceptance exercises this self-executing dispatch. */
284
+ installSchedulerYield();
285
+ /**
286
+ * Run the public qilin command-line interface.
287
+ * @returns a promise that settles when the selected command mode finishes.
288
+ */
289
+ async function runCli() {
290
+ const version = getQilinRuntimeVersion();
291
+ const invocation = parseQilinArgs(process.argv.slice(2), version);
292
+ switch (invocation.mode) {
293
+ case "profile": {
294
+ const { ENTRY_PROFILES, isWebEntry, runWebEntry } = await import("./web-entry-C--0C6wG.js");
295
+ if (ENTRY_PROFILES.includes(invocation.profile) && isWebEntry()) {
296
+ try {
297
+ process.exitCode = await runWebEntry({
298
+ args: invocation.args,
299
+ profile: invocation.profile,
300
+ patches: invocation.patches,
301
+ fromDefaultProfile: invocation.fromDefaultProfile
302
+ });
303
+ } catch (error) {
304
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
305
+ process.exit(1);
306
+ }
307
+ break;
308
+ }
309
+ const { runProfile } = await import("./profile-boot.js");
310
+ try {
311
+ await runProfile({
312
+ environment: loadLayeredEnv("qilin"),
313
+ profile: invocation.profile,
314
+ fromDefaultProfile: invocation.fromDefaultProfile,
315
+ patchFiles: invocation.patches,
316
+ args: invocation.args
317
+ });
318
+ } catch (error) {
319
+ if (!(error instanceof StartupError)) throw error;
320
+ await reportStartupFailure(error, {
321
+ home: resolveQilinHome(),
322
+ version,
323
+ profile: invocation.profile
324
+ });
325
+ process.exit(1);
326
+ }
327
+ break;
328
+ }
329
+ case "plugin": {
330
+ const { runPlugin } = await import("./plugin-BQ1sn-63.js");
331
+ process.exit(await runPlugin(invocation.profile, invocation.args));
332
+ break;
333
+ }
334
+ case "tenants": {
335
+ const { runTenantsRoster } = await import("./tenants-roster-Bhr9x2Jw.js");
336
+ process.exitCode = await runTenantsRoster(invocation.request);
337
+ break;
338
+ }
339
+ case "dump-config": {
340
+ const { runDumpConfig } = await import("./dump-config-_HRfhhWz.js");
341
+ runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches, invocation.fromDefaultProfile);
342
+ break;
343
+ }
344
+ default: throw new Error(`qilin: unhandled invocation mode ${JSON.stringify(invocation)}`);
345
+ }
346
+ }
347
+ if (import.meta.main) await runCli();
348
+ //#endregion
349
+ export { runCli };
@@ -0,0 +1,53 @@
1
+ import { a as prepareProfile, n as PROFILE_ROOT_FILENAME, r as homePatchPath } from "./profile-boot-DSy6PUHQ.js";
2
+ import { loadOptionalPatches, loadOverlayPatches, renderConfigDump } from "@qilin-agent/app-boot";
3
+ import { join, resolve } from "node:path";
4
+ import { existsSync } from "node:fs";
5
+ //#region lib/types/dump-config.js
6
+ /**
7
+ * Config-dump entry for `qilin --profile <name> --dump-config`: compose the
8
+ * profile's patch layers through the include plugin's patch algorithm without
9
+ * booting or evaluating `!!js`, with one source layer per bundle, the
10
+ * profile's own patch file, and each `--patch` overlay.
11
+ * @module @qilin-agent/cli/dump-config
12
+ */
13
+ const NAME = "qilin";
14
+ /* v8 ignore start -- built-bin acceptance drives this boot-free dispatch */
15
+ /**
16
+ * Print a profile composition with comments naming each source file and patch layer.
17
+ * @param profile - the profile name.
18
+ * @param defaultOnly - omit the profile's user layer and `--patch` overlays
19
+ * (the recovery diagnostic for a broken `cordis.patch.yml`, which is then
20
+ * never parsed).
21
+ * @param patches - `--patch` overlay paths, in argv order.
22
+ * @param fromDefaultProfile - shipped template used once to initialize a missing profile.
23
+ */
24
+ function runDumpConfig(profile, defaultOnly, patches, fromDefaultProfile) {
25
+ const loaded = prepareProfile(profile, !defaultOnly, fromDefaultProfile);
26
+ const layers = loaded.layers.map((layer) => ({
27
+ label: layer.packageName,
28
+ patches: layer.patches
29
+ }));
30
+ if (!defaultOnly) {
31
+ if (existsSync(loaded.patchPath)) layers.push({
32
+ label: loaded.patchPath,
33
+ patches: loaded.patches
34
+ });
35
+ const homePatchFile = homePatchPath();
36
+ const homePatches = loadOptionalPatches(NAME, homePatchFile);
37
+ if (homePatches !== void 0) layers.push({
38
+ label: homePatchFile,
39
+ patches: homePatches
40
+ });
41
+ for (const file of patches) {
42
+ const absolute = resolve(file);
43
+ layers.push({
44
+ label: absolute,
45
+ patches: loadOverlayPatches(NAME, absolute)
46
+ });
47
+ }
48
+ }
49
+ process.stdout.write(renderConfigDump(NAME, join(loaded.dir, PROFILE_ROOT_FILENAME), layers));
50
+ }
51
+ /* v8 ignore stop */
52
+ //#endregion
53
+ export { runDumpConfig };