dexto 1.6.13 → 1.6.15

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.
@@ -0,0 +1,834 @@
1
+ import * as p from '@clack/prompts';
2
+ import { deriveDisplayName, findProjectRegistryPath as findSharedProjectRegistryPath, getPrimaryApiKeyEnvVar, getProjectRegistryPath as getCanonicalProjectRegistryPath, ProjectRegistrySchema, readProjectRegistry as readSharedProjectRegistry, writeConfigFile, } from '@dexto/agent-management';
3
+ import chalk from 'chalk';
4
+ import { promises as fs } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { ExitSignal, safeExit, withAnalytics } from '../../analytics/wrapper.js';
7
+ import { getDeployConfigPath, isWorkspaceDeployAgent, loadDeployConfig } from './deploy/config.js';
8
+ import { discoverPrimaryWorkspaceAgent } from './deploy/entry-agent.js';
9
+ import { selectOrExit, textOrExit } from '../utils/prompt-helpers.js';
10
+ const AGENTS_FILENAME = 'AGENTS.md';
11
+ const WORKSPACE_DIRECTORIES = ['agents', 'skills'];
12
+ const DEFAULT_AGENT_PROVIDER = 'openai';
13
+ const DEFAULT_AGENT_MODEL = 'gpt-5.3-codex';
14
+ const DEFAULT_AGENT_VERSION = '0.1.0';
15
+ const DEFAULT_AGENTS_MD = `<!-- dexto-workspace -->
16
+
17
+ # Dexto Workspace
18
+
19
+ This workspace can define project-specific agents and skills.
20
+
21
+ ## Structure
22
+ - Put custom agents and subagents in \`agents/\`
23
+ - Put custom skills in \`skills/<skill-id>/SKILL.md\`
24
+ - Use \`.dexto/\` only for Dexto-managed state and installed assets
25
+
26
+ ## Defaults
27
+ - If no workspace agent is defined, Dexto uses your global default agent locally
28
+ - Cloud deploys without a workspace agent use the managed cloud default agent
29
+ `;
30
+ const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
31
+ function isSubagentEntry(entry) {
32
+ return (entry.tags?.includes('subagent') ?? false) || Boolean(entry.parentAgentId);
33
+ }
34
+ function isPrimaryCandidate(entry) {
35
+ return !isSubagentEntry(entry);
36
+ }
37
+ function getEffectiveWorkspacePrimaryAgentId(registry) {
38
+ if (registry.primaryAgent) {
39
+ return registry.primaryAgent;
40
+ }
41
+ const candidates = registry.agents.filter(isPrimaryCandidate);
42
+ if (candidates.length === 1) {
43
+ return candidates[0]?.id ?? null;
44
+ }
45
+ return null;
46
+ }
47
+ function getWorkspaceAgentEntry(registry, agentId) {
48
+ return registry.agents.find((entry) => entry.id === agentId) ?? null;
49
+ }
50
+ async function ensureWorkspaceRoot(root) {
51
+ const stat = await fs.stat(root);
52
+ if (!stat.isDirectory()) {
53
+ throw new Error(`${root} exists and is not a directory`);
54
+ }
55
+ }
56
+ async function getExistingEntryType(entryPath) {
57
+ try {
58
+ const stat = await fs.stat(entryPath);
59
+ if (stat.isDirectory()) {
60
+ return 'directory';
61
+ }
62
+ if (stat.isFile()) {
63
+ return 'file';
64
+ }
65
+ return 'file';
66
+ }
67
+ catch (error) {
68
+ if (error.code === 'ENOENT') {
69
+ return 'missing';
70
+ }
71
+ throw error;
72
+ }
73
+ }
74
+ function normalizeScaffoldId(id, kind) {
75
+ const normalized = id.trim();
76
+ if (!ID_PATTERN.test(normalized)) {
77
+ throw new Error(`Invalid ${kind} id '${id}'. Use kebab-case like '${kind === 'agent' ? 'coding-agent' : 'code-review'}'.`);
78
+ }
79
+ return normalized;
80
+ }
81
+ async function ensureDirectory(dirPath) {
82
+ try {
83
+ const stat = await fs.stat(dirPath);
84
+ if (!stat.isDirectory()) {
85
+ throw new Error(`${dirPath} exists and is not a directory`);
86
+ }
87
+ return 'existing';
88
+ }
89
+ catch (error) {
90
+ if (error.code !== 'ENOENT') {
91
+ throw error;
92
+ }
93
+ }
94
+ await fs.mkdir(dirPath, { recursive: true });
95
+ return 'created';
96
+ }
97
+ async function ensureFile(filePath, content) {
98
+ try {
99
+ const stat = await fs.stat(filePath);
100
+ if (!stat.isFile()) {
101
+ throw new Error(`${filePath} exists and is not a file`);
102
+ }
103
+ return 'existing';
104
+ }
105
+ catch (error) {
106
+ if (error.code !== 'ENOENT') {
107
+ throw error;
108
+ }
109
+ }
110
+ await fs.writeFile(filePath, content, 'utf-8');
111
+ return 'created';
112
+ }
113
+ async function loadInitialAgentLlmConfig() {
114
+ return {
115
+ provider: DEFAULT_AGENT_PROVIDER,
116
+ model: DEFAULT_AGENT_MODEL,
117
+ apiKey: `$${getPrimaryApiKeyEnvVar(DEFAULT_AGENT_PROVIDER)}`,
118
+ };
119
+ }
120
+ function buildAgentDescription(agentId, options) {
121
+ if (options.subagent) {
122
+ return `Workspace sub-agent '${agentId}' for delegated tasks.`;
123
+ }
124
+ if (agentId === 'coding-agent') {
125
+ return 'Primary workspace agent for this project.';
126
+ }
127
+ return `Workspace agent '${agentId}' for this project.`;
128
+ }
129
+ async function buildAgentConfig(agentId, options) {
130
+ const llmConfig = await loadInitialAgentLlmConfig();
131
+ const displayName = deriveDisplayName(agentId);
132
+ const description = buildAgentDescription(agentId, options);
133
+ const llm = {
134
+ provider: llmConfig.provider,
135
+ model: llmConfig.model,
136
+ apiKey: llmConfig.apiKey,
137
+ };
138
+ return {
139
+ image: '@dexto/image-local',
140
+ agentId,
141
+ agentCard: {
142
+ name: displayName,
143
+ description,
144
+ url: `https://example.com/agents/${agentId}`,
145
+ version: DEFAULT_AGENT_VERSION,
146
+ },
147
+ systemPrompt: options.subagent
148
+ ? [
149
+ `You are ${displayName}, a specialized sub-agent for this workspace.`,
150
+ '',
151
+ 'Complete delegated tasks efficiently and concisely.',
152
+ 'Read the relevant files before responding.',
153
+ 'Return a clear result to the parent agent with concrete findings or next steps.',
154
+ ].join('\n')
155
+ : [
156
+ `You are ${displayName}, the workspace agent for this project.`,
157
+ '',
158
+ 'Help the user understand, edit, run, and deploy the files in this workspace.',
159
+ 'Read relevant files before making changes.',
160
+ 'Keep changes focused and explain what changed.',
161
+ ].join('\n'),
162
+ greeting: options.subagent
163
+ ? `Ready to help as ${displayName}.`
164
+ : 'Ready to work in this workspace.',
165
+ llm,
166
+ permissions: {
167
+ mode: 'manual',
168
+ allowedToolsStorage: 'storage',
169
+ },
170
+ };
171
+ }
172
+ function buildRegistryEntry(agentId, options) {
173
+ const description = buildAgentDescription(agentId, options);
174
+ return {
175
+ id: agentId,
176
+ name: deriveDisplayName(agentId),
177
+ description,
178
+ configPath: `./${agentId}/${agentId}.yml`,
179
+ ...(options.subagent ? { tags: ['subagent'] } : {}),
180
+ };
181
+ }
182
+ function addSubagentTag(entry) {
183
+ const tags = new Set(entry.tags ?? []);
184
+ tags.add('subagent');
185
+ return {
186
+ ...entry,
187
+ tags: Array.from(tags).sort(),
188
+ };
189
+ }
190
+ function validateInitAgentOptions(options) {
191
+ if (options.primary && options.subagent) {
192
+ throw new Error('A sub-agent cannot also be the primary workspace agent.');
193
+ }
194
+ }
195
+ async function promptForAgentId(kind) {
196
+ const placeholder = kind === 'subagent'
197
+ ? 'explore-agent'
198
+ : kind === 'primary'
199
+ ? 'review-agent'
200
+ : 'helper-agent';
201
+ return await textOrExit({
202
+ message: 'Agent id',
203
+ placeholder,
204
+ validate(value) {
205
+ try {
206
+ normalizeScaffoldId(value, 'agent');
207
+ return undefined;
208
+ }
209
+ catch (error) {
210
+ return error instanceof Error ? error.message : 'Invalid agent id';
211
+ }
212
+ },
213
+ }, 'Agent initialization cancelled');
214
+ }
215
+ async function resolveInitAgentInput(agentIdInput, options, workspaceRoot) {
216
+ validateInitAgentOptions(options);
217
+ if (agentIdInput) {
218
+ return {
219
+ agentId: normalizeScaffoldId(agentIdInput, 'agent'),
220
+ options,
221
+ };
222
+ }
223
+ if (options.subagent || options.primary) {
224
+ const kind = options.subagent ? 'subagent' : 'primary';
225
+ return {
226
+ agentId: normalizeScaffoldId(await promptForAgentId(kind), 'agent'),
227
+ options,
228
+ };
229
+ }
230
+ const registryState = await loadWorkspaceProjectRegistry(path.resolve(workspaceRoot));
231
+ const currentPrimaryAgentId = getEffectiveWorkspacePrimaryAgentId(registryState.registry);
232
+ const kind = await selectOrExit({
233
+ message: 'What kind of agent do you want to create?',
234
+ initialValue: currentPrimaryAgentId ? 'agent' : 'primary',
235
+ options: [
236
+ {
237
+ value: 'primary',
238
+ label: 'Primary agent',
239
+ hint: currentPrimaryAgentId
240
+ ? `Replace current primary (${currentPrimaryAgentId})`
241
+ : 'Main workspace agent used by default',
242
+ },
243
+ {
244
+ value: 'agent',
245
+ label: 'Additional agent',
246
+ hint: 'Workspace agent that is available but not the default',
247
+ },
248
+ {
249
+ value: 'subagent',
250
+ label: 'Subagent',
251
+ hint: 'Delegated helper agent for the primary workspace agent',
252
+ },
253
+ ],
254
+ }, 'Agent initialization cancelled');
255
+ const resolvedOptions = kind === 'primary' ? { primary: true } : kind === 'subagent' ? { subagent: true } : {};
256
+ return {
257
+ agentId: normalizeScaffoldId(await promptForAgentId(kind), 'agent'),
258
+ options: resolvedOptions,
259
+ };
260
+ }
261
+ function buildSkillTemplate(skillId) {
262
+ const displayName = deriveDisplayName(skillId);
263
+ return `---
264
+ name: "${skillId}"
265
+ description: "TODO: Describe when to use this skill."
266
+ ---
267
+
268
+ # ${displayName}
269
+
270
+ ## Purpose
271
+ Describe what this skill helps the agent accomplish.
272
+
273
+ ## Inputs
274
+ - The task or context that should trigger this skill
275
+ - Relevant files, paths, or constraints
276
+
277
+ ## Steps
278
+ 1. Review the relevant context.
279
+ 2. Apply the workflow for this skill.
280
+ 3. Return a concise result with any important follow-up actions.
281
+
282
+ ## Output Format
283
+ - Summary of what was found or changed
284
+ - Key decisions or recommendations
285
+ - Follow-up actions, if any
286
+ `;
287
+ }
288
+ async function loadWorkspaceProjectRegistry(workspaceRoot) {
289
+ const existingPath = await findSharedProjectRegistryPath(workspaceRoot);
290
+ if (!existingPath) {
291
+ return {
292
+ path: getCanonicalProjectRegistryPath(workspaceRoot),
293
+ registry: { allowGlobalAgents: false, agents: [] },
294
+ status: 'created',
295
+ };
296
+ }
297
+ return {
298
+ path: existingPath,
299
+ registry: await readSharedProjectRegistry(existingPath),
300
+ status: 'existing',
301
+ };
302
+ }
303
+ async function saveWorkspaceProjectRegistry(registryPath, registry) {
304
+ await fs.mkdir(path.dirname(registryPath), { recursive: true });
305
+ const validatedRegistry = ProjectRegistrySchema.parse(registry);
306
+ await fs.writeFile(registryPath, `${JSON.stringify(validatedRegistry, null, 2)}\n`, 'utf8');
307
+ }
308
+ export async function createWorkspaceScaffold(workspaceRoot = process.cwd()) {
309
+ const root = path.resolve(workspaceRoot);
310
+ const agentsFilePath = path.join(root, AGENTS_FILENAME);
311
+ await ensureWorkspaceRoot(root);
312
+ const agentsFileType = await getExistingEntryType(agentsFilePath);
313
+ if (agentsFileType === 'directory') {
314
+ throw new Error(`${agentsFilePath} exists and is not a file`);
315
+ }
316
+ for (const directory of WORKSPACE_DIRECTORIES) {
317
+ const dirPath = path.join(root, directory);
318
+ const entryType = await getExistingEntryType(dirPath);
319
+ if (entryType === 'file') {
320
+ throw new Error(`${dirPath} exists and is not a directory`);
321
+ }
322
+ }
323
+ const agentsFileStatus = await ensureFile(agentsFilePath, DEFAULT_AGENTS_MD);
324
+ const directories = [];
325
+ for (const directory of WORKSPACE_DIRECTORIES) {
326
+ const dirPath = path.join(root, directory);
327
+ const status = await ensureDirectory(dirPath);
328
+ directories.push({ path: dirPath, status });
329
+ }
330
+ return {
331
+ root,
332
+ agentsFile: { path: agentsFilePath, status: agentsFileStatus },
333
+ directories,
334
+ };
335
+ }
336
+ export async function createWorkspaceAgentScaffold(agentIdInput, options = {}, workspaceRoot = process.cwd()) {
337
+ validateInitAgentOptions(options);
338
+ const agentId = normalizeScaffoldId(agentIdInput, 'agent');
339
+ const workspace = await createWorkspaceScaffold(workspaceRoot);
340
+ const agentDirPath = path.join(workspace.root, 'agents', agentId);
341
+ const agentConfigPath = path.join(agentDirPath, `${agentId}.yml`);
342
+ const registryState = await loadWorkspaceProjectRegistry(workspace.root);
343
+ const existingEntry = registryState.registry.agents.find((entry) => entry.id === agentId);
344
+ const expectedRegistryEntry = buildRegistryEntry(agentId, options);
345
+ const currentPrimaryAgentId = getEffectiveWorkspacePrimaryAgentId(registryState.registry);
346
+ if (existingEntry && existingEntry.configPath !== expectedRegistryEntry.configPath) {
347
+ throw new Error(`Agent '${agentId}' already exists in ${registryState.path} with configPath '${existingEntry.configPath}'.`);
348
+ }
349
+ await ensureDirectory(agentDirPath);
350
+ let agentConfigStatus;
351
+ const agentConfigEntryType = await getExistingEntryType(agentConfigPath);
352
+ if (agentConfigEntryType === 'directory') {
353
+ throw new Error(`${agentConfigPath} exists and is not a file`);
354
+ }
355
+ if (agentConfigEntryType === 'file') {
356
+ agentConfigStatus = 'existing';
357
+ }
358
+ else {
359
+ const config = await buildAgentConfig(agentId, options);
360
+ await writeConfigFile(agentConfigPath, config);
361
+ agentConfigStatus = 'created';
362
+ }
363
+ if (existingEntry) {
364
+ let registryUpdated = false;
365
+ if (options.subagent && !isSubagentEntry(existingEntry)) {
366
+ if (currentPrimaryAgentId === agentId) {
367
+ throw new Error(`Agent '${agentId}' is currently the workspace primary agent. Set another primary agent before converting it to a subagent.`);
368
+ }
369
+ registryState.registry.agents = registryState.registry.agents.map((entry) => entry.id === agentId ? addSubagentTag(entry) : entry);
370
+ registryUpdated = true;
371
+ }
372
+ let primaryAgentStatus = 'unchanged';
373
+ if (options.primary && registryState.registry.primaryAgent !== agentId) {
374
+ const updatedEntry = getWorkspaceAgentEntry(registryState.registry, agentId);
375
+ if (!updatedEntry || !isPrimaryCandidate(updatedEntry)) {
376
+ throw new Error(`Agent '${agentId}' is marked as a subagent and cannot be selected as the workspace primary agent.`);
377
+ }
378
+ registryState.registry.primaryAgent = agentId;
379
+ primaryAgentStatus = 'set';
380
+ registryUpdated = true;
381
+ }
382
+ if (registryUpdated) {
383
+ await saveWorkspaceProjectRegistry(registryState.path, registryState.registry);
384
+ }
385
+ return {
386
+ workspace,
387
+ registry: {
388
+ path: registryState.path,
389
+ status: registryUpdated ? 'updated' : registryState.status,
390
+ },
391
+ agentConfig: { path: agentConfigPath, status: agentConfigStatus },
392
+ primaryAgent: {
393
+ id: getEffectiveWorkspacePrimaryAgentId(registryState.registry),
394
+ status: primaryAgentStatus,
395
+ },
396
+ };
397
+ }
398
+ registryState.registry.agents.push(expectedRegistryEntry);
399
+ registryState.registry.agents.sort((left, right) => left.id.localeCompare(right.id));
400
+ const primaryCandidatesAfterAdd = registryState.registry.agents.filter(isPrimaryCandidate);
401
+ let primaryAgentStatus = 'unchanged';
402
+ if (!options.subagent &&
403
+ (options.primary || (!currentPrimaryAgentId && primaryCandidatesAfterAdd.length === 1)) &&
404
+ registryState.registry.primaryAgent !== agentId) {
405
+ registryState.registry.primaryAgent = agentId;
406
+ primaryAgentStatus = 'set';
407
+ }
408
+ await saveWorkspaceProjectRegistry(registryState.path, registryState.registry);
409
+ return {
410
+ workspace,
411
+ registry: {
412
+ path: registryState.path,
413
+ status: registryState.status === 'created' ? 'created' : 'updated',
414
+ },
415
+ agentConfig: { path: agentConfigPath, status: agentConfigStatus },
416
+ primaryAgent: {
417
+ id: getEffectiveWorkspacePrimaryAgentId(registryState.registry),
418
+ status: primaryAgentStatus,
419
+ },
420
+ };
421
+ }
422
+ export async function createWorkspaceSkillScaffold(skillIdInput, workspaceRoot = process.cwd()) {
423
+ const skillId = normalizeScaffoldId(skillIdInput, 'skill');
424
+ const workspace = await createWorkspaceScaffold(workspaceRoot);
425
+ const skillDirPath = path.join(workspace.root, 'skills', skillId);
426
+ const skillFilePath = path.join(skillDirPath, 'SKILL.md');
427
+ await ensureDirectory(skillDirPath);
428
+ const skillFileStatus = await ensureFile(skillFilePath, buildSkillTemplate(skillId));
429
+ return {
430
+ workspace,
431
+ skillFile: {
432
+ path: skillFilePath,
433
+ status: skillFileStatus,
434
+ },
435
+ };
436
+ }
437
+ export async function setWorkspacePrimaryAgent(agentIdInput, workspaceRoot = process.cwd()) {
438
+ const agentId = normalizeScaffoldId(agentIdInput, 'agent');
439
+ const workspace = await createWorkspaceScaffold(workspaceRoot);
440
+ const registryState = await loadWorkspaceProjectRegistry(workspace.root);
441
+ const existingEntry = registryState.registry.agents.find((entry) => entry.id === agentId);
442
+ if (!existingEntry) {
443
+ throw new Error(`Agent '${agentId}' is not registered in ${path.relative(workspace.root, registryState.path)}. Run \`dexto init agent ${agentId}\` first or update the registry manually.`);
444
+ }
445
+ if (!isPrimaryCandidate(existingEntry)) {
446
+ throw new Error(`Agent '${agentId}' is marked as a subagent and cannot be selected as the workspace primary agent.`);
447
+ }
448
+ if (registryState.registry.primaryAgent === agentId) {
449
+ return {
450
+ workspace,
451
+ registry: { path: registryState.path, status: 'existing' },
452
+ primaryAgent: { id: agentId, status: 'existing' },
453
+ };
454
+ }
455
+ registryState.registry.primaryAgent = agentId;
456
+ await saveWorkspaceProjectRegistry(registryState.path, registryState.registry);
457
+ return {
458
+ workspace,
459
+ registry: { path: registryState.path, status: 'updated' },
460
+ primaryAgent: { id: agentId, status: 'set' },
461
+ };
462
+ }
463
+ export async function linkWorkspaceSubagentToPrimaryAgent(subagentIdInput, workspaceRoot = process.cwd()) {
464
+ const subagentId = normalizeScaffoldId(subagentIdInput, 'agent');
465
+ const workspace = await createWorkspaceScaffold(workspaceRoot);
466
+ const registryState = await loadWorkspaceProjectRegistry(workspace.root);
467
+ const subagentEntry = getWorkspaceAgentEntry(registryState.registry, subagentId);
468
+ if (!subagentEntry) {
469
+ throw new Error(`Agent '${subagentId}' is not registered in ${path.relative(workspace.root, registryState.path)}.`);
470
+ }
471
+ const primaryAgentId = getEffectiveWorkspacePrimaryAgentId(registryState.registry);
472
+ if (!primaryAgentId || primaryAgentId === subagentId) {
473
+ return {
474
+ workspace,
475
+ registry: { path: registryState.path, status: 'existing' },
476
+ subagentId,
477
+ parentAgentId: primaryAgentId,
478
+ status: 'no-primary',
479
+ };
480
+ }
481
+ const needsSubagentTag = !isSubagentEntry(subagentEntry);
482
+ if (subagentEntry.parentAgentId === primaryAgentId && !needsSubagentTag) {
483
+ return {
484
+ workspace,
485
+ registry: { path: registryState.path, status: 'existing' },
486
+ subagentId,
487
+ parentAgentId: primaryAgentId,
488
+ status: 'existing',
489
+ };
490
+ }
491
+ registryState.registry.agents = registryState.registry.agents.map((entry) => entry.id === subagentId
492
+ ? { ...addSubagentTag(entry), parentAgentId: primaryAgentId }
493
+ : entry);
494
+ await saveWorkspaceProjectRegistry(registryState.path, registryState.registry);
495
+ return {
496
+ workspace,
497
+ registry: { path: registryState.path, status: 'updated' },
498
+ subagentId,
499
+ parentAgentId: primaryAgentId,
500
+ status: 'set',
501
+ };
502
+ }
503
+ function formatCreatedPaths(result) {
504
+ const createdPaths = [];
505
+ if (result.agentsFile.status === 'created') {
506
+ createdPaths.push(path.relative(result.root, result.agentsFile.path) || AGENTS_FILENAME);
507
+ }
508
+ for (const directory of result.directories) {
509
+ if (directory.status === 'created') {
510
+ createdPaths.push(path.relative(result.root, directory.path) || path.basename(directory.path));
511
+ }
512
+ }
513
+ return createdPaths;
514
+ }
515
+ function formatAgentPaths(result) {
516
+ const createdPaths = formatCreatedPaths(result.workspace);
517
+ if (result.registry.status === 'created' || result.registry.status === 'updated') {
518
+ createdPaths.push(path.relative(result.workspace.root, result.registry.path));
519
+ }
520
+ if (result.agentConfig.status === 'created') {
521
+ createdPaths.push(path.relative(result.workspace.root, result.agentConfig.path));
522
+ }
523
+ return createdPaths;
524
+ }
525
+ function formatSkillPaths(result) {
526
+ const createdPaths = formatCreatedPaths(result.workspace);
527
+ if (result.skillFile.status === 'created') {
528
+ createdPaths.push(path.relative(result.workspace.root, result.skillFile.path));
529
+ }
530
+ return createdPaths;
531
+ }
532
+ async function listWorkspaceSkillIds(workspaceRoot) {
533
+ const skillsRoot = path.join(workspaceRoot, 'skills');
534
+ try {
535
+ const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
536
+ const skillIds = [];
537
+ for (const entry of entries) {
538
+ if (!entry.isDirectory()) {
539
+ continue;
540
+ }
541
+ const skillFilePath = path.join(skillsRoot, entry.name, 'SKILL.md');
542
+ try {
543
+ const stat = await fs.stat(skillFilePath);
544
+ if (stat.isFile()) {
545
+ skillIds.push(entry.name);
546
+ }
547
+ }
548
+ catch (error) {
549
+ if (error.code === 'ENOENT') {
550
+ continue;
551
+ }
552
+ throw error;
553
+ }
554
+ }
555
+ return skillIds.sort((left, right) => left.localeCompare(right));
556
+ }
557
+ catch (error) {
558
+ if (error.code === 'ENOENT') {
559
+ return [];
560
+ }
561
+ throw error;
562
+ }
563
+ }
564
+ function describeEffectiveDeployAgent(input) {
565
+ if (input.deployConfig) {
566
+ const configuredAgent = isWorkspaceDeployAgent(input.deployConfig.agent)
567
+ ? `workspace agent (${input.deployConfig.agent.path})`
568
+ : 'default cloud agent';
569
+ return `${configuredAgent} via ${input.deployConfigPath ?? '.dexto/deploy.json'}`;
570
+ }
571
+ if (input.implicitWorkspaceAgent) {
572
+ return `workspace agent (${input.implicitWorkspaceAgent}) if you run \`dexto deploy\``;
573
+ }
574
+ return 'default cloud agent if you run `dexto deploy`';
575
+ }
576
+ export async function inspectWorkspaceStatus(workspaceRoot = process.cwd()) {
577
+ const root = path.resolve(workspaceRoot);
578
+ await ensureWorkspaceRoot(root);
579
+ const agentsFilePresent = (await getExistingEntryType(path.join(root, AGENTS_FILENAME))) === 'file';
580
+ const agentsDirectoryPresent = (await getExistingEntryType(path.join(root, 'agents'))) === 'directory';
581
+ const skillsDirectoryPresent = (await getExistingEntryType(path.join(root, 'skills'))) === 'directory';
582
+ const registryPath = await findSharedProjectRegistryPath(root);
583
+ const registry = registryPath ? await readSharedProjectRegistry(registryPath) : null;
584
+ const primaryAgentId = registry ? getEffectiveWorkspacePrimaryAgentId(registry) : null;
585
+ const agents = registry?.agents
586
+ .map((entry) => ({
587
+ id: entry.id,
588
+ isPrimary: primaryAgentId === entry.id,
589
+ isSubagent: isSubagentEntry(entry),
590
+ parentAgentId: entry.parentAgentId ?? null,
591
+ }))
592
+ .sort((left, right) => left.id.localeCompare(right.id)) ?? [];
593
+ const skills = await listWorkspaceSkillIds(root);
594
+ const deployConfigPath = getDeployConfigPath(root);
595
+ const deployConfig = await loadDeployConfig(root);
596
+ const implicitWorkspaceAgent = await discoverPrimaryWorkspaceAgent(root);
597
+ return {
598
+ workspaceRoot: root,
599
+ agentsFilePresent,
600
+ agentsDirectoryPresent,
601
+ skillsDirectoryPresent,
602
+ registryPath,
603
+ primaryAgentId,
604
+ allowGlobalAgents: registry ? registry.allowGlobalAgents : null,
605
+ agents,
606
+ skills,
607
+ deployConfigPath: deployConfig ? deployConfigPath : null,
608
+ effectiveDeploySummary: describeEffectiveDeployAgent({
609
+ deployConfigPath: deployConfig ? deployConfigPath : null,
610
+ deployConfig,
611
+ implicitWorkspaceAgent,
612
+ }),
613
+ };
614
+ }
615
+ function formatWorkspaceStatus(result) {
616
+ return [
617
+ `Workspace: ${result.workspaceRoot}`,
618
+ `AGENTS.md: ${result.agentsFilePresent ? 'present' : 'missing'}`,
619
+ `agents/: ${result.agentsDirectoryPresent ? 'present' : 'missing'}`,
620
+ `skills/: ${result.skillsDirectoryPresent ? 'present' : 'missing'}`,
621
+ `Registry: ${result.registryPath ? path.relative(result.workspaceRoot, result.registryPath) : 'none'}`,
622
+ `Primary agent: ${result.primaryAgentId ?? 'none (global default used locally)'}`,
623
+ `Allow global agents: ${result.allowGlobalAgents === null
624
+ ? 'n/a (no workspace registry)'
625
+ : String(result.allowGlobalAgents)}`,
626
+ `Deploy: ${result.effectiveDeploySummary}`,
627
+ '',
628
+ 'Agents:',
629
+ ...(result.agents.length > 0
630
+ ? result.agents.map((agent) => {
631
+ const details = [
632
+ agent.isPrimary ? 'primary' : null,
633
+ agent.isSubagent ? 'subagent' : null,
634
+ agent.parentAgentId ? `parent: ${agent.parentAgentId}` : null,
635
+ ].filter(Boolean);
636
+ return `- ${agent.id}${details.length > 0 ? ` (${details.join(', ')})` : ''}`;
637
+ })
638
+ : ['- none']),
639
+ '',
640
+ 'Skills:',
641
+ ...(result.skills.length > 0 ? result.skills.map((skillId) => `- ${skillId}`) : ['- none']),
642
+ ].join('\n');
643
+ }
644
+ async function resolvePrimaryAgentSelection(agentIdInput, workspaceRoot) {
645
+ if (agentIdInput) {
646
+ return normalizeScaffoldId(agentIdInput, 'agent');
647
+ }
648
+ const workspace = await createWorkspaceScaffold(workspaceRoot);
649
+ const registryState = await loadWorkspaceProjectRegistry(workspace.root);
650
+ const primaryCandidates = registryState.registry.agents.filter(isPrimaryCandidate);
651
+ if (primaryCandidates.length === 0) {
652
+ throw new Error('No primary-capable workspace agents found. Run `dexto init agent` first.');
653
+ }
654
+ if (primaryCandidates.length === 1) {
655
+ return primaryCandidates[0]?.id ?? '';
656
+ }
657
+ const currentPrimaryAgentId = getEffectiveWorkspacePrimaryAgentId(registryState.registry);
658
+ const initialValue = primaryCandidates.some((entry) => entry.id === currentPrimaryAgentId)
659
+ ? currentPrimaryAgentId
660
+ : undefined;
661
+ return await selectOrExit({
662
+ message: 'Which agent should be the workspace primary?',
663
+ initialValue,
664
+ options: primaryCandidates.map((entry) => ({
665
+ value: entry.id,
666
+ label: `${entry.name} (${entry.id})`,
667
+ hint: registryState.registry.primaryAgent === entry.id
668
+ ? 'Current primary'
669
+ : entry.description,
670
+ })),
671
+ }, 'Primary agent selection cancelled');
672
+ }
673
+ export async function handleInitCommand(workspaceRoot = process.cwd()) {
674
+ p.intro(chalk.inverse('Dexto Init'));
675
+ const result = await createWorkspaceScaffold(workspaceRoot);
676
+ const createdPaths = formatCreatedPaths(result);
677
+ if (createdPaths.length === 0) {
678
+ p.outro(chalk.green('Workspace already initialized.'));
679
+ return;
680
+ }
681
+ p.note(createdPaths.map((item) => `- ${item}`).join('\n'), 'Created');
682
+ p.outro(chalk.green('Workspace initialized.'));
683
+ }
684
+ export async function handleInitAgentCommand(agentIdInput, options = {}, workspaceRoot = process.cwd()) {
685
+ p.intro(chalk.inverse('Dexto Init Agent'));
686
+ const resolved = await resolveInitAgentInput(agentIdInput, options, workspaceRoot);
687
+ const result = await createWorkspaceAgentScaffold(resolved.agentId, resolved.options, workspaceRoot);
688
+ const subagentLinkResult = resolved.options.subagent
689
+ ? await linkWorkspaceSubagentToPrimaryAgent(resolved.agentId, workspaceRoot)
690
+ : null;
691
+ const createdPaths = formatAgentPaths(result);
692
+ if (createdPaths.length === 0) {
693
+ p.outro(chalk.green([
694
+ `Agent '${resolved.agentId}' already initialized.`,
695
+ ...(subagentLinkResult?.status === 'set' && subagentLinkResult.parentAgentId
696
+ ? [`Linked to primary agent: ${subagentLinkResult.parentAgentId}`]
697
+ : subagentLinkResult?.status === 'no-primary'
698
+ ? ['No primary agent found to link this subagent.']
699
+ : []),
700
+ ].join('\n')));
701
+ return;
702
+ }
703
+ p.note(createdPaths.map((item) => `- ${item}`).join('\n'), 'Created');
704
+ p.outro(chalk.green([
705
+ resolved.options.subagent
706
+ ? `Sub-agent '${resolved.agentId}' initialized.`
707
+ : `Agent '${resolved.agentId}' initialized.`,
708
+ ...(result.primaryAgent.status === 'set' && result.primaryAgent.id
709
+ ? [`Primary agent: ${result.primaryAgent.id}`]
710
+ : []),
711
+ ...(subagentLinkResult?.status === 'set' && subagentLinkResult.parentAgentId
712
+ ? [`Linked to primary agent: ${subagentLinkResult.parentAgentId}`]
713
+ : subagentLinkResult?.status === 'no-primary'
714
+ ? [
715
+ 'No primary agent found to link this subagent. Use `dexto init primary <id>` and rerun if needed.',
716
+ ]
717
+ : []),
718
+ ].join('\n')));
719
+ }
720
+ export async function handleInitSkillCommand(skillId, workspaceRoot = process.cwd()) {
721
+ p.intro(chalk.inverse('Dexto Init Skill'));
722
+ const result = await createWorkspaceSkillScaffold(skillId, workspaceRoot);
723
+ const createdPaths = formatSkillPaths(result);
724
+ if (createdPaths.length === 0) {
725
+ p.outro(chalk.green(`Skill '${skillId}' already initialized.`));
726
+ return;
727
+ }
728
+ p.note(createdPaths.map((item) => `- ${item}`).join('\n'), 'Created');
729
+ p.outro(chalk.green(`Skill '${skillId}' initialized.`));
730
+ }
731
+ export async function handleInitPrimaryCommand(agentIdInput, workspaceRoot = process.cwd()) {
732
+ p.intro(chalk.inverse('Dexto Init Primary'));
733
+ const agentId = await resolvePrimaryAgentSelection(agentIdInput, workspaceRoot);
734
+ const result = await setWorkspacePrimaryAgent(agentId, workspaceRoot);
735
+ if (result.primaryAgent.status === 'existing') {
736
+ p.outro(chalk.green(`'${agentId}' is already the workspace primary agent.`));
737
+ return;
738
+ }
739
+ p.note(path.relative(result.workspace.root, result.registry.path), 'Updated');
740
+ p.outro(chalk.green(`Primary agent set to '${agentId}'.`));
741
+ }
742
+ export async function handleInitStatusCommand(workspaceRoot = process.cwd()) {
743
+ p.intro(chalk.inverse('Dexto Init Status'));
744
+ const result = await inspectWorkspaceStatus(workspaceRoot);
745
+ p.outro(formatWorkspaceStatus(result));
746
+ }
747
+ export function registerInitCommand({ program }) {
748
+ const initCommand = program
749
+ .command('init')
750
+ .description('Initialize the current folder as a Dexto workspace');
751
+ initCommand.addHelpText('after', `
752
+ Examples:
753
+ $ dexto init
754
+ $ dexto init agent
755
+ $ dexto init agent explore-agent --subagent
756
+ $ dexto init primary review-agent
757
+ $ dexto init skill code-review
758
+ $ dexto init status
759
+ `);
760
+ initCommand.action(withAnalytics('init', async () => {
761
+ try {
762
+ await handleInitCommand();
763
+ safeExit('init', 0);
764
+ }
765
+ catch (err) {
766
+ if (err instanceof ExitSignal)
767
+ throw err;
768
+ console.error(`❌ dexto init command failed: ${err}`);
769
+ safeExit('init', 1, 'error');
770
+ }
771
+ }));
772
+ initCommand
773
+ .command('agent [id]')
774
+ .description('Create a workspace agent scaffold')
775
+ .option('--subagent', 'Create a specialized sub-agent scaffold')
776
+ .option('--primary', 'Set this agent as the workspace primary')
777
+ .action(withAnalytics('init agent', async (id, options) => {
778
+ try {
779
+ await handleInitAgentCommand(id, options);
780
+ safeExit('init agent', 0);
781
+ }
782
+ catch (err) {
783
+ if (err instanceof ExitSignal)
784
+ throw err;
785
+ console.error(`❌ dexto init agent command failed: ${err}`);
786
+ safeExit('init agent', 1, 'error');
787
+ }
788
+ }));
789
+ initCommand
790
+ .command('primary [id]')
791
+ .description('Set the workspace primary agent')
792
+ .action(withAnalytics('init primary', async (id) => {
793
+ try {
794
+ await handleInitPrimaryCommand(id);
795
+ safeExit('init primary', 0);
796
+ }
797
+ catch (err) {
798
+ if (err instanceof ExitSignal)
799
+ throw err;
800
+ console.error(`❌ dexto init primary command failed: ${err}`);
801
+ safeExit('init primary', 1, 'error');
802
+ }
803
+ }));
804
+ initCommand
805
+ .command('skill <id>')
806
+ .description('Create a workspace skill scaffold')
807
+ .action(withAnalytics('init skill', async (id) => {
808
+ try {
809
+ await handleInitSkillCommand(id);
810
+ safeExit('init skill', 0);
811
+ }
812
+ catch (err) {
813
+ if (err instanceof ExitSignal)
814
+ throw err;
815
+ console.error(`❌ dexto init skill command failed: ${err}`);
816
+ safeExit('init skill', 1, 'error');
817
+ }
818
+ }));
819
+ initCommand
820
+ .command('status')
821
+ .description('Show the current workspace configuration and deploy preview')
822
+ .action(withAnalytics('init status', async () => {
823
+ try {
824
+ await handleInitStatusCommand();
825
+ safeExit('init status', 0);
826
+ }
827
+ catch (err) {
828
+ if (err instanceof ExitSignal)
829
+ throw err;
830
+ console.error(`❌ dexto init status command failed: ${err}`);
831
+ safeExit('init status', 1, 'error');
832
+ }
833
+ }));
834
+ }