Merge latest master into codex/migrate-to-oxlint
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/lsp/lsp-local/README.md
|
||||
README.md: 85254eea2bb74df277be6fd5de1529b5da4ea178
|
||||
README.zh.md: 2390b02371980da39cd8c801197b7df41571e127
|
||||
README.md: 40515ad173dec8cfe6a0937525612b7f18d22fb8
|
||||
README.zh.md: f8b17957dbd0d239aa2c0b0aeadf6136427788aa
|
||||
|
||||
@@ -23,7 +23,7 @@ The `servers` record key is the stable provider id reserved on `ctx.lsp`; each v
|
||||
|---|---|---|
|
||||
| `command` | (required) | Executable to spawn — absolute, or resolved on the child PATH at load. Launch uses no shell. |
|
||||
| `args` | `[]` | Arguments passed to the executable. |
|
||||
| `env` | `{}` | Extra env merged on top of the credential-scrubbed ambient env (vars matching `KEY`/`SECRET`/`TOKEN` are not forwarded); an explicit `DSH_*` entry merges after the seam's scrub of ambient ones. |
|
||||
| `env` | `{}` | Extra env merged on top of the credential-scrubbed ambient env (vars matching `KEY`/`PASSWORD`/`SECRET`/`TOKEN` are not forwarded); an explicit `DSH_*` entry merges after the seam's scrub of ambient ones. |
|
||||
| `extensionToLanguage` | (required) | Lowercase leading-dot extension → LSP language id (e.g. `{ '.ts': 'typescript' }`). |
|
||||
| `initializationOptions` | `null` | Static `initialize` options forwarded to the server. |
|
||||
| `configuration` | `null` | Static answer to every `workspace/configuration` item. |
|
||||
|
||||
@@ -23,7 +23,7 @@ Namespace 插件(`name`/`inject`/`Config`/`apply`,无默认导出)
|
||||
|---|---|---|
|
||||
| `command` | (必填) | 要 spawn 的可执行文件:绝对路径,或在加载时从子进程 PATH 解析。不使用 shell 启动。 |
|
||||
| `args` | `[]` | 传给可执行文件的参数。 |
|
||||
| `env` | `{}` | 合并到已清理 credential 的环境之上的额外 env(匹配 `KEY`/`SECRET`/`TOKEN` 的变量不会转发);显式 `DSH_*` 条目在 seam 清除环境中同名值之后合并。 |
|
||||
| `env` | `{}` | 合并到已清理 credential 的环境之上的额外 env(匹配 `KEY`/`PASSWORD`/`SECRET`/`TOKEN` 的变量不会转发);显式 `DSH_*` 条目在 seam 清除环境中同名值之后合并。 |
|
||||
| `extensionToLanguage` | (必填) | 小写、以点开头的扩展名 → LSP language id(例如 `{ '.ts': 'typescript' }`)。 |
|
||||
| `initializationOptions` | `null` | 转发给服务器的静态 `initialize` 选项。 |
|
||||
| `configuration` | `null` | 每个 `workspace/configuration` 配置项的静态答案。 |
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { execFile, spawn } from 'node:child_process'
|
||||
import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'
|
||||
import { scrubbedParentEnv, SENSITIVE_ENV_PATTERN } from '@deepseek-ai/dsh-subprocess'
|
||||
import { promisify } from 'node:util'
|
||||
import type { PackageJsonFile } from '../documents/package-json-file.ts'
|
||||
import { PnpmWorkspaceFile } from '../documents/pnpm-workspace-file.ts'
|
||||
@@ -60,7 +60,7 @@ export async function probePackageManagerVersion(name: PackageManagerName, cwd:
|
||||
*/
|
||||
export function scrubEnvironment(environment?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
if (environment === undefined) return scrubbedParentEnv()
|
||||
return Object.fromEntries(Object.entries(environment).filter(([name]) => !/(?:KEY|SECRET|TOKEN)/i.test(name)))
|
||||
return Object.fromEntries(Object.entries(environment).filter(([name]) => !SENSITIVE_ENV_PATTERN.test(name)))
|
||||
}
|
||||
|
||||
/** Node child-process command runner with inherited stdio and quiescent completion. */
|
||||
|
||||
@@ -309,7 +309,12 @@ describe('package manager strategies', () => {
|
||||
await expect(npm.install('/tmp', failed)).rejects.toThrow('exited with code 2')
|
||||
const killed: CommandRunner = { run: async () => ({ exitCode: null, signal: 'SIGTERM' }) }
|
||||
await expect(npm.build('/tmp', killed)).rejects.toThrow('killed by SIGTERM')
|
||||
expect(scrubEnvironment({ PATH: '/bin', API_KEY: 'secret', TOKEN_VALUE: 'secret' })).toEqual({ PATH: '/bin' })
|
||||
expect(scrubEnvironment({
|
||||
PATH: '/bin',
|
||||
API_KEY: 'secret',
|
||||
DB_PASSWORD: 'secret',
|
||||
TOKEN_VALUE: 'secret',
|
||||
})).toEqual({ PATH: '/bin' })
|
||||
})
|
||||
|
||||
it('probes versions and runs real child-process boundaries', async () => {
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subprocess/subprocess-local/README.md
|
||||
README.md: a4e20b5c64afd14cb9dce9f8cc46fa96a1f5f19d
|
||||
README.zh.md: 170ff154864de8634a981d571e3ce1a8d704f546
|
||||
README.md: 202fc57080400afcbf5a65c17ea6bc8ac758c96f
|
||||
README.zh.md: c3a00cfd327c6865eda57cc63b87fd5f40e2b24f
|
||||
|
||||
@@ -8,7 +8,7 @@ Local implementation of the [`@deepseek-ai/dsh-subprocess`](../subprocess/README
|
||||
|
||||
- **Detached process trees with platform-correct signalling** — POSIX children are spawned `detached` (own process group) and signalled by negative pgid with a direct-child fallback; Windows terminates the tree via `taskkill /PID <pid> /T /F` (injectable for tests). `terminate()` — the handle's only termination verb — sends SIGTERM then SIGKILL after the spec's grace (OpenCode's escalation; pipelines and subshells die with the parent) and is a no-op once the tree is gone; `waitForExit()` polls whole-tree liveness so consumer teardown confirms real quiescence. After the leader exits, still-open pipes receive the same bounded drain grace so a surviving descendant cannot hold the outcome open indefinitely. ESRCH is tolerated; daemons that re-parent away from the group can still survive — the same caveat as the surveyed tools.
|
||||
- **Per-stream dispositions** — `'pipe'` hands the raw stream to the caller untouched (protocol framing stays consumer-owned); `'inherit'` passes the parent descriptor through; collect mode keeps the in-memory TAIL beyond its cap (errors and results cluster at the end — pi/OpenCode rationale) while the FULL stream is appended to a private temp file when a spill cap is configured — omitting `spill` keeps only the tail, the diagnostic shape. A stream larger than the spill cap discards its now-incomplete spill and returns only the marked truncated tail; spill fds are sealed at settlement, and a failed final close withholds the path rather than advertising an incomplete file. Spill files are `0600` with random names under a lazily-created `0700` per-process directory.
|
||||
- **Credential scrub + explicit merge** — `process.env` minus credential-shaped vars (`*KEY*`/`*SECRET*`/`*TOKEN*`) and all ambient `DSH_*` names; the spec's explicit `env` merges after that scrub with no namespace validation, so a deliberately supplied credential or current `DSH_*` fact wins while stale nested-harness identity cannot leak in ambiently. Supplied stdin is written and closed; otherwise fd 0 is `/dev/null`. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [managed environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
|
||||
- **Credential scrub + explicit merge** — `process.env` minus credential-shaped vars (`*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`) and all ambient `DSH_*` names; the spec's explicit `env` merges after that scrub with no namespace validation, so a deliberately supplied credential or current `DSH_*` fact wins while stale nested-harness identity cannot leak in ambiently. Supplied stdin is written and closed; otherwise fd 0 is `/dev/null`. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [managed environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
|
||||
- **Offset-based reads** — collect-mode readers return deltas in whole-stream byte coordinates; the service never holds a cursor, so consumer-owned cursors (the bash background read path) and full-stream re-reads coexist, before and after settlement.
|
||||
- **Terminate-and-join disposal** — the service retains live handles only so its own disposal can escalate every running tree and await its exit; settled and spawn-failed handles leave the live set on settlement.
|
||||
|
||||
@@ -23,7 +23,7 @@ No direct invalidation; the named consumers own any request-prefix changes.
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Windows tree support is best-effort and untested in CI** — termination routes through `taskkill /PID <pid> /T /F` with all outcomes contained (absent tree, races, missing binary), and liveness falls back to the direct-child boundary; the suites cover the routing through an injected runner only, and `packages/subprocess/*` is excluded from the Windows test matrix.
|
||||
- **The credential scrub is a name heuristic** — `*KEY*`/`*SECRET*`/`*TOKEN*` only; differently-named secrets (e.g. `*PASSWORD*`) pass through, and a whitelist for over-scrubbed vars is noted future work.
|
||||
- **The credential scrub is a name heuristic** — `*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*` only; differently-named secrets (e.g. `*PASSPHRASE*`) pass through, and a whitelist for over-scrubbed vars is noted future work.
|
||||
- **Completed spill files are not deleted** — bounded full-output recovery files (and the private per-process spill dir) accumulate under the OS tmpdir until something external cleans them; oversize incomplete spills are discarded and deletion is attempted immediately, but a cleanup failure can leave a bounded file behind.
|
||||
|
||||
The raw process handling lives in `src/spawn.ts`; `src/index.ts` is the service wiring.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
- **以适合平台的方式发送信号的 detached 进程树**:POSIX 子进程使用 `detached` spawn(拥有独立进程组),信号以负 pgid 发送并以直接子进程作为回退;Windows 通过 `taskkill /PID <pid> /T /F` 终止进程树(可为测试注入)。`terminate()`(句柄唯一的终止操作)先发送 SIGTERM,经过 spec 的宽限期后再发送 SIGKILL(沿用 OpenCode 的升级策略;流水线与子 shell 会随父进程一起结束),进程树消亡后为空操作;`waitForExit()` 轮询整棵进程树的存活状态,使消费方的拆卸能确认真正的完全停稳。组长进程退出后,仍然打开的管道也只获得同样有界的排空宽限期,因此存活的后代进程无法无限期地拖住结果不结算。系统会容忍 ESRCH;重新指定父进程并脱离该组的 daemon 仍可能存活,这与所调研工具的局限相同。
|
||||
- **按流划分的处置方式**:`'pipe'` 把原始流原样交给调用方(协议分帧仍归消费方所有);`'inherit'` 直通父进程的描述符;收集模式(collect)在输出超过上限后于内存中保留尾部(错误与结果通常聚集在末尾,沿用 pi/OpenCode 的理由),并在配置了 spill 上限时把完整流追加到一个私有临时文件;省略 `spill` 则只保留用于诊断的尾部。某条流大于 spill 上限时,会丢弃已不完整的 spill,仅返回带截断标记的尾部;spill 文件描述符在结算时封存,最终关闭失败时则不公布路径,以免声称存在不完整的文件。spill 文件权限为 `0600`、名称随机,位于按需创建、权限为 `0700` 的每进程目录之下。
|
||||
- **凭据清除 + 显式合并**:以 `process.env` 为基础,移除形似凭据的变量(`*KEY*`/`*SECRET*`/`*TOKEN*`)和所有环境中已有的 `DSH_*` 名称;spec 的显式 `env` 在该清除之后合并且不做命名空间校验,因此有意提供的凭据或当前 `DSH_*` 事实会胜出,而陈旧的嵌套 harness 身份无法从环境中隐式漏入。提供的 stdin 会被写入后关闭;否则 fd 0 指向 `/dev/null`。参见 [stdin/env Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)与[受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
|
||||
- **凭据清除 + 显式合并**:以 `process.env` 为基础,移除形似凭据的变量(`*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`)和所有环境中已有的 `DSH_*` 名称;spec 的显式 `env` 在该清除之后合并且不做命名空间校验,因此有意提供的凭据或当前 `DSH_*` 事实会胜出,而陈旧的嵌套 harness 身份无法从环境中隐式漏入。提供的 stdin 会被写入后关闭;否则 fd 0 指向 `/dev/null`。参见 [stdin/env Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)与[受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
|
||||
- **基于偏移量的读取**:收集模式的读取器按完整流的字节坐标返回增量;服务自身从不持有游标,因此消费方自有的游标(bash 的后台读取路径)与完整流重读可以共存,结算前后皆然。
|
||||
- **先终止再等待退出的 dispose(资源释放)**:服务保留存活句柄,只为让自身的 dispose 能对每个仍在运行的进程树执行升级并等待其退出;已结算与 spawn 失败的句柄在结算时即离开存活集合。
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **Windows 进程树支持仅为尽力而为,且未经 CI 测试**:终止经由 `taskkill /PID <pid> /T /F` 完成,所有结果都被就地吸收,不向外抛出(进程树已不存在、竞态、二进制缺失),存活探测则回退到直接子进程边界;测试套件只通过注入的运行器覆盖这条路由,且 `packages/subprocess/*` 被排除在 Windows 测试矩阵之外。
|
||||
- **凭据清除依赖名称启发式规则**:只匹配 `*KEY*`/`*SECRET*`/`*TOKEN*`;名称不同的 secret(例如 `*PASSWORD*`)会继续传递,对误删变量引入白名单属于已记录的后续工作。
|
||||
- **凭据清除依赖名称启发式规则**:只匹配 `*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`;名称不同的 secret(例如 `*PASSPHRASE*`)会继续传递,对误删变量引入白名单属于已记录的后续工作。
|
||||
- **不会删除已完成的 spill 文件**:有界的完整输出恢复文件(以及每个进程的私有 spill 目录)会在 OS tmpdir 下累积,直到外部机制进行清理;超大的不完整 spill 会被丢弃并立即尝试删除,但清理失败可能留下一个有界文件。
|
||||
|
||||
原始进程处理位于 `src/spawn.ts`;`src/index.ts` 负责服务接线。
|
||||
|
||||
@@ -255,10 +255,10 @@ describe('stdin and extra env (set by in-process plugins)', () => {
|
||||
})
|
||||
|
||||
it('an explicit extra env entry overrides the credential scrub', async () => {
|
||||
// EXPLICIT_OVERRIDE_KEY matches the credential scrub pattern, yet an explicit
|
||||
// EXPLICIT_OVERRIDE_PASSWORD matches the credential scrub pattern, yet an explicit
|
||||
// entry is still honored — the scrub only drops AMBIENT process.env creds.
|
||||
const result = await finish(spawnSubprocess(spec('echo "$EXPLICIT_OVERRIDE_KEY"', {
|
||||
env: { EXPLICIT_OVERRIDE_KEY: 'explicit-wins' },
|
||||
const result = await finish(spawnSubprocess(spec('echo "$EXPLICIT_OVERRIDE_PASSWORD"', {
|
||||
env: { EXPLICIT_OVERRIDE_PASSWORD: 'explicit-wins' },
|
||||
})))
|
||||
expect(result.stdout.text).toBe('explicit-wins\n')
|
||||
})
|
||||
@@ -748,13 +748,17 @@ describe('environment and spill-file hardening', () => {
|
||||
it('scrubs credential-shaped and ambient DSH env vars from child processes', async () => {
|
||||
process.env.DSH_TEST_API_KEY = 'super-secret'
|
||||
process.env.DSH_TEST_TOKEN = 'also-secret'
|
||||
process.env.SUBPROCESS_TEST_PASSWORD = 'password-secret'
|
||||
process.env.DSH_TEST_PLAIN = 'visible'
|
||||
try {
|
||||
const result = await finish(spawnSubprocess(spec('echo "[${DSH_TEST_API_KEY:-absent}|${DSH_TEST_TOKEN:-absent}|${DSH_TEST_PLAIN:-absent}]"')))
|
||||
expect(result.stdout.text.trim()).toBe('[absent|absent|absent]')
|
||||
const result = await finish(spawnSubprocess(spec(
|
||||
'echo "[${DSH_TEST_API_KEY:-absent}|${DSH_TEST_TOKEN:-absent}|${SUBPROCESS_TEST_PASSWORD:-absent}|${DSH_TEST_PLAIN:-absent}]"',
|
||||
)))
|
||||
expect(result.stdout.text.trim()).toBe('[absent|absent|absent|absent]')
|
||||
} finally {
|
||||
delete process.env.DSH_TEST_API_KEY
|
||||
delete process.env.DSH_TEST_TOKEN
|
||||
delete process.env.SUBPROCESS_TEST_PASSWORD
|
||||
delete process.env.DSH_TEST_PLAIN
|
||||
}
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ export type {
|
||||
* deliberately supplied entry survives because explicit env layers merge
|
||||
* after the scrub.
|
||||
*/
|
||||
export const SENSITIVE_ENV_PATTERN = /KEY|SECRET|TOKEN/i
|
||||
export const SENSITIVE_ENV_PATTERN = /KEY|PASSWORD|SECRET|TOKEN/i
|
||||
|
||||
/**
|
||||
* The ambient parent environment minus credential-shaped names and minus all
|
||||
|
||||
@@ -55,16 +55,19 @@ describe('SubprocessService seam', () => {
|
||||
it('scrubbedParentEnv drops credential-shaped and DSH_ names but keeps PATH', () => {
|
||||
process.env.DSH_SCRUB_PROBE = 'stale'
|
||||
process.env.SCRUB_PROBE_TOKEN = 'secret'
|
||||
process.env.SCRUB_PROBE_PASSWORD = 'secret'
|
||||
process.env.SCRUB_PROBE_PLAIN = 'visible'
|
||||
try {
|
||||
const env = scrubbedParentEnv()
|
||||
expect(env.DSH_SCRUB_PROBE).toBeUndefined()
|
||||
expect(env.SCRUB_PROBE_TOKEN).toBeUndefined()
|
||||
expect(env.SCRUB_PROBE_PASSWORD).toBeUndefined()
|
||||
expect(env.SCRUB_PROBE_PLAIN).toBe('visible')
|
||||
expect(env.PATH).toBeDefined()
|
||||
} finally {
|
||||
delete process.env.DSH_SCRUB_PROBE
|
||||
delete process.env.SCRUB_PROBE_TOKEN
|
||||
delete process.env.SCRUB_PROBE_PASSWORD
|
||||
delete process.env.SCRUB_PROBE_PLAIN
|
||||
}
|
||||
})
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"@deepseek-ai/dsh-session-reference": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-title": "^0.0.1",
|
||||
"@deepseek-ai/dsh-skill": "^0.0.1",
|
||||
"@deepseek-ai/dsh-subprocess": "^0.0.1",
|
||||
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
||||
"@deepseek-ai/dsh-token-meter": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
@@ -82,6 +83,7 @@
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-title": "workspace:^",
|
||||
"@deepseek-ai/dsh-skill": "workspace:^",
|
||||
"@deepseek-ai/dsh-subprocess": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-token-meter": "workspace:^",
|
||||
"@deepseek-ai/dsh-tool-cordis": "workspace:^",
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
visibleWidth,
|
||||
} from '@earendil-works/pi-tui'
|
||||
import type { Session } from '@deepseek-ai/dsh-session'
|
||||
import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'
|
||||
|
||||
/** Editor that shows a placeholder without making it editable content. */
|
||||
export class HintEditor extends Editor {
|
||||
@@ -65,13 +66,10 @@ export function formatCwd(cwd: string | undefined): string {
|
||||
*/
|
||||
export function gitBranch(cwd: string): string | undefined {
|
||||
try {
|
||||
const env = Object.fromEntries(
|
||||
Object.entries(process.env).filter(([name]) => !/(?:KEY|SECRET|TOKEN)/iu.test(name)),
|
||||
)
|
||||
const branch = execFileSync('git', ['branch', '--show-current'], {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
env,
|
||||
env: scrubbedParentEnv(),
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
timeout: 1_000,
|
||||
}).trim()
|
||||
|
||||
29
packages/ui/tui/tests/chat-helpers.spec.ts
Normal file
29
packages/ui/tui/tests/chat-helpers.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { gitBranch } from '../src/chat/helpers.ts'
|
||||
|
||||
vi.mock('node:child_process', () => ({
|
||||
execFileSync: vi.fn(() => 'main\n'),
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('chat helpers', () => {
|
||||
it('scrubs ambient credentials and DSH names from the Git child', () => {
|
||||
vi.stubEnv('TUI_TEST_PASSWORD', 'ambient-password')
|
||||
vi.stubEnv('DSH_TUI_TEST_FLAG', 'ambient-harness-state')
|
||||
expect(gitBranch('/workspace')).toBe('main')
|
||||
const call = vi.mocked(execFileSync).mock.calls[0] as unknown as [
|
||||
string,
|
||||
string[],
|
||||
{ env: NodeJS.ProcessEnv },
|
||||
]
|
||||
expect(call[0]).toBe('git')
|
||||
expect(call[1]).toEqual(['branch', '--show-current'])
|
||||
expect(call[2].env).not.toHaveProperty('TUI_TEST_PASSWORD')
|
||||
expect(call[2].env).not.toHaveProperty('DSH_TUI_TEST_FLAG')
|
||||
})
|
||||
})
|
||||
@@ -56,6 +56,9 @@
|
||||
{
|
||||
"path": "../../skill/skill"
|
||||
},
|
||||
{
|
||||
"path": "../../subprocess/subprocess"
|
||||
},
|
||||
{
|
||||
"path": "../user-interaction"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user