@qzhuli/qzhuli-cli 0.2.0 → 0.3.0-rc.1
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 +21 -0
- package/README.md +2 -1
- package/dist/cmd.js +1351 -210
- package/package.json +6 -1
- package/scripts/postinstall.mjs +226 -0
- package/skills/qzhuli-cli/SKILL.md +56 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qzhuli/qzhuli-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-rc.1",
|
|
4
4
|
"description": "CLI tool for Q助理 (QZhuli)",
|
|
5
5
|
"main": "dist/cmd.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
+
"scripts/postinstall.mjs",
|
|
11
12
|
"skills",
|
|
12
13
|
"README.md",
|
|
13
14
|
"CONTRIBUTING.md"
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"test": "vitest run",
|
|
27
28
|
"test:watch": "vitest",
|
|
28
29
|
"prepublishOnly": "pnpm build",
|
|
30
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
29
31
|
"qz": "node dist/cmd.js",
|
|
30
32
|
"install:local:test": "cross-env QZ_BUILD_ENV=test pnpm build && npm install -g .",
|
|
31
33
|
"install:local:prod": "pnpm build && npm install -g .",
|
|
@@ -36,11 +38,13 @@
|
|
|
36
38
|
"qzhuli"
|
|
37
39
|
],
|
|
38
40
|
"author": "shootdev <npmjs@aisheshou.com>",
|
|
41
|
+
"license": "MIT",
|
|
39
42
|
"packageManager": "pnpm@10.33.0",
|
|
40
43
|
"engines": {
|
|
41
44
|
"node": ">=18.0.0"
|
|
42
45
|
},
|
|
43
46
|
"dependencies": {
|
|
47
|
+
"better-sqlite3": "^12.10.0",
|
|
44
48
|
"commander": "^14.0.3",
|
|
45
49
|
"protobufjs": "^8.0.1",
|
|
46
50
|
"qrcode-terminal": "^0.12.0",
|
|
@@ -48,6 +52,7 @@
|
|
|
48
52
|
},
|
|
49
53
|
"devDependencies": {
|
|
50
54
|
"@biomejs/biome": "^2.4.12",
|
|
55
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
51
56
|
"@types/node": "^25.6.0",
|
|
52
57
|
"@types/qrcode-terminal": "^0.12.2",
|
|
53
58
|
"@types/ws": "^8.18.1",
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postinstall script for @qzhuli/qzhuli-cli
|
|
3
|
+
*
|
|
4
|
+
* Auto-installs the qzhuli-cli skill to all detected AI agents.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Detects installed agents by checking home/project config dirs
|
|
8
|
+
* - Copies the skill to each agent's skill directory (global + project)
|
|
9
|
+
* - Skips in CI environments (CI, CONTINUOUS_INTEGRATION env vars)
|
|
10
|
+
* - Skips if AX_SKIP_SKILL_INSTALL=1
|
|
11
|
+
* - Silent failure — never breaks npm install
|
|
12
|
+
* - Cross-platform (macOS, Linux, Windows)
|
|
13
|
+
* - Content-dedup: skips if identical file already exists
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
existsSync,
|
|
18
|
+
readdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
copyFileSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
rmSync,
|
|
23
|
+
statSync,
|
|
24
|
+
} from 'node:fs';
|
|
25
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { homedir } from 'node:os';
|
|
28
|
+
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = dirname(__filename);
|
|
31
|
+
|
|
32
|
+
// ── Agent path mapping ─────────────────────────────────────────────
|
|
33
|
+
// Source: https://www.agentskills.in/docs/getting-started
|
|
34
|
+
const AGENT_PATHS = [
|
|
35
|
+
{ name: 'claude-code', global: '~/.claude/skills', project: '.claude/skills' },
|
|
36
|
+
{ name: 'cursor', global: '~/.cursor/skills', project: '.cursor/skills' },
|
|
37
|
+
{ name: 'codex', global: '~/.codex/skills', project: '.codex/skills' },
|
|
38
|
+
{ name: 'copilot', global: '~/.copilot/skills', project: '.github/skills' },
|
|
39
|
+
{ name: 'windsurf', global: '~/.codeium/windsurf/skills', project: '.windsurf/skills' },
|
|
40
|
+
{ name: 'openclaw', global: '~/.openclaw/skills', project: 'skills' },
|
|
41
|
+
{ name: 'opencode', global: '~/.config/opencode/skill',project: '.opencode/skill' },
|
|
42
|
+
{ name: 'cline', global: '~/.cline/skills', project: '.cline/skills' },
|
|
43
|
+
{ name: 'gemini-cli', global: '~/.gemini/skills', project: '.gemini/skills' },
|
|
44
|
+
{ name: 'amp', global: '~/.config/agents/skills', project: '.agents/skills' },
|
|
45
|
+
{ name: 'roo', global: '~/.roo/skills', project: '.roo/skills' },
|
|
46
|
+
{ name: 'goose', global: '~/.config/goose/skills', project: '.goose/skills' },
|
|
47
|
+
{ name: 'kiro', global: '~/.kiro/skills', project: '.kiro/skills' },
|
|
48
|
+
{ name: 'qwen-code', global: '~/.qwen/skills', project: '.qwen/skills' },
|
|
49
|
+
{ name: 'qoder', global: '~/.qoder/skills', project: '.qoder/skills' },
|
|
50
|
+
{ name: 'trae', global: '~/.trae/skills', project: '.trae/skills' },
|
|
51
|
+
{ name: 'augment', global: '~/.augment/skills', project: '.augment/skills' },
|
|
52
|
+
{ name: 'zed', global: '~/.config/zed/skills', project: '.zed/skills' },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const SKILL_NAME = 'qzhuli-cli';
|
|
56
|
+
|
|
57
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function resolveHome(p) {
|
|
60
|
+
if (p.startsWith('~/')) return join(homedir(), p.slice(2));
|
|
61
|
+
return p;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function dirExists(p) {
|
|
65
|
+
try {
|
|
66
|
+
return statSync(resolveHome(p)).isDirectory();
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function shouldSkip() {
|
|
73
|
+
return (
|
|
74
|
+
process.env.CI === 'true' ||
|
|
75
|
+
process.env.CONTINUOUS_INTEGRATION === 'true' ||
|
|
76
|
+
process.env.AX_SKIP_SKILL_INSTALL === '1'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isInteractive() {
|
|
81
|
+
return process.stdout.isTTY;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Walk up from startDir to find the project root (where node_modules/@qzhuli/qzhuli-cli lives).
|
|
86
|
+
* Returns null for global installs, or false for dev/source installs.
|
|
87
|
+
*
|
|
88
|
+
* Detection logic:
|
|
89
|
+
* - If __dirname contains node_modules/@qzhuli/qzhuli-cli → it's an npm install
|
|
90
|
+
* - If path matches global node_modules prefix → global install → return null
|
|
91
|
+
* - Otherwise → local project install → return project root
|
|
92
|
+
* - Otherwise → dev/source install → return false (skip skill install)
|
|
93
|
+
*/
|
|
94
|
+
function findProjectRoot() {
|
|
95
|
+
const scriptDir = resolve(__dirname, '..');
|
|
96
|
+
const normalized = scriptDir.replace(/\\/g, '/');
|
|
97
|
+
const nmIdx = normalized.lastIndexOf('node_modules/@qzhuli/qzhuli-cli');
|
|
98
|
+
|
|
99
|
+
if (nmIdx === -1) {
|
|
100
|
+
// Not installed via npm — this is a dev/source checkout, skip
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// It's in node_modules. Determine if global or project-level.
|
|
105
|
+
const projectRoot = normalized.slice(0, nmIdx).replace(/\/$/, '');
|
|
106
|
+
|
|
107
|
+
// Global installs end up in paths like:
|
|
108
|
+
// /usr/lib/node_modules/@qzhuli/qzhuli-cli
|
|
109
|
+
// C:/Users/X/AppData/Roaming/npm/node_modules/@qzhuli/qzhuli-cli
|
|
110
|
+
// /Users/X/.nvm/versions/node/v22.X/lib/node_modules/@qzhuli/qzhuli-cli
|
|
111
|
+
// After slicing off node_modules/..., the projectRoot is either empty
|
|
112
|
+
// or just a system prefix. We check if there's a package.json at projectRoot.
|
|
113
|
+
if (!projectRoot) return null; // no prefix → treat as global
|
|
114
|
+
|
|
115
|
+
const pkgAtRoot = join(projectRoot.replace(/\//g, sep), 'package.json');
|
|
116
|
+
if (!existsSync(pkgAtRoot)) return null;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const pkg = JSON.parse(readFileSync(pkgAtRoot, 'utf8'));
|
|
120
|
+
// If the project root's package.json is our own package, it's global
|
|
121
|
+
if (pkg.name === '@qzhuli/qzhuli-cli') return null;
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return projectRoot.replace(/\//g, sep);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Detect which agents are installed by checking if their config dirs exist globally.
|
|
131
|
+
*/
|
|
132
|
+
function detectAgents() {
|
|
133
|
+
return AGENT_PATHS
|
|
134
|
+
.filter(agent => dirExists(agent.global))
|
|
135
|
+
.map(agent => agent.name);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Recursively copy a directory.
|
|
140
|
+
*/
|
|
141
|
+
function copyDir(src, dest) {
|
|
142
|
+
mkdirSync(dest, { recursive: true });
|
|
143
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
const srcPath = join(src, entry.name);
|
|
146
|
+
const destPath = join(dest, entry.name);
|
|
147
|
+
if (entry.isDirectory()) {
|
|
148
|
+
copyDir(srcPath, destPath);
|
|
149
|
+
} else {
|
|
150
|
+
copyFileSync(srcPath, destPath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if a skill is already installed with identical content.
|
|
157
|
+
*/
|
|
158
|
+
function isInstalled(skillTarget, sourceDir) {
|
|
159
|
+
if (!existsSync(skillTarget)) return false;
|
|
160
|
+
try {
|
|
161
|
+
const srcFiles = readdirSync(sourceDir);
|
|
162
|
+
const destFiles = readdirSync(skillTarget);
|
|
163
|
+
if (srcFiles.length !== destFiles.length) return false;
|
|
164
|
+
// Compare SKILL.md content (primary file)
|
|
165
|
+
const srcMd = join(sourceDir, 'SKILL.md');
|
|
166
|
+
const destMd = join(skillTarget, 'SKILL.md');
|
|
167
|
+
if (!existsSync(destMd)) return false;
|
|
168
|
+
return readFileSync(srcMd, 'utf8') === readFileSync(destMd, 'utf8');
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Main ───────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
function install() {
|
|
177
|
+
if (shouldSkip()) { console.log('SKIP: CI or env'); return; }
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const sourceDir = join(__dirname, '..', 'skills', SKILL_NAME);
|
|
181
|
+
if (!existsSync(sourceDir)) return;
|
|
182
|
+
|
|
183
|
+
const projectRoot = findProjectRoot();
|
|
184
|
+
// false = dev/source install, skip
|
|
185
|
+
if (projectRoot === false) { console.log('SKIP: dev install'); return; }
|
|
186
|
+
const isGlobal = projectRoot === null;
|
|
187
|
+
const cwd = projectRoot || process.cwd();
|
|
188
|
+
|
|
189
|
+
// Detect installed agents
|
|
190
|
+
const detectedAgents = detectAgents();
|
|
191
|
+
// Always include claude-code as fallback if nothing detected
|
|
192
|
+
const targets = detectedAgents.length > 0 ? detectedAgents : ['claude-code'];
|
|
193
|
+
|
|
194
|
+
const results = [];
|
|
195
|
+
|
|
196
|
+
for (const agentName of targets) {
|
|
197
|
+
const agentConfig = AGENT_PATHS.find(a => a.name === agentName);
|
|
198
|
+
if (!agentConfig) continue;
|
|
199
|
+
|
|
200
|
+
const targetPath = isGlobal
|
|
201
|
+
? resolveHome(agentConfig.global)
|
|
202
|
+
: join(cwd, agentConfig.project);
|
|
203
|
+
|
|
204
|
+
const skillTarget = join(targetPath, SKILL_NAME);
|
|
205
|
+
|
|
206
|
+
// Skip if already installed with identical content
|
|
207
|
+
if (isInstalled(skillTarget, sourceDir)) continue;
|
|
208
|
+
|
|
209
|
+
copyDir(sourceDir, skillTarget);
|
|
210
|
+
results.push(agentName);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Report (only in interactive terminals)
|
|
214
|
+
if (results.length > 0 && isInteractive()) {
|
|
215
|
+
const noun = results.length === 1 ? 'agent' : 'agents';
|
|
216
|
+
console.log(
|
|
217
|
+
`✓ qzhuli-cli skill installed to ${results.length} ${noun}: ${results.join(', ')}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
// Silent failure — never break npm install
|
|
222
|
+
// Users can follow AGENT-SETUP.md for manual installation
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
install();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qzhuli-cli
|
|
3
|
-
description: Use when operating the QZhuli CLI with qz, including login, auth status, config, friends, relations, users, conversations, messages, JSON filtering, dry-run, command help, and interpreting test-environment banners or config files.
|
|
4
|
-
version:
|
|
3
|
+
description: Use when operating the QZhuli CLI with qz, including login, auth status, config, friends, relations, users, conversations, messages, cache management, JSON filtering, dry-run, command help, and interpreting test-environment banners or config files.
|
|
4
|
+
version: 2
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# QZhuli CLI
|
|
@@ -47,7 +47,7 @@ Use `--jq` for simple dot-path filtering:
|
|
|
47
47
|
```bash
|
|
48
48
|
qz --jq ".data.uid" auth status
|
|
49
49
|
qz --jq ".data.links" friend list
|
|
50
|
-
qz --jq ".data" conversation list --limit 5
|
|
50
|
+
qz --jq ".data" conversation list --limit 5 --offset 0
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
`--jq` is not full jq. Prefer simple paths such as `.data`, `.data.uid`, `.data.links`.
|
|
@@ -55,6 +55,26 @@ qz --jq ".data" conversation list --limit 5
|
|
|
55
55
|
Use `--dry-run` when you need to avoid side effects. It is wired through output handling, HTTP API calls, IM WebSocket
|
|
56
56
|
actions, auth login/logout, and preference writes.
|
|
57
57
|
|
|
58
|
+
## Cache Architecture
|
|
59
|
+
|
|
60
|
+
All read operations use a **Repository Pattern** with SQLite-backed caching:
|
|
61
|
+
|
|
62
|
+
- **Local-first**: reads hit a local SQLite database (`~/.qzhuli-cli/cache.db`) before the remote API
|
|
63
|
+
- **TTL-based expiration**: contacts/relations expire after 5 minutes, user profiles after 1 hour
|
|
64
|
+
- **Auto-sync on miss**: cache miss triggers a remote fetch + automatic cache write-back
|
|
65
|
+
- **Manual sync**: use `qz cache sync` to pre-fetch all data
|
|
66
|
+
|
|
67
|
+
Cache database tables:
|
|
68
|
+
|
|
69
|
+
- `conversation_profiles` — conversation metadata with user_ids index
|
|
70
|
+
- `contacts_cache` — full contacts list per owner UID
|
|
71
|
+
- `user_profiles` — individual user profiles
|
|
72
|
+
- `relations_cache` — friend relations (remark, type)
|
|
73
|
+
- `messages_cache` — message history per conversation
|
|
74
|
+
|
|
75
|
+
Write operations (send message, create conversation, update relation) bypass the cache and write directly to the API,
|
|
76
|
+
then invalidate relevant cache entries.
|
|
77
|
+
|
|
58
78
|
## Command Map
|
|
59
79
|
|
|
60
80
|
| Goal | Command |
|
|
@@ -73,10 +93,15 @@ actions, auth login/logout, and preference writes.
|
|
|
73
93
|
| Resolve profile (remark) | `qz friend profile <query> --remark` |
|
|
74
94
|
| Read relation | `qz relation get <uid>` |
|
|
75
95
|
| Update relation | `qz relation set <uid> [-r, --remark <name>] [-t, --type <type>]` |
|
|
76
|
-
| List conversations | `qz conversation list [--limit <n>]`
|
|
96
|
+
| List conversations | `qz conversation list [--limit <n>] [--offset <n>]` |
|
|
97
|
+
| Get conversation profile | `qz conversation profile <conversation-id> [--type <n>]` |
|
|
77
98
|
| Create conversation | `qz conversation create <uid> --agent-id <id>` |
|
|
99
|
+
| Search user conversations | `qz conversation search <query> [--uid]` |
|
|
78
100
|
| Send message | `qz message send <conversation-id> <target-cid> <content>` |
|
|
79
101
|
| Read message history | `qz message history <conversation-id> [--from <id>] [--direction newer\|older] [--limit <n>]` |
|
|
102
|
+
| **Sync cache** | `qz cache sync` |
|
|
103
|
+
| **Cache status** | `qz cache status` |
|
|
104
|
+
| **Clear cache** | `qz cache clear [--table <name>]` |
|
|
80
105
|
|
|
81
106
|
Relation type values are `0=stranger`, `1=friend`, `2=family`, `3=colleague`.
|
|
82
107
|
|
|
@@ -123,6 +148,23 @@ Do not run `relation set` without `--remark` or `--type`; the CLI returns `INVAL
|
|
|
123
148
|
qz user add 10000
|
|
124
149
|
```
|
|
125
150
|
|
|
151
|
+
### Find All Conversations with a User
|
|
152
|
+
|
|
153
|
+
Search by Q助号 (default):
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
qz conversation search 10000
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Search by UID:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
qz conversation search 79345121120f6e5288238749 --uid
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The response includes `id` (Q助号, number), `uid` (internal user ID, string), and `conversations` with full profile
|
|
166
|
+
data. Each conversation entry contains `conversationId`, `isGroup`, `users` (with camelCase fields), and `visitors`.
|
|
167
|
+
|
|
126
168
|
### Send a Message
|
|
127
169
|
|
|
128
170
|
1. Confirm auth:
|
|
@@ -154,6 +196,14 @@ qz message history <conversation-id> --from <message-id> --direction older --lim
|
|
|
154
196
|
qz message history <conversation-id> --from <message-id> --direction newer --limit 20
|
|
155
197
|
```
|
|
156
198
|
|
|
199
|
+
### Pre-Sync Cache for Offline Speed
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
qz cache sync # fetch all data into local SQLite
|
|
203
|
+
qz cache status # verify record counts and sync time
|
|
204
|
+
qz conversation search 10000 # now instant from cache
|
|
205
|
+
```
|
|
206
|
+
|
|
157
207
|
## Troubleshooting
|
|
158
208
|
|
|
159
209
|
| Symptom | Action |
|
|
@@ -164,3 +214,5 @@ qz message history <conversation-id> --from <message-id> --direction newer --lim
|
|
|
164
214
|
| Too much JSON | Use `--jq ".data"` or another simple dot path. |
|
|
165
215
|
| Need a no-op preview | Use `--dry-run`. |
|
|
166
216
|
| Message send cid error | Re-check `auth status` and choose `target-cid` from `conversation list`. |
|
|
217
|
+
| Slow queries | Run `qz cache sync` first, then retry — results come from local SQLite. |
|
|
218
|
+
| Cache corrupted | Run `qz cache clear` to reset, then retry (falls back to API). |
|