@dex-ai/coding-agent 0.1.92

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.
Files changed (70) hide show
  1. package/bin/dex.ts +402 -0
  2. package/package.json +45 -0
  3. package/src/__tests__/command-validation.test.ts +205 -0
  4. package/src/__tests__/history.test.ts +183 -0
  5. package/src/cli-extension.ts +153 -0
  6. package/src/commands/extension-loader.ts +399 -0
  7. package/src/commands/extension.ts +924 -0
  8. package/src/commands/update.ts +419 -0
  9. package/src/env.d.ts +5 -0
  10. package/src/extensions/cli-tui-components/ActivityPanel.vue +24 -0
  11. package/src/extensions/cli-tui-components/ActivityPanel.vue.compiled.ts +96 -0
  12. package/src/extensions/cli-tui-components/App.vue +127 -0
  13. package/src/extensions/cli-tui-components/App.vue.compiled.ts +374 -0
  14. package/src/extensions/cli-tui-components/ApprovalPrompt.vue +30 -0
  15. package/src/extensions/cli-tui-components/ApprovalPrompt.vue.compiled.ts +72 -0
  16. package/src/extensions/cli-tui-components/AskPanel.vue +228 -0
  17. package/src/extensions/cli-tui-components/AskPanel.vue.compiled.ts +419 -0
  18. package/src/extensions/cli-tui-components/CommandPalette.vue +19 -0
  19. package/src/extensions/cli-tui-components/CommandPalette.vue.compiled.ts +65 -0
  20. package/src/extensions/cli-tui-components/ConfirmModal.vue +29 -0
  21. package/src/extensions/cli-tui-components/ConfirmModal.vue.compiled.ts +72 -0
  22. package/src/extensions/cli-tui-components/DiffView.vue +139 -0
  23. package/src/extensions/cli-tui-components/DiffView.vue.compiled.ts +274 -0
  24. package/src/extensions/cli-tui-components/FormModal.vue +58 -0
  25. package/src/extensions/cli-tui-components/FormModal.vue.compiled.ts +156 -0
  26. package/src/extensions/cli-tui-components/Header.vue +13 -0
  27. package/src/extensions/cli-tui-components/Header.vue.compiled.ts +42 -0
  28. package/src/extensions/cli-tui-components/InputArea.vue +202 -0
  29. package/src/extensions/cli-tui-components/InputArea.vue.compiled.ts +243 -0
  30. package/src/extensions/cli-tui-components/InteractivePanel.vue +32 -0
  31. package/src/extensions/cli-tui-components/InteractivePanel.vue.compiled.ts +103 -0
  32. package/src/extensions/cli-tui-components/ListModal.vue +58 -0
  33. package/src/extensions/cli-tui-components/ListModal.vue.compiled.ts +130 -0
  34. package/src/extensions/cli-tui-components/MarkdownContent.ts +54 -0
  35. package/src/extensions/cli-tui-components/Messages.vue +68 -0
  36. package/src/extensions/cli-tui-components/Messages.vue.compiled.ts +253 -0
  37. package/src/extensions/cli-tui-components/Modal.vue +56 -0
  38. package/src/extensions/cli-tui-components/Modal.vue.compiled.ts +61 -0
  39. package/src/extensions/cli-tui-components/SettingsPanel.vue +178 -0
  40. package/src/extensions/cli-tui-components/SettingsPanel.vue.compiled.ts +359 -0
  41. package/src/extensions/cli-tui-components/Spinner.vue +19 -0
  42. package/src/extensions/cli-tui-components/Spinner.vue.compiled.ts +42 -0
  43. package/src/extensions/cli-tui-components/StatusBar.vue +45 -0
  44. package/src/extensions/cli-tui-components/StatusBar.vue.compiled.ts +106 -0
  45. package/src/extensions/cli-tui-components/SteeringPreview.vue +11 -0
  46. package/src/extensions/cli-tui-components/SteeringPreview.vue.compiled.ts +38 -0
  47. package/src/extensions/cli-tui-components/ThinkingBlock.vue +40 -0
  48. package/src/extensions/cli-tui-components/ThinkingBlock.vue.compiled.ts +82 -0
  49. package/src/extensions/cli-tui-components/ToolCall.vue +114 -0
  50. package/src/extensions/cli-tui-components/ToolCall.vue.compiled.ts +319 -0
  51. package/src/extensions/cli-tui-components/UserMessage.vue +40 -0
  52. package/src/extensions/cli-tui-components/UserMessage.vue.compiled.ts +148 -0
  53. package/src/extensions/cli-tui-components/ask-panel-controller.ts +573 -0
  54. package/src/extensions/cli-tui-components/settings-panel-controller.ts +958 -0
  55. package/src/extensions/cli-tui.ts +2349 -0
  56. package/src/extensions/debug.ts +46 -0
  57. package/src/extensions/headless.ts +55 -0
  58. package/src/extensions/modal-system.ts +719 -0
  59. package/src/host.ts +505 -0
  60. package/src/index.ts +9 -0
  61. package/src/input/history.ts +233 -0
  62. package/src/input/index.ts +6 -0
  63. package/src/panels/dynamic-panel.ts +5 -0
  64. package/src/panels/index.ts +43 -0
  65. package/src/panels/state.ts +73 -0
  66. package/src/panels/types.ts +79 -0
  67. package/src/panels/widget.ts +25 -0
  68. package/src/provider-registry.ts +44 -0
  69. package/src/stderr-capture.ts +248 -0
  70. package/src/types.ts +20 -0
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Extension loader — loads installed extensions from registry.
3
+ *
4
+ * Reads registry.json from user and project scopes, resolves each extension's
5
+ * entry point from its isolated node_modules/, and imports it.
6
+ *
7
+ * Each extension is expected to export:
8
+ * - default: Extension | Extension[] (native dex extension)
9
+ * - For pi-compat: detected via "pi" field in package.json, loaded via @dex-ai/pi-compat
10
+ */
11
+
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { join, resolve } from "node:path";
14
+ import { homedir } from "node:os";
15
+ import type { Extension } from "@dex-ai/sdk";
16
+
17
+ /* ------------------------------------------------------------------ */
18
+ /* Constants */
19
+ /* ------------------------------------------------------------------ */
20
+
21
+ const USER_EXTENSIONS_DIR = join(homedir(), ".dex", "extensions");
22
+ const USER_PACKAGES_DIR = join(USER_EXTENSIONS_DIR, "packages");
23
+ const USER_REGISTRY_PATH = join(USER_EXTENSIONS_DIR, "registry.json");
24
+
25
+ /**
26
+ * Extensions that are always injected by CodingAgent.create() (the SDK layer).
27
+ * User-installed copies of these would result in duplicates — skip loading them.
28
+ */
29
+ const SDK_BUILTIN_PACKAGES = new Set([
30
+ "@dex-ai/tools-extension",
31
+ "@dex-ai/skills-extension",
32
+ "@dex-ai/ask-extension",
33
+ ]);
34
+
35
+ /* ------------------------------------------------------------------ */
36
+ /* Types */
37
+ /* ------------------------------------------------------------------ */
38
+
39
+ interface RegistryEntry {
40
+ source: "npm" | "file";
41
+ package: string;
42
+ version?: string;
43
+ path?: string;
44
+ type?: "native" | "pi-compat";
45
+ installedAt: string;
46
+ }
47
+
48
+ interface Registry {
49
+ version: 1;
50
+ extensions: Record<string, RegistryEntry>;
51
+ }
52
+
53
+ /* ------------------------------------------------------------------ */
54
+ /* Registry reading */
55
+ /* ------------------------------------------------------------------ */
56
+
57
+ function loadRegistry(registryPath: string): Registry {
58
+ if (!existsSync(registryPath)) {
59
+ return { version: 1, extensions: {} };
60
+ }
61
+ try {
62
+ const data = JSON.parse(readFileSync(registryPath, "utf-8"));
63
+ if (data.version === 1 && data.extensions) return data;
64
+ return { version: 1, extensions: {} };
65
+ } catch {
66
+ return { version: 1, extensions: {} };
67
+ }
68
+ }
69
+
70
+ /* ------------------------------------------------------------------ */
71
+ /* Entry point resolution */
72
+ /* ------------------------------------------------------------------ */
73
+
74
+ /**
75
+ * Resolve the entry point for an extension package.
76
+ *
77
+ * Checks in order:
78
+ * 1. package.json "dex.extensions" field
79
+ * 2. package.json "exports"."." field
80
+ * 3. package.json "main" field
81
+ * 4. index.ts / index.js
82
+ */
83
+ function resolveEntryPoint(packageDir: string): string | null {
84
+ const pkgJsonPath = join(packageDir, "package.json");
85
+ if (!existsSync(pkgJsonPath)) return null;
86
+
87
+ try {
88
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
89
+
90
+ // 1. dex.extensions field
91
+ if (pkg.dex?.extensions?.length) {
92
+ const first = pkg.dex.extensions[0];
93
+ const resolved = join(packageDir, first);
94
+ if (existsSync(resolved)) return resolved;
95
+ }
96
+
97
+ // 2. exports."." field
98
+ if (pkg.exports?.["."]?.default) {
99
+ const resolved = join(packageDir, pkg.exports["."].default);
100
+ if (existsSync(resolved)) return resolved;
101
+ }
102
+ if (pkg.exports?.["."]) {
103
+ const exp = pkg.exports["."];
104
+ if (typeof exp === "string") {
105
+ const resolved = join(packageDir, exp);
106
+ if (existsSync(resolved)) return resolved;
107
+ }
108
+ }
109
+
110
+ // 3. main field
111
+ if (pkg.main) {
112
+ const resolved = join(packageDir, pkg.main);
113
+ if (existsSync(resolved)) return resolved;
114
+ }
115
+
116
+ // 4. index.ts / index.js
117
+ const indexTs = join(packageDir, "index.ts");
118
+ if (existsSync(indexTs)) return indexTs;
119
+ const indexJs = join(packageDir, "index.js");
120
+ if (existsSync(indexJs)) return indexJs;
121
+
122
+ // 5. src/index.ts (common convention)
123
+ const srcIndexTs = join(packageDir, "src", "index.ts");
124
+ if (existsSync(srcIndexTs)) return srcIndexTs;
125
+
126
+ return null;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ /* ------------------------------------------------------------------ */
133
+ /* Pi-compat loading */
134
+ /* ------------------------------------------------------------------ */
135
+
136
+ async function loadPiCompatExtension(
137
+ extensionDir: string,
138
+ packageName: string,
139
+ ): Promise<Extension | null> {
140
+ // Find @dex-ai/pi-compat — check user packages first
141
+ const piCompatDir = join(USER_PACKAGES_DIR, "pi-compat");
142
+ const piCompatPkgDir = join(
143
+ piCompatDir,
144
+ "node_modules",
145
+ "@dex-ai",
146
+ "pi-compat",
147
+ );
148
+
149
+ if (!existsSync(piCompatPkgDir)) {
150
+ console.error(
151
+ `Cannot load pi-compat extension "${packageName}": @dex-ai/pi-compat is not installed.`,
152
+ );
153
+ console.error(
154
+ "Install it with: dex extension install npm:@dex-ai/pi-compat",
155
+ );
156
+ return null;
157
+ }
158
+
159
+ try {
160
+ const piCompatEntry = resolveEntryPoint(piCompatPkgDir);
161
+ if (!piCompatEntry) {
162
+ console.error(
163
+ "Cannot resolve @dex-ai/pi-compat entry point. Try reinstalling it.",
164
+ );
165
+ return null;
166
+ }
167
+
168
+ const piCompat = await import(piCompatEntry);
169
+ const piCompatExtension =
170
+ piCompat.piCompatExtension ?? piCompat.default?.piCompatExtension;
171
+
172
+ if (typeof piCompatExtension !== "function") {
173
+ console.error(
174
+ "@dex-ai/pi-compat does not export piCompatExtension function.",
175
+ );
176
+ return null;
177
+ }
178
+
179
+ // The pi-compat adapter loads the extension from the package's node_modules
180
+ const packageDir = join(extensionDir, "node_modules", packageName);
181
+ const ext = await piCompatExtension({
182
+ paths: [packageDir],
183
+ });
184
+ return ext;
185
+ } catch (err) {
186
+ console.error(
187
+ `Failed to load pi-compat extension "${packageName}":`,
188
+ err instanceof Error ? err.message : err,
189
+ );
190
+ return null;
191
+ }
192
+ }
193
+
194
+ /* ------------------------------------------------------------------ */
195
+ /* Native extension loading */
196
+ /* ------------------------------------------------------------------ */
197
+
198
+ async function loadNativeExtension(
199
+ extensionDir: string,
200
+ packageName: string,
201
+ ): Promise<Extension[]> {
202
+ const packageDir = join(extensionDir, "node_modules", packageName);
203
+
204
+ if (!existsSync(packageDir)) {
205
+ console.error(`Extension package not found at: ${packageDir}`);
206
+ return [];
207
+ }
208
+
209
+ const entryPoint = resolveEntryPoint(packageDir);
210
+ if (!entryPoint) {
211
+ console.error(
212
+ `Cannot resolve entry point for "${packageName}" in ${packageDir}`,
213
+ );
214
+ return [];
215
+ }
216
+
217
+ try {
218
+ const mod = await import(entryPoint);
219
+ const ext = mod.default;
220
+
221
+ if (Array.isArray(ext)) {
222
+ return ext.filter(
223
+ (e: unknown) => e && typeof e === "object" && (e as any).name,
224
+ );
225
+ }
226
+ if (ext && typeof ext === "object" && ext.name) {
227
+ return [ext];
228
+ }
229
+ if (typeof ext === "function") {
230
+ let result = ext();
231
+ // Await if the factory returns a promise (async factories)
232
+ if (
233
+ result &&
234
+ typeof result === "object" &&
235
+ typeof result.then === "function"
236
+ ) {
237
+ result = await result;
238
+ }
239
+ if (result && typeof result === "object" && result.name) {
240
+ return [result];
241
+ }
242
+ }
243
+
244
+ // Try named exports
245
+ const extensions: Extension[] = [];
246
+ for (const [key, value] of Object.entries(mod)) {
247
+ if (key === "default") continue;
248
+
249
+ // Named export is already an Extension object
250
+ if (value && typeof value === "object" && (value as any).name) {
251
+ extensions.push(value as Extension);
252
+ continue;
253
+ }
254
+
255
+ // Named export is a factory function (e.g. devtoolsExtension())
256
+ if (typeof value === "function" && key.includes("xtension")) {
257
+ try {
258
+ let result = (value as Function)();
259
+ // Await if the factory returns a promise (async factories)
260
+ if (
261
+ result &&
262
+ typeof result === "object" &&
263
+ typeof result.then === "function"
264
+ ) {
265
+ result = await result;
266
+ }
267
+ if (result && typeof result === "object" && (result as any).name) {
268
+ extensions.push(result as Extension);
269
+ }
270
+ } catch {
271
+ // Factory requires args — skip
272
+ }
273
+ }
274
+ }
275
+ if (extensions.length > 0) return extensions;
276
+
277
+ // Extension loaded but no Extension objects found.
278
+ // This is normal for extensions that require configuration.
279
+ return [];
280
+ } catch (err) {
281
+ console.error(
282
+ `Failed to load extension "${packageName}":`,
283
+ err instanceof Error ? err.message : err,
284
+ );
285
+ return [];
286
+ }
287
+ }
288
+
289
+ /* ------------------------------------------------------------------ */
290
+ /* Public API */
291
+ /* ------------------------------------------------------------------ */
292
+
293
+ export interface LoadedExtensions {
294
+ extensions: Extension[];
295
+ providers: Map<string, Extension>;
296
+ }
297
+
298
+ /**
299
+ * Load all installed extensions from user and project registries.
300
+ * Project scope wins on name collision.
301
+ */
302
+ export async function loadInstalledExtensions(
303
+ cwd: string,
304
+ ): Promise<LoadedExtensions> {
305
+ const extensions: Extension[] = [];
306
+ const providers = new Map<string, Extension>();
307
+
308
+ // Load registries
309
+ const userRegistry = loadRegistry(USER_REGISTRY_PATH);
310
+ const projectRegistryPath = join(cwd, ".dex", "extensions", "registry.json");
311
+
312
+ // Skip project scope if it resolves to the same path as user scope
313
+ const isSamePath =
314
+ resolve(projectRegistryPath) === resolve(USER_REGISTRY_PATH);
315
+ const projectRegistry = isSamePath
316
+ ? { version: 1 as const, extensions: {} as Record<string, RegistryEntry> }
317
+ : loadRegistry(projectRegistryPath);
318
+
319
+ // Merge: project wins on collision
320
+ const merged: Record<
321
+ string,
322
+ { entry: RegistryEntry; scope: "user" | "project" }
323
+ > = {};
324
+ for (const [name, entry] of Object.entries(userRegistry.extensions)) {
325
+ merged[name] = { entry, scope: "user" };
326
+ }
327
+ for (const [name, entry] of Object.entries(projectRegistry.extensions)) {
328
+ merged[name] = { entry, scope: "project" };
329
+ }
330
+
331
+ // Prepend installed extension binaries to PATH
332
+ for (const [name, { scope }] of Object.entries(merged)) {
333
+ const extDir =
334
+ scope === "project"
335
+ ? join(cwd, ".dex", "extensions", name)
336
+ : join(USER_PACKAGES_DIR, name);
337
+ const binPath = join(extDir, "node_modules", ".bin");
338
+ if (existsSync(binPath)) {
339
+ process.env.PATH = `${binPath}:${process.env.PATH}`;
340
+ }
341
+ }
342
+
343
+ // Load each extension
344
+ for (const [name, { entry, scope }] of Object.entries(merged)) {
345
+ // Skip extensions that the SDK injects internally (avoids duplicates)
346
+ if (SDK_BUILTIN_PACKAGES.has(entry.package)) {
347
+ continue;
348
+ }
349
+
350
+ const extensionDir =
351
+ scope === "project"
352
+ ? join(cwd, ".dex", "extensions", name)
353
+ : join(USER_PACKAGES_DIR, name);
354
+
355
+ if (!existsSync(extensionDir)) {
356
+ console.error(
357
+ `Extension "${name}" directory not found. Run: dex extension install ${entry.source}:${entry.package}`,
358
+ );
359
+ continue;
360
+ }
361
+
362
+ if (entry.type === "pi-compat") {
363
+ const ext = await loadPiCompatExtension(extensionDir, entry.package);
364
+ if (ext) {
365
+ // Pi-compat extensions may provide models (providers)
366
+ if ((ext as any).models && (ext as any).models.length > 0) {
367
+ providers.set(name, ext);
368
+ } else {
369
+ extensions.push(ext);
370
+ }
371
+ }
372
+ } else {
373
+ const exts = await loadNativeExtension(extensionDir, entry.package);
374
+ for (const ext of exts) {
375
+ if ((ext as any).models && (ext as any).models.length > 0) {
376
+ providers.set(name, ext);
377
+ } else {
378
+ extensions.push(ext);
379
+ }
380
+ }
381
+ }
382
+ }
383
+
384
+ return { extensions, providers };
385
+ }
386
+
387
+ /**
388
+ * Get the disabled extensions list from settings.
389
+ */
390
+ export function getDisabledExtensions(): string[] {
391
+ const settingsPath = join(homedir(), ".dex", "settings.json");
392
+ if (!existsSync(settingsPath)) return [];
393
+ try {
394
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
395
+ return settings.extensions?.disabled ?? [];
396
+ } catch {
397
+ return [];
398
+ }
399
+ }