@pi-unipi/command-enchantment 2.0.1 → 2.0.3
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 +36 -0
- package/package.json +1 -1
- package/src/__tests__/command-registry.audit.test.ts +120 -0
- package/src/constants.ts +26 -3
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @pi-unipi/command-enchantment
|
|
2
|
+
|
|
3
|
+
Enhanced autocomplete for Unipi commands. It wraps Pi's base autocomplete provider and makes `/unipi:*` suggestions easier to scan with package grouping, stable sorting, short descriptions, and package colors.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
Command Enchantment has no user commands. It improves the editor autocomplete experience automatically when the package is installed.
|
|
8
|
+
|
|
9
|
+
## What It Does
|
|
10
|
+
|
|
11
|
+
- Groups `/unipi:*` commands by package so workflow, memory, web, footer, and other commands are visually distinct.
|
|
12
|
+
- Sorts matches in predictable tiers: exact Unipi matches first, then other Unipi matches, then system commands.
|
|
13
|
+
- Preserves dynamic argument completions from command providers, including workflow document and worktree suggestions.
|
|
14
|
+
- Ships an audit test that checks registered Unipi commands are represented in the autocomplete registry and have descriptions.
|
|
15
|
+
|
|
16
|
+
## Development Checks
|
|
17
|
+
|
|
18
|
+
Run the registry audit before releases:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm --workspace packages/autocomplete test -- src/__tests__/command-registry.audit.test.ts
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Run all autocomplete tests:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm --workspace packages/autocomplete test
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Autocomplete enhancement is enabled by default. The package stores its toggle in the Unipi config and can be disabled by setting `autocompleteEnhanced` to `false`.
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
MIT
|
package/package.json
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { existsSync, globSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
function findRepoRoot(start: string): string {
|
|
6
|
+
let dir = start;
|
|
7
|
+
while (dir !== dirname(dir)) {
|
|
8
|
+
if (existsSync(join(dir, "packages", "autocomplete", "src", "constants.ts"))) return dir;
|
|
9
|
+
dir = dirname(dir);
|
|
10
|
+
}
|
|
11
|
+
return start;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const root = findRepoRoot(process.cwd());
|
|
15
|
+
|
|
16
|
+
function read(path: string): string {
|
|
17
|
+
return readFileSync(join(root, path), "utf-8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function collectConstants(): Map<string, string> {
|
|
21
|
+
const constants = new Map<string, string>([["UNIPI_PREFIX", "unipi:"]]);
|
|
22
|
+
|
|
23
|
+
for (const path of globSync("packages/**/*.ts", { cwd: root })) {
|
|
24
|
+
const text = read(path);
|
|
25
|
+
for (const obj of text.matchAll(/(?:export\s+)?const\s+(\w+)\s*=\s*\{([\s\S]*?)\}\s*as\s+const/g)) {
|
|
26
|
+
const [, name, body] = obj;
|
|
27
|
+
for (const item of body.matchAll(/(\w+):\s*"([^"]+)"/g)) {
|
|
28
|
+
constants.set(`${name}.${item[1]}`, item[2]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return constants;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function evaluateCommandExpression(expr: string, constants: Map<string, string>): string | null {
|
|
37
|
+
const trimmed = expr.trim();
|
|
38
|
+
|
|
39
|
+
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
|
|
40
|
+
const quote = trimmed[0];
|
|
41
|
+
const end = trimmed.indexOf(quote, 1);
|
|
42
|
+
return end > 0 ? trimmed.slice(1, end) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (trimmed.startsWith("`")) {
|
|
46
|
+
const end = trimmed.lastIndexOf("`");
|
|
47
|
+
if (end <= 0) return null;
|
|
48
|
+
return trimmed.slice(1, end).replace(/\$\{([^}]+)\}/g, (_match, key: string) => {
|
|
49
|
+
const resolved = constants.get(key.trim());
|
|
50
|
+
return resolved ?? `\${${key}}`;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return constants.get(trimmed) ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function registeredCommands(): { commands: Set<string>; nonUnipi: string[]; unresolved: string[] } {
|
|
58
|
+
const constants = collectConstants();
|
|
59
|
+
const commands = new Set<string>();
|
|
60
|
+
const nonUnipi: string[] = [];
|
|
61
|
+
const unresolved: string[] = [];
|
|
62
|
+
|
|
63
|
+
for (const path of globSync("packages/**/*.ts", { cwd: root }).sort()) {
|
|
64
|
+
const text = read(path);
|
|
65
|
+
if (!text.includes("registerCommand")) continue;
|
|
66
|
+
|
|
67
|
+
for (const match of text.matchAll(/\.registerCommand\(\s*([^,\n]+)/g)) {
|
|
68
|
+
const expr = match[1].trim();
|
|
69
|
+
|
|
70
|
+
// Workflow registers a loop over WORKFLOW_COMMANDS via local `fullCommand`.
|
|
71
|
+
if (path === "packages/workflow/commands.ts" && expr === "fullCommand") {
|
|
72
|
+
for (const [key, value] of constants) {
|
|
73
|
+
if (key.startsWith("WORKFLOW_COMMANDS.")) commands.add(`unipi:${value}`);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const command = evaluateCommandExpression(expr, constants);
|
|
79
|
+
if (!command || command.includes("${")) {
|
|
80
|
+
unresolved.push(`${path}: ${expr}`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (command.startsWith("unipi:")) {
|
|
85
|
+
commands.add(command);
|
|
86
|
+
} else {
|
|
87
|
+
nonUnipi.push(`${command} (${path})`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { commands, nonUnipi, unresolved };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function autocompleteRegistry(): { registry: Set<string>; descriptions: Set<string>; registryPackages: Set<string>; labels: Set<string> } {
|
|
96
|
+
const text = read("packages/autocomplete/src/constants.ts");
|
|
97
|
+
const registryBody = text.match(/export const COMMAND_REGISTRY[^=]*= \{([\s\S]*?)\n\};/)?.[1] ?? "";
|
|
98
|
+
const descriptionsBody = text.match(/export const COMMAND_DESCRIPTIONS[^=]*= \{([\s\S]*?)\n\};/)?.[1] ?? "";
|
|
99
|
+
const labelsBody = text.match(/export const PACKAGE_LABELS[^=]*= \{([\s\S]*?)\n\};/)?.[1] ?? "";
|
|
100
|
+
|
|
101
|
+
const registry = new Set([...registryBody.matchAll(/"(unipi:[^"]+)"\s*:/g)].map((m) => m[1]));
|
|
102
|
+
const descriptions = new Set([...descriptionsBody.matchAll(/"(unipi:[^"]+)"\s*:/g)].map((m) => m[1]));
|
|
103
|
+
const registryPackages = new Set([...registryBody.matchAll(/"unipi:[^"]+"\s*:\s*"([^"]+)"/g)].map((m) => m[1]));
|
|
104
|
+
const labels = new Set([...labelsBody.matchAll(/^\s*"?([a-z][a-z0-9-]*)"?:\s*"/gm)].map((m) => m[1]));
|
|
105
|
+
|
|
106
|
+
return { registry, descriptions, registryPackages, labels };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
describe("autocomplete command registry audit", () => {
|
|
110
|
+
it("mirrors every registered /unipi:* command and has no non-unipi package commands", () => {
|
|
111
|
+
const registered = registeredCommands();
|
|
112
|
+
const autocomplete = autocompleteRegistry();
|
|
113
|
+
|
|
114
|
+
expect(registered.unresolved, "unresolved registerCommand expressions").toEqual([]);
|
|
115
|
+
expect(registered.nonUnipi, "package commands must use the unipi: prefix").toEqual([]);
|
|
116
|
+
expect([...registered.commands].sort()).toEqual([...autocomplete.registry].sort());
|
|
117
|
+
expect([...autocomplete.registry].sort()).toEqual([...autocomplete.descriptions].sort());
|
|
118
|
+
expect([...autocomplete.registryPackages].filter((pkg) => !autocomplete.labels.has(pkg)).sort()).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
});
|
package/src/constants.ts
CHANGED
|
@@ -20,6 +20,7 @@ export const PACKAGE_ORDER: string[] = [
|
|
|
20
20
|
"workflow",
|
|
21
21
|
"ralph",
|
|
22
22
|
"memory",
|
|
23
|
+
"btw",
|
|
23
24
|
"milestone",
|
|
24
25
|
"mcp",
|
|
25
26
|
"utility",
|
|
@@ -41,6 +42,7 @@ export const PACKAGE_COLORS: Record<string, string> = {
|
|
|
41
42
|
workflow: `${ESC}[91m`, // Bright Red
|
|
42
43
|
ralph: `${ESC}[33m`, // Yellow/Orange
|
|
43
44
|
memory: `${ESC}[93m`, // Bright Yellow
|
|
45
|
+
btw: `${ESC}[95m`, // Bright Magenta
|
|
44
46
|
milestone: `${ESC}[32m`, // Green
|
|
45
47
|
mcp: `${ESC}[32m`, // Green
|
|
46
48
|
utility: `${ESC}[36m`, // Cyan
|
|
@@ -57,7 +59,7 @@ export const PACKAGE_COLORS: Record<string, string> = {
|
|
|
57
59
|
};
|
|
58
60
|
|
|
59
61
|
// ─── Command Registry ────────────────────────────────────────────────
|
|
60
|
-
/** Mapping of full command name → package name (
|
|
62
|
+
/** Mapping of full command name → package name (88 verified commands) */
|
|
61
63
|
export const COMMAND_REGISTRY: Record<string, string> = {
|
|
62
64
|
// workflow (20 commands)
|
|
63
65
|
"unipi:brainstorm": "workflow",
|
|
@@ -95,6 +97,14 @@ export const COMMAND_REGISTRY: Record<string, string> = {
|
|
|
95
97
|
"unipi:global-memory-list": "memory",
|
|
96
98
|
"unipi:memory-settings": "memory",
|
|
97
99
|
|
|
100
|
+
// btw (6 commands)
|
|
101
|
+
"unipi:btw": "btw",
|
|
102
|
+
"unipi:btw-tangent": "btw",
|
|
103
|
+
"unipi:btw-new": "btw",
|
|
104
|
+
"unipi:btw-clear": "btw",
|
|
105
|
+
"unipi:btw-inject": "btw",
|
|
106
|
+
"unipi:btw-summarize": "btw",
|
|
107
|
+
|
|
98
108
|
// mcp (5 commands)
|
|
99
109
|
"unipi:mcp-status": "mcp",
|
|
100
110
|
"unipi:mcp-sync": "mcp",
|
|
@@ -126,14 +136,16 @@ export const COMMAND_REGISTRY: Record<string, string> = {
|
|
|
126
136
|
"unipi:web-settings": "web-api",
|
|
127
137
|
"unipi:web-cache-clear": "web-api",
|
|
128
138
|
|
|
129
|
-
// compact (
|
|
139
|
+
// compact (9 commands)
|
|
130
140
|
"unipi:lossless-compact": "compact",
|
|
131
141
|
"unipi:compact": "compact",
|
|
142
|
+
"unipi:session-recall": "compact",
|
|
132
143
|
"unipi:compact-recall": "compact",
|
|
133
144
|
"unipi:compact-stats": "compact",
|
|
134
145
|
"unipi:compact-doctor": "compact",
|
|
135
146
|
"unipi:compact-settings": "compact",
|
|
136
147
|
"unipi:compact-preset": "compact",
|
|
148
|
+
"unipi:compact-help": "compact",
|
|
137
149
|
|
|
138
150
|
// cocoindex (5 commands)
|
|
139
151
|
"unipi:cocoindex-update": "cocoindex",
|
|
@@ -208,6 +220,13 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
208
220
|
"unipi:global-memory-list": "List all project memories",
|
|
209
221
|
"unipi:memory-settings": "Configure memory settings",
|
|
210
222
|
|
|
223
|
+
"unipi:btw": "Run a parallel side conversation",
|
|
224
|
+
"unipi:btw-tangent": "Start a contextless BTW tangent thread",
|
|
225
|
+
"unipi:btw-new": "Start a fresh BTW thread with session context",
|
|
226
|
+
"unipi:btw-clear": "Dismiss and clear the BTW thread",
|
|
227
|
+
"unipi:btw-inject": "Inject the BTW thread into the main agent",
|
|
228
|
+
"unipi:btw-summarize": "Summarize and inject the BTW thread",
|
|
229
|
+
|
|
211
230
|
"unipi:mcp-status": "Show MCP server status",
|
|
212
231
|
"unipi:mcp-sync": "Sync MCP server connections",
|
|
213
232
|
"unipi:mcp-add": "Add a new MCP server",
|
|
@@ -238,11 +257,13 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
238
257
|
|
|
239
258
|
"unipi:lossless-compact": "Immediate zero-LLM compaction",
|
|
240
259
|
"unipi:compact": "(DEPRECATED) Use /unipi:lossless-compact instead",
|
|
241
|
-
"unipi:
|
|
260
|
+
"unipi:session-recall": "Search session history, including compacted-away messages",
|
|
261
|
+
"unipi:compact-recall": "(DEPRECATED) Use /unipi:session-recall instead",
|
|
242
262
|
"unipi:compact-stats": "Show compaction statistics",
|
|
243
263
|
"unipi:compact-doctor": "Diagnose compaction issues",
|
|
244
264
|
"unipi:compact-settings": "Configure compaction settings",
|
|
245
265
|
"unipi:compact-preset": "Manage compaction presets",
|
|
266
|
+
"unipi:compact-help": "Show compactor command help",
|
|
246
267
|
"unipi:cocoindex-update": "Run CocoIndex update to index project",
|
|
247
268
|
"unipi:cocoindex-status": "Show CocoIndex indexing status",
|
|
248
269
|
"unipi:cocoindex-init": "Initialize CocoIndex pipeline",
|
|
@@ -276,6 +297,7 @@ export const PACKAGE_LABELS: Record<string, string> = {
|
|
|
276
297
|
workflow: "workflow",
|
|
277
298
|
ralph: "ralph",
|
|
278
299
|
memory: "memory",
|
|
300
|
+
btw: "btw",
|
|
279
301
|
milestone: "milestone",
|
|
280
302
|
mcp: "mcp",
|
|
281
303
|
utility: "utility",
|
|
@@ -288,4 +310,5 @@ export const PACKAGE_LABELS: Record<string, string> = {
|
|
|
288
310
|
footer: "footer",
|
|
289
311
|
updater: "updater",
|
|
290
312
|
"input-shortcuts": "input-shortcuts",
|
|
313
|
+
cocoindex: "cocoindex",
|
|
291
314
|
};
|