@docyrus/docyrus 0.0.22 → 0.0.24

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 (28) hide show
  1. package/README.md +12 -0
  2. package/main.js +234 -11
  3. package/main.js.map +4 -4
  4. package/package.json +6 -4
  5. package/resources/pi-agent/assets/docyrus-logo.svg +16 -0
  6. package/resources/pi-agent/extensions/architect.ts +771 -0
  7. package/resources/pi-agent/extensions/notify.ts +57 -55
  8. package/resources/pi-agent/extensions/pi-custom-compaction/CHANGELOG.md +27 -0
  9. package/resources/pi-agent/extensions/pi-custom-compaction/LICENSE +21 -0
  10. package/resources/pi-agent/extensions/pi-custom-compaction/README.md +244 -0
  11. package/resources/pi-agent/extensions/pi-custom-compaction/VENDORED_FROM.md +6 -0
  12. package/resources/pi-agent/extensions/pi-custom-compaction/banner.png +0 -0
  13. package/resources/pi-agent/extensions/pi-custom-compaction/commands/register-commands.ts +63 -0
  14. package/resources/pi-agent/extensions/pi-custom-compaction/events/register-events.ts +229 -0
  15. package/resources/pi-agent/extensions/pi-custom-compaction/index.ts +10 -0
  16. package/resources/pi-agent/extensions/pi-custom-compaction/package.json +57 -0
  17. package/resources/pi-agent/extensions/pi-custom-compaction/paths.ts +13 -0
  18. package/resources/pi-agent/extensions/pi-custom-compaction/policy/config.ts +32 -0
  19. package/resources/pi-agent/extensions/pi-custom-compaction/policy/merge.ts +67 -0
  20. package/resources/pi-agent/extensions/pi-custom-compaction/policy/parse.ts +354 -0
  21. package/resources/pi-agent/extensions/pi-custom-compaction/policy/types.ts +131 -0
  22. package/resources/pi-agent/extensions/pi-custom-compaction/runtime/model-resolution.ts +77 -0
  23. package/resources/pi-agent/extensions/pi-custom-compaction/runtime/pure.ts +56 -0
  24. package/resources/pi-agent/extensions/pi-custom-compaction/runtime/session-state.ts +244 -0
  25. package/resources/pi-agent/extensions/pi-custom-compaction/summary/generate.ts +184 -0
  26. package/resources/pi-agent/extensions/pi-custom-compaction/summary/template.ts +124 -0
  27. package/server-loader.js +4017 -0
  28. package/server-loader.js.map +7 -0
@@ -0,0 +1,771 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ const ARCHITECT_OUTPUT_ROOT_SEGMENTS = ["docs", "architect"] as const;
6
+ const ARCHITECT_DISCOVERY_FILE_NAME = "discovery.snapshot.json";
7
+ const ARCHITECT_DATA_SOURCES_FILE_NAME = "DATA_SOURCES.md";
8
+ const ARCHITECT_PLAN_FILE_NAME = "PLAN.md";
9
+ const ARCHITECT_PLAN_JSON_FILE_NAME = "data-sources.plan.json";
10
+ const ARCHITECT_BULK_CREATE_FILE_NAME = "data-sources.bulk-create.json";
11
+ const ARCHITECT_DISCOVERY_CONCURRENCY = 4;
12
+ const DOCYRUS_SCOPE_VALUES = new Set(["local", "global"]);
13
+ const ENUM_FIELD_TYPES = new Set([
14
+ "field-select",
15
+ "field-status",
16
+ "field-multiSelect",
17
+ "field-tagSelect",
18
+ "field-radioGroup",
19
+ ]);
20
+
21
+ export interface IArchitectCliEnvironment {
22
+ executable: string;
23
+ entryPath: string;
24
+ scope: "local" | "global";
25
+ }
26
+
27
+ export interface IArchitectCliCommandSuccess {
28
+ ok: true;
29
+ command: string;
30
+ payload: unknown;
31
+ messages: string[];
32
+ }
33
+
34
+ export interface IArchitectCliCommandFailure {
35
+ ok: false;
36
+ command: string;
37
+ error: string;
38
+ details?: string;
39
+ }
40
+
41
+ export type IArchitectCliCommandResult =
42
+ | IArchitectCliCommandSuccess
43
+ | IArchitectCliCommandFailure;
44
+
45
+ export interface IArchitectCliEnvelope {
46
+ data: unknown;
47
+ context: Record<string, unknown> | null;
48
+ }
49
+
50
+ export interface IArchitectDiscoveryWarning {
51
+ code: string;
52
+ message: string;
53
+ command?: string;
54
+ appId?: string;
55
+ appSlug?: string;
56
+ dataSourceId?: string;
57
+ dataSourceSlug?: string;
58
+ fieldId?: string;
59
+ fieldSlug?: string;
60
+ }
61
+
62
+ export interface IArchitectFieldSnapshot {
63
+ field: Record<string, unknown>;
64
+ enums: Record<string, unknown>[];
65
+ }
66
+
67
+ export interface IArchitectDataSourceSnapshot {
68
+ dataSource: Record<string, unknown>;
69
+ fields: IArchitectFieldSnapshot[];
70
+ }
71
+
72
+ export interface IArchitectAppSnapshot {
73
+ app: Record<string, unknown>;
74
+ dataSources: IArchitectDataSourceSnapshot[];
75
+ }
76
+
77
+ export interface IArchitectDiscoverySnapshot {
78
+ version: 1;
79
+ generatedAt: string;
80
+ tenantContext: Record<string, unknown> | null;
81
+ authenticatedUser: Record<string, unknown> | null;
82
+ apps: IArchitectAppSnapshot[];
83
+ warnings: IArchitectDiscoveryWarning[];
84
+ summary: {
85
+ appCount: number;
86
+ dataSourceCount: number;
87
+ fieldCount: number;
88
+ enumFieldCount: number;
89
+ enumOptionCount: number;
90
+ };
91
+ }
92
+
93
+ export interface IArchitectDiscoverySuccess {
94
+ ok: true;
95
+ snapshot: IArchitectDiscoverySnapshot;
96
+ }
97
+
98
+ export interface IArchitectDiscoveryFailure {
99
+ ok: false;
100
+ error: string;
101
+ command?: string;
102
+ details?: string;
103
+ }
104
+
105
+ export type IArchitectDiscoveryResult =
106
+ | IArchitectDiscoverySuccess
107
+ | IArchitectDiscoveryFailure;
108
+
109
+ export type IArchitectCliRunner = (args: string[]) => Promise<IArchitectCliCommandResult>;
110
+
111
+ interface IParsedCliJsonOutput {
112
+ payload: unknown;
113
+ messages: string[];
114
+ }
115
+
116
+ function isRecord(value: unknown): value is Record<string, unknown> {
117
+ return typeof value === "object" && value !== null && !Array.isArray(value);
118
+ }
119
+
120
+ function extractString(record: Record<string, unknown>, key: string): string | undefined {
121
+ const value = record[key];
122
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
123
+ }
124
+
125
+ function omitKeys(record: Record<string, unknown>, keys: string[]): Record<string, unknown> {
126
+ const omitted = new Set(keys);
127
+ return Object.fromEntries(Object.entries(record).filter(([key]) => !omitted.has(key)));
128
+ }
129
+
130
+ function toRecordArray(value: unknown): Record<string, unknown>[] {
131
+ if (Array.isArray(value)) {
132
+ return value.filter((item): item is Record<string, unknown> => isRecord(item));
133
+ }
134
+
135
+ if (isRecord(value)) {
136
+ if (Array.isArray(value.data)) {
137
+ return value.data.filter((item): item is Record<string, unknown> => isRecord(item));
138
+ }
139
+
140
+ if (Array.isArray(value.items)) {
141
+ return value.items.filter((item): item is Record<string, unknown> => isRecord(item));
142
+ }
143
+ }
144
+
145
+ return [];
146
+ }
147
+
148
+ function sanitizeCliArgs(args: string[]): string[] {
149
+ return args
150
+ .map((value) => value.trim())
151
+ .filter((value) => value.length > 0)
152
+ .filter((value) => value !== "--json");
153
+ }
154
+
155
+ export function parseArchitectBrief(rawArgs: string): string | null {
156
+ const normalized = rawArgs.trim();
157
+ return normalized.length > 0 ? normalized : null;
158
+ }
159
+
160
+ export function slugifyArchitectBrief(brief: string): string {
161
+ const slug = brief
162
+ .toLowerCase()
163
+ .replace(/[^a-z0-9]+/g, "-")
164
+ .replace(/^-+|-+$/g, "")
165
+ .slice(0, 60);
166
+
167
+ return slug || "app-idea";
168
+ }
169
+
170
+ export function formatArchitectTimestamp(date: Date): string {
171
+ const year = `${date.getFullYear()}`;
172
+ const month = `${date.getMonth() + 1}`.padStart(2, "0");
173
+ const day = `${date.getDate()}`.padStart(2, "0");
174
+ const hour = `${date.getHours()}`.padStart(2, "0");
175
+ const minute = `${date.getMinutes()}`.padStart(2, "0");
176
+ const second = `${date.getSeconds()}`.padStart(2, "0");
177
+ return `${year}${month}${day}-${hour}${minute}${second}`;
178
+ }
179
+
180
+ export function createArchitectRunDirectoryPath(params: {
181
+ cwd: string;
182
+ brief: string;
183
+ date?: Date;
184
+ }): string {
185
+ const timestamp = formatArchitectTimestamp(params.date ?? new Date());
186
+ const slug = slugifyArchitectBrief(params.brief);
187
+ return path.join(params.cwd, ...ARCHITECT_OUTPUT_ROOT_SEGMENTS, `${timestamp}-${slug}`);
188
+ }
189
+
190
+ export function normalizeArchitectCliEnvelope(payload: unknown): IArchitectCliEnvelope {
191
+ if (isRecord(payload) && Object.hasOwn(payload, "data")) {
192
+ return {
193
+ data: payload.data,
194
+ context: isRecord(payload.context) ? payload.context : null,
195
+ };
196
+ }
197
+
198
+ return {
199
+ data: payload,
200
+ context: isRecord(payload) && isRecord(payload.context) ? payload.context : null,
201
+ };
202
+ }
203
+
204
+ export function parseArchitectCliJsonOutput(rawOutput: string): IParsedCliJsonOutput {
205
+ const trimmedOutput = rawOutput.trim();
206
+ if (!trimmedOutput) {
207
+ throw new Error("Command produced no output.");
208
+ }
209
+
210
+ const lines = trimmedOutput
211
+ .split(/\r?\n/)
212
+ .map((line) => line.trim())
213
+ .filter((line) => line.length > 0);
214
+
215
+ const lastLine = lines.at(-1);
216
+ if (lastLine) {
217
+ try {
218
+ return {
219
+ payload: JSON.parse(lastLine) as unknown,
220
+ messages: lines.slice(0, -1),
221
+ };
222
+ }
223
+ catch {
224
+ // Fall back to parsing the whole output.
225
+ }
226
+ }
227
+
228
+ return {
229
+ payload: JSON.parse(trimmedOutput) as unknown,
230
+ messages: [],
231
+ };
232
+ }
233
+
234
+ export function appendArchitectWarning(
235
+ warnings: IArchitectDiscoveryWarning[],
236
+ warning: IArchitectDiscoveryWarning,
237
+ ): IArchitectDiscoveryWarning[] {
238
+ const key = JSON.stringify(warning);
239
+ const seen = new Set(warnings.map((item) => JSON.stringify(item)));
240
+ if (seen.has(key)) {
241
+ return warnings;
242
+ }
243
+
244
+ return [...warnings, warning];
245
+ }
246
+
247
+ function buildDocyrusCliCommand(args: string[], scope: "local" | "global"): string[] {
248
+ const sanitizedArgs = sanitizeCliArgs(args);
249
+ const scopedArgs = scope === "global" ? ["-g", ...sanitizedArgs] : sanitizedArgs;
250
+ return [...scopedArgs, "--json"];
251
+ }
252
+
253
+ function formatDocyrusCommandLabel(args: string[], scope: "local" | "global"): string {
254
+ const parts = ["docyrus", ...(scope === "global" ? ["-g"] : []), ...sanitizeCliArgs(args), "--json"];
255
+ return parts.join(" ");
256
+ }
257
+
258
+ function summarizeCliFailure(command: string, stderr: string, stdout: string, code: number | null | undefined): string {
259
+ const trimmedStderr = stderr.trim();
260
+ if (trimmedStderr) {
261
+ return trimmedStderr;
262
+ }
263
+
264
+ const trimmedStdout = stdout.trim();
265
+ if (trimmedStdout) {
266
+ return trimmedStdout.split(/\r?\n/).at(-1) || trimmedStdout;
267
+ }
268
+
269
+ if (typeof code === "number") {
270
+ return `Command exited with code ${code}.`;
271
+ }
272
+
273
+ return `Command failed: ${command}`;
274
+ }
275
+
276
+ function readArchitectCliEnvironment(env: NodeJS.ProcessEnv = process.env): IArchitectCliEnvironment {
277
+ const executable = env.DOCYRUS_CLI_EXECUTABLE?.trim();
278
+ const entryPath = env.DOCYRUS_CLI_ENTRY?.trim();
279
+ const scope = env.DOCYRUS_CLI_SCOPE?.trim() as "local" | "global" | undefined;
280
+
281
+ if (!executable || !entryPath || !scope || !DOCYRUS_SCOPE_VALUES.has(scope)) {
282
+ throw new Error(
283
+ "Missing Docyrus CLI runtime env. Expected DOCYRUS_CLI_EXECUTABLE, DOCYRUS_CLI_ENTRY, and DOCYRUS_CLI_SCOPE.",
284
+ );
285
+ }
286
+
287
+ return {
288
+ executable,
289
+ entryPath,
290
+ scope,
291
+ };
292
+ }
293
+
294
+ function shouldDiscoverEnums(field: Record<string, unknown>): boolean {
295
+ const type = extractString(field, "type");
296
+ return typeof type === "string" && ENUM_FIELD_TYPES.has(type);
297
+ }
298
+
299
+ async function mapWithConcurrency<TInput, TOutput>(
300
+ items: TInput[],
301
+ limit: number,
302
+ mapper: (item: TInput, index: number) => Promise<TOutput>,
303
+ ): Promise<TOutput[]> {
304
+ if (items.length === 0) {
305
+ return [];
306
+ }
307
+
308
+ const safeLimit = Math.max(1, Math.min(limit, items.length));
309
+ const results = new Array<TOutput>(items.length);
310
+ let nextIndex = 0;
311
+
312
+ await Promise.all(
313
+ Array.from({ length: safeLimit }, async() => {
314
+ while (nextIndex < items.length) {
315
+ const currentIndex = nextIndex;
316
+ nextIndex += 1;
317
+ results[currentIndex] = await mapper(items[currentIndex], currentIndex);
318
+ }
319
+ }),
320
+ );
321
+
322
+ return results;
323
+ }
324
+
325
+ export async function discoverArchitectTenantState(params: {
326
+ runCliCommand: IArchitectCliRunner;
327
+ generatedAt?: string;
328
+ maxConcurrency?: number;
329
+ }): Promise<IArchitectDiscoveryResult> {
330
+ const runCliCommand = params.runCliCommand;
331
+ const generatedAt = params.generatedAt ?? new Date().toISOString();
332
+ const maxConcurrency = params.maxConcurrency ?? ARCHITECT_DISCOVERY_CONCURRENCY;
333
+
334
+ const authResult = await runCliCommand(["auth", "who"]);
335
+ if (!authResult.ok) {
336
+ return {
337
+ ok: false,
338
+ command: authResult.command,
339
+ details: authResult.details,
340
+ error: "Architect requires an active Docyrus session. Run `docyrus auth login` first.",
341
+ };
342
+ }
343
+
344
+ const authEnvelope = normalizeArchitectCliEnvelope(authResult.payload);
345
+ const authenticatedUser = isRecord(authEnvelope.data) ? authEnvelope.data : null;
346
+ const tenantContext = authEnvelope.context;
347
+
348
+ const appsResult = await runCliCommand(["apps", "list"]);
349
+ if (!appsResult.ok) {
350
+ return {
351
+ ok: false,
352
+ command: appsResult.command,
353
+ details: appsResult.details,
354
+ error: `Failed to list tenant apps: ${appsResult.error}`,
355
+ };
356
+ }
357
+
358
+ const apps = toRecordArray(normalizeArchitectCliEnvelope(appsResult.payload).data);
359
+ const appResults = await mapWithConcurrency(apps, maxConcurrency, async(app) => {
360
+ const appId = extractString(app, "id");
361
+ const appSlug = extractString(app, "slug");
362
+ let warnings: IArchitectDiscoveryWarning[] = [];
363
+
364
+ if (!appId) {
365
+ warnings = appendArchitectWarning(warnings, {
366
+ code: "app_missing_id",
367
+ message: "Skipping app because it has no id in CLI output.",
368
+ appSlug,
369
+ });
370
+
371
+ return {
372
+ app,
373
+ dataSources: [] as IArchitectDataSourceSnapshot[],
374
+ warnings,
375
+ };
376
+ }
377
+
378
+ const dataSourcesResult = await runCliCommand([
379
+ "studio",
380
+ "list-data-sources",
381
+ "--appId",
382
+ appId,
383
+ "--expand",
384
+ "fields",
385
+ ]);
386
+
387
+ if (!dataSourcesResult.ok) {
388
+ warnings = appendArchitectWarning(warnings, {
389
+ code: "data_source_list_failed",
390
+ message: dataSourcesResult.error,
391
+ command: dataSourcesResult.command,
392
+ appId,
393
+ appSlug,
394
+ });
395
+
396
+ return {
397
+ app,
398
+ dataSources: [] as IArchitectDataSourceSnapshot[],
399
+ warnings,
400
+ };
401
+ }
402
+
403
+ const dataSources = toRecordArray(normalizeArchitectCliEnvelope(dataSourcesResult.payload).data);
404
+ const dataSourceResults = await mapWithConcurrency(dataSources, maxConcurrency, async(dataSource) => {
405
+ const dataSourceId = extractString(dataSource, "id");
406
+ const dataSourceSlug = extractString(dataSource, "slug");
407
+ const fields = toRecordArray(dataSource.fields);
408
+ const fieldResults = await mapWithConcurrency(fields, maxConcurrency, async(field) => {
409
+ let fieldWarnings: IArchitectDiscoveryWarning[] = [];
410
+
411
+ if (!shouldDiscoverEnums(field)) {
412
+ return {
413
+ fieldSnapshot: {
414
+ field,
415
+ enums: [],
416
+ },
417
+ warnings: fieldWarnings,
418
+ };
419
+ }
420
+
421
+ const fieldId = extractString(field, "id");
422
+ const fieldSlug = extractString(field, "slug");
423
+ if (!dataSourceId || !fieldId) {
424
+ fieldWarnings = appendArchitectWarning(fieldWarnings, {
425
+ code: "enum_lookup_skipped",
426
+ message: "Skipping enum lookup because the field or data source id is missing.",
427
+ appId,
428
+ appSlug,
429
+ dataSourceId,
430
+ dataSourceSlug,
431
+ fieldId,
432
+ fieldSlug,
433
+ });
434
+
435
+ return {
436
+ fieldSnapshot: {
437
+ field,
438
+ enums: [],
439
+ },
440
+ warnings: fieldWarnings,
441
+ };
442
+ }
443
+
444
+ const enumsResult = await runCliCommand([
445
+ "studio",
446
+ "list-enums",
447
+ "--appId",
448
+ appId,
449
+ "--dataSourceId",
450
+ dataSourceId,
451
+ "--fieldId",
452
+ fieldId,
453
+ ]);
454
+
455
+ if (!enumsResult.ok) {
456
+ fieldWarnings = appendArchitectWarning(fieldWarnings, {
457
+ code: "enum_lookup_failed",
458
+ message: enumsResult.error,
459
+ command: enumsResult.command,
460
+ appId,
461
+ appSlug,
462
+ dataSourceId,
463
+ dataSourceSlug,
464
+ fieldId,
465
+ fieldSlug,
466
+ });
467
+
468
+ return {
469
+ fieldSnapshot: {
470
+ field,
471
+ enums: [],
472
+ },
473
+ warnings: fieldWarnings,
474
+ };
475
+ }
476
+
477
+ return {
478
+ fieldSnapshot: {
479
+ field,
480
+ enums: toRecordArray(normalizeArchitectCliEnvelope(enumsResult.payload).data),
481
+ },
482
+ warnings: fieldWarnings,
483
+ };
484
+ });
485
+
486
+ const fieldSnapshots = fieldResults.map((item) => item.fieldSnapshot);
487
+ const nestedWarnings = fieldResults.flatMap((item) => item.warnings);
488
+ return {
489
+ dataSourceSnapshot: {
490
+ dataSource: omitKeys(dataSource, ["fields"]),
491
+ fields: fieldSnapshots,
492
+ },
493
+ warnings: nestedWarnings,
494
+ };
495
+ });
496
+
497
+ return {
498
+ app,
499
+ dataSources: dataSourceResults.map((item) => item.dataSourceSnapshot),
500
+ warnings: [...warnings, ...dataSourceResults.flatMap((item) => item.warnings)],
501
+ };
502
+ });
503
+
504
+ const warnings = appResults.flatMap((item) => item.warnings);
505
+ const snapshot: IArchitectDiscoverySnapshot = {
506
+ version: 1,
507
+ generatedAt,
508
+ tenantContext,
509
+ authenticatedUser,
510
+ apps: appResults.map((item) => ({
511
+ app: item.app,
512
+ dataSources: item.dataSources,
513
+ })),
514
+ warnings,
515
+ summary: {
516
+ appCount: appResults.length,
517
+ dataSourceCount: appResults.reduce((count, item) => count + item.dataSources.length, 0),
518
+ fieldCount: appResults.reduce(
519
+ (count, item) => count + item.dataSources.reduce((inner, dataSource) => inner + dataSource.fields.length, 0),
520
+ 0,
521
+ ),
522
+ enumFieldCount: appResults.reduce(
523
+ (count, item) => count + item.dataSources.reduce(
524
+ (inner, dataSource) => inner + dataSource.fields.filter((field) => field.enums.length > 0).length,
525
+ 0,
526
+ ),
527
+ 0,
528
+ ),
529
+ enumOptionCount: appResults.reduce(
530
+ (count, item) => count + item.dataSources.reduce(
531
+ (inner, dataSource) => inner + dataSource.fields.reduce((fieldCount, field) => fieldCount + field.enums.length, 0),
532
+ 0,
533
+ ),
534
+ 0,
535
+ ),
536
+ },
537
+ };
538
+
539
+ return {
540
+ ok: true,
541
+ snapshot,
542
+ };
543
+ }
544
+
545
+ function buildArchitectPrompt(params: {
546
+ brief: string;
547
+ outputDir: string;
548
+ snapshot: IArchitectDiscoverySnapshot;
549
+ }): string {
550
+ const discoveryPath = path.join(params.outputDir, ARCHITECT_DISCOVERY_FILE_NAME);
551
+ const dataSourcesPath = path.join(params.outputDir, ARCHITECT_DATA_SOURCES_FILE_NAME);
552
+ const planPath = path.join(params.outputDir, ARCHITECT_PLAN_FILE_NAME);
553
+ const planJsonPath = path.join(params.outputDir, ARCHITECT_PLAN_JSON_FILE_NAME);
554
+ const bulkCreatePath = path.join(params.outputDir, ARCHITECT_BULK_CREATE_FILE_NAME);
555
+ const tenantDisplay = params.snapshot.tenantContext?.tenantDisplay || "Unknown tenant";
556
+ const warningLines = params.snapshot.warnings.length > 0
557
+ ? params.snapshot.warnings.map((warning) => `- [${warning.code}] ${warning.message}`).join("\n")
558
+ : "- None";
559
+
560
+ return [
561
+ "You are running the Docyrus `/architect` workflow.",
562
+ "",
563
+ "This is planning only. Do not mutate remote Docyrus state.",
564
+ "Do not run `docyrus studio` create/update/delete/bulk-create commands, `docyrus ds create/update/delete`, or any other remote mutation.",
565
+ "Work only from the discovery snapshot plus any non-mutating local inspection you need.",
566
+ "",
567
+ "## User Brief",
568
+ params.brief,
569
+ "",
570
+ "## Discovery Summary",
571
+ `- Tenant: ${tenantDisplay}`,
572
+ `- Apps discovered: ${params.snapshot.summary.appCount}`,
573
+ `- Data sources discovered: ${params.snapshot.summary.dataSourceCount}`,
574
+ `- Fields discovered: ${params.snapshot.summary.fieldCount}`,
575
+ `- Enum-bearing fields with discovered options: ${params.snapshot.summary.enumFieldCount}`,
576
+ `- Discovery warnings: ${params.snapshot.warnings.length}`,
577
+ "",
578
+ "## Discovery Warnings",
579
+ warningLines,
580
+ "",
581
+ "## Required Local Artifacts",
582
+ `- Read discovery snapshot: ${discoveryPath}`,
583
+ `- Write ${dataSourcesPath}`,
584
+ `- Write ${planPath}`,
585
+ `- Write ${planJsonPath}`,
586
+ `- Optionally write ${bulkCreatePath} only if the proposed schema is directly representable by the current \`docyrus studio bulk-create-data-sources\` payload; otherwise do not create that file and explain the limitation in PLAN.md.`,
587
+ "",
588
+ "## DATA_SOURCES.md Requirements",
589
+ "- Explicitly list reusable existing data sources and why each is reused or rejected.",
590
+ "- Explicitly list every proposed new data source, required fields, enum options, and relations.",
591
+ "- Call out unresolved assumptions and any discovery gaps. If no reusable data sources exist, say that explicitly.",
592
+ "",
593
+ "## PLAN.md Requirements",
594
+ "- Provide an ordered implementation/apply plan.",
595
+ "- Use exact `docyrus studio` workflows and commands conceptually, but do not run them.",
596
+ "- Include verification steps.",
597
+ "- Explain relation-field follow-up when newly proposed data sources cannot be referenced directly during bulk create because relation fields need concrete IDs.",
598
+ "",
599
+ "## data-sources.plan.json Contract",
600
+ "- Include `brief`, `generatedAt`, and `tenantContext`.",
601
+ "- Include `existingDataSources[]` with reuse decision and rationale.",
602
+ "- Include `newDataSources[]` with `title`, `name`, `slug`, `icon`, and `fields[]`.",
603
+ "- Each field entry must include `name`, `slug`, `type`, `validations`, and `defaultValue`.",
604
+ "- Add optional `enumOptions[]` when applicable.",
605
+ "- Add optional `relationTarget` as either `{ \"kind\": \"existing\", \"dataSourceId\": \"...\" }` or `{ \"kind\": \"new\", \"slug\": \"...\" }`.",
606
+ "- Include `applyPhases[]` covering `bulkCreate`, `postCreateRelations`, `postCreateEnums`, and `verification`.",
607
+ "",
608
+ "## Final Response",
609
+ "After writing the files, summarize what you wrote, note whether `data-sources.bulk-create.json` was created, and list any assumptions that still need human confirmation.",
610
+ ].join("\n");
611
+ }
612
+
613
+ async function promptForArchitectBrief(ctx: ExtensionCommandContext, args: string): Promise<string | null> {
614
+ const inlineBrief = parseArchitectBrief(args);
615
+ if (inlineBrief) {
616
+ return inlineBrief;
617
+ }
618
+
619
+ if (!ctx.hasUI) {
620
+ return null;
621
+ }
622
+
623
+ const brief = await ctx.ui.editor(
624
+ "Describe the app idea to architect:",
625
+ "",
626
+ );
627
+
628
+ return parseArchitectBrief(brief || "");
629
+ }
630
+
631
+ function createArchitectCliRunner(params: {
632
+ pi: ExtensionAPI;
633
+ ctx: ExtensionCommandContext;
634
+ environment: IArchitectCliEnvironment;
635
+ }): IArchitectCliRunner {
636
+ return async(args: string[]) => {
637
+ const commandArgs = buildDocyrusCliCommand(args, params.environment.scope);
638
+ const commandLabel = formatDocyrusCommandLabel(args, params.environment.scope);
639
+ try {
640
+ const result = await params.pi.exec(
641
+ params.environment.executable,
642
+ [params.environment.entryPath, ...commandArgs],
643
+ { cwd: params.ctx.cwd },
644
+ );
645
+ const stdout = result.stdout?.toString() || "";
646
+ const stderr = result.stderr?.toString() || "";
647
+
648
+ if (result.code !== 0) {
649
+ return {
650
+ ok: false,
651
+ command: commandLabel,
652
+ error: summarizeCliFailure(commandLabel, stderr, stdout, result.code),
653
+ details: [stdout.trim(), stderr.trim()].filter(Boolean).join("\n") || undefined,
654
+ };
655
+ }
656
+
657
+ try {
658
+ const parsed = parseArchitectCliJsonOutput(stdout);
659
+ return {
660
+ ok: true,
661
+ command: commandLabel,
662
+ payload: parsed.payload,
663
+ messages: parsed.messages,
664
+ };
665
+ }
666
+ catch (error) {
667
+ return {
668
+ ok: false,
669
+ command: commandLabel,
670
+ error: error instanceof Error ? error.message : String(error),
671
+ details: stdout.trim() || undefined,
672
+ };
673
+ }
674
+ }
675
+ catch (error) {
676
+ return {
677
+ ok: false,
678
+ command: commandLabel,
679
+ error: error instanceof Error ? error.message : String(error),
680
+ };
681
+ }
682
+ };
683
+ }
684
+
685
+ async function architectHandler(pi: ExtensionAPI, ctx: ExtensionCommandContext, args: string): Promise<void> {
686
+ const brief = await promptForArchitectBrief(ctx, args);
687
+ if (!brief) {
688
+ if (ctx.hasUI) {
689
+ ctx.ui.notify("Usage: /architect <app-idea brief>", "error");
690
+ }
691
+ return;
692
+ }
693
+
694
+ let environment: IArchitectCliEnvironment;
695
+ try {
696
+ environment = readArchitectCliEnvironment();
697
+ }
698
+ catch (error) {
699
+ if (ctx.hasUI) {
700
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
701
+ }
702
+ return;
703
+ }
704
+
705
+ if (ctx.hasUI) {
706
+ ctx.ui.notify("Architect: discovering current tenant apps and data sources...", "info");
707
+ }
708
+
709
+ const runCliCommand = createArchitectCliRunner({
710
+ pi,
711
+ ctx,
712
+ environment,
713
+ });
714
+ const discovery = await discoverArchitectTenantState({
715
+ runCliCommand,
716
+ });
717
+
718
+ if (!discovery.ok) {
719
+ if (ctx.hasUI) {
720
+ ctx.ui.notify(discovery.error, "error");
721
+ }
722
+ return;
723
+ }
724
+
725
+ const outputDir = createArchitectRunDirectoryPath({
726
+ cwd: ctx.cwd,
727
+ brief,
728
+ date: new Date(discovery.snapshot.generatedAt),
729
+ });
730
+ const discoveryPath = path.join(outputDir, ARCHITECT_DISCOVERY_FILE_NAME);
731
+
732
+ try {
733
+ await fs.mkdir(outputDir, {
734
+ recursive: true,
735
+ });
736
+
737
+ await fs.writeFile(
738
+ discoveryPath,
739
+ `${JSON.stringify(discovery.snapshot, null, 2)}\n`,
740
+ "utf8",
741
+ );
742
+ }
743
+ catch (error) {
744
+ if (ctx.hasUI) {
745
+ ctx.ui.notify(
746
+ `Architect failed to write ${discoveryPath}: ${error instanceof Error ? error.message : String(error)}`,
747
+ "error",
748
+ );
749
+ }
750
+ return;
751
+ }
752
+
753
+ if (ctx.hasUI) {
754
+ ctx.ui.notify(`Architect: saved discovery snapshot to ${discoveryPath}`, "info");
755
+ }
756
+
757
+ pi.sendUserMessage(buildArchitectPrompt({
758
+ brief,
759
+ outputDir,
760
+ snapshot: discovery.snapshot,
761
+ }));
762
+ }
763
+
764
+ export default function architectExtension(pi: ExtensionAPI) {
765
+ pi.registerCommand("architect", {
766
+ description: "Analyze tenant data sources and plan Docyrus schema artifacts for an app idea",
767
+ handler: async(args, ctx) => {
768
+ await architectHandler(pi, ctx, args);
769
+ },
770
+ });
771
+ }