@qzhuli/qzhuli-cli 0.3.0-rc.1 → 0.4.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/README.md +38 -0
- package/dist/cmd.js +53 -6
- package/package.json +5 -2
- package/scripts/install-skill.mjs +201 -0
- package/scripts/postinstall.mjs +40 -116
- package/scripts/skill-agent.mjs +149 -0
- package/skills/qzhuli-cli/SKILL.md +124 -112
package/README.md
CHANGED
|
@@ -23,6 +23,44 @@ npm install @qzhuli/qzhuli-cli
|
|
|
23
23
|
npx qz --version
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
## AI Agent Skill
|
|
27
|
+
|
|
28
|
+
The CLI ships with a `SKILL.md` that teaches AI agents how to use it. During `npm install -g`, the skill is automatically installed to your agent's skill directory.
|
|
29
|
+
|
|
30
|
+
### Supported agents (18)
|
|
31
|
+
|
|
32
|
+
Claude Code, Cursor, Codex, Copilot, Windsurf, OpenClaw, OpenCode, Cline, Gemini CLI, Amp, Roo, Goose, Kiro, Qwen Code, Qoder, Trae, Augment, Zed.
|
|
33
|
+
|
|
34
|
+
### Manual skill install
|
|
35
|
+
|
|
36
|
+
If the automatic install didn't work (e.g. local `npm install` without `-g`):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Install to all detected agents (global)
|
|
40
|
+
pnpm install:skill
|
|
41
|
+
|
|
42
|
+
# Install to current project's skill directory
|
|
43
|
+
pnpm install:skill --project
|
|
44
|
+
|
|
45
|
+
# Install to a specific agent only
|
|
46
|
+
pnpm install:skill --agent claude-code
|
|
47
|
+
|
|
48
|
+
# Update existing installs if skill version changed
|
|
49
|
+
pnpm install:skill --update
|
|
50
|
+
|
|
51
|
+
# From npm package directly
|
|
52
|
+
npx @qzhuli/qzhuli-cli scripts/install-skill.mjs
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Updating
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm update -g @qzhuli/qzhuli-cli
|
|
59
|
+
pnpm install:skill --update
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The postinstall script auto-installs the skill on `npm install -g`. Use `install:skill --update` to update skills without reinstalling the whole package.
|
|
63
|
+
|
|
26
64
|
## Commands
|
|
27
65
|
|
|
28
66
|
```bash
|
package/dist/cmd.js
CHANGED
|
@@ -14183,14 +14183,41 @@ var SqliteConversationRepository = class {
|
|
|
14183
14183
|
"INSERT INTO conversation_profiles (conversation_id, data, user_ids, cached_at) VALUES (?, ?, ?, ?) ON CONFLICT(conversation_id) DO UPDATE SET data=?, user_ids=?, cached_at=?"
|
|
14184
14184
|
).run(conversationId, data, user_ids, cached_at, data, user_ids, cached_at);
|
|
14185
14185
|
}
|
|
14186
|
+
// MARK: Conversation Index (for incremental sync)
|
|
14187
|
+
upsertIndex(conv) {
|
|
14188
|
+
const members = JSON.stringify(conv.cids);
|
|
14189
|
+
const nicks = conv.nicks ? JSON.stringify(conv.nicks) : null;
|
|
14190
|
+
const im_version = conv.version;
|
|
14191
|
+
const cached_at = Date.now();
|
|
14192
|
+
this.db.prepare(
|
|
14193
|
+
"INSERT INTO conversations_index (conversation_id, im_version, members, nicks, cached_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(conversation_id) DO UPDATE SET im_version=?, members=?, nicks=?, cached_at=?"
|
|
14194
|
+
).run(conv.id, im_version, members, nicks, cached_at, im_version, members, nicks, cached_at);
|
|
14195
|
+
}
|
|
14196
|
+
getIndexById(conversationId) {
|
|
14197
|
+
return this.db.prepare(
|
|
14198
|
+
"SELECT conversation_id, im_version, members, nicks, cached_at FROM conversations_index WHERE conversation_id = ?"
|
|
14199
|
+
).get(conversationId);
|
|
14200
|
+
}
|
|
14201
|
+
getAllIds() {
|
|
14202
|
+
const rows = this.db.prepare("SELECT conversation_id FROM conversations_index").all();
|
|
14203
|
+
return rows.map((r) => r.conversation_id);
|
|
14204
|
+
}
|
|
14205
|
+
deleteStale(conversationIds) {
|
|
14206
|
+
if (conversationIds.length === 0) return;
|
|
14207
|
+
const placeholders = conversationIds.map(() => "?").join(",");
|
|
14208
|
+
this.db.prepare(`DELETE FROM conversation_profiles WHERE conversation_id NOT IN (${placeholders})`).run(...conversationIds);
|
|
14209
|
+
this.db.prepare(`DELETE FROM conversations_index WHERE conversation_id NOT IN (${placeholders})`).run(...conversationIds);
|
|
14210
|
+
}
|
|
14186
14211
|
invalidate(conversationId) {
|
|
14187
14212
|
this.db.prepare("DELETE FROM conversation_profiles WHERE conversation_id = ?").run(conversationId);
|
|
14213
|
+
this.db.prepare("DELETE FROM conversations_index WHERE conversation_id = ?").run(conversationId);
|
|
14188
14214
|
}
|
|
14189
14215
|
sync() {
|
|
14190
14216
|
return Promise.resolve();
|
|
14191
14217
|
}
|
|
14192
14218
|
clear() {
|
|
14193
14219
|
this.db.exec("DELETE FROM conversation_profiles");
|
|
14220
|
+
this.db.exec("DELETE FROM conversations_index");
|
|
14194
14221
|
this.db.exec("DELETE FROM cache_metadata WHERE key = 'last_sync_conversations'");
|
|
14195
14222
|
}
|
|
14196
14223
|
getStatus() {
|
|
@@ -14203,6 +14230,7 @@ var SqliteConversationRepository = class {
|
|
|
14203
14230
|
}
|
|
14204
14231
|
};
|
|
14205
14232
|
var CachedConversationRepository = class {
|
|
14233
|
+
// 30 minutes
|
|
14206
14234
|
constructor(local, remote) {
|
|
14207
14235
|
this.local = local;
|
|
14208
14236
|
this.remote = remote;
|
|
@@ -14210,6 +14238,7 @@ var CachedConversationRepository = class {
|
|
|
14210
14238
|
local;
|
|
14211
14239
|
remote;
|
|
14212
14240
|
syncInProgress = false;
|
|
14241
|
+
TTL_MS = 30 * 60 * 1e3;
|
|
14213
14242
|
async queryAll(options3) {
|
|
14214
14243
|
return this.remote.queryAll(options3);
|
|
14215
14244
|
}
|
|
@@ -14232,7 +14261,7 @@ var CachedConversationRepository = class {
|
|
|
14232
14261
|
async ensureSynced() {
|
|
14233
14262
|
if (this.syncInProgress) return;
|
|
14234
14263
|
const status = await this.local.getStatus();
|
|
14235
|
-
const isFresh = status.lastSyncAt !== null && Date.now() - status.lastSyncAt <
|
|
14264
|
+
const isFresh = status.lastSyncAt !== null && Date.now() - status.lastSyncAt < this.TTL_MS;
|
|
14236
14265
|
if (isFresh) return;
|
|
14237
14266
|
this.syncInProgress = true;
|
|
14238
14267
|
try {
|
|
@@ -14244,12 +14273,22 @@ var CachedConversationRepository = class {
|
|
|
14244
14273
|
async sync() {
|
|
14245
14274
|
const convResult = await this.remote.queryAll({ limit: 0, offset: 0 });
|
|
14246
14275
|
if (!convResult.ok || !convResult.data) return;
|
|
14247
|
-
|
|
14248
|
-
|
|
14249
|
-
|
|
14250
|
-
|
|
14276
|
+
const remoteConversations = convResult.data;
|
|
14277
|
+
const remoteIds = new Set(remoteConversations.map((c) => c.id));
|
|
14278
|
+
const cachedIds = new Set(this.local.getAllIds());
|
|
14279
|
+
const newIds = remoteConversations.filter((c) => !cachedIds.has(c.id)).map((c) => c.id);
|
|
14280
|
+
for (const conv of remoteConversations) {
|
|
14281
|
+
if (newIds.includes(conv.id)) {
|
|
14282
|
+
const profileResult = await this.remote.getProfile(conv.id);
|
|
14283
|
+
if (profileResult.ok) {
|
|
14284
|
+
this.local.upsertProfile(conv.id, profileResult.data);
|
|
14285
|
+
}
|
|
14251
14286
|
}
|
|
14252
14287
|
}
|
|
14288
|
+
for (const conv of remoteConversations) {
|
|
14289
|
+
this.local.upsertIndex(conv);
|
|
14290
|
+
}
|
|
14291
|
+
this.local.deleteStale([...remoteIds]);
|
|
14253
14292
|
this.local.db.prepare("INSERT OR REPLACE INTO cache_metadata (key, value) VALUES (?, ?)").run("last_sync_conversations", String(Date.now()));
|
|
14254
14293
|
}
|
|
14255
14294
|
invalidate(conversationId) {
|
|
@@ -14495,6 +14534,14 @@ CREATE TABLE IF NOT EXISTS messages_cache (
|
|
|
14495
14534
|
);
|
|
14496
14535
|
CREATE INDEX IF NOT EXISTS idx_msg_conv ON messages_cache(conversation_id);
|
|
14497
14536
|
|
|
14537
|
+
CREATE TABLE IF NOT EXISTS conversations_index (
|
|
14538
|
+
conversation_id TEXT PRIMARY KEY,
|
|
14539
|
+
im_version INTEGER NOT NULL,
|
|
14540
|
+
members TEXT NOT NULL,
|
|
14541
|
+
nicks TEXT,
|
|
14542
|
+
cached_at INTEGER NOT NULL
|
|
14543
|
+
);
|
|
14544
|
+
|
|
14498
14545
|
CREATE TABLE IF NOT EXISTS cache_metadata (
|
|
14499
14546
|
key TEXT PRIMARY KEY,
|
|
14500
14547
|
value TEXT
|
|
@@ -14760,7 +14807,7 @@ async function main() {
|
|
|
14760
14807
|
${t("cli.banner")}` : t("cli.banner");
|
|
14761
14808
|
program.addHelpText("beforeAll", `${banner}
|
|
14762
14809
|
`);
|
|
14763
|
-
program.name("qz").version(`v${"0.
|
|
14810
|
+
program.name("qz").version(`v${"0.4.0-rc.1"}`, "-v, --version", t("options.version")).helpOption("-h, --help", t("options.help")).option("-q, --jq <expr>", t("options.jq")).option("--dry-run", t("options.dryRun"));
|
|
14764
14811
|
program.usage("<command> [subcommand] [options]");
|
|
14765
14812
|
program.hook("preAction", () => {
|
|
14766
14813
|
const opts = program.opts();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qzhuli/qzhuli-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-rc.1",
|
|
4
4
|
"description": "CLI tool for Q助理 (QZhuli)",
|
|
5
5
|
"main": "dist/cmd.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
11
|
"scripts/postinstall.mjs",
|
|
12
|
+
"scripts/install-skill.mjs",
|
|
13
|
+
"scripts/skill-agent.mjs",
|
|
12
14
|
"skills",
|
|
13
15
|
"README.md",
|
|
14
16
|
"CONTRIBUTING.md"
|
|
@@ -31,7 +33,8 @@
|
|
|
31
33
|
"qz": "node dist/cmd.js",
|
|
32
34
|
"install:local:test": "cross-env QZ_BUILD_ENV=test pnpm build && npm install -g .",
|
|
33
35
|
"install:local:prod": "pnpm build && npm install -g .",
|
|
34
|
-
"uninstall:local": "npm uninstall -g @qzhuli/qzhuli-cli"
|
|
36
|
+
"uninstall:local": "npm uninstall -g @qzhuli/qzhuli-cli",
|
|
37
|
+
"install:skill": "node scripts/install-skill.mjs"
|
|
35
38
|
},
|
|
36
39
|
"keywords": [
|
|
37
40
|
"cli",
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone skill installer for @qzhuli/qzhuli-cli
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* node scripts/install-skill.mjs # install to all detected agents (global)
|
|
6
|
+
* node scripts/install-skill.mjs --project # install to current project's skills dir
|
|
7
|
+
* node scripts/install-skill.mjs --agent claude-code # install to specific agent only
|
|
8
|
+
* node scripts/install-skill.mjs --help # show help
|
|
9
|
+
*
|
|
10
|
+
* Works with:
|
|
11
|
+
* - Published npm package: `npx @qzhuli/qzhuli-cli scripts/install-skill.mjs`
|
|
12
|
+
* - Dev/source checkout: `node scripts/install-skill.mjs` (from repo root)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { AGENT_PATHS, SKILL_NAME, resolveHome, copyDir, isInstalled, detectAgents, writeManifest, needsUpdate, readManifest } from './skill-agent.mjs';
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = dirname(__filename);
|
|
22
|
+
|
|
23
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function findSourceDir() {
|
|
26
|
+
// Case 1: running from repo root (dev checkout)
|
|
27
|
+
const repoSkills = resolve(__dirname, '..', 'skills', SKILL_NAME);
|
|
28
|
+
if (existsSync(join(repoSkills, 'SKILL.md'))) return repoSkills;
|
|
29
|
+
|
|
30
|
+
// Case 2: running from npm package (node_modules)
|
|
31
|
+
const pkgSkills = resolve(__dirname, '..', '..', 'skills', SKILL_NAME);
|
|
32
|
+
if (existsSync(join(pkgSkills, 'SKILL.md'))) return pkgSkills;
|
|
33
|
+
|
|
34
|
+
// Case 3: skills dir is at package root (flattened install)
|
|
35
|
+
const flatSkills = resolve(__dirname, '..', 'skills');
|
|
36
|
+
if (existsSync(join(flatSkills, SKILL_NAME, 'SKILL.md'))) return join(flatSkills, SKILL_NAME);
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getPkgVersion() {
|
|
42
|
+
try {
|
|
43
|
+
const pkgJson = resolve(__dirname, '..', 'package.json');
|
|
44
|
+
const pkg = JSON.parse(readFileSync(pkgJson, 'utf8'));
|
|
45
|
+
return pkg.version || 'unknown';
|
|
46
|
+
} catch {
|
|
47
|
+
return 'unknown';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── CLI parsing ────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function parseArgs(argv) {
|
|
54
|
+
const args = { project: false, agent: null, help: false, update: false };
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
switch (argv[i]) {
|
|
57
|
+
case '--project':
|
|
58
|
+
args.project = true;
|
|
59
|
+
break;
|
|
60
|
+
case '--agent':
|
|
61
|
+
args.agent = argv[++i];
|
|
62
|
+
break;
|
|
63
|
+
case '--update':
|
|
64
|
+
args.update = true;
|
|
65
|
+
break;
|
|
66
|
+
case '--help':
|
|
67
|
+
case '-h':
|
|
68
|
+
args.help = true;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return args;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function printHelp() {
|
|
76
|
+
console.log(`
|
|
77
|
+
Usage: node install-skill.mjs [options]
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
--project Install to current project's .claude/skills (or equivalent)
|
|
81
|
+
--agent <name> Install to a specific agent only (e.g. claude-code, cursor)
|
|
82
|
+
--update Check for updates and reinstall if version changed
|
|
83
|
+
--help, -h Show this help message
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
node scripts/install-skill.mjs # global, all agents
|
|
87
|
+
node scripts/install-skill.mjs --project # project-level, all agents
|
|
88
|
+
node scripts/install-skill.mjs --agent claude-code # global, claude-code only
|
|
89
|
+
node scripts/install-skill.mjs --update # update all installed agents
|
|
90
|
+
npx @qzhuli/qzhuli-cli scripts/install-skill.mjs # from published npm
|
|
91
|
+
`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Main ───────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function main() {
|
|
97
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
98
|
+
if (opts.help) { printHelp(); process.exit(0); }
|
|
99
|
+
|
|
100
|
+
const sourceDir = findSourceDir();
|
|
101
|
+
if (!sourceDir) {
|
|
102
|
+
console.error('Error: Cannot find skill source directory.');
|
|
103
|
+
console.error('Run this script from the repo root, or install the npm package first.');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const pkgVersion = getPkgVersion();
|
|
108
|
+
|
|
109
|
+
// Resolve target agents
|
|
110
|
+
let targets = [];
|
|
111
|
+
if (opts.update) {
|
|
112
|
+
// --update: check agents that already have the skill installed
|
|
113
|
+
const candidates = opts.agent
|
|
114
|
+
? [AGENT_PATHS.find(a => a.name === opts.agent)]
|
|
115
|
+
: AGENT_PATHS;
|
|
116
|
+
|
|
117
|
+
if (opts.agent && !candidates[0]) {
|
|
118
|
+
console.error(`Error: Unknown agent "${opts.agent}".`);
|
|
119
|
+
console.error('Supported agents:', AGENT_PATHS.map(a => a.name).join(', '));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const cfg of candidates) {
|
|
124
|
+
if (!cfg) continue;
|
|
125
|
+
const globalPath = resolveHome(cfg.global);
|
|
126
|
+
const skillTarget = join(globalPath, SKILL_NAME);
|
|
127
|
+
if (existsSync(skillTarget) && needsUpdate(skillTarget, sourceDir)) {
|
|
128
|
+
targets.push({ cfg, isProject: false });
|
|
129
|
+
}
|
|
130
|
+
if (opts.project) {
|
|
131
|
+
const projectPath = join(process.cwd(), cfg.project);
|
|
132
|
+
const projectTarget = join(projectPath, SKILL_NAME);
|
|
133
|
+
if (existsSync(projectTarget) && needsUpdate(projectTarget, sourceDir)) {
|
|
134
|
+
targets.push({ cfg, isProject: true });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (targets.length === 0) {
|
|
139
|
+
console.log('All skills are up to date.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
} else if (opts.agent) {
|
|
143
|
+
const cfg = AGENT_PATHS.find(a => a.name === opts.agent);
|
|
144
|
+
if (!cfg) {
|
|
145
|
+
console.error(`Error: Unknown agent "${opts.agent}".`);
|
|
146
|
+
console.error('Supported agents:', AGENT_PATHS.map(a => a.name).join(', '));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
targets.push({ cfg, isProject: opts.project });
|
|
150
|
+
} else {
|
|
151
|
+
const detected = detectAgents();
|
|
152
|
+
const fallback = AGENT_PATHS.find(a => a.name === 'claude-code');
|
|
153
|
+
if (detected.length === 0) {
|
|
154
|
+
targets.push({ cfg: fallback, isProject: false });
|
|
155
|
+
} else {
|
|
156
|
+
for (const name of detected) {
|
|
157
|
+
const cfg = AGENT_PATHS.find(a => a.name === name);
|
|
158
|
+
if (cfg) targets.push({ cfg, isProject: false });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let installed = 0;
|
|
164
|
+
let skipped = 0;
|
|
165
|
+
let updated = 0;
|
|
166
|
+
|
|
167
|
+
for (const target of targets) {
|
|
168
|
+
const agentConfig = target.cfg;
|
|
169
|
+
const targetPath = target.isProject
|
|
170
|
+
? join(process.cwd(), agentConfig.project)
|
|
171
|
+
: resolveHome(agentConfig.global);
|
|
172
|
+
|
|
173
|
+
const skillTarget = join(targetPath, SKILL_NAME);
|
|
174
|
+
|
|
175
|
+
if (!opts.update && isInstalled(skillTarget, sourceDir)) {
|
|
176
|
+
console.log(` (skip) ${agentConfig.name}: already up to date`);
|
|
177
|
+
skipped++;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
copyDir(sourceDir, skillTarget);
|
|
182
|
+
writeManifest(skillTarget, sourceDir, pkgVersion);
|
|
183
|
+
|
|
184
|
+
if (opts.update) {
|
|
185
|
+
const manifest = readManifest(skillTarget);
|
|
186
|
+
console.log(` ✓ ${agentConfig.name}: updated to skill v${manifest?.skill_version || '?'}`);
|
|
187
|
+
updated++;
|
|
188
|
+
} else {
|
|
189
|
+
console.log(` ✓ ${agentConfig.name}: ${targetPath}`);
|
|
190
|
+
installed++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const total = installed + updated;
|
|
195
|
+
console.log(`\nInstalled: ${installed}, Updated: ${updated}, Skipped: ${skipped}`);
|
|
196
|
+
if (total === 0 && skipped === 0) {
|
|
197
|
+
console.log('No targets found. Use --agent to specify one manually.');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
main();
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Features:
|
|
7
7
|
* - Detects installed agents by checking home/project config dirs
|
|
8
8
|
* - Copies the skill to each agent's skill directory (global + project)
|
|
9
|
+
* - Writes a manifest file for version tracking and future updates
|
|
9
10
|
* - Skips in CI environments (CI, CONTINUOUS_INTEGRATION env vars)
|
|
10
11
|
* - Skips if AX_SKIP_SKILL_INSTALL=1
|
|
11
12
|
* - Silent failure — never breaks npm install
|
|
@@ -13,62 +14,14 @@
|
|
|
13
14
|
* - Content-dedup: skips if identical file already exists
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
import {
|
|
17
|
-
existsSync,
|
|
18
|
-
readdirSync,
|
|
19
|
-
readFileSync,
|
|
20
|
-
copyFileSync,
|
|
21
|
-
mkdirSync,
|
|
22
|
-
rmSync,
|
|
23
|
-
statSync,
|
|
24
|
-
} from 'node:fs';
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
25
18
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
26
19
|
import { fileURLToPath } from 'node:url';
|
|
27
|
-
import {
|
|
20
|
+
import { AGENT_PATHS, SKILL_NAME, resolveHome, copyDir, isInstalled, detectAgents, writeManifest } from './skill-agent.mjs';
|
|
28
21
|
|
|
29
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
30
23
|
const __dirname = dirname(__filename);
|
|
31
24
|
|
|
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
25
|
function shouldSkip() {
|
|
73
26
|
return (
|
|
74
27
|
process.env.CI === 'true' ||
|
|
@@ -84,12 +37,6 @@ function isInteractive() {
|
|
|
84
37
|
/**
|
|
85
38
|
* Walk up from startDir to find the project root (where node_modules/@qzhuli/qzhuli-cli lives).
|
|
86
39
|
* 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
40
|
*/
|
|
94
41
|
function findProjectRoot() {
|
|
95
42
|
const scriptDir = resolve(__dirname, '..');
|
|
@@ -97,28 +44,18 @@ function findProjectRoot() {
|
|
|
97
44
|
const nmIdx = normalized.lastIndexOf('node_modules/@qzhuli/qzhuli-cli');
|
|
98
45
|
|
|
99
46
|
if (nmIdx === -1) {
|
|
100
|
-
|
|
101
|
-
return false;
|
|
47
|
+
return false; // dev/source checkout, skip
|
|
102
48
|
}
|
|
103
49
|
|
|
104
|
-
// It's in node_modules. Determine if global or project-level.
|
|
105
50
|
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
|
|
51
|
+
if (!projectRoot) return null;
|
|
114
52
|
|
|
115
53
|
const pkgAtRoot = join(projectRoot.replace(/\//g, sep), 'package.json');
|
|
116
54
|
if (!existsSync(pkgAtRoot)) return null;
|
|
117
55
|
|
|
118
56
|
try {
|
|
119
57
|
const pkg = JSON.parse(readFileSync(pkgAtRoot, 'utf8'));
|
|
120
|
-
|
|
121
|
-
if (pkg.name === '@qzhuli/qzhuli-cli') return null;
|
|
58
|
+
if (pkg.name === '@qzhuli/qzhuli-cli') return null; // global install
|
|
122
59
|
} catch {
|
|
123
60
|
return null;
|
|
124
61
|
}
|
|
@@ -127,47 +64,15 @@ function findProjectRoot() {
|
|
|
127
64
|
}
|
|
128
65
|
|
|
129
66
|
/**
|
|
130
|
-
*
|
|
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.
|
|
67
|
+
* Get the CLI package version from the nearest package.json.
|
|
157
68
|
*/
|
|
158
|
-
function
|
|
159
|
-
if (!existsSync(skillTarget)) return false;
|
|
69
|
+
function getPkgVersion() {
|
|
160
70
|
try {
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
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');
|
|
71
|
+
const pkgJson = resolve(__dirname, '..', 'package.json');
|
|
72
|
+
const pkg = JSON.parse(readFileSync(pkgJson, 'utf8'));
|
|
73
|
+
return pkg.version || 'unknown';
|
|
169
74
|
} catch {
|
|
170
|
-
return
|
|
75
|
+
return 'unknown';
|
|
171
76
|
}
|
|
172
77
|
}
|
|
173
78
|
|
|
@@ -181,17 +86,16 @@ function install() {
|
|
|
181
86
|
if (!existsSync(sourceDir)) return;
|
|
182
87
|
|
|
183
88
|
const projectRoot = findProjectRoot();
|
|
184
|
-
// false = dev/source install, skip
|
|
185
89
|
if (projectRoot === false) { console.log('SKIP: dev install'); return; }
|
|
186
90
|
const isGlobal = projectRoot === null;
|
|
187
91
|
const cwd = projectRoot || process.cwd();
|
|
92
|
+
const pkgVersion = getPkgVersion();
|
|
188
93
|
|
|
189
|
-
// Detect installed agents
|
|
190
94
|
const detectedAgents = detectAgents();
|
|
191
|
-
// Always include claude-code as fallback if nothing detected
|
|
192
95
|
const targets = detectedAgents.length > 0 ? detectedAgents : ['claude-code'];
|
|
193
96
|
|
|
194
97
|
const results = [];
|
|
98
|
+
const updated = [];
|
|
195
99
|
|
|
196
100
|
for (const agentName of targets) {
|
|
197
101
|
const agentConfig = AGENT_PATHS.find(a => a.name === agentName);
|
|
@@ -203,23 +107,43 @@ function install() {
|
|
|
203
107
|
|
|
204
108
|
const skillTarget = join(targetPath, SKILL_NAME);
|
|
205
109
|
|
|
206
|
-
//
|
|
207
|
-
if (isInstalled(skillTarget, sourceDir))
|
|
110
|
+
// Update flow: if installed but version changed, re-copy
|
|
111
|
+
if (isInstalled(skillTarget, sourceDir)) {
|
|
112
|
+
// Still write manifest (in case it was missing)
|
|
113
|
+
writeManifest(skillTarget, sourceDir, pkgVersion);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check if existing install needs update
|
|
118
|
+
if (existsSync(skillTarget)) {
|
|
119
|
+
copyDir(sourceDir, skillTarget);
|
|
120
|
+
writeManifest(skillTarget, sourceDir, pkgVersion);
|
|
121
|
+
updated.push(agentName);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
208
124
|
|
|
209
125
|
copyDir(sourceDir, skillTarget);
|
|
126
|
+
writeManifest(skillTarget, sourceDir, pkgVersion);
|
|
210
127
|
results.push(agentName);
|
|
211
128
|
}
|
|
212
129
|
|
|
213
|
-
// Report (only in interactive terminals)
|
|
214
130
|
if (results.length > 0 && isInteractive()) {
|
|
215
131
|
const noun = results.length === 1 ? 'agent' : 'agents';
|
|
216
132
|
console.log(
|
|
217
133
|
`✓ qzhuli-cli skill installed to ${results.length} ${noun}: ${results.join(', ')}`
|
|
218
134
|
);
|
|
219
135
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
136
|
+
if (updated.length > 0 && isInteractive()) {
|
|
137
|
+
const noun = updated.length === 1 ? 'agent' : 'agents';
|
|
138
|
+
console.log(
|
|
139
|
+
`✓ qzhuli-cli skill updated in ${updated.length} ${noun}: ${updated.join(', ')}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (isInteractive()) {
|
|
144
|
+
console.warn('[qzhuli-cli] Skill installation failed:', err?.message || err);
|
|
145
|
+
console.warn('[qzhuli-cli] Manual install: node scripts/install-skill.mjs');
|
|
146
|
+
}
|
|
223
147
|
}
|
|
224
148
|
}
|
|
225
149
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared agent detection and skill installation utilities.
|
|
3
|
+
* Used by both postinstall.mjs and install-skill.mjs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
copyFileSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
statSync,
|
|
15
|
+
} from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
|
|
19
|
+
// ── Agent path mapping ─────────────────────────────────────────────
|
|
20
|
+
// Source: https://www.agentskills.in/docs/getting-started
|
|
21
|
+
export const AGENT_PATHS = [
|
|
22
|
+
{ name: 'claude-code', global: '~/.claude/skills', project: '.claude/skills' },
|
|
23
|
+
{ name: 'cursor', global: '~/.cursor/skills', project: '.cursor/skills' },
|
|
24
|
+
{ name: 'codex', global: '~/.codex/skills', project: '.codex/skills' },
|
|
25
|
+
{ name: 'copilot', global: '~/.copilot/skills', project: '.github/skills' },
|
|
26
|
+
{ name: 'windsurf', global: '~/.codeium/windsurf/skills', project: '.windsurf/skills' },
|
|
27
|
+
{ name: 'openclaw', global: '~/.openclaw/skills', project: 'skills' },
|
|
28
|
+
{ name: 'opencode', global: '~/.config/opencode/skill', project: '.opencode/skill' },
|
|
29
|
+
{ name: 'cline', global: '~/.cline/skills', project: '.cline/skills' },
|
|
30
|
+
{ name: 'gemini-cli', global: '~/.gemini/skills', project: '.gemini/skills' },
|
|
31
|
+
{ name: 'amp', global: '~/.config/agents/skills', project: '.agents/skills' },
|
|
32
|
+
{ name: 'roo', global: '~/.roo/skills', project: '.roo/skills' },
|
|
33
|
+
{ name: 'goose', global: '~/.config/goose/skills', project: '.goose/skills' },
|
|
34
|
+
{ name: 'kiro', global: '~/.kiro/skills', project: '.kiro/skills' },
|
|
35
|
+
{ name: 'qwen-code', global: '~/.qwen/skills', project: '.qwen/skills' },
|
|
36
|
+
{ name: 'qoder', global: '~/.qoder/skills', project: '.qoder/skills' },
|
|
37
|
+
{ name: 'trae', global: '~/.trae/skills', project: '.trae/skills' },
|
|
38
|
+
{ name: 'augment', global: '~/.augment/skills', project: '.augment/skills' },
|
|
39
|
+
{ name: 'zed', global: '~/.config/zed/skills', project: '.zed/skills' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export const SKILL_NAME = 'qzhuli-cli';
|
|
43
|
+
export const MANIFEST_FILE = '.qzhuli-manifest.json';
|
|
44
|
+
|
|
45
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export function resolveHome(p) {
|
|
48
|
+
if (p.startsWith('~/')) return join(homedir(), p.slice(2));
|
|
49
|
+
return p;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function dirExists(p) {
|
|
53
|
+
try {
|
|
54
|
+
return statSync(resolveHome(p)).isDirectory();
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function copyDir(src, dest) {
|
|
61
|
+
mkdirSync(dest, { recursive: true });
|
|
62
|
+
|
|
63
|
+
// Remove stale files from dest that no longer exist in source
|
|
64
|
+
const srcNames = new Set(readdirSync(src).map(e => e));
|
|
65
|
+
if (existsSync(dest)) {
|
|
66
|
+
for (const name of readdirSync(dest)) {
|
|
67
|
+
if (!srcNames.has(name)) {
|
|
68
|
+
rmSync(join(dest, name), { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Copy source to dest
|
|
74
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const srcPath = join(src, entry.name);
|
|
77
|
+
const destPath = join(dest, entry.name);
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
copyDir(srcPath, destPath);
|
|
80
|
+
} else if (entry.isSymbolicLink()) {
|
|
81
|
+
continue;
|
|
82
|
+
} else {
|
|
83
|
+
copyFileSync(srcPath, destPath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isInstalled(skillTarget, sourceDir) {
|
|
89
|
+
if (!existsSync(skillTarget)) return false;
|
|
90
|
+
try {
|
|
91
|
+
const srcMd = join(sourceDir, 'SKILL.md');
|
|
92
|
+
const destMd = join(skillTarget, 'SKILL.md');
|
|
93
|
+
if (!existsSync(destMd)) return false;
|
|
94
|
+
return readFileSync(srcMd, 'utf8') === readFileSync(destMd, 'utf8');
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function detectAgents() {
|
|
101
|
+
return AGENT_PATHS
|
|
102
|
+
.filter(agent => dirExists(agent.global))
|
|
103
|
+
.map(agent => agent.name);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Manifest tracking ──────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export function writeManifest(skillTarget, sourceDir, pkgVersion) {
|
|
109
|
+
const manifestPath = join(skillTarget, MANIFEST_FILE);
|
|
110
|
+
const files = readdirSync(sourceDir, { withFileTypes: true })
|
|
111
|
+
.filter(e => !e.isDirectory())
|
|
112
|
+
.map(e => e.name);
|
|
113
|
+
const manifest = {
|
|
114
|
+
source: `@qzhuli/qzhuli-cli@${pkgVersion || 'unknown'}`,
|
|
115
|
+
installed_at: new Date().toISOString(),
|
|
116
|
+
skill_version: readSkillVersion(sourceDir),
|
|
117
|
+
files,
|
|
118
|
+
};
|
|
119
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function readManifest(skillTarget) {
|
|
123
|
+
const manifestPath = join(skillTarget, MANIFEST_FILE);
|
|
124
|
+
if (!existsSync(manifestPath)) return null;
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function needsUpdate(skillTarget, sourceDir) {
|
|
133
|
+
const manifest = readManifest(skillTarget);
|
|
134
|
+
if (!manifest) return true;
|
|
135
|
+
const currentVersion = readSkillVersion(sourceDir);
|
|
136
|
+
return manifest.skill_version !== currentVersion;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readSkillVersion(sourceDir) {
|
|
140
|
+
const skillMd = join(sourceDir, 'SKILL.md');
|
|
141
|
+
if (!existsSync(skillMd)) return 'unknown';
|
|
142
|
+
try {
|
|
143
|
+
const content = readFileSync(skillMd, 'utf8');
|
|
144
|
+
const match = content.match(/^version:\s*(.+)$/m);
|
|
145
|
+
return match ? match[1].trim() : 'unknown';
|
|
146
|
+
} catch {
|
|
147
|
+
return 'unknown';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qzhuli-cli
|
|
3
|
-
description: Use when operating the QZhuli CLI
|
|
4
|
-
version:
|
|
3
|
+
description: Use when operating the QZhuli CLI (`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: 3
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# QZhuli CLI
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Concise operating manual for the `qz` command.
|
|
10
10
|
|
|
11
11
|
## First Checks
|
|
12
12
|
|
|
@@ -22,58 +22,96 @@ Use this skill as a concise operating manual for the `qz` command.
|
|
|
22
22
|
```bash
|
|
23
23
|
qz --help
|
|
24
24
|
qz <command> --help
|
|
25
|
-
qz <command> <subcommand> --help
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
##
|
|
27
|
+
## Safety Rules
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
stores config under `./.qzhuli-cli/` relative to the process working directory.
|
|
29
|
+
**These rules prevent data loss and unwanted side effects. Follow them strictly.**
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|-------------------|-------------|------------------|
|
|
35
|
-
| Shows test banner | test | `./.qzhuli-cli/` |
|
|
36
|
-
| No test banner | production | `~/.qzhuli-cli/` |
|
|
31
|
+
### Ask Before Acting
|
|
37
32
|
|
|
38
|
-
|
|
33
|
+
When ANY of the following applies, STOP and ask the user first:
|
|
39
34
|
|
|
40
|
-
|
|
35
|
+
- **Ambiguous search results**: `user search` returns multiple candidates or `status: "needs_resolution"` — show options
|
|
36
|
+
and ask.
|
|
37
|
+
- **Friend operations**: Before `user add`, show the target profile and confirm.
|
|
38
|
+
- **Relation changes**: Before `relation set`, show the current value and the new value, then confirm.
|
|
39
|
+
- **Message sending**: Before `message send`, show the target conversation, recipient, and message content.
|
|
40
|
+
- **Cache clearing**: Before `cache clear`, confirm scope (all tables vs single table).
|
|
41
|
+
- **Any write operation** (`user add`, `relation set`, `message send`, `conversation create`, `cache clear`): confirm
|
|
42
|
+
with user.
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
friend lookups can return `status: "needs_resolution"`.
|
|
44
|
+
### Use --dry-run for Preview
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
Before any write operation the user hasn't explicitly confirmed, run with `--dry-run` first:
|
|
46
47
|
|
|
47
48
|
```bash
|
|
48
|
-
qz --
|
|
49
|
-
qz --
|
|
50
|
-
qz --jq ".data" conversation list --limit 5 --offset 0
|
|
49
|
+
qz --dry-run message send <id> <cid> "hello"
|
|
50
|
+
qz --dry-run relation set <uid> --remark "New Name"
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
### Least-Surprise Principle
|
|
54
|
+
|
|
55
|
+
- Never auto-send messages without explicit content approval.
|
|
56
|
+
- Never change a friend's remark without showing both old and new.
|
|
57
|
+
- Never delete cache data without confirming scope.
|
|
58
|
+
- If the user says "send a message" but doesn't specify content, draft it and ask before sending.
|
|
59
|
+
|
|
60
|
+
## ID Reference
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
actions, auth login/logout, and preference writes.
|
|
62
|
+
The CLI uses 5 distinct ID types. **Using the wrong type will fail silently or hit the wrong target.**
|
|
57
63
|
|
|
58
|
-
|
|
64
|
+
| ID Type | Field Name | Format | Example | Used By |
|
|
65
|
+
|-----------------|------------------|-------------------------|----------------------------------------|---------------------------------------------------------------------------------------------------------------------|
|
|
66
|
+
| Q助号 | `id` | Number, short | `10003` | `user add <q-number>`, `user search`, `conversation search` (default) |
|
|
67
|
+
| UID | `uid` | 32-char hex string | `d5b6308e3abad6bc96573c58` | `relation get/set`, `friend profile --uid`, `user search --uid`, `conversation search --uid`, `conversation create` |
|
|
68
|
+
| CID | `cid` | UUID | `5c2f46c2-b0d3-405d-ad21-d833538b77f7` | `message send <target-cid>` — **use the OTHER participant's cid, not your own** |
|
|
69
|
+
| Conversation ID | `conversationId` | Base64-like long string | `9boGaR7iii2Jdjhmb5LSo37...` | `message send`, `message history`, `conversation profile` |
|
|
70
|
+
| Agent ID | `agent.id` | Number | `5` | `conversation create --agent-id` |
|
|
59
71
|
|
|
60
|
-
|
|
72
|
+
**Quick identification by format**:
|
|
61
73
|
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
74
|
+
- A short integer → Q助号
|
|
75
|
+
- A 32-char hex string → UID
|
|
76
|
+
- A UUID with dashes → CID
|
|
77
|
+
- A long Base64-like string → conversationId
|
|
66
78
|
|
|
67
|
-
|
|
79
|
+
**Common mistake**: Using UID for `message send` instead of conversationId. Always resolve via `conversation search` or
|
|
80
|
+
`conversation list` first.
|
|
81
|
+
|
|
82
|
+
## Environment and Config Files
|
|
83
|
+
|
|
84
|
+
| Visible behavior | Environment | Config directory |
|
|
85
|
+
|----------------------------------------------------------------|-------------|------------------------------------|
|
|
86
|
+
| Shows `DEVELOPMENT BUILD / Running in TEST environment` banner | test | `./.qzhuli-cli/` (relative to CWD) |
|
|
87
|
+
| No test banner | production | `~/.qzhuli-cli/` |
|
|
88
|
+
|
|
89
|
+
Treat `credentials.json` as secret. Preferences live in `preferences.json`.
|
|
90
|
+
|
|
91
|
+
## Output and Global Options
|
|
68
92
|
|
|
69
|
-
|
|
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
|
|
93
|
+
Commands return JSON with `status`, `code`, `message`, and `data`.
|
|
74
94
|
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
| status | Meaning | Exit code |
|
|
96
|
+
|----------------------|-------------------------------------|-----------|
|
|
97
|
+
| `"success"` | Operation completed | 0 |
|
|
98
|
+
| `"needs_resolution"` | Ambiguous result (multiple matches) | 0 |
|
|
99
|
+
| `"error"` | Failed operation | 1 |
|
|
100
|
+
|
|
101
|
+
**Always check `status`, not just exit code.**
|
|
102
|
+
|
|
103
|
+
Use `--jq` for simple dot-path filtering:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
qz --jq ".data.uid" auth status
|
|
107
|
+
qz --jq ".data.links" friend list
|
|
108
|
+
qz --jq ".data" conversation list --limit 5
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`--jq` is not full jq. Prefer simple paths: `.data`, `.data.uid`, `.data.links`.
|
|
112
|
+
|
|
113
|
+
Use `--dry-run` to preview without side effects. Wired through: output, HTTP API calls, IM WebSocket actions, auth
|
|
114
|
+
login/logout, and preference writes.
|
|
77
115
|
|
|
78
116
|
## Command Map
|
|
79
117
|
|
|
@@ -99,94 +137,54 @@ then invalidate relevant cache entries.
|
|
|
99
137
|
| Search user conversations | `qz conversation search <query> [--uid]` |
|
|
100
138
|
| Send message | `qz message send <conversation-id> <target-cid> <content>` |
|
|
101
139
|
| Read message history | `qz message history <conversation-id> [--from <id>] [--direction newer\|older] [--limit <n>]` |
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
140
|
+
| Sync cache | `qz cache sync` |
|
|
141
|
+
| Cache status | `qz cache status` |
|
|
142
|
+
| Clear cache | `qz cache clear [--table <name>]` |
|
|
105
143
|
|
|
106
|
-
Relation type values
|
|
144
|
+
Relation type values: `0=stranger`, `1=friend`, `2=family`, `3=colleague`.
|
|
107
145
|
|
|
108
146
|
## Common Workflows
|
|
109
147
|
|
|
110
148
|
### Find a Friend and Inspect Relation
|
|
111
149
|
|
|
112
|
-
1.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
```
|
|
116
|
-
2. Resolve ambiguous names:
|
|
117
|
-
```bash
|
|
118
|
-
qz friend profile "<nickname>"
|
|
119
|
-
```
|
|
120
|
-
3. Use the resolved `uid`:
|
|
121
|
-
```bash
|
|
122
|
-
qz relation get <uid>
|
|
123
|
-
```
|
|
150
|
+
1. `qz friend list` — list candidates
|
|
151
|
+
2. `qz friend profile "<nickname>"` — resolve ambiguous names
|
|
152
|
+
3. `qz relation get <uid>` — inspect relation with resolved uid
|
|
124
153
|
|
|
125
154
|
### Update a Friend Relation
|
|
126
155
|
|
|
127
|
-
1. Resolve the exact `uid`
|
|
128
|
-
2.
|
|
129
|
-
3. Execute
|
|
130
|
-
|
|
131
|
-
qz relation set <uid> --remark "Product Manager" --type 1
|
|
132
|
-
```
|
|
133
|
-
4. Verify:
|
|
134
|
-
```bash
|
|
135
|
-
qz relation get <uid>
|
|
136
|
-
```
|
|
156
|
+
1. Resolve the exact `uid` with `friend list` or `friend profile`.
|
|
157
|
+
2. Show current value to user, confirm the change.
|
|
158
|
+
3. Execute: `qz relation set <uid> --remark "New Name" --type 1`
|
|
159
|
+
4. Verify: `qz relation get <uid>`
|
|
137
160
|
|
|
138
|
-
Do not run `relation set` without `--remark` or `--type`; the CLI returns `INVALID_ARGUMENT
|
|
161
|
+
**Do not run `relation set` without `--remark` or `--type`; the CLI returns `INVALID_ARGUMENT`.**
|
|
139
162
|
|
|
140
163
|
### Search and Add a Friend
|
|
141
164
|
|
|
142
|
-
1. Search
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
```
|
|
146
|
-
2. Add directly by Q助号:
|
|
147
|
-
```bash
|
|
148
|
-
qz user add 10000
|
|
149
|
-
```
|
|
165
|
+
1. Search: `qz user search 10000`
|
|
166
|
+
2. Show profile to user, confirm.
|
|
167
|
+
3. Add: `qz user add 10000` (internally: search → create conversation)
|
|
150
168
|
|
|
151
169
|
### Find All Conversations with a User
|
|
152
170
|
|
|
153
|
-
Search by Q助号 (default):
|
|
154
|
-
|
|
155
|
-
```bash
|
|
156
|
-
qz conversation search 10000
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Search by UID:
|
|
160
|
-
|
|
161
171
|
```bash
|
|
162
|
-
qz conversation search
|
|
172
|
+
qz conversation search 10000 # by Q助号 (default)
|
|
173
|
+
qz conversation search d5b6308e3abad6bc96573c58 --uid # by UID
|
|
163
174
|
```
|
|
164
175
|
|
|
165
|
-
|
|
166
|
-
|
|
176
|
+
Response includes `id` (Q助号), `uid` (internal user ID), and `conversations` with full profile data. Each conversation
|
|
177
|
+
entry contains `conversationId`, `isGroup`, `users`, and `visitors`.
|
|
167
178
|
|
|
168
179
|
### Send a Message
|
|
169
180
|
|
|
170
|
-
1. Confirm auth:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
```
|
|
178
|
-
3. Pick the conversation `id`.
|
|
179
|
-
4. Pick `target-cid` from that conversation's `cids`; for one-to-one chats this is usually the other participant's cid,
|
|
180
|
-
not the current user's own `cid`.
|
|
181
|
-
5. Confirm recipient and content with the user if there is any ambiguity.
|
|
182
|
-
6. Send:
|
|
183
|
-
```bash
|
|
184
|
-
qz message send <conversation-id> <target-cid> "message text"
|
|
185
|
-
```
|
|
186
|
-
7. Verify:
|
|
187
|
-
```bash
|
|
188
|
-
qz message history <conversation-id> --limit 5
|
|
189
|
-
```
|
|
181
|
+
1. Confirm auth: `qz auth status`
|
|
182
|
+
2. Get conversations: `qz conversation list --limit 10`
|
|
183
|
+
3. Pick the conversation `id` (conversationId).
|
|
184
|
+
4. Pick `target-cid` from that conversation's `users` array — **use the OTHER participant's cid**.
|
|
185
|
+
5. Show recipient name and message content to user, confirm.
|
|
186
|
+
6. Send: `qz message send <conversation-id> <target-cid> "message text"`
|
|
187
|
+
7. Verify: `qz message history <conversation-id> --limit 5`
|
|
190
188
|
|
|
191
189
|
### Page Through Message History
|
|
192
190
|
|
|
@@ -204,15 +202,29 @@ qz cache status # verify record counts and sync time
|
|
|
204
202
|
qz conversation search 10000 # now instant from cache
|
|
205
203
|
```
|
|
206
204
|
|
|
205
|
+
## Cache Architecture (Reference)
|
|
206
|
+
|
|
207
|
+
Read operations use a **Repository Pattern** with SQLite-backed caching (`~/.qzhuli-cli/cache.db`):
|
|
208
|
+
|
|
209
|
+
- **TTL**: conversations 30 min, contacts/relations 5 min, user profiles 1 hour
|
|
210
|
+
- **Incremental sync**: only fetches profiles for *new* conversations
|
|
211
|
+
- **Cache miss** → auto incremental sync (not full refetch)
|
|
212
|
+
- **Write operations** bypass cache, invalidate relevant entries
|
|
213
|
+
|
|
214
|
+
Tables: `conversations_index`, `conversation_profiles`, `contacts_cache`, `user_profiles`, `relations_cache`,
|
|
215
|
+
`messages_cache`.
|
|
216
|
+
|
|
207
217
|
## Troubleshooting
|
|
208
218
|
|
|
209
|
-
| Symptom
|
|
210
|
-
|
|
211
|
-
| Command not found
|
|
212
|
-
| Auth failure
|
|
213
|
-
| Unexpected language
|
|
214
|
-
| Too much JSON
|
|
215
|
-
| Need
|
|
216
|
-
| Message send cid error
|
|
217
|
-
| Slow queries
|
|
218
|
-
| Cache corrupted
|
|
219
|
+
| Symptom | Action |
|
|
220
|
+
|---------------------------------|---------------------------------------------------------------------------------------------------------------|
|
|
221
|
+
| Command not found | Confirm `qz` is on PATH. Install: `npm install -g @qzhuli/qzhuli-cli` |
|
|
222
|
+
| Auth failure | `qz auth status`; then `qz auth login` if needed |
|
|
223
|
+
| Unexpected language | `qz config --locale en` or `--locale zh` |
|
|
224
|
+
| Too much JSON | Use `--jq ".data"` or another simple dot path |
|
|
225
|
+
| Need no-op preview | Use `--dry-run` |
|
|
226
|
+
| Message send cid error | Re-check `auth status`, choose `target-cid` from `conversation list` — it must be the other participant's cid |
|
|
227
|
+
| Slow queries | Run `qz cache sync` first (incremental, fast), then retry |
|
|
228
|
+
| Cache corrupted | `qz cache clear` to reset, then retry (falls back to API) |
|
|
229
|
+
| Ambiguous search | `status: "needs_resolution"` — refine query with `--uid` or `--remark` flag |
|
|
230
|
+
| `relation set` INVALID_ARGUMENT | Must include at least one of `--remark` or `--type` |
|