@carllee1983/dbcli 1.8.0 → 1.9.0
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 +16 -0
- package/README.md +39 -1
- package/README.zh-TW.md +1 -1
- package/assets/SKILL.md +34 -17
- package/assets/reference.md +27 -0
- package/assets/tasks/README.md +30 -0
- package/assets/tasks/diagnose-slow-query.md +33 -0
- package/dist/cli.mjs +722 -44
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to dbcli are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.9.0] - 2026-05-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Agent Task Packs(plan-only 第一版)**:`dbcli skill tasks list/show/plan` 讓 AI agent 可探索團隊定義的資料庫任務範本並產生安全可審查的執行計畫。
|
|
13
|
+
- 三層儲存:`assets/tasks/`(內建)< `.dbcli-shared/tasks/`(團隊共享)< `.dbcli/tasks/`(個人覆蓋)。
|
|
14
|
+
- Task 檔為 `.md`:YAML frontmatter(name/description/tags/engines/params/safety/steps)+ markdown agent notes。
|
|
15
|
+
- 嚴格 schema:`safety.mode` 僅接受 `plan-only`、`step.type` 僅接受 `command`,未知欄位直接 fail 解析而非靜默忽略。
|
|
16
|
+
- `plan` 輸出包含原始 `command`、`resolvedCommand`、`argv`(shell-aware 切分),方便 agent 直接消費。
|
|
17
|
+
- 內建第一版 `diagnose-slow-query` 任務作為範例。
|
|
18
|
+
- 文件:`assets/SKILL.md` 與 `assets/reference.md` 同步加入 Agent Task Packs 章節;`docs/feature-matrix.md` 補充 `skill tasks` 子命令說明。
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `src/core/saved-queries/yaml-mini.ts`:擴充支援 YAML block list 語法(`- scalar`、`- key: value` 起始的 sub-map),以承載 Agent Task Packs 的 frontmatter;既有 saved-queries 解析行為不變、66 個既有測試全綠。
|
|
23
|
+
|
|
8
24
|
## [1.8.0] - 2026-05-06
|
|
9
25
|
|
|
10
26
|
### Added
|
package/README.md
CHANGED
|
@@ -114,7 +114,45 @@ dbcli query '{"status":"active"}' --collection users --use atlas
|
|
|
114
114
|
|
|
115
115
|
For MongoDB, `list` and `query` operate on the database configured for the connection, and `query` requires `--collection <name>`.
|
|
116
116
|
|
|
117
|
-
For a command-by-command support matrix across PostgreSQL, MySQL, MariaDB, and
|
|
117
|
+
For a command-by-command support matrix across PostgreSQL, MySQL, MariaDB, MongoDB, Redis, and Elasticsearch, see [docs/feature-matrix.md](./docs/feature-matrix.md).
|
|
118
|
+
|
|
119
|
+
### Redis & Elasticsearch Support
|
|
120
|
+
|
|
121
|
+
dbcli extends its unified interface to Redis and Elasticsearch, providing consistent discovery and querying.
|
|
122
|
+
|
|
123
|
+
#### Redis
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Connect to Redis
|
|
127
|
+
dbcli init --system redis --host localhost --port 6379
|
|
128
|
+
|
|
129
|
+
# List keys (uses SCAN)
|
|
130
|
+
dbcli list
|
|
131
|
+
|
|
132
|
+
# Inspect a key (type, TTL, size, sample)
|
|
133
|
+
dbcli schema my-key
|
|
134
|
+
|
|
135
|
+
# Run Redis commands (whitelisted)
|
|
136
|
+
dbcli query "GET my-key"
|
|
137
|
+
dbcli query "HGETALL user:1"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### Elasticsearch
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Connect to Elasticsearch
|
|
144
|
+
dbcli init --system elasticsearch --host localhost --port 9200
|
|
145
|
+
|
|
146
|
+
# List indices and document counts
|
|
147
|
+
dbcli list
|
|
148
|
+
|
|
149
|
+
# Show mapping/structure of an index
|
|
150
|
+
dbcli schema my-index
|
|
151
|
+
|
|
152
|
+
# Query using Lucene or DSL JSON
|
|
153
|
+
dbcli query "status:active" --index my-index
|
|
154
|
+
dbcli query '{"query": {"match_all": {}}}' --index my-index
|
|
155
|
+
```
|
|
118
156
|
|
|
119
157
|
---
|
|
120
158
|
|
package/README.zh-TW.md
CHANGED
|
@@ -189,7 +189,7 @@ dbcli init [OPTIONS]
|
|
|
189
189
|
```
|
|
190
190
|
|
|
191
191
|
**選項 (基本):**
|
|
192
|
-
- `--system <type>` — 資料庫系統:`postgresql`、`mysql`、`mariadb`、`mongodb`
|
|
192
|
+
- `--system <type>` — 資料庫系統:`postgresql`、`mysql`、`mariadb`、`mongodb`、`redis`、`elasticsearch`
|
|
193
193
|
- `--host <host>` — 主機
|
|
194
194
|
- `--port <port>` — 埠號
|
|
195
195
|
- `--user <user>` — 使用者
|
package/assets/SKILL.md
CHANGED
|
@@ -11,13 +11,32 @@ Database CLI for AI agents with permission-based access control.
|
|
|
11
11
|
|
|
12
12
|
1. `dbcli status` — permission level and system summary (no credentials).
|
|
13
13
|
2. `dbcli blacklist list` — sensitive data boundaries.
|
|
14
|
-
3. `dbcli schema <table> --format json` — real column names. **Never guess.**
|
|
14
|
+
3. `dbcli schema <table> --format json` — real column names (SQL/Mongo/ES) or `schema <key>` (Redis). **Never guess.**
|
|
15
15
|
4. Run `query` / `insert` / `update` / `delete` / `export` within permission.
|
|
16
|
-
5. All writes: `--dry-run` → run → `query` read-back to confirm.
|
|
16
|
+
5. All writes: `--dry-run` (SQL/Mongo) → run → `query` read-back to confirm.
|
|
17
17
|
|
|
18
18
|
Prefer `--format json` for agent-friendly output.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
## Agent Task Packs
|
|
21
|
+
|
|
22
|
+
When the user asks for a database workflow (e.g. "diagnose this slow query", "audit
|
|
23
|
+
permissions", "review long-running operations"), prefer published task templates
|
|
24
|
+
over inventing steps from memory.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
dbcli skill tasks list --format json # discover
|
|
28
|
+
dbcli skill tasks show <task> # inspect
|
|
29
|
+
dbcli skill tasks plan <task> --param key=value --format json # generate plan
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The plan output is an ordered list of dbcli commands with rationale and risk
|
|
33
|
+
labels. Execute them one at a time — task plans do **not** override blacklist,
|
|
34
|
+
schema, dry-run, or confirmation requirements.
|
|
35
|
+
|
|
36
|
+
Tasks live under `assets/tasks/` (builtin), `.dbcli-shared/tasks/` (shared), and
|
|
37
|
+
`.dbcli/tasks/` (local override).
|
|
38
|
+
|
|
39
|
+
Full flags, per-command copy-paste blocks, `migrate` DDL, interactive `shell`, and MongoDB/Redis/ES walkthroughs are in [reference.md](reference.md) (installed next to this file).
|
|
21
40
|
|
|
22
41
|
## Quick start
|
|
23
42
|
|
|
@@ -34,8 +53,8 @@ dbcli query "SELECT * FROM users" # Execute SQL (auto LIMIT 1000)
|
|
|
34
53
|
| `init` | n/a | Create `.dbcli` (v1 single or v2 multi via `--conn-name` / `--env-file`). **Usually run by the human** — do NOT re-run to strip `{"$env"}` references; that format is intentional. |
|
|
35
54
|
| `use` | n/a | Show/switch default named connection (v2 only). |
|
|
36
55
|
| `list` | query-only+ | Tables (SQL), collections (MongoDB), keys (Redis), or indices (Elasticsearch). |
|
|
37
|
-
| `schema` | query-only+ | SQL: per-table or full scan into `.dbcli/schemas
|
|
38
|
-
| `query` | query-only+ | SQL, Mongo JSON (`--collection`), Redis command, or ES DSL/Lucene (`--collection
|
|
56
|
+
| `schema` | query-only+ | SQL: per-table or full scan into `.dbcli/schemas/`. MongoDB: sampled. ES: flattened mapping. Redis: per-key only (type/TTL/size). |
|
|
57
|
+
| `query` | query-only+ | SQL, Mongo JSON (`--collection`), Redis command, or ES DSL/Lucene (`--collection`). |
|
|
39
58
|
| `insert` / `update` | read-write+ | SQL or MongoDB only. JSON `--data` / `--set`; `--where` required on `update`; `--dry-run` first. Redis writes go through `query`. |
|
|
40
59
|
| `delete` | data-admin+ | SQL or MongoDB only. `--where` required; `--dry-run` first. |
|
|
41
60
|
| `export` | query-only+ | SQL or MongoDB only. Query → CSV/JSON(L) file or stdout. |
|
|
@@ -43,11 +62,11 @@ dbcli query "SELECT * FROM users" # Execute SQL (auto LIMIT 1000)
|
|
|
43
62
|
| `check` | query-only+ | SQL only (best on MySQL/MariaDB). |
|
|
44
63
|
| `diff` | query-only+ | SQL only. Save/compare schema snapshots. |
|
|
45
64
|
| `status` | query-only+ | Safe JSON/text summary (no credentials). |
|
|
46
|
-
| `doctor` | n/a | Environment, config, connection, SRV diagnostics (Mongo), schema cache age
|
|
65
|
+
| `doctor` | n/a | Environment, config, connection, SRV diagnostics (Mongo), schema cache age. |
|
|
47
66
|
| `completion` | n/a | bash / zsh / fish scripts. |
|
|
48
67
|
| `upgrade` | n/a | Self-update from npm; 24h-cached version hints on every command. |
|
|
49
|
-
| `shell` | (same as query+) | Interactive REPL. SQL engines + MongoDB shell only
|
|
50
|
-
| `migrate` | admin | SQL only. **DDL; dry-run by default** — needs `--execute
|
|
68
|
+
| `shell` | (same as query+) | Interactive REPL. SQL engines + MongoDB shell only. |
|
|
69
|
+
| `migrate` | admin | SQL only. **DDL; dry-run by default** — needs `--execute`. |
|
|
51
70
|
|
|
52
71
|
`--use <name>` on any subcommand targets a v2 connection without changing the default.
|
|
53
72
|
|
|
@@ -71,29 +90,27 @@ dbcli query "SELECT * FROM users" # Execute SQL (auto LIMIT 1000)
|
|
|
71
90
|
- JSON filter object (`find`) or JSON array (`aggregate`); SQL is rejected. `--collection <name>` is required on `query`.
|
|
72
91
|
- **Supported:** `init`, `list`, `schema` (sampled), `query`, `insert`, `update`, `delete`, `export`, `status`, `use`, `shell`, `doctor`, `upgrade`, `completion`.
|
|
73
92
|
- **Not supported:** `q` (saved queries), `diff`, `migrate`, `check`.
|
|
74
|
-
- Schema is **sampled** (default 50 docs
|
|
75
|
-
- `--limit` applies on `find`/aggregate; query-only mode caps at 1000 unless `--no-limit`.
|
|
93
|
+
- Schema is **sampled** (default 50 docs); types are JS `typeof` strings.
|
|
76
94
|
- See reference.md MongoDB section for full syntax and examples.
|
|
77
95
|
|
|
78
96
|
## Redis
|
|
79
97
|
|
|
80
98
|
- Command-style execution; `query` runs a whitelisted Redis command (e.g. `GET`, `HSET`, `DEL`).
|
|
81
99
|
- **Supported:** `init`, `list` (keys via SCAN), `schema <key>` (type / TTL / size / sample), `query`, `status`, `use`, `doctor`, `upgrade`, `completion`.
|
|
82
|
-
- **Not supported:** `schema` full scan
|
|
100
|
+
- **Not supported:** `schema` full scan, `insert`, `update`, `delete`, `export`, `check`, `diff`, `migrate`, `q`.
|
|
83
101
|
Use `query "DEL <key>"` etc. for writes — they go through the same permission gate.
|
|
84
|
-
- Permission tiers map to commands: read commands → `query-only`; mutators (`SET`, `HSET`,
|
|
102
|
+
- Permission tiers map to commands: read commands → `query-only`; mutators (`SET`, `HSET`, ...) → `read-write`; `DEL` / `UNLINK` → `data-admin`.
|
|
85
103
|
- `database` field is the logical DB index (default `0`); `list` returns ≤ 100 000 keys via SCAN.
|
|
86
104
|
- See reference.md Redis section.
|
|
87
105
|
|
|
88
106
|
## Elasticsearch
|
|
89
107
|
|
|
90
|
-
- DSL (JSON body) or Lucene query string; `--collection <index>`
|
|
108
|
+
- DSL (JSON body) or Lucene query string; `--collection <index>` is required on `query`.
|
|
91
109
|
- **Supported:** `init`, `list` (indices with doc count), `schema [index]` (flattened mapping), `query`, `status`, `use`, `doctor`, `upgrade`, `completion`.
|
|
92
110
|
- **Not supported:** `insert`, `update`, `delete`, `export`, `check`, `diff`, `migrate`, `q`.
|
|
93
|
-
Writes are not exposed via dedicated subcommands yet — use
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
- Schema flattens nested fields (`a.b.c`) and surfaces `.fields` multi-fields (e.g. `text.keyword`).
|
|
111
|
+
Writes are not exposed via dedicated subcommands yet — use `query` if the cluster allows or external tools.
|
|
112
|
+
- Query-only mode caps at 1000 hits; `--no-limit` is bounded at 10 000.
|
|
113
|
+
- Schema flattens nested fields (`a.b.c`) and surfaces `.fields` multi-fields.
|
|
97
114
|
- See reference.md Elasticsearch section.
|
|
98
115
|
|
|
99
116
|
## Saved queries
|
package/assets/reference.md
CHANGED
|
@@ -498,6 +498,33 @@ dbcli migrate drop-enum status --execute --force
|
|
|
498
498
|
|
|
499
499
|
**AI agent note:** Always use dry-run first (no `--execute`) to preview generated SQL. Only add `--execute` after confirming the SQL is correct. For DROP operations, both `--execute` and `--force` are required.
|
|
500
500
|
|
|
501
|
+
### skill tasks (Agent Task Packs)
|
|
502
|
+
|
|
503
|
+
```bash
|
|
504
|
+
dbcli skill tasks list # human table
|
|
505
|
+
dbcli skill tasks list --format json --tag diagnostics
|
|
506
|
+
dbcli skill tasks list --engine postgres --source builtin
|
|
507
|
+
dbcli skill tasks show diagnose-slow-query
|
|
508
|
+
dbcli skill tasks show diagnose-slow-query --format json
|
|
509
|
+
dbcli skill tasks plan diagnose-slow-query --param query="SELECT 1"
|
|
510
|
+
dbcli skill tasks plan diagnose-slow-query --param query="..." --format json
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
- **list filters:** `--tag <tag>`, `--engine <postgres|mysql|mongodb|redis|elasticsearch>`, `--source <builtin|shared|local>`, `--format <table|json>`.
|
|
514
|
+
- **show:** prints the full task definition (frontmatter + Agent Notes). Use `--format json` for an agent-friendly contract.
|
|
515
|
+
- **plan:** resolves `{{param}}` placeholders, validates required parameters, and emits a stable plan. Plans are **plan-only** in this version — dbcli will never execute the resulting commands automatically.
|
|
516
|
+
|
|
517
|
+
Task storage layers:
|
|
518
|
+
|
|
519
|
+
| Source | Path | Notes |
|
|
520
|
+
| --- | --- | --- |
|
|
521
|
+
| builtin | `assets/tasks/` | shipped with dbcli |
|
|
522
|
+
| shared | `.dbcli-shared/tasks/` | team-managed, version-controlled |
|
|
523
|
+
| local | `.dbcli/tasks/` | personal, gitignored |
|
|
524
|
+
|
|
525
|
+
Higher tiers override lower tiers by task name. Task name is derived from the
|
|
526
|
+
file path under the tier root (e.g. `diag/inspect.md` → `diag/inspect`).
|
|
527
|
+
|
|
501
528
|
## MongoDB Support
|
|
502
529
|
|
|
503
530
|
MongoDB connections use a JSON-based query model instead of SQL. Treat MongoDB support as a narrower document-database path, not as a full SQL feature equivalent.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# dbcli Agent Tasks (built-in)
|
|
2
|
+
|
|
3
|
+
Built-in task templates shipped with dbcli for AI agents.
|
|
4
|
+
|
|
5
|
+
## Resolution order
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
assets/tasks/ # builtin (lowest)
|
|
9
|
+
.dbcli-shared/tasks/ # shared, version-controlled
|
|
10
|
+
.dbcli/tasks/ # local, gitignored (highest)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
A task with the same name in a higher tier overrides the lower one. Use this to
|
|
14
|
+
customize built-in workflows without modifying dbcli source.
|
|
15
|
+
|
|
16
|
+
## File format
|
|
17
|
+
|
|
18
|
+
Each task is a `.md` file with a YAML frontmatter block:
|
|
19
|
+
|
|
20
|
+
- `name` (required, must match the file path without `.md`)
|
|
21
|
+
- `description`, `tags`, `engines`
|
|
22
|
+
- `params` (map of name → `{ type, required?, default?, description?, enum? }`)
|
|
23
|
+
- `safety.mode` — only `plan-only` is supported in this version
|
|
24
|
+
- `steps[]` — each step is `{ type: command, command, reason?, risk? }`
|
|
25
|
+
|
|
26
|
+
Use block-style YAML (no inline `{ ... }` maps) — the built-in YAML parser does
|
|
27
|
+
not support inline maps.
|
|
28
|
+
|
|
29
|
+
The markdown body below the frontmatter is `Agent Notes` and is shown in
|
|
30
|
+
`dbcli skill tasks show <name>`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: diagnose-slow-query
|
|
3
|
+
description: Diagnose slow query causes using safe read-only dbcli steps.
|
|
4
|
+
tags: [diagnostics, performance, readonly]
|
|
5
|
+
engines: [postgres, mysql]
|
|
6
|
+
params:
|
|
7
|
+
query:
|
|
8
|
+
type: string
|
|
9
|
+
required: true
|
|
10
|
+
description: The slow SQL query or query fingerprint to inspect.
|
|
11
|
+
safety:
|
|
12
|
+
mode: plan-only
|
|
13
|
+
requires:
|
|
14
|
+
- blacklist-list
|
|
15
|
+
- schema-check
|
|
16
|
+
steps:
|
|
17
|
+
- type: command
|
|
18
|
+
command: blacklist list
|
|
19
|
+
reason: Confirm sensitive tables and columns are protected before inspection.
|
|
20
|
+
risk: readonly
|
|
21
|
+
- type: command
|
|
22
|
+
command: plan "{{query}}"
|
|
23
|
+
reason: Analyze SQL risk without executing the query.
|
|
24
|
+
risk: readonly
|
|
25
|
+
- type: command
|
|
26
|
+
command: q @diag/long-running --format json
|
|
27
|
+
reason: Inspect active long-running queries through a saved diagnostic snippet.
|
|
28
|
+
risk: readonly
|
|
29
|
+
---
|
|
30
|
+
# Agent Notes
|
|
31
|
+
|
|
32
|
+
Use this task when the user reports a slow SQL query and wants safe diagnostic next steps.
|
|
33
|
+
Do not run write operations.
|
package/dist/cli.mjs
CHANGED
|
@@ -87311,38 +87311,97 @@ var init_types2 = __esm(() => {
|
|
|
87311
87311
|
|
|
87312
87312
|
// src/core/saved-queries/yaml-mini.ts
|
|
87313
87313
|
function parseYamlMini(text2) {
|
|
87314
|
-
const
|
|
87315
|
-
|
|
87316
|
-
|
|
87317
|
-
|
|
87318
|
-
|
|
87314
|
+
const tokens = [];
|
|
87315
|
+
for (const raw of text2.split(`
|
|
87316
|
+
`)) {
|
|
87317
|
+
if (/^\s*(#.*)?$/.test(raw))
|
|
87318
|
+
continue;
|
|
87319
87319
|
if (raw.includes("\t")) {
|
|
87320
87320
|
throw new Error(`YAML mini: tab indentation not supported: "${raw}"`);
|
|
87321
87321
|
}
|
|
87322
87322
|
const indent = raw.match(/^( *)/)[1].length;
|
|
87323
|
-
const
|
|
87324
|
-
|
|
87325
|
-
|
|
87323
|
+
const lineBody = raw.slice(indent);
|
|
87324
|
+
const isDash = lineBody === "-" || lineBody.startsWith("- ");
|
|
87325
|
+
const content = isDash ? lineBody.slice(2).trim() : lineBody;
|
|
87326
|
+
tokens.push({ indent, content, isDash, raw });
|
|
87327
|
+
}
|
|
87328
|
+
let i = 0;
|
|
87329
|
+
function parseMapBlock(baseIndent) {
|
|
87330
|
+
const map = {};
|
|
87331
|
+
while (i < tokens.length && tokens[i].indent > baseIndent && !tokens[i].isDash) {
|
|
87332
|
+
const tok = tokens[i];
|
|
87333
|
+
consumeKeyValueInto(map, tok);
|
|
87334
|
+
}
|
|
87335
|
+
return map;
|
|
87336
|
+
}
|
|
87337
|
+
function parseListBlock(baseIndent) {
|
|
87338
|
+
const list = [];
|
|
87339
|
+
while (i < tokens.length && tokens[i].indent > baseIndent && tokens[i].isDash) {
|
|
87340
|
+
const tok = tokens[i];
|
|
87341
|
+
const itemIndent = tok.indent;
|
|
87342
|
+
if (tok.content === "") {
|
|
87343
|
+
i++;
|
|
87344
|
+
if (i < tokens.length && tokens[i].indent > itemIndent) {
|
|
87345
|
+
list.push(tokens[i].isDash ? parseListBlock(itemIndent) : parseMapBlock(itemIndent));
|
|
87346
|
+
} else {
|
|
87347
|
+
list.push(null);
|
|
87348
|
+
}
|
|
87349
|
+
continue;
|
|
87350
|
+
}
|
|
87351
|
+
const colonAt = colonOutsideBrackets(tok.content);
|
|
87352
|
+
if (colonAt === -1) {
|
|
87353
|
+
list.push(parseScalar(tok.content));
|
|
87354
|
+
i++;
|
|
87355
|
+
continue;
|
|
87356
|
+
}
|
|
87357
|
+
const itemMap = {};
|
|
87358
|
+
consumeKeyValueInto(itemMap, tok);
|
|
87359
|
+
while (i < tokens.length && tokens[i].indent > itemIndent && !tokens[i].isDash) {
|
|
87360
|
+
consumeKeyValueInto(itemMap, tokens[i]);
|
|
87361
|
+
}
|
|
87362
|
+
list.push(itemMap);
|
|
87326
87363
|
}
|
|
87327
|
-
|
|
87328
|
-
|
|
87364
|
+
return list;
|
|
87365
|
+
}
|
|
87366
|
+
function consumeKeyValueInto(target, tok) {
|
|
87367
|
+
const colon = colonOutsideBrackets(tok.content);
|
|
87329
87368
|
if (colon === -1) {
|
|
87330
|
-
throw new Error(`YAML mini: expected "key:" at "${raw}"`);
|
|
87369
|
+
throw new Error(`YAML mini: expected "key:" at "${tok.raw}"`);
|
|
87331
87370
|
}
|
|
87332
|
-
const key2 =
|
|
87333
|
-
const rest =
|
|
87371
|
+
const key2 = tok.content.slice(0, colon).trim();
|
|
87372
|
+
const rest = tok.content.slice(colon + 1).trim();
|
|
87334
87373
|
if (/^[&*]\w/.test(rest)) {
|
|
87335
|
-
throw new Error(`YAML mini: anchor/reference unsupported: "${raw}"`);
|
|
87374
|
+
throw new Error(`YAML mini: anchor/reference unsupported: "${tok.raw}"`);
|
|
87336
87375
|
}
|
|
87376
|
+
i++;
|
|
87337
87377
|
if (rest === "") {
|
|
87338
|
-
|
|
87339
|
-
|
|
87340
|
-
|
|
87378
|
+
if (i < tokens.length && tokens[i].indent > tok.indent) {
|
|
87379
|
+
if (tokens[i].isDash) {
|
|
87380
|
+
target[key2] = parseListBlock(tok.indent);
|
|
87381
|
+
} else {
|
|
87382
|
+
target[key2] = parseMapBlock(tok.indent);
|
|
87383
|
+
}
|
|
87384
|
+
} else {
|
|
87385
|
+
target[key2] = {};
|
|
87386
|
+
}
|
|
87341
87387
|
} else {
|
|
87342
|
-
|
|
87388
|
+
target[key2] = parseScalarOrInlineList(rest);
|
|
87343
87389
|
}
|
|
87344
87390
|
}
|
|
87345
|
-
return
|
|
87391
|
+
return parseMapBlock(-1);
|
|
87392
|
+
}
|
|
87393
|
+
function colonOutsideBrackets(s) {
|
|
87394
|
+
let depth = 0;
|
|
87395
|
+
for (let k = 0;k < s.length; k++) {
|
|
87396
|
+
const c = s[k];
|
|
87397
|
+
if (c === "[" || c === "{")
|
|
87398
|
+
depth++;
|
|
87399
|
+
else if (c === "]" || c === "}")
|
|
87400
|
+
depth--;
|
|
87401
|
+
else if (c === ":" && depth === 0)
|
|
87402
|
+
return k;
|
|
87403
|
+
}
|
|
87404
|
+
return -1;
|
|
87346
87405
|
}
|
|
87347
87406
|
function parseScalarOrInlineList(s) {
|
|
87348
87407
|
if (s.startsWith("[") && s.endsWith("]")) {
|
|
@@ -88128,7 +88187,7 @@ var {
|
|
|
88128
88187
|
// package.json
|
|
88129
88188
|
var package_default = {
|
|
88130
88189
|
name: "@carllee1983/dbcli",
|
|
88131
|
-
version: "1.
|
|
88190
|
+
version: "1.9.0",
|
|
88132
88191
|
description: "Database CLI for AI agents",
|
|
88133
88192
|
type: "module",
|
|
88134
88193
|
publishConfig: {
|
|
@@ -95297,6 +95356,631 @@ async function ensureDir(dirPath) {
|
|
|
95297
95356
|
}
|
|
95298
95357
|
}
|
|
95299
95358
|
}
|
|
95359
|
+
function registerSkillCommand(program2) {
|
|
95360
|
+
return program2.command("skill").description(t("skill.description")).option("--install <platform>", "Install to platform directory (claude, gemini, copilot, cursor)").option("--output <path>", "Write skill to file instead of stdout").action(async (options) => {
|
|
95361
|
+
try {
|
|
95362
|
+
await skillCommand(program2, options);
|
|
95363
|
+
} catch (error) {
|
|
95364
|
+
console.error(error.message);
|
|
95365
|
+
process.exit(1);
|
|
95366
|
+
}
|
|
95367
|
+
});
|
|
95368
|
+
}
|
|
95369
|
+
|
|
95370
|
+
// src/core/agent-tasks/types.ts
|
|
95371
|
+
class AgentTaskError extends Error {
|
|
95372
|
+
code;
|
|
95373
|
+
file;
|
|
95374
|
+
constructor(message, code, file) {
|
|
95375
|
+
super(message);
|
|
95376
|
+
this.code = code;
|
|
95377
|
+
this.file = file;
|
|
95378
|
+
this.name = "AgentTaskError";
|
|
95379
|
+
Object.setPrototypeOf(this, AgentTaskError.prototype);
|
|
95380
|
+
}
|
|
95381
|
+
}
|
|
95382
|
+
// src/core/agent-tasks/task-paths.ts
|
|
95383
|
+
import { join as join14, resolve as resolve4 } from "path";
|
|
95384
|
+
function resolveBuiltinDir2() {
|
|
95385
|
+
return resolve4(import.meta.dir, "..", "..", "..", "assets", "tasks");
|
|
95386
|
+
}
|
|
95387
|
+
function resolveAgentTaskDirs(workspaceRoot) {
|
|
95388
|
+
return {
|
|
95389
|
+
builtinDir: resolveBuiltinDir2(),
|
|
95390
|
+
sharedDir: join14(workspaceRoot, ".dbcli-shared", "tasks"),
|
|
95391
|
+
localDir: join14(workspaceRoot, ".dbcli", "tasks")
|
|
95392
|
+
};
|
|
95393
|
+
}
|
|
95394
|
+
// src/core/agent-tasks/parser.ts
|
|
95395
|
+
var VALID_ENGINES2 = ["postgres", "mysql", "mongodb", "redis", "elasticsearch"];
|
|
95396
|
+
var VALID_PARAM_TYPES = ["string", "number", "boolean"];
|
|
95397
|
+
var VALID_RISKS = ["readonly", "dry-run", "write", "unknown"];
|
|
95398
|
+
function parseAgentTask(input) {
|
|
95399
|
+
const { frontmatter, body } = splitFrontmatter2(input.text, input.file);
|
|
95400
|
+
if (!frontmatter.trim()) {
|
|
95401
|
+
throw new AgentTaskError(`Task '${input.name}' has no frontmatter`, "PARSE_ERROR", input.file);
|
|
95402
|
+
}
|
|
95403
|
+
let raw;
|
|
95404
|
+
try {
|
|
95405
|
+
raw = parseYamlMini(frontmatter);
|
|
95406
|
+
} catch (e) {
|
|
95407
|
+
throw new AgentTaskError(`Invalid frontmatter in '${input.name}': ${e.message}`, "PARSE_ERROR", input.file);
|
|
95408
|
+
}
|
|
95409
|
+
const declaredName = raw.name;
|
|
95410
|
+
if (typeof declaredName !== "string" || declaredName.trim() === "") {
|
|
95411
|
+
throw new AgentTaskError(`Task '${input.name}' is missing required field 'name'`, "PARSE_ERROR", input.file);
|
|
95412
|
+
}
|
|
95413
|
+
if (declaredName !== input.name) {
|
|
95414
|
+
throw new AgentTaskError(`Task name '${declaredName}' does not match expected '${input.name}' from filename`, "PARSE_ERROR", input.file);
|
|
95415
|
+
}
|
|
95416
|
+
const description = typeof raw.description === "string" ? raw.description : undefined;
|
|
95417
|
+
const tags = Array.isArray(raw.tags) ? raw.tags.map(String) : [];
|
|
95418
|
+
const engines = parseEngines(raw.engines, input);
|
|
95419
|
+
const params = parseParams(raw.params, input);
|
|
95420
|
+
const safety = parseSafety(raw.safety, input);
|
|
95421
|
+
const steps = parseSteps(raw.steps, input);
|
|
95422
|
+
const notes = body.trim() ? body.trim() : undefined;
|
|
95423
|
+
return {
|
|
95424
|
+
name: declaredName,
|
|
95425
|
+
description,
|
|
95426
|
+
tags,
|
|
95427
|
+
engines,
|
|
95428
|
+
params,
|
|
95429
|
+
safety,
|
|
95430
|
+
steps,
|
|
95431
|
+
notes,
|
|
95432
|
+
source: input.source,
|
|
95433
|
+
file: input.file
|
|
95434
|
+
};
|
|
95435
|
+
}
|
|
95436
|
+
function splitFrontmatter2(text2, file) {
|
|
95437
|
+
const lines2 = text2.split(`
|
|
95438
|
+
`);
|
|
95439
|
+
if (lines2[0]?.trim() !== "---")
|
|
95440
|
+
return { frontmatter: "", body: text2 };
|
|
95441
|
+
const end = lines2.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
95442
|
+
if (end === -1) {
|
|
95443
|
+
throw new AgentTaskError(`Unterminated frontmatter in ${file}`, "PARSE_ERROR", file);
|
|
95444
|
+
}
|
|
95445
|
+
return {
|
|
95446
|
+
frontmatter: lines2.slice(1, end).join(`
|
|
95447
|
+
`),
|
|
95448
|
+
body: lines2.slice(end + 1).join(`
|
|
95449
|
+
`)
|
|
95450
|
+
};
|
|
95451
|
+
}
|
|
95452
|
+
function parseEngines(value, input) {
|
|
95453
|
+
if (value === undefined || value === null)
|
|
95454
|
+
return;
|
|
95455
|
+
const list = Array.isArray(value) ? value : [value];
|
|
95456
|
+
const cleaned = [];
|
|
95457
|
+
for (const v of list) {
|
|
95458
|
+
const s = String(v).toLowerCase();
|
|
95459
|
+
if (!VALID_ENGINES2.includes(s)) {
|
|
95460
|
+
throw new AgentTaskError(`Unknown engine '${s}' in task '${input.name}' (allowed: ${VALID_ENGINES2.join(", ")})`, "PARSE_ERROR", input.file);
|
|
95461
|
+
}
|
|
95462
|
+
cleaned.push(s);
|
|
95463
|
+
}
|
|
95464
|
+
return cleaned;
|
|
95465
|
+
}
|
|
95466
|
+
function parseParams(value, input) {
|
|
95467
|
+
if (value === undefined || value === null)
|
|
95468
|
+
return [];
|
|
95469
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
95470
|
+
throw new AgentTaskError(`'params' in task '${input.name}' must be a map`, "PARSE_ERROR", input.file);
|
|
95471
|
+
}
|
|
95472
|
+
return Object.entries(value).map(([name, spec]) => {
|
|
95473
|
+
const type = String(spec?.type ?? "string");
|
|
95474
|
+
if (!VALID_PARAM_TYPES.includes(type)) {
|
|
95475
|
+
throw new AgentTaskError(`Param '${name}' in task '${input.name}': invalid type '${type}' (allowed: ${VALID_PARAM_TYPES.join(", ")})`, "PARSE_ERROR", input.file);
|
|
95476
|
+
}
|
|
95477
|
+
const hasDefault = spec && Object.prototype.hasOwnProperty.call(spec, "default");
|
|
95478
|
+
const required = spec?.required === true ? true : !hasDefault && spec?.required !== false;
|
|
95479
|
+
const out = { name, type, required };
|
|
95480
|
+
if (typeof spec?.description === "string")
|
|
95481
|
+
out.description = spec.description;
|
|
95482
|
+
if (hasDefault)
|
|
95483
|
+
out.default = spec.default;
|
|
95484
|
+
if (Array.isArray(spec?.enum))
|
|
95485
|
+
out.enum = spec.enum;
|
|
95486
|
+
return out;
|
|
95487
|
+
});
|
|
95488
|
+
}
|
|
95489
|
+
function parseSafety(value, input) {
|
|
95490
|
+
if (!value || typeof value !== "object") {
|
|
95491
|
+
throw new AgentTaskError(`Task '${input.name}' is missing required field 'safety'`, "PARSE_ERROR", input.file);
|
|
95492
|
+
}
|
|
95493
|
+
const obj = value;
|
|
95494
|
+
if (obj.mode !== "plan-only") {
|
|
95495
|
+
throw new AgentTaskError(`Task '${input.name}' has invalid safety.mode '${String(obj.mode)}' (only 'plan-only' is supported)`, "PARSE_ERROR", input.file);
|
|
95496
|
+
}
|
|
95497
|
+
const requires = Array.isArray(obj.requires) ? obj.requires.map(String) : undefined;
|
|
95498
|
+
return requires ? { mode: "plan-only", requires } : { mode: "plan-only" };
|
|
95499
|
+
}
|
|
95500
|
+
function parseSteps(value, input) {
|
|
95501
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
95502
|
+
throw new AgentTaskError(`Task '${input.name}' must declare at least one step`, "PARSE_ERROR", input.file);
|
|
95503
|
+
}
|
|
95504
|
+
return value.map((raw, idx) => {
|
|
95505
|
+
if (!raw || typeof raw !== "object") {
|
|
95506
|
+
throw new AgentTaskError(`Task '${input.name}' step #${idx + 1} is not an object`, "PARSE_ERROR", input.file);
|
|
95507
|
+
}
|
|
95508
|
+
const obj = raw;
|
|
95509
|
+
if (obj.type !== "command") {
|
|
95510
|
+
throw new AgentTaskError(`Task '${input.name}' step #${idx + 1} has unsupported type '${String(obj.type)}' (only 'command' is supported)`, "PARSE_ERROR", input.file);
|
|
95511
|
+
}
|
|
95512
|
+
if (typeof obj.command !== "string" || obj.command.trim() === "") {
|
|
95513
|
+
throw new AgentTaskError(`Task '${input.name}' step #${idx + 1} is missing 'command'`, "PARSE_ERROR", input.file);
|
|
95514
|
+
}
|
|
95515
|
+
const risk = obj.risk === undefined ? undefined : String(obj.risk);
|
|
95516
|
+
if (risk !== undefined && !VALID_RISKS.includes(risk)) {
|
|
95517
|
+
throw new AgentTaskError(`Task '${input.name}' step #${idx + 1} has invalid risk '${risk}'`, "PARSE_ERROR", input.file);
|
|
95518
|
+
}
|
|
95519
|
+
const step = { type: "command", command: obj.command.trim() };
|
|
95520
|
+
if (typeof obj.reason === "string")
|
|
95521
|
+
step.reason = obj.reason;
|
|
95522
|
+
if (risk !== undefined)
|
|
95523
|
+
step.risk = risk;
|
|
95524
|
+
return step;
|
|
95525
|
+
});
|
|
95526
|
+
}
|
|
95527
|
+
// src/core/agent-tasks/loader.ts
|
|
95528
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
95529
|
+
import { join as join15, relative as relative2, sep as sep2 } from "path";
|
|
95530
|
+
async function loadAgentTasks(opts, flags) {
|
|
95531
|
+
const errors3 = [];
|
|
95532
|
+
const builtin = await walkAndParse2(opts.builtinDir, "builtin", errors3);
|
|
95533
|
+
const shared = await walkAndParse2(opts.sharedDir, "shared", errors3);
|
|
95534
|
+
const local = await walkAndParse2(opts.localDir, "local", errors3);
|
|
95535
|
+
const merged = new Map;
|
|
95536
|
+
for (const t2 of builtin)
|
|
95537
|
+
merged.set(t2.name, { task: t2, hasOverride: false });
|
|
95538
|
+
for (const t2 of shared) {
|
|
95539
|
+
const had = merged.has(t2.name);
|
|
95540
|
+
merged.set(t2.name, { task: t2, hasOverride: had });
|
|
95541
|
+
}
|
|
95542
|
+
for (const t2 of local) {
|
|
95543
|
+
const had = merged.has(t2.name);
|
|
95544
|
+
merged.set(t2.name, { task: t2, hasOverride: had });
|
|
95545
|
+
}
|
|
95546
|
+
Object.defineProperty(merged, "errors", {
|
|
95547
|
+
value: flags?.collectErrors ? errors3 : [],
|
|
95548
|
+
enumerable: false,
|
|
95549
|
+
writable: false
|
|
95550
|
+
});
|
|
95551
|
+
return merged;
|
|
95552
|
+
}
|
|
95553
|
+
async function walkAndParse2(root, source, errors3) {
|
|
95554
|
+
const files = await safeCollectMd(root);
|
|
95555
|
+
const out = [];
|
|
95556
|
+
for (const file of files) {
|
|
95557
|
+
const rel = relative2(root, file).split(sep2).join("/");
|
|
95558
|
+
if (!rel.endsWith(".md"))
|
|
95559
|
+
continue;
|
|
95560
|
+
if (rel.toLowerCase() === "readme.md")
|
|
95561
|
+
continue;
|
|
95562
|
+
const name = rel.slice(0, -".md".length);
|
|
95563
|
+
try {
|
|
95564
|
+
const text2 = await Bun.file(file).text();
|
|
95565
|
+
const task = parseAgentTask({ name, file, source, text: text2 });
|
|
95566
|
+
out.push(task);
|
|
95567
|
+
} catch (e) {
|
|
95568
|
+
if (e instanceof AgentTaskError)
|
|
95569
|
+
errors3.push(e);
|
|
95570
|
+
else
|
|
95571
|
+
errors3.push(new AgentTaskError(e.message, "IO_ERROR", file));
|
|
95572
|
+
}
|
|
95573
|
+
}
|
|
95574
|
+
return out;
|
|
95575
|
+
}
|
|
95576
|
+
async function safeCollectMd(root) {
|
|
95577
|
+
const out = [];
|
|
95578
|
+
async function walk(dir) {
|
|
95579
|
+
let entries;
|
|
95580
|
+
try {
|
|
95581
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
95582
|
+
} catch {
|
|
95583
|
+
return;
|
|
95584
|
+
}
|
|
95585
|
+
for (const e of entries) {
|
|
95586
|
+
const full = join15(dir, e.name);
|
|
95587
|
+
if (e.isDirectory())
|
|
95588
|
+
await walk(full);
|
|
95589
|
+
else
|
|
95590
|
+
out.push(full);
|
|
95591
|
+
}
|
|
95592
|
+
}
|
|
95593
|
+
await walk(root);
|
|
95594
|
+
return out;
|
|
95595
|
+
}
|
|
95596
|
+
// src/core/agent-tasks/resolver.ts
|
|
95597
|
+
function filterTasks(map, opts) {
|
|
95598
|
+
return [...map.values()].filter((entry) => {
|
|
95599
|
+
const t2 = entry.task;
|
|
95600
|
+
if (opts.tag && !t2.tags.includes(opts.tag))
|
|
95601
|
+
return false;
|
|
95602
|
+
if (opts.engine) {
|
|
95603
|
+
const declared = t2.engines;
|
|
95604
|
+
if (declared && declared.length > 0 && !declared.includes(opts.engine))
|
|
95605
|
+
return false;
|
|
95606
|
+
}
|
|
95607
|
+
if (opts.source && t2.source !== opts.source)
|
|
95608
|
+
return false;
|
|
95609
|
+
return true;
|
|
95610
|
+
});
|
|
95611
|
+
}
|
|
95612
|
+
function resolveTaskByName(map, name) {
|
|
95613
|
+
const direct = map.get(name);
|
|
95614
|
+
if (direct)
|
|
95615
|
+
return direct;
|
|
95616
|
+
const suggestions = suggestSimilar2([...map.keys()], name);
|
|
95617
|
+
const hint = suggestions.length > 0 ? `
|
|
95618
|
+
Did you mean: ${suggestions.join(", ")}?` : "";
|
|
95619
|
+
throw new AgentTaskError(`Task not found: ${name}${hint}`, "NOT_FOUND");
|
|
95620
|
+
}
|
|
95621
|
+
function suggestSimilar2(all, name, limit = 5) {
|
|
95622
|
+
return all.map((k) => ({ k, d: levenshteinDistance(k, name) })).sort((a, b) => a.d - b.d).slice(0, limit).map((x) => x.k);
|
|
95623
|
+
}
|
|
95624
|
+
// src/core/agent-tasks/argv-split.ts
|
|
95625
|
+
function splitArgv(input) {
|
|
95626
|
+
const out = [];
|
|
95627
|
+
let current = "";
|
|
95628
|
+
let inSingle = false;
|
|
95629
|
+
let inDouble = false;
|
|
95630
|
+
let hasToken = false;
|
|
95631
|
+
for (let i = 0;i < input.length; i++) {
|
|
95632
|
+
const ch = input[i];
|
|
95633
|
+
if (inSingle) {
|
|
95634
|
+
if (ch === "'") {
|
|
95635
|
+
inSingle = false;
|
|
95636
|
+
} else {
|
|
95637
|
+
current += ch;
|
|
95638
|
+
hasToken = true;
|
|
95639
|
+
}
|
|
95640
|
+
continue;
|
|
95641
|
+
}
|
|
95642
|
+
if (inDouble) {
|
|
95643
|
+
if (ch === "\\" && (input[i + 1] === '"' || input[i + 1] === "\\")) {
|
|
95644
|
+
current += input[i + 1];
|
|
95645
|
+
i++;
|
|
95646
|
+
hasToken = true;
|
|
95647
|
+
continue;
|
|
95648
|
+
}
|
|
95649
|
+
if (ch === '"') {
|
|
95650
|
+
inDouble = false;
|
|
95651
|
+
} else {
|
|
95652
|
+
current += ch;
|
|
95653
|
+
hasToken = true;
|
|
95654
|
+
}
|
|
95655
|
+
continue;
|
|
95656
|
+
}
|
|
95657
|
+
if (ch === "'") {
|
|
95658
|
+
inSingle = true;
|
|
95659
|
+
hasToken = true;
|
|
95660
|
+
continue;
|
|
95661
|
+
}
|
|
95662
|
+
if (ch === '"') {
|
|
95663
|
+
inDouble = true;
|
|
95664
|
+
hasToken = true;
|
|
95665
|
+
continue;
|
|
95666
|
+
}
|
|
95667
|
+
if (ch === "\\" && i + 1 < input.length) {
|
|
95668
|
+
current += input[i + 1];
|
|
95669
|
+
i++;
|
|
95670
|
+
hasToken = true;
|
|
95671
|
+
continue;
|
|
95672
|
+
}
|
|
95673
|
+
if (ch === " " || ch === "\t" || ch === `
|
|
95674
|
+
`) {
|
|
95675
|
+
if (hasToken) {
|
|
95676
|
+
out.push(current);
|
|
95677
|
+
current = "";
|
|
95678
|
+
hasToken = false;
|
|
95679
|
+
}
|
|
95680
|
+
continue;
|
|
95681
|
+
}
|
|
95682
|
+
current += ch;
|
|
95683
|
+
hasToken = true;
|
|
95684
|
+
}
|
|
95685
|
+
if (inSingle || inDouble) {
|
|
95686
|
+
throw new Error(`Unterminated quote in command: ${input}`);
|
|
95687
|
+
}
|
|
95688
|
+
if (hasToken)
|
|
95689
|
+
out.push(current);
|
|
95690
|
+
return out;
|
|
95691
|
+
}
|
|
95692
|
+
|
|
95693
|
+
// src/core/agent-tasks/planner.ts
|
|
95694
|
+
var TEMPLATE_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
|
|
95695
|
+
function planAgentTask(input) {
|
|
95696
|
+
const warnings = [];
|
|
95697
|
+
const resolved = resolveParams(input.task.params, input.params, warnings);
|
|
95698
|
+
const steps = input.task.steps.map((step) => {
|
|
95699
|
+
const resolvedCommand = applyTemplate(step.command, resolved, input.task);
|
|
95700
|
+
const argv = splitArgv(resolvedCommand);
|
|
95701
|
+
const out = {
|
|
95702
|
+
command: step.command,
|
|
95703
|
+
resolvedCommand,
|
|
95704
|
+
argv
|
|
95705
|
+
};
|
|
95706
|
+
if (step.reason)
|
|
95707
|
+
out.reason = step.reason;
|
|
95708
|
+
if (step.risk)
|
|
95709
|
+
out.risk = step.risk;
|
|
95710
|
+
return out;
|
|
95711
|
+
});
|
|
95712
|
+
return {
|
|
95713
|
+
name: input.task.name,
|
|
95714
|
+
source: input.task.source,
|
|
95715
|
+
file: input.task.file,
|
|
95716
|
+
description: input.task.description,
|
|
95717
|
+
mode: input.task.safety.mode,
|
|
95718
|
+
requires: input.task.safety.requires ?? [],
|
|
95719
|
+
parameters: resolved,
|
|
95720
|
+
steps,
|
|
95721
|
+
warnings
|
|
95722
|
+
};
|
|
95723
|
+
}
|
|
95724
|
+
function resolveParams(spec, provided, warnings) {
|
|
95725
|
+
const out = {};
|
|
95726
|
+
const knownNames = new Set(spec.map((p) => p.name));
|
|
95727
|
+
for (const p of spec) {
|
|
95728
|
+
const raw = provided[p.name];
|
|
95729
|
+
if (raw === undefined || raw === "") {
|
|
95730
|
+
if (p.default !== undefined) {
|
|
95731
|
+
out[p.name] = p.default;
|
|
95732
|
+
continue;
|
|
95733
|
+
}
|
|
95734
|
+
if (p.required) {
|
|
95735
|
+
throw new AgentTaskError(`Missing required parameter '${p.name}'`, "PARAM_MISSING");
|
|
95736
|
+
}
|
|
95737
|
+
continue;
|
|
95738
|
+
}
|
|
95739
|
+
out[p.name] = coerce2(p, raw);
|
|
95740
|
+
}
|
|
95741
|
+
for (const key2 of Object.keys(provided)) {
|
|
95742
|
+
if (!knownNames.has(key2)) {
|
|
95743
|
+
warnings.push(`Unknown parameter '${key2}' (ignored)`);
|
|
95744
|
+
}
|
|
95745
|
+
}
|
|
95746
|
+
return out;
|
|
95747
|
+
}
|
|
95748
|
+
function coerce2(p, raw) {
|
|
95749
|
+
let value;
|
|
95750
|
+
if (p.type === "number") {
|
|
95751
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
95752
|
+
if (!Number.isFinite(n)) {
|
|
95753
|
+
throw new AgentTaskError(`Parameter '${p.name}' must be a number (got '${raw}')`, "PARAM_INVALID");
|
|
95754
|
+
}
|
|
95755
|
+
value = n;
|
|
95756
|
+
} else if (p.type === "boolean") {
|
|
95757
|
+
if (typeof raw === "boolean")
|
|
95758
|
+
value = raw;
|
|
95759
|
+
else if (raw === "true")
|
|
95760
|
+
value = true;
|
|
95761
|
+
else if (raw === "false")
|
|
95762
|
+
value = false;
|
|
95763
|
+
else
|
|
95764
|
+
throw new AgentTaskError(`Parameter '${p.name}' must be a boolean (got '${raw}')`, "PARAM_INVALID");
|
|
95765
|
+
} else {
|
|
95766
|
+
value = String(raw);
|
|
95767
|
+
}
|
|
95768
|
+
if (p.enum && !p.enum.includes(value)) {
|
|
95769
|
+
throw new AgentTaskError(`Parameter '${p.name}' must match enum [${p.enum.join(", ")}] (got '${value}')`, "PARAM_INVALID");
|
|
95770
|
+
}
|
|
95771
|
+
return value;
|
|
95772
|
+
}
|
|
95773
|
+
function applyTemplate(command, values, task) {
|
|
95774
|
+
return command.replace(TEMPLATE_RE, (_match, key2) => {
|
|
95775
|
+
if (!(key2 in values)) {
|
|
95776
|
+
throw new AgentTaskError(`Template references unknown parameter '${key2}' in task '${task.name}'`, "TEMPLATE_SYNTAX", task.file);
|
|
95777
|
+
}
|
|
95778
|
+
return String(values[key2]);
|
|
95779
|
+
});
|
|
95780
|
+
}
|
|
95781
|
+
function renderMarkdownPlan(plan) {
|
|
95782
|
+
const lines2 = [];
|
|
95783
|
+
lines2.push(`# Task: ${plan.name}`);
|
|
95784
|
+
if (plan.description)
|
|
95785
|
+
lines2.push("", plan.description);
|
|
95786
|
+
lines2.push("", `- **source**: ${plan.source}`);
|
|
95787
|
+
lines2.push(`- **file**: ${plan.file}`);
|
|
95788
|
+
lines2.push(`- **mode**: ${plan.mode}`);
|
|
95789
|
+
if (plan.requires.length > 0) {
|
|
95790
|
+
lines2.push(`- **requires**: ${plan.requires.join(", ")}`);
|
|
95791
|
+
}
|
|
95792
|
+
lines2.push("", "## Parameters");
|
|
95793
|
+
if (Object.keys(plan.parameters).length === 0) {
|
|
95794
|
+
lines2.push("_(none)_");
|
|
95795
|
+
} else {
|
|
95796
|
+
for (const [k, v] of Object.entries(plan.parameters)) {
|
|
95797
|
+
lines2.push(`- ${k}: ${String(v)}`);
|
|
95798
|
+
}
|
|
95799
|
+
}
|
|
95800
|
+
lines2.push("", "## Steps");
|
|
95801
|
+
plan.steps.forEach((s, i) => {
|
|
95802
|
+
lines2.push(`### ${i + 1}. \`${s.resolvedCommand}\``);
|
|
95803
|
+
if (s.risk)
|
|
95804
|
+
lines2.push(`- risk: ${s.risk}`);
|
|
95805
|
+
if (s.reason)
|
|
95806
|
+
lines2.push(`- reason: ${s.reason}`);
|
|
95807
|
+
if (s.command !== s.resolvedCommand) {
|
|
95808
|
+
lines2.push(`- template: \`${s.command}\``);
|
|
95809
|
+
}
|
|
95810
|
+
lines2.push("");
|
|
95811
|
+
});
|
|
95812
|
+
if (plan.warnings.length > 0) {
|
|
95813
|
+
lines2.push("## Warnings");
|
|
95814
|
+
for (const w of plan.warnings)
|
|
95815
|
+
lines2.push(`- ${w}`);
|
|
95816
|
+
}
|
|
95817
|
+
return lines2.join(`
|
|
95818
|
+
`);
|
|
95819
|
+
}
|
|
95820
|
+
// src/commands/skill-tasks.ts
|
|
95821
|
+
function registerSkillTasksCommand(parent) {
|
|
95822
|
+
const tasks = parent.command("tasks").description("Discover and plan AI-agent task templates");
|
|
95823
|
+
tasks.command("list").description("List available agent task templates").option("--format <type>", "Output format: table | json", "table").option("--tag <tag>", "Filter by tag").option("--engine <engine>", "Filter by engine: postgres | mysql | mongodb | redis | elasticsearch").option("--source <source>", "Filter by source: builtin | shared | local").action(async (options) => {
|
|
95824
|
+
try {
|
|
95825
|
+
await runList(options);
|
|
95826
|
+
} catch (e) {
|
|
95827
|
+
console.error(e.message);
|
|
95828
|
+
process.exit(1);
|
|
95829
|
+
}
|
|
95830
|
+
});
|
|
95831
|
+
tasks.command("show <task>").description("Show full task definition and notes").option("--format <type>", "Output format: markdown | json", "markdown").action(async (taskName, options) => {
|
|
95832
|
+
try {
|
|
95833
|
+
await runShow(taskName, options);
|
|
95834
|
+
} catch (e) {
|
|
95835
|
+
console.error(e.message);
|
|
95836
|
+
process.exit(1);
|
|
95837
|
+
}
|
|
95838
|
+
});
|
|
95839
|
+
tasks.command("plan <task>").description("Generate an executable plan for a task (no execution)").option("--format <type>", "Output format: markdown | json", "markdown").option("--param <kv>", "Pass parameter as key=value (repeatable)", (val, prev = []) => prev.concat([val]), []).action(async (taskName, options) => {
|
|
95840
|
+
try {
|
|
95841
|
+
await runPlan(taskName, options);
|
|
95842
|
+
} catch (e) {
|
|
95843
|
+
console.error(e.message);
|
|
95844
|
+
process.exit(1);
|
|
95845
|
+
}
|
|
95846
|
+
});
|
|
95847
|
+
return tasks;
|
|
95848
|
+
}
|
|
95849
|
+
async function runList(options) {
|
|
95850
|
+
const map = await loadAgentTasks(resolveAgentTaskDirs(process.cwd()));
|
|
95851
|
+
const filtered = filterTasks(map, {
|
|
95852
|
+
tag: options.tag,
|
|
95853
|
+
engine: options.engine,
|
|
95854
|
+
source: options.source
|
|
95855
|
+
});
|
|
95856
|
+
if (options.format === "json") {
|
|
95857
|
+
const json = filtered.map((entry) => ({
|
|
95858
|
+
name: entry.task.name,
|
|
95859
|
+
description: entry.task.description,
|
|
95860
|
+
source: entry.task.source,
|
|
95861
|
+
tags: entry.task.tags,
|
|
95862
|
+
engines: entry.task.engines,
|
|
95863
|
+
hasOverride: entry.hasOverride || undefined,
|
|
95864
|
+
params: entry.task.params.map((p) => ({
|
|
95865
|
+
name: p.name,
|
|
95866
|
+
type: p.type,
|
|
95867
|
+
...p.required ? { required: true } : {},
|
|
95868
|
+
...p.default !== undefined ? { default: p.default } : {}
|
|
95869
|
+
})),
|
|
95870
|
+
file: entry.task.file
|
|
95871
|
+
}));
|
|
95872
|
+
console.log(JSON.stringify(json, null, 2));
|
|
95873
|
+
return;
|
|
95874
|
+
}
|
|
95875
|
+
if (filtered.length === 0) {
|
|
95876
|
+
console.log("No agent tasks found.");
|
|
95877
|
+
return;
|
|
95878
|
+
}
|
|
95879
|
+
const header = ["NAME", "SOURCE", "ENGINES", "TAGS", "DESCRIPTION"];
|
|
95880
|
+
const rows = filtered.map((entry) => [
|
|
95881
|
+
entry.task.name + (entry.hasOverride ? "*" : ""),
|
|
95882
|
+
entry.task.source,
|
|
95883
|
+
(entry.task.engines ?? []).join(",") || "-",
|
|
95884
|
+
entry.task.tags.join(",") || "-",
|
|
95885
|
+
entry.task.description ?? ""
|
|
95886
|
+
]);
|
|
95887
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
95888
|
+
const fmt = (line) => line.map((c, i) => (c ?? "").padEnd(widths[i] ?? 0)).join(" ");
|
|
95889
|
+
console.log(fmt(header));
|
|
95890
|
+
for (const r of rows)
|
|
95891
|
+
console.log(fmt(r));
|
|
95892
|
+
}
|
|
95893
|
+
async function runShow(taskName, options) {
|
|
95894
|
+
const map = await loadAgentTasks(resolveAgentTaskDirs(process.cwd()));
|
|
95895
|
+
const entry = resolveTaskByName(map, taskName);
|
|
95896
|
+
const task = entry.task;
|
|
95897
|
+
if (options.format === "json") {
|
|
95898
|
+
console.log(JSON.stringify({
|
|
95899
|
+
name: task.name,
|
|
95900
|
+
description: task.description,
|
|
95901
|
+
source: task.source,
|
|
95902
|
+
file: task.file,
|
|
95903
|
+
tags: task.tags,
|
|
95904
|
+
engines: task.engines,
|
|
95905
|
+
params: task.params,
|
|
95906
|
+
safety: task.safety,
|
|
95907
|
+
steps: task.steps,
|
|
95908
|
+
notes: task.notes
|
|
95909
|
+
}, null, 2));
|
|
95910
|
+
return;
|
|
95911
|
+
}
|
|
95912
|
+
console.log(`# ${task.name} (${task.source})`);
|
|
95913
|
+
if (task.description)
|
|
95914
|
+
console.log(task.description);
|
|
95915
|
+
console.log("");
|
|
95916
|
+
console.log(`- file: ${task.file}`);
|
|
95917
|
+
if (task.engines)
|
|
95918
|
+
console.log(`- engines: ${task.engines.join(", ")}`);
|
|
95919
|
+
if (task.tags.length > 0)
|
|
95920
|
+
console.log(`- tags: ${task.tags.join(", ")}`);
|
|
95921
|
+
console.log(`- safety: ${task.safety.mode}`);
|
|
95922
|
+
if (task.safety.requires?.length) {
|
|
95923
|
+
console.log(`- requires: ${task.safety.requires.join(", ")}`);
|
|
95924
|
+
}
|
|
95925
|
+
if (task.params.length > 0) {
|
|
95926
|
+
console.log(`
|
|
95927
|
+
## Parameters`);
|
|
95928
|
+
for (const p of task.params) {
|
|
95929
|
+
const def = p.default !== undefined ? ` (default: ${p.default})` : "";
|
|
95930
|
+
const req = p.required ? " (required)" : "";
|
|
95931
|
+
console.log(`- ${p.name}: ${p.type}${req}${def}`);
|
|
95932
|
+
if (p.description)
|
|
95933
|
+
console.log(` ${p.description}`);
|
|
95934
|
+
}
|
|
95935
|
+
}
|
|
95936
|
+
console.log(`
|
|
95937
|
+
## Steps`);
|
|
95938
|
+
task.steps.forEach((s, i) => {
|
|
95939
|
+
console.log(`${i + 1}. \`${s.command}\``);
|
|
95940
|
+
if (s.reason)
|
|
95941
|
+
console.log(` reason: ${s.reason}`);
|
|
95942
|
+
if (s.risk)
|
|
95943
|
+
console.log(` risk: ${s.risk}`);
|
|
95944
|
+
});
|
|
95945
|
+
if (task.notes) {
|
|
95946
|
+
console.log(`
|
|
95947
|
+
## Notes
|
|
95948
|
+
`);
|
|
95949
|
+
console.log(task.notes);
|
|
95950
|
+
}
|
|
95951
|
+
}
|
|
95952
|
+
async function runPlan(taskName, options) {
|
|
95953
|
+
const map = await loadAgentTasks(resolveAgentTaskDirs(process.cwd()));
|
|
95954
|
+
const entry = resolveTaskByName(map, taskName);
|
|
95955
|
+
const params = parseParamPairs(options.param ?? []);
|
|
95956
|
+
const plan = planAgentTask({ task: entry.task, params });
|
|
95957
|
+
if (options.format === "json") {
|
|
95958
|
+
console.log(JSON.stringify({
|
|
95959
|
+
name: plan.name,
|
|
95960
|
+
source: plan.source,
|
|
95961
|
+
file: plan.file,
|
|
95962
|
+
description: plan.description,
|
|
95963
|
+
mode: plan.mode,
|
|
95964
|
+
requires: plan.requires,
|
|
95965
|
+
parameters: plan.parameters,
|
|
95966
|
+
steps: plan.steps,
|
|
95967
|
+
warnings: plan.warnings
|
|
95968
|
+
}, null, 2));
|
|
95969
|
+
return;
|
|
95970
|
+
}
|
|
95971
|
+
console.log(renderMarkdownPlan(plan));
|
|
95972
|
+
}
|
|
95973
|
+
function parseParamPairs(pairs) {
|
|
95974
|
+
const out = {};
|
|
95975
|
+
for (const p of pairs) {
|
|
95976
|
+
const eq = p.indexOf("=");
|
|
95977
|
+
if (eq === -1) {
|
|
95978
|
+
throw new Error(`Invalid --param '${p}' (expected key=value)`);
|
|
95979
|
+
}
|
|
95980
|
+
out[p.slice(0, eq).trim()] = p.slice(eq + 1);
|
|
95981
|
+
}
|
|
95982
|
+
return out;
|
|
95983
|
+
}
|
|
95300
95984
|
|
|
95301
95985
|
// src/commands/blacklist.ts
|
|
95302
95986
|
var DEFAULT_CONFIG_PATH = ".dbcli";
|
|
@@ -95847,7 +96531,7 @@ var statusCommand = new Command("status").description("Show current configuratio
|
|
|
95847
96531
|
// src/commands/doctor.ts
|
|
95848
96532
|
init_validation();
|
|
95849
96533
|
init_schema_path();
|
|
95850
|
-
import { join as
|
|
96534
|
+
import { join as join16 } from "path";
|
|
95851
96535
|
import { resolveSrv as resolveSrv2 } from "dns/promises";
|
|
95852
96536
|
var ALLOWED_FORMATS8 = ["text", "json"];
|
|
95853
96537
|
var SENSITIVE_PATTERNS = [
|
|
@@ -95921,7 +96605,7 @@ var runDoctorChecks = {
|
|
|
95921
96605
|
}
|
|
95922
96606
|
},
|
|
95923
96607
|
async checkConfigExists(configPath, existsFn) {
|
|
95924
|
-
const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(
|
|
96608
|
+
const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join16(configPath, "config.json")).exists();
|
|
95925
96609
|
return {
|
|
95926
96610
|
group: "Configuration",
|
|
95927
96611
|
label: "Config exists",
|
|
@@ -96078,7 +96762,7 @@ var runDoctorChecks = {
|
|
|
96078
96762
|
async checkV2Config(configPath) {
|
|
96079
96763
|
const results = [];
|
|
96080
96764
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
96081
|
-
const configFile = Bun.file(
|
|
96765
|
+
const configFile = Bun.file(join16(storagePath, "config.json"));
|
|
96082
96766
|
if (!await configFile.exists())
|
|
96083
96767
|
return results;
|
|
96084
96768
|
let raw;
|
|
@@ -96118,7 +96802,7 @@ var runDoctorChecks = {
|
|
|
96118
96802
|
}
|
|
96119
96803
|
for (const [name, conn] of Object.entries(config.connections)) {
|
|
96120
96804
|
if (conn.envFile) {
|
|
96121
|
-
const envPath =
|
|
96805
|
+
const envPath = join16(storagePath, conn.envFile);
|
|
96122
96806
|
const exists = await Bun.file(envPath).exists();
|
|
96123
96807
|
results.push({
|
|
96124
96808
|
group: "Configuration",
|
|
@@ -96337,7 +97021,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
|
|
|
96337
97021
|
}
|
|
96338
97022
|
try {
|
|
96339
97023
|
const schemaConnName = await getSchemaIsolationConnectionName(configPath);
|
|
96340
|
-
const indexPath =
|
|
97024
|
+
const indexPath = join16(resolveSchemaPath(storagePath, schemaConnName), "index.json");
|
|
96341
97025
|
const indexFile = Bun.file(indexPath);
|
|
96342
97026
|
let indexParsed = null;
|
|
96343
97027
|
if (await indexFile.exists()) {
|
|
@@ -96379,7 +97063,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
|
|
|
96379
97063
|
});
|
|
96380
97064
|
|
|
96381
97065
|
// src/commands/completion.ts
|
|
96382
|
-
import { join as
|
|
97066
|
+
import { join as join17 } from "path";
|
|
96383
97067
|
import { homedir as homedir3 } from "os";
|
|
96384
97068
|
function extractCommands(program2) {
|
|
96385
97069
|
return program2.commands.map((cmd) => ({
|
|
@@ -96481,11 +97165,11 @@ function getInstallPath2(shell) {
|
|
|
96481
97165
|
const home = homedir3();
|
|
96482
97166
|
switch (shell) {
|
|
96483
97167
|
case "bash":
|
|
96484
|
-
return
|
|
97168
|
+
return join17(home, ".bashrc");
|
|
96485
97169
|
case "zsh":
|
|
96486
|
-
return
|
|
97170
|
+
return join17(home, ".zshrc");
|
|
96487
97171
|
case "fish":
|
|
96488
|
-
return
|
|
97172
|
+
return join17(home, ".config", "fish", "completions", "dbcli.fish");
|
|
96489
97173
|
default:
|
|
96490
97174
|
throw new Error(`Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
|
|
96491
97175
|
}
|
|
@@ -96505,7 +97189,7 @@ var MARKER_END = "# <<< dbcli completion <<<";
|
|
|
96505
97189
|
async function installCompletion(shell, script) {
|
|
96506
97190
|
const targetPath = getInstallPath2(shell);
|
|
96507
97191
|
if (shell === "fish") {
|
|
96508
|
-
const dir =
|
|
97192
|
+
const dir = join17(homedir3(), ".config", "fish", "completions");
|
|
96509
97193
|
await Bun.$`mkdir -p ${dir}`.quiet();
|
|
96510
97194
|
await Bun.file(targetPath).write(script);
|
|
96511
97195
|
console.log(colors.success(`\u2713 Fish completion installed to ${targetPath}`));
|
|
@@ -96732,7 +97416,7 @@ ${t("upgrade.failed")}`));
|
|
|
96732
97416
|
|
|
96733
97417
|
// src/commands/shell.ts
|
|
96734
97418
|
import { createInterface as createInterface2 } from "readline";
|
|
96735
|
-
import { join as
|
|
97419
|
+
import { join as join18 } from "path";
|
|
96736
97420
|
import { homedir as homedir4 } from "os";
|
|
96737
97421
|
|
|
96738
97422
|
// src/core/repl/types.ts
|
|
@@ -97455,7 +98139,7 @@ class MongoShellAdapter {
|
|
|
97455
98139
|
}
|
|
97456
98140
|
|
|
97457
98141
|
// src/commands/shell.ts
|
|
97458
|
-
var HISTORY_PATH =
|
|
98142
|
+
var HISTORY_PATH = join18(homedir4(), ".dbcli_history");
|
|
97459
98143
|
var MONGO_COMPLETION_EAGER_THRESHOLD = 20;
|
|
97460
98144
|
async function populateMongoColumns(mongoAdapter, collectionNames, threshold = MONGO_COMPLETION_EAGER_THRESHOLD) {
|
|
97461
98145
|
const columnsByTable = {};
|
|
@@ -98426,7 +99110,7 @@ addExecOpts(migrateCommand.command("drop-enum <name>").description(t("migrate.dr
|
|
|
98426
99110
|
});
|
|
98427
99111
|
|
|
98428
99112
|
// src/commands/use.ts
|
|
98429
|
-
import { join as
|
|
99113
|
+
import { join as join19 } from "path";
|
|
98430
99114
|
async function switchDefault(configPath, name, config) {
|
|
98431
99115
|
if (!config.connections[name]) {
|
|
98432
99116
|
const available = Object.keys(config.connections).join(", ");
|
|
@@ -98449,7 +99133,7 @@ function listConnectionsForDisplay(config) {
|
|
|
98449
99133
|
}
|
|
98450
99134
|
async function ensureV2Config(configPath) {
|
|
98451
99135
|
const storagePath = await resolveConfigStoragePath(configPath);
|
|
98452
|
-
const configFile = Bun.file(
|
|
99136
|
+
const configFile = Bun.file(join19(storagePath, "config.json"));
|
|
98453
99137
|
const legacyFile = Bun.file(configPath);
|
|
98454
99138
|
if (!await configFile.exists() && !await legacyFile.exists()) {
|
|
98455
99139
|
throw new ConfigError(t("init.config_not_found"));
|
|
@@ -98511,7 +99195,7 @@ var useCommand = new Command("use").description("Switch or display the default d
|
|
|
98511
99195
|
});
|
|
98512
99196
|
|
|
98513
99197
|
// src/cli.ts
|
|
98514
|
-
import { join as
|
|
99198
|
+
import { join as join20 } from "path";
|
|
98515
99199
|
var _bgVersionCheckResult;
|
|
98516
99200
|
function shouldSkipBackgroundChecks() {
|
|
98517
99201
|
return process.env.DBCLI_NO_UPDATE_CHECK === "1" || process.env.DBCLI_NO_UPDATE_CHECK === "true" || false;
|
|
@@ -98540,7 +99224,7 @@ program2.hook("preAction", (thisCommand, actionCommand) => {
|
|
|
98540
99224
|
try {
|
|
98541
99225
|
let cache = null;
|
|
98542
99226
|
try {
|
|
98543
|
-
const cacheFile = Bun.file(
|
|
99227
|
+
const cacheFile = Bun.file(join20(configPath, "version-check.json"));
|
|
98544
99228
|
if (await cacheFile.exists()) {
|
|
98545
99229
|
cache = await cacheFile.json();
|
|
98546
99230
|
}
|
|
@@ -98618,14 +99302,8 @@ program2.command("export <sql>").description(t("export.description")).option("--
|
|
|
98618
99302
|
process.exit(1);
|
|
98619
99303
|
}
|
|
98620
99304
|
});
|
|
98621
|
-
|
|
98622
|
-
|
|
98623
|
-
await skillCommand(program2, options);
|
|
98624
|
-
} catch (error) {
|
|
98625
|
-
console.error(error.message);
|
|
98626
|
-
process.exit(1);
|
|
98627
|
-
}
|
|
98628
|
-
});
|
|
99305
|
+
var skillCmd = registerSkillCommand(program2);
|
|
99306
|
+
registerSkillTasksCommand(skillCmd);
|
|
98629
99307
|
program2.addCommand(blacklistCommand);
|
|
98630
99308
|
program2.addCommand(checkCommand);
|
|
98631
99309
|
program2.addCommand(diffCommand);
|