@pi-unipi/command-enchantment 0.1.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/package.json +40 -0
- package/src/constants.ts +214 -0
- package/src/index.ts +31 -0
- package/src/provider.ts +307 -0
- package/src/settings.ts +84 -0
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-unipi/command-enchantment",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Enhanced TUI autocomplete for /unipi:* commands — colored, sorted, and grouped by package",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Neuron Mr White",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Neuron-Mr-White/unipi.git",
|
|
11
|
+
"directory": "packages/autocomplete"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/Neuron-Mr-White/unipi#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/Neuron-Mr-White/unipi/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"pi-package",
|
|
19
|
+
"pi-extension",
|
|
20
|
+
"pi-coding-agent",
|
|
21
|
+
"unipi",
|
|
22
|
+
"autocomplete"
|
|
23
|
+
],
|
|
24
|
+
"pi": {
|
|
25
|
+
"extensions": [
|
|
26
|
+
"src/index.ts"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"src/**/*.ts",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
35
|
+
"@mariozechner/pi-tui": "*"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^25.6.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/command-enchantment — Constants
|
|
3
|
+
*
|
|
4
|
+
* Static mappings for the command registry, package ordering, and package colors.
|
|
5
|
+
* These drive the enhanced autocomplete display for /unipi:* commands.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─── ANSI Color Helpers ──────────────────────────────────────────────
|
|
9
|
+
const ESC = "\x1b";
|
|
10
|
+
const RESET = `${ESC}[0m`;
|
|
11
|
+
|
|
12
|
+
/** Wrap text in an ANSI color code */
|
|
13
|
+
export function colorize(ansiCode: string, text: string): string {
|
|
14
|
+
return `${ansiCode}${text}${RESET}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─── Package Order ───────────────────────────────────────────────────
|
|
18
|
+
/** Packages sorted by display priority (top-to-bottom in autocomplete) */
|
|
19
|
+
export const PACKAGE_ORDER: string[] = [
|
|
20
|
+
"workflow",
|
|
21
|
+
"ralph",
|
|
22
|
+
"memory",
|
|
23
|
+
"mcp",
|
|
24
|
+
"utility",
|
|
25
|
+
"ask-user",
|
|
26
|
+
"info",
|
|
27
|
+
"web-api",
|
|
28
|
+
"compact",
|
|
29
|
+
"notify",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// ─── Package Colors ──────────────────────────────────────────────────
|
|
33
|
+
/** ANSI bright-color codes per package */
|
|
34
|
+
export const PACKAGE_COLORS: Record<string, string> = {
|
|
35
|
+
workflow: `${ESC}[91m`, // Bright Red
|
|
36
|
+
ralph: `${ESC}[33m`, // Yellow/Orange
|
|
37
|
+
memory: `${ESC}[93m`, // Bright Yellow
|
|
38
|
+
mcp: `${ESC}[32m`, // Green
|
|
39
|
+
utility: `${ESC}[36m`, // Cyan
|
|
40
|
+
"ask-user": `${ESC}[94m`, // Bright Blue
|
|
41
|
+
info: `${ESC}[35m`, // Magenta
|
|
42
|
+
"web-api": `${ESC}[95m`, // Bright Magenta
|
|
43
|
+
compact: `${ESC}[37m`, // White
|
|
44
|
+
notify: `${ESC}[96m`, // Bright Cyan
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ─── Command Registry ────────────────────────────────────────────────
|
|
48
|
+
/** Mapping of full command name → package name (58 verified commands) */
|
|
49
|
+
export const COMMAND_REGISTRY: Record<string, string> = {
|
|
50
|
+
// workflow (20 commands)
|
|
51
|
+
"unipi:brainstorm": "workflow",
|
|
52
|
+
"unipi:plan": "workflow",
|
|
53
|
+
"unipi:work": "workflow",
|
|
54
|
+
"unipi:review-work": "workflow",
|
|
55
|
+
"unipi:consolidate": "workflow",
|
|
56
|
+
"unipi:worktree-create": "workflow",
|
|
57
|
+
"unipi:worktree-list": "workflow",
|
|
58
|
+
"unipi:worktree-merge": "workflow",
|
|
59
|
+
"unipi:consultant": "workflow",
|
|
60
|
+
"unipi:quick-work": "workflow",
|
|
61
|
+
"unipi:gather-context": "workflow",
|
|
62
|
+
"unipi:document": "workflow",
|
|
63
|
+
"unipi:scan-issues": "workflow",
|
|
64
|
+
"unipi:auto": "workflow",
|
|
65
|
+
"unipi:debug": "workflow",
|
|
66
|
+
"unipi:fix": "workflow",
|
|
67
|
+
"unipi:quick-fix": "workflow",
|
|
68
|
+
"unipi:research": "workflow",
|
|
69
|
+
"unipi:chore-create": "workflow",
|
|
70
|
+
"unipi:chore-execute": "workflow",
|
|
71
|
+
|
|
72
|
+
// ralph (2 commands)
|
|
73
|
+
"unipi:ralph": "ralph",
|
|
74
|
+
"unipi:ralph-stop": "ralph",
|
|
75
|
+
|
|
76
|
+
// memory (7 commands)
|
|
77
|
+
"unipi:memory-process": "memory",
|
|
78
|
+
"unipi:memory-search": "memory",
|
|
79
|
+
"unipi:memory-consolidate": "memory",
|
|
80
|
+
"unipi:memory-forget": "memory",
|
|
81
|
+
"unipi:global-memory-search": "memory",
|
|
82
|
+
"unipi:global-memory-list": "memory",
|
|
83
|
+
"unipi:memory-settings": "memory",
|
|
84
|
+
|
|
85
|
+
// mcp (5 commands)
|
|
86
|
+
"unipi:mcp-status": "mcp",
|
|
87
|
+
"unipi:mcp-sync": "mcp",
|
|
88
|
+
"unipi:mcp-add": "mcp",
|
|
89
|
+
"unipi:mcp-settings": "mcp",
|
|
90
|
+
"unipi:mcp-reload": "mcp",
|
|
91
|
+
|
|
92
|
+
// utility (6 commands)
|
|
93
|
+
"unipi:continue": "utility",
|
|
94
|
+
"unipi:reload": "utility",
|
|
95
|
+
"unipi:status": "utility",
|
|
96
|
+
"unipi:cleanup": "utility",
|
|
97
|
+
"unipi:env": "utility",
|
|
98
|
+
"unipi:doctor": "utility",
|
|
99
|
+
|
|
100
|
+
// ask-user (1 command)
|
|
101
|
+
"unipi:ask-user-settings": "ask-user",
|
|
102
|
+
|
|
103
|
+
// info (2 commands)
|
|
104
|
+
"unipi:info": "info",
|
|
105
|
+
"unipi:info-settings": "info",
|
|
106
|
+
|
|
107
|
+
// web-api (2 commands)
|
|
108
|
+
"unipi:web-settings": "web-api",
|
|
109
|
+
"unipi:web-cache-clear": "web-api",
|
|
110
|
+
|
|
111
|
+
// compact (9 commands)
|
|
112
|
+
"unipi:compact": "compact",
|
|
113
|
+
"unipi:compact-recall": "compact",
|
|
114
|
+
"unipi:compact-stats": "compact",
|
|
115
|
+
"unipi:compact-doctor": "compact",
|
|
116
|
+
"unipi:compact-settings": "compact",
|
|
117
|
+
"unipi:compact-preset": "compact",
|
|
118
|
+
"unipi:compact-index": "compact",
|
|
119
|
+
"unipi:compact-search": "compact",
|
|
120
|
+
"unipi:compact-purge": "compact",
|
|
121
|
+
|
|
122
|
+
// notify (4 commands)
|
|
123
|
+
"unipi:notify-settings": "notify",
|
|
124
|
+
"unipi:notify-set-gotify": "notify",
|
|
125
|
+
"unipi:notify-set-tg": "notify",
|
|
126
|
+
"unipi:notify-test": "notify",
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// ─── Description Map ─────────────────────────────────────────────────
|
|
130
|
+
/** Short descriptions for each command (used when base suggestions lack them) */
|
|
131
|
+
export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
132
|
+
"unipi:brainstorm": "Collaborative discovery — explore problem space",
|
|
133
|
+
"unipi:plan": "Strategic planning — tasks, dependencies",
|
|
134
|
+
"unipi:work": "Execute plan — implement tasks, test, commit",
|
|
135
|
+
"unipi:review-work": "Review work — check task completion, run lint",
|
|
136
|
+
"unipi:consolidate": "Save learnings to memory, craft skills",
|
|
137
|
+
"unipi:worktree-create": "Create git worktree for parallel work",
|
|
138
|
+
"unipi:worktree-list": "List all unipi worktrees",
|
|
139
|
+
"unipi:worktree-merge": "Merge worktree branches back to main",
|
|
140
|
+
"unipi:consultant": "Expert consultation — advisory analysis",
|
|
141
|
+
"unipi:quick-work": "Fast single-task execution — one shot",
|
|
142
|
+
"unipi:gather-context": "Research codebase — surface patterns",
|
|
143
|
+
"unipi:document": "Generate documentation — README, API docs",
|
|
144
|
+
"unipi:scan-issues": "Deep investigation — find bugs, issues",
|
|
145
|
+
"unipi:auto": "Full pipeline — brainstorm → plan → work → review",
|
|
146
|
+
"unipi:debug": "Active bug investigation — reproduce, diagnose",
|
|
147
|
+
"unipi:fix": "Fix bugs using debug reports",
|
|
148
|
+
"unipi:quick-fix": "Fast bug fix without debug report",
|
|
149
|
+
"unipi:research": "Read-only research with bash access",
|
|
150
|
+
"unipi:chore-create": "Create reusable chore definition",
|
|
151
|
+
"unipi:chore-execute": "Execute a saved chore",
|
|
152
|
+
|
|
153
|
+
"unipi:ralph": "Ralph loop — start/resume coding session",
|
|
154
|
+
"unipi:ralph-stop": "Stop the active ralph loop",
|
|
155
|
+
|
|
156
|
+
"unipi:memory-process": "Process and store conversation learnings",
|
|
157
|
+
"unipi:memory-search": "Search project memory for past context",
|
|
158
|
+
"unipi:memory-consolidate": "Consolidate memory entries",
|
|
159
|
+
"unipi:memory-forget": "Remove memory entries",
|
|
160
|
+
"unipi:global-memory-search": "Search across all project memories",
|
|
161
|
+
"unipi:global-memory-list": "List all project memories",
|
|
162
|
+
"unipi:memory-settings": "Configure memory settings",
|
|
163
|
+
|
|
164
|
+
"unipi:mcp-status": "Show MCP server status",
|
|
165
|
+
"unipi:mcp-sync": "Sync MCP server connections",
|
|
166
|
+
"unipi:mcp-add": "Add a new MCP server",
|
|
167
|
+
"unipi:mcp-settings": "Configure MCP settings",
|
|
168
|
+
"unipi:mcp-reload": "Reload MCP connections",
|
|
169
|
+
|
|
170
|
+
"unipi:continue": "Continue the last conversation",
|
|
171
|
+
"unipi:reload": "Reload extensions and settings",
|
|
172
|
+
"unipi:status": "Show system status",
|
|
173
|
+
"unipi:cleanup": "Clean up old sessions and cache",
|
|
174
|
+
"unipi:env": "Show environment info",
|
|
175
|
+
"unipi:doctor": "Run diagnostics",
|
|
176
|
+
|
|
177
|
+
"unipi:ask-user-settings": "Configure ask-user settings",
|
|
178
|
+
|
|
179
|
+
"unipi:info": "Show system information",
|
|
180
|
+
"unipi:info-settings": "Configure info display",
|
|
181
|
+
|
|
182
|
+
"unipi:web-settings": "Configure web search settings",
|
|
183
|
+
"unipi:web-cache-clear": "Clear web search cache",
|
|
184
|
+
|
|
185
|
+
"unipi:compact": "Compact context window",
|
|
186
|
+
"unipi:compact-recall": "Recall a compacted session",
|
|
187
|
+
"unipi:compact-stats": "Show compaction statistics",
|
|
188
|
+
"unipi:compact-doctor": "Diagnose compaction issues",
|
|
189
|
+
"unipi:compact-settings": "Configure compaction settings",
|
|
190
|
+
"unipi:compact-preset": "Manage compaction presets",
|
|
191
|
+
"unipi:compact-index": "Show compaction index",
|
|
192
|
+
"unipi:compact-search": "Search compacted sessions",
|
|
193
|
+
"unipi:compact-purge": "Purge old compacted sessions",
|
|
194
|
+
|
|
195
|
+
"unipi:notify-settings": "Configure notification platforms and events",
|
|
196
|
+
"unipi:notify-set-gotify": "Set up Gotify push notifications",
|
|
197
|
+
"unipi:notify-set-tg": "Set up Telegram bot notifications",
|
|
198
|
+
"unipi:notify-test": "Test all enabled notification platforms",
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// ─── Package Display Names ───────────────────────────────────────────
|
|
202
|
+
/** Pretty names for package tags in autocomplete items */
|
|
203
|
+
export const PACKAGE_LABELS: Record<string, string> = {
|
|
204
|
+
workflow: "workflow",
|
|
205
|
+
ralph: "ralph",
|
|
206
|
+
memory: "memory",
|
|
207
|
+
mcp: "mcp",
|
|
208
|
+
utility: "utility",
|
|
209
|
+
"ask-user": "ask-user",
|
|
210
|
+
info: "info",
|
|
211
|
+
"web-api": "web-api",
|
|
212
|
+
compact: "compact",
|
|
213
|
+
notify: "notify",
|
|
214
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/command-enchantment — Extension Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Registers an enhanced autocomplete provider for /unipi:* commands.
|
|
5
|
+
* Intercepts slash command suggestions and returns colored, sorted,
|
|
6
|
+
* package-grouped items with descriptions.
|
|
7
|
+
*
|
|
8
|
+
* Toggle via: ~/.unipi/config/command-enchantment/config.json
|
|
9
|
+
* Setting: autocompleteEnhanced (default: true)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import { createEnchantedProvider } from "./provider.js";
|
|
14
|
+
import { isAutocompleteEnhanced } from "./settings.js";
|
|
15
|
+
|
|
16
|
+
export default function commandEnchantment(pi: ExtensionAPI): void {
|
|
17
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
18
|
+
// Check if enhancement is enabled
|
|
19
|
+
const enabled = isAutocompleteEnhanced();
|
|
20
|
+
|
|
21
|
+
if (!enabled) {
|
|
22
|
+
// When disabled, don't register the provider at all — pure passthrough
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Register the autocomplete provider that wraps the base provider
|
|
27
|
+
ctx.ui.addAutocompleteProvider((current) =>
|
|
28
|
+
createEnchantedProvider(current, enabled),
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/command-enchantment — Autocomplete Provider
|
|
3
|
+
*
|
|
4
|
+
* Intercepts /unipi:* autocomplete and returns enhanced items with
|
|
5
|
+
* package-colored tags, sorted grouping, and descriptions.
|
|
6
|
+
* Non-unipi commands pass through unchanged.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
AutocompleteItem,
|
|
11
|
+
AutocompleteProvider,
|
|
12
|
+
AutocompleteSuggestions,
|
|
13
|
+
} from "@mariozechner/pi-tui";
|
|
14
|
+
import { fuzzyFilter } from "@mariozechner/pi-tui";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
COMMAND_REGISTRY,
|
|
18
|
+
COMMAND_DESCRIPTIONS,
|
|
19
|
+
PACKAGE_COLORS,
|
|
20
|
+
PACKAGE_LABELS,
|
|
21
|
+
PACKAGE_ORDER,
|
|
22
|
+
colorize,
|
|
23
|
+
} from "./constants.js";
|
|
24
|
+
|
|
25
|
+
// ─── Fuzzy matching ──────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/** Simple character-subsequence fuzzy match (case-insensitive) */
|
|
28
|
+
function fuzzyMatch(text: string, query: string): boolean {
|
|
29
|
+
if (!query) return true;
|
|
30
|
+
const lower = text.toLowerCase();
|
|
31
|
+
const q = query.toLowerCase();
|
|
32
|
+
let qi = 0;
|
|
33
|
+
for (let i = 0; i < lower.length && qi < q.length; i++) {
|
|
34
|
+
if (lower[i] === q[qi]) qi++;
|
|
35
|
+
}
|
|
36
|
+
return qi === q.length;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Namespace detection ─────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* If the query looks like a package namespace (e.g. "workflow", "memory",
|
|
43
|
+
* "utility"), return that package name so its commands sort to the top.
|
|
44
|
+
* Returns null when the query isn't a pure namespace search.
|
|
45
|
+
*/
|
|
46
|
+
function detectNamespaceBoost(query: string): string | null {
|
|
47
|
+
if (!query) return null;
|
|
48
|
+
const q = query.toLowerCase();
|
|
49
|
+
// Direct match against known package names (and common aliases)
|
|
50
|
+
const NAMESPACE_ALIASES: Record<string, string> = {
|
|
51
|
+
// Full package names
|
|
52
|
+
workflow: "workflow",
|
|
53
|
+
ralph: "ralph",
|
|
54
|
+
memory: "memory",
|
|
55
|
+
mcp: "mcp",
|
|
56
|
+
utility: "utility",
|
|
57
|
+
"ask-user": "ask-user",
|
|
58
|
+
info: "info",
|
|
59
|
+
"web-api": "web-api",
|
|
60
|
+
compact: "compact",
|
|
61
|
+
notify: "notify",
|
|
62
|
+
// Unambiguous short aliases
|
|
63
|
+
mem: "memory",
|
|
64
|
+
util: "utility",
|
|
65
|
+
web: "web-api",
|
|
66
|
+
notification: "notify",
|
|
67
|
+
};
|
|
68
|
+
return NAMESPACE_ALIASES[q] ?? null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Enhanced item generation ────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generate enhanced autocomplete items for unipi commands,
|
|
75
|
+
* sorted by package order then alphabetically within each package.
|
|
76
|
+
* When the query matches a package namespace, that package floats to top.
|
|
77
|
+
*/
|
|
78
|
+
function getEnhancedUnipiItems(
|
|
79
|
+
prefix: string,
|
|
80
|
+
descriptionOverrides: Map<string, string> = new Map(),
|
|
81
|
+
): AutocompleteItem[] {
|
|
82
|
+
// The base provider sets prefix = full textBeforeCursor e.g. "/uni", "/unipi:brain".
|
|
83
|
+
// Two cases:
|
|
84
|
+
// Case A "/unipi:something" — user typed past the colon; match the short name.
|
|
85
|
+
// Case B "/something" — user is still forming the command word; the base
|
|
86
|
+
// fuzzyFilter matched against the full "unipi:work"
|
|
87
|
+
// string, so we must do the same.
|
|
88
|
+
const stripped = prefix.replace(/^\//, "").toLowerCase();
|
|
89
|
+
const isPastUnipiColon = stripped.startsWith("unipi:");
|
|
90
|
+
const query = isPastUnipiColon
|
|
91
|
+
? stripped.slice("unipi:".length) // e.g. "brain" from "/unipi:brain"
|
|
92
|
+
: stripped; // e.g. "uni" from "/uni"
|
|
93
|
+
|
|
94
|
+
const entries = Object.entries(COMMAND_REGISTRY);
|
|
95
|
+
|
|
96
|
+
// Detect namespace query: when the query is exactly a package name/alias
|
|
97
|
+
// (e.g. "workflow", "mem", "utility") short-circuit and return ALL commands
|
|
98
|
+
// from that package, sorted by name, with other packages following.
|
|
99
|
+
const boostedPackage = detectNamespaceBoost(query);
|
|
100
|
+
|
|
101
|
+
let matched: [string, string][];
|
|
102
|
+
|
|
103
|
+
if (boostedPackage) {
|
|
104
|
+
// Namespace mode: show boosted package first (all its commands), then
|
|
105
|
+
// remaining packages in normal order.
|
|
106
|
+
// Works for both "/workflow" and "/unipi:workflow".
|
|
107
|
+
matched = entries;
|
|
108
|
+
} else {
|
|
109
|
+
// Case A: match short name ("brain" against "brainstorm")
|
|
110
|
+
// Case B: match full value ("uni" against "unipi:work") so all unipi
|
|
111
|
+
// commands surface when the user hasn't typed the full prefix.
|
|
112
|
+
matched = entries.filter(([cmd]) => {
|
|
113
|
+
if (isPastUnipiColon) {
|
|
114
|
+
return fuzzyMatch(cmd.replace("unipi:", "").toLowerCase(), query);
|
|
115
|
+
}
|
|
116
|
+
return fuzzyMatch(cmd.toLowerCase(), query);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Sort: boosted package first, then by PACKAGE_ORDER, then alphabetically.
|
|
121
|
+
matched.sort((a, b) => {
|
|
122
|
+
const pkgA = a[1];
|
|
123
|
+
const pkgB = b[1];
|
|
124
|
+
|
|
125
|
+
if (boostedPackage) {
|
|
126
|
+
const aIsBoosted = pkgA === boostedPackage;
|
|
127
|
+
const bIsBoosted = pkgB === boostedPackage;
|
|
128
|
+
if (aIsBoosted && !bIsBoosted) return -1;
|
|
129
|
+
if (!aIsBoosted && bIsBoosted) return 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const orderA = PACKAGE_ORDER.indexOf(pkgA);
|
|
133
|
+
const orderB = PACKAGE_ORDER.indexOf(pkgB);
|
|
134
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
135
|
+
return a[0].localeCompare(b[0]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Map to AutocompleteItem format
|
|
139
|
+
return matched.map(([cmd, pkg]) => {
|
|
140
|
+
const color = PACKAGE_COLORS[pkg] ?? "";
|
|
141
|
+
const label = PACKAGE_LABELS[pkg] ?? pkg;
|
|
142
|
+
|
|
143
|
+
// Use description from base suggestions if available, else from our map
|
|
144
|
+
const desc =
|
|
145
|
+
descriptionOverrides.get(cmd) ??
|
|
146
|
+
COMMAND_DESCRIPTIONS[cmd] ??
|
|
147
|
+
"";
|
|
148
|
+
|
|
149
|
+
const tag = colorize(color, `[${label}]`);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
value: cmd,
|
|
153
|
+
label: cmd.replace("unipi:", ""),
|
|
154
|
+
description: desc ? `${tag} ${desc}` : tag,
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Argument re-trigger helpers ─────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Return true when textBeforeCursor is inside the arguments of a /unipi:* command.
|
|
163
|
+
* e.g. "/unipi:work " or "/unipi:work plan:foo" → true
|
|
164
|
+
* "/unipi:work" (no space) → false
|
|
165
|
+
*/
|
|
166
|
+
function isInUnipiArgPosition(textBeforeCursor: string): boolean {
|
|
167
|
+
const spaceIdx = textBeforeCursor.indexOf(" ");
|
|
168
|
+
if (spaceIdx === -1) return false;
|
|
169
|
+
const cmdName = textBeforeCursor.slice(1, spaceIdx); // strip leading "/"
|
|
170
|
+
return cmdName.startsWith("unipi:");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Provider factory ────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Create an enhanced autocomplete provider that wraps the base provider.
|
|
177
|
+
*
|
|
178
|
+
* @param current - The base (CombinedAutocompleteProvider) to delegate to
|
|
179
|
+
* @param enabled - Whether enhancement is active (if false, pure delegation)
|
|
180
|
+
*/
|
|
181
|
+
export function createEnchantedProvider(
|
|
182
|
+
current: AutocompleteProvider,
|
|
183
|
+
enabled: boolean,
|
|
184
|
+
): AutocompleteProvider {
|
|
185
|
+
return {
|
|
186
|
+
async getSuggestions(
|
|
187
|
+
lines: string[],
|
|
188
|
+
cursorLine: number,
|
|
189
|
+
cursorCol: number,
|
|
190
|
+
options: { signal: AbortSignal; force?: boolean },
|
|
191
|
+
): Promise<AutocompleteSuggestions | null> {
|
|
192
|
+
// When disabled, pure passthrough
|
|
193
|
+
if (!enabled) {
|
|
194
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const currentLine = lines[cursorLine] ?? "";
|
|
198
|
+
const textBeforeCursor = currentLine.slice(0, cursorCol);
|
|
199
|
+
|
|
200
|
+
// Only intercept slash commands
|
|
201
|
+
if (!textBeforeCursor.startsWith("/")) {
|
|
202
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Argument position ──────────────────────────────────────────
|
|
206
|
+
// When there's a space, the user is typing arguments for a command.
|
|
207
|
+
// The base provider handles argument completions only when force=false
|
|
208
|
+
// (it skips the slash-command path entirely when force=true, falling
|
|
209
|
+
// back to file suggestions instead). We fix that by always calling
|
|
210
|
+
// base with force=false so the getArgumentCompletions path is taken.
|
|
211
|
+
if (textBeforeCursor.includes(" ")) {
|
|
212
|
+
if (isInUnipiArgPosition(textBeforeCursor)) {
|
|
213
|
+
// Force the non-force path so argument completions are returned
|
|
214
|
+
// even when the editor called us with force=true (Tab key after space).
|
|
215
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, {
|
|
216
|
+
...options,
|
|
217
|
+
force: false,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Command-name position ──────────────────────────────────────
|
|
224
|
+
// Get base suggestions (includes all commands)
|
|
225
|
+
const baseSuggestions = await current.getSuggestions(
|
|
226
|
+
lines,
|
|
227
|
+
cursorLine,
|
|
228
|
+
cursorCol,
|
|
229
|
+
options,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Separate: keep non-unipi items, collect unipi descriptions
|
|
233
|
+
const nonUnipiItems: AutocompleteItem[] = [];
|
|
234
|
+
const descriptionOverrides = new Map<string, string>();
|
|
235
|
+
|
|
236
|
+
if (baseSuggestions) {
|
|
237
|
+
for (const item of baseSuggestions.items) {
|
|
238
|
+
if (item.value.startsWith("unipi:")) {
|
|
239
|
+
if (item.description) {
|
|
240
|
+
descriptionOverrides.set(item.value, item.description);
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
nonUnipiItems.push(item);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// The prefix we pass to getEnhancedUnipiItems: prefer base's prefix
|
|
249
|
+
// (which is the full textBeforeCursor), fall back to textBeforeCursor.
|
|
250
|
+
const effectivePrefix = baseSuggestions?.prefix ?? textBeforeCursor;
|
|
251
|
+
|
|
252
|
+
// Generate enhanced unipi items (handles namespace queries too)
|
|
253
|
+
const enhancedUnipiItems = getEnhancedUnipiItems(
|
|
254
|
+
effectivePrefix,
|
|
255
|
+
descriptionOverrides,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// If no unipi items match, just return non-unipi (or null if empty)
|
|
259
|
+
if (enhancedUnipiItems.length === 0) {
|
|
260
|
+
return nonUnipiItems.length > 0
|
|
261
|
+
? { items: nonUnipiItems, prefix: effectivePrefix }
|
|
262
|
+
: null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Merge: non-unipi first, then enhanced unipi (sorted by package)
|
|
266
|
+
return {
|
|
267
|
+
items: [...nonUnipiItems, ...enhancedUnipiItems],
|
|
268
|
+
prefix: effectivePrefix,
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
applyCompletion(
|
|
273
|
+
lines: string[],
|
|
274
|
+
cursorLine: number,
|
|
275
|
+
cursorCol: number,
|
|
276
|
+
item: AutocompleteItem,
|
|
277
|
+
prefix: string,
|
|
278
|
+
) {
|
|
279
|
+
return current.applyCompletion(
|
|
280
|
+
lines,
|
|
281
|
+
cursorLine,
|
|
282
|
+
cursorCol,
|
|
283
|
+
item,
|
|
284
|
+
prefix,
|
|
285
|
+
);
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
shouldTriggerFileCompletion(
|
|
289
|
+
lines: string[],
|
|
290
|
+
cursorLine: number,
|
|
291
|
+
cursorCol: number,
|
|
292
|
+
): boolean {
|
|
293
|
+
const currentLine = lines[cursorLine] ?? "";
|
|
294
|
+
const textBeforeCursor = currentLine.slice(0, cursorCol);
|
|
295
|
+
|
|
296
|
+
// When Tab is pressed inside a /unipi:* argument context we still
|
|
297
|
+
// want getSuggestions to be called (returning true here allows that),
|
|
298
|
+
// and getSuggestions will override force=false so the base provider
|
|
299
|
+
// takes the argument-completion path instead of the file path.
|
|
300
|
+
if (isInUnipiArgPosition(textBeforeCursor)) {
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/command-enchantment — Settings
|
|
3
|
+
*
|
|
4
|
+
* Manages the autocompleteEnhanced toggle.
|
|
5
|
+
* Persists to ~/.unipi/config/command-enchantment/config.json
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
|
|
12
|
+
/** Config structure */
|
|
13
|
+
export interface CommandEnchantmentConfig {
|
|
14
|
+
autocompleteEnhanced: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Default configuration */
|
|
18
|
+
const DEFAULT_CONFIG: CommandEnchantmentConfig = {
|
|
19
|
+
autocompleteEnhanced: true,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Config directory path */
|
|
23
|
+
function getConfigDir(): string {
|
|
24
|
+
return path.join(os.homedir(), ".unipi", "config", "command-enchantment");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Config file path */
|
|
28
|
+
function getConfigPath(): string {
|
|
29
|
+
return path.join(getConfigDir(), "config.json");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Ensure config directory exists */
|
|
33
|
+
function ensureConfigDir(): void {
|
|
34
|
+
const dir = getConfigDir();
|
|
35
|
+
if (!fs.existsSync(dir)) {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load configuration from disk.
|
|
42
|
+
* Returns defaults if file doesn't exist or is malformed.
|
|
43
|
+
*/
|
|
44
|
+
export function loadConfig(): CommandEnchantmentConfig {
|
|
45
|
+
try {
|
|
46
|
+
const configPath = getConfigPath();
|
|
47
|
+
if (fs.existsSync(configPath)) {
|
|
48
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
49
|
+
const config = JSON.parse(content) as Partial<CommandEnchantmentConfig>;
|
|
50
|
+
return {
|
|
51
|
+
...DEFAULT_CONFIG,
|
|
52
|
+
...config,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error("[command-enchantment] Failed to load config:", error);
|
|
57
|
+
}
|
|
58
|
+
return DEFAULT_CONFIG;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Save configuration to disk.
|
|
63
|
+
*/
|
|
64
|
+
export function saveConfig(config: CommandEnchantmentConfig): void {
|
|
65
|
+
ensureConfigDir();
|
|
66
|
+
const configPath = getConfigPath();
|
|
67
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if autocomplete enhancement is enabled.
|
|
72
|
+
*/
|
|
73
|
+
export function isAutocompleteEnhanced(): boolean {
|
|
74
|
+
return loadConfig().autocompleteEnhanced;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Enable or disable autocomplete enhancement.
|
|
79
|
+
*/
|
|
80
|
+
export function setAutocompleteEnhanced(enabled: boolean): void {
|
|
81
|
+
const config = loadConfig();
|
|
82
|
+
config.autocompleteEnhanced = enabled;
|
|
83
|
+
saveConfig(config);
|
|
84
|
+
}
|