@compilr-dev/cli 0.5.17 → 0.6.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.
Files changed (35) hide show
  1. package/dist/.tsbuildinfo.app +1 -1
  2. package/dist/.tsbuildinfo.data +1 -1
  3. package/dist/.tsbuildinfo.domain +1 -1
  4. package/dist/agent.d.ts +41 -111
  5. package/dist/agent.js +238 -390
  6. package/dist/commands-v2/handlers/project.d.ts +1 -0
  7. package/dist/commands-v2/handlers/project.js +36 -2
  8. package/dist/commands-v2/handlers/team.js +23 -3
  9. package/dist/compilr-diff-companion.vsix +0 -0
  10. package/dist/entitlements/index.d.ts +23 -0
  11. package/dist/entitlements/index.js +110 -0
  12. package/dist/guide/cli-guide-entries.d.ts +15 -0
  13. package/dist/guide/cli-guide-entries.js +99 -0
  14. package/dist/guide/index.d.ts +5 -4
  15. package/dist/guide/index.js +4 -3
  16. package/dist/guide/shared-content.js +188 -21
  17. package/dist/handlers/permission-handler.js +10 -3
  18. package/dist/index.js +18 -0
  19. package/dist/repl-v2.d.ts +16 -0
  20. package/dist/repl-v2.js +51 -17
  21. package/dist/tools/db-tools.d.ts +1 -1
  22. package/dist/tools/platform-adapter.d.ts +1 -1
  23. package/dist/tools/platform-adapter.js +6 -1
  24. package/dist/tools.js +6 -1
  25. package/dist/ui/overlay/impl/app-model-overlay-v2.d.ts +57 -0
  26. package/dist/ui/overlay/impl/app-model-overlay-v2.js +232 -0
  27. package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.d.ts +23 -1
  28. package/dist/ui/overlay/impl/custom-agent-form-overlay-v2.js +203 -47
  29. package/dist/ui/overlay/impl/model-overlay-v2.js +2 -2
  30. package/dist/ui/overlay/impl/new-overlay-v2.d.ts +2 -2
  31. package/dist/ui/overlay/impl/new-overlay-v2.js +10 -17
  32. package/dist/ui/overlay/impl/team-overlay-v2.js +2 -2
  33. package/dist/ui/overlay/index.d.ts +1 -0
  34. package/dist/ui/overlay/index.js +1 -0
  35. package/package.json +4 -4
@@ -27,10 +27,17 @@ export function createPermissionHandler(deps) {
27
27
  if (permissionMode === 'auto-accept') {
28
28
  return true;
29
29
  }
30
- // "Plan" mode: always prompt (no auto-approval based on rules)
30
+ // "Plan" mode: block write/execute tools, allow read/plan tools
31
+ if (permissionMode === 'plan') {
32
+ const { isToolAllowedInPlanMode } = await import('@compilr-dev/sdk');
33
+ if (!isToolAllowedInPlanMode(request.toolName)) {
34
+ return false; // Blocked — agent gets denial in tool result
35
+ }
36
+ // Read/plan tools pass through without prompting
37
+ return true;
38
+ }
31
39
  // "Default" mode: use rule-based checking
32
- // Check custom permission rules (for non-plan mode)
33
- if (permissionMode !== 'plan') {
40
+ {
34
41
  const rule = findMatchingRule(request.toolName);
35
42
  if (rule) {
36
43
  if (rule.level === 'always') {
package/dist/index.js CHANGED
@@ -560,6 +560,19 @@ async function main() {
560
560
  // MCP tools injected later via agent.registerTools() when background init completes
561
561
  // Episode recorder for work history tracking
562
562
  episodeRecorder: defaultEpisodeRecorder,
563
+ // Plan mode callbacks — wired to ReplV2's handlePlanApproval
564
+ planModeCallbacks: {
565
+ onPlanSubmit: async (info) => {
566
+ // Delegate to ReplV2's existing plan approval flow
567
+ if (sharedState.onPlanSubmit)
568
+ return sharedState.onPlanSubmit(info);
569
+ return { action: 'reject' }; // Fallback if repl not ready
570
+ },
571
+ onPlanModeExit: async (info) => {
572
+ if (sharedState.onPlanModeExit)
573
+ return sharedState.onPlanModeExit(info);
574
+ },
575
+ },
563
576
  });
564
577
  perf('create-agent-done');
565
578
  // Mutable ref so grantSession and onDefaultAgentSwapped closures track the current default agent
@@ -731,6 +744,11 @@ async function main() {
731
744
  onBackgroundDelegationReady: (runDelegation) => {
732
745
  sharedState.runBackgroundDelegation = runDelegation;
733
746
  },
747
+ // Plan mode: wire approval and exit callbacks to ReplV2
748
+ onPlanModeReady: (callbacks) => {
749
+ sharedState.onPlanSubmit = callbacks.onPlanSubmit;
750
+ sharedState.onPlanModeExit = callbacks.onPlanModeExit;
751
+ },
734
752
  // Update sharedState.team when session restore replaces the team object
735
753
  // (so delegation handlers see restored agents, not just 'default')
736
754
  onTeamReplaced: (newTeam) => {
package/dist/repl-v2.d.ts CHANGED
@@ -149,6 +149,21 @@ export interface ReplV2Options {
149
149
  * Phase 3d-beta: Coordinator Mode
150
150
  */
151
151
  onBackgroundDelegationReady?: (runDelegation: (agentId: string, message: string) => 'running' | 'queued' | null) => void;
152
+ /**
153
+ * Plan mode: wire plan_submit and plan_mode_exit callbacks to the approval overlay.
154
+ */
155
+ onPlanModeReady?: (callbacks: {
156
+ onPlanSubmit: (info: {
157
+ planId: number;
158
+ summary?: string;
159
+ }) => Promise<{
160
+ action: 'approve-auto' | 'approve' | 'revise' | 'reject';
161
+ feedback?: string;
162
+ }>;
163
+ onPlanModeExit: (info: {
164
+ reason: string;
165
+ }) => Promise<void>;
166
+ }) => void;
152
167
  /**
153
168
  * Callback invoked when terminal session is registered.
154
169
  * Passes the session ID so index.ts can set sessionPrefix for IPC.
@@ -234,6 +249,7 @@ export declare class ReplV2 {
234
249
  private readonly onPendingDelegation?;
235
250
  private readonly onBackgroundAgentReady?;
236
251
  private readonly onBackgroundDelegationReady?;
252
+ private readonly onPlanModeReady?;
237
253
  private readonly onSessionRegistered?;
238
254
  private readonly onTeamReplaced?;
239
255
  private readonly onProjectStateUpdate?;
package/dist/repl-v2.js CHANGED
@@ -31,7 +31,7 @@ import { renderMascotWithLogo } from './ui/mascot/renderer.js';
31
31
  import { getStartupHighlights } from './changelog/index.js';
32
32
  import { registerCommands, executeCommand, allCommands, getAutocompleteCommands, saveCurrentSession, saveCurrentTeam, loadProjectSession, archiveCurrentSession, convertMessagesToItems, } from './commands-v2/index.js';
33
33
  import { getCustomCommandRegistry } from './commands/custom-registry.js';
34
- import { getPlanModePrompt } from './prompts/plan-mode-prompt.js';
34
+ import { getPlanModePrompt } from '@compilr-dev/sdk';
35
35
  import { planRepository } from './db/repositories/index.js';
36
36
  import { PlanApprovalOverlayV2, } from './ui/overlay/impl/plan-approval-overlay-v2.js';
37
37
  import { PermissionOverlayV2, } from './ui/overlay/impl/permission-overlay-v2.js';
@@ -197,6 +197,7 @@ export class ReplV2 {
197
197
  onPendingDelegation;
198
198
  onBackgroundAgentReady;
199
199
  onBackgroundDelegationReady;
200
+ onPlanModeReady;
200
201
  onSessionRegistered;
201
202
  onTeamReplaced;
202
203
  onProjectStateUpdate;
@@ -245,6 +246,7 @@ export class ReplV2 {
245
246
  this.onPendingDelegation = options.onPendingDelegation;
246
247
  this.onBackgroundAgentReady = options.onBackgroundAgentReady;
247
248
  this.onBackgroundDelegationReady = options.onBackgroundDelegationReady;
249
+ this.onPlanModeReady = options.onPlanModeReady;
248
250
  this.onSessionRegistered = options.onSessionRegistered;
249
251
  this.onTeamReplaced = options.onTeamReplaced;
250
252
  this.onProjectStateUpdate = options.onProjectStateUpdate;
@@ -987,6 +989,26 @@ export class ReplV2 {
987
989
  return 'running';
988
990
  });
989
991
  }
992
+ // Plan mode: wire plan_submit and plan_mode_exit to handlePlanApproval
993
+ if (this.onPlanModeReady) {
994
+ this.onPlanModeReady({
995
+ onPlanSubmit: async (info) => {
996
+ await this.handlePlanApproval(info.planId, info.summary ?? 'Plan');
997
+ // Map the internal action to PlanSubmitResult
998
+ // handlePlanApproval already updates DB and mode — return a simplified result
999
+ const mode = this.ui.getMode();
1000
+ if (mode === 'auto-accept')
1001
+ return { action: 'approve-auto' };
1002
+ return { action: 'approve' };
1003
+ },
1004
+ onPlanModeExit: (info) => {
1005
+ this.ui.setMode('normal');
1006
+ this.ui.clearActivePlan();
1007
+ this.ui.print({ type: 'info', message: `Agent exited plan mode: ${info.reason}` });
1008
+ return Promise.resolve();
1009
+ },
1010
+ });
1011
+ }
990
1012
  // Event handlers
991
1013
  this.ui.on('submit', (input) => {
992
1014
  void this.processInput(input);
@@ -1547,6 +1569,12 @@ export class ReplV2 {
1547
1569
  // Start session heartbeat in background (don't block UI)
1548
1570
  const auth = getAuthManager();
1549
1571
  void auth.startSession();
1572
+ // Warm entitlement cache in background (non-blocking)
1573
+ if (await auth.isAuthenticated()) {
1574
+ import('./entitlements/index.js').then(({ warmEntitlementCache }) => {
1575
+ void warmEntitlementCache(() => auth.getAccessToken());
1576
+ }).catch(() => { });
1577
+ }
1550
1578
  const startupMode = getStartupMode();
1551
1579
  if (startupMode === 'menu') {
1552
1580
  await this.handleCommand('menu', '');
@@ -2209,6 +2237,25 @@ export class ReplV2 {
2209
2237
  async runAgentReal(userMessage) {
2210
2238
  if (!this.agent)
2211
2239
  return;
2240
+ // Entitlement check: daily message limit
2241
+ try {
2242
+ const { dailyMessageCounter, getEntitlementCache } = await import('./entitlements/index.js');
2243
+ const auth = (await import('./auth/index.js')).getAuthManager();
2244
+ if (await auth.isAuthenticated()) {
2245
+ const cache = getEntitlementCache(() => auth.getAccessToken());
2246
+ const msgCount = dailyMessageCounter.increment();
2247
+ const check = cache.checkLimit('maxAgentMessagesPerDay', msgCount);
2248
+ if (!check.allowed) {
2249
+ const { formatTimeUntilReset } = await import('@compilr-dev/sdk');
2250
+ this.ui.print({
2251
+ type: 'warning',
2252
+ message: `Daily message limit reached (${String(check.current)}/${String(check.limit)}). ${formatTimeUntilReset()}.`,
2253
+ });
2254
+ return;
2255
+ }
2256
+ }
2257
+ }
2258
+ catch { /* allow if entitlements unavailable */ }
2212
2259
  const abortController = new AbortController();
2213
2260
  const signal = abortController.signal;
2214
2261
  const setAction = (action) => {
@@ -2252,7 +2299,7 @@ export class ReplV2 {
2252
2299
  let messageToSend = userMessage;
2253
2300
  if (this.ui.getMode() === 'plan') {
2254
2301
  const activePlan = this.ui.getActivePlan();
2255
- const planModePrompt = getPlanModePrompt(activePlan.id ?? undefined, activePlan.name ?? undefined);
2302
+ const planModePrompt = getPlanModePrompt(activePlan.id && activePlan.name ? { id: activePlan.id, name: activePlan.name } : undefined);
2256
2303
  messageToSend = `${planModePrompt}\n\n---\n\nUser request: ${userMessage}`;
2257
2304
  }
2258
2305
  // Inject delegation completion notifications (deferred delivery)
@@ -2687,22 +2734,9 @@ export class ReplV2 {
2687
2734
  });
2688
2735
  }
2689
2736
  }
2690
- // Flush remaining text and check for PLAN_READY marker
2737
+ // Flush remaining text (PLAN_READY marker detection removed — plan_submit tool handles approval)
2691
2738
  if (this.textAccumulator.trim()) {
2692
- const text = this.textAccumulator.trim();
2693
- // Check for PLAN_READY marker
2694
- const planReadyInfo = this.detectPlanReady(text);
2695
- // Print the text (remove the marker from display if present)
2696
- const displayText = planReadyInfo
2697
- ? text.replace(ReplV2.PLAN_READY_REGEX, '').trim()
2698
- : text;
2699
- if (displayText) {
2700
- this.ui.print({ type: 'agent-text', text: displayText, expression: this.getActiveAgentMascot(), agentId: activeAgentId });
2701
- }
2702
- // If PLAN_READY marker found, show approval overlay
2703
- if (planReadyInfo) {
2704
- await this.handlePlanApproval(planReadyInfo.planId, planReadyInfo.planName);
2705
- }
2739
+ this.ui.print({ type: 'agent-text', text: this.textAccumulator.trim(), expression: this.getActiveAgentMascot(), agentId: activeAgentId });
2706
2740
  }
2707
2741
  // Print per-turn summary before stopping agent
2708
2742
  this.printTurnSummary();
@@ -10,7 +10,7 @@ export { getActiveProject, setActiveProject } from './project-db.js';
10
10
  /**
11
11
  * All database tools combined (32 tools from SDK)
12
12
  */
13
- export declare const allDbTools: import("@compilr-dev/sdk").Tool<never>[];
13
+ export declare const allDbTools: import("@compilr-dev/agents").Tool<never>[];
14
14
  /**
15
15
  * All factory tools (5 tools from @compilr-dev/factory)
16
16
  */
@@ -22,4 +22,4 @@ export declare const allPlatformTools: import("@compilr-dev/sdk").Tool<never>[];
22
22
  * - 3 model tools (app_model_get, app_model_update, app_model_validate)
23
23
  * - 2 factory tools (factory_scaffold, factory_list_toolkits)
24
24
  */
25
- export declare const allFactoryTools: import("@compilr-dev/agents").Tool<never>[];
25
+ export declare const allFactoryTools: import('@compilr-dev/agents').Tool<never>[];
@@ -9,6 +9,7 @@ import { createPlatformTools, createSQLiteRepositories, ProjectAnchorStore, } fr
9
9
  import { createFactoryTools } from '@compilr-dev/factory';
10
10
  import { getDb } from '../db/index.js';
11
11
  import { getDataPath, getSessionsPath } from '../settings/paths.js';
12
+ import { getPermissionMode } from '../settings/index.js';
12
13
  import { existsSync, rmSync } from 'fs';
13
14
  import { join } from 'path';
14
15
  import { getCurrentProject, setCurrentProject } from './project-db.js';
@@ -273,7 +274,11 @@ const config = {
273
274
  * - 4 artifact tools (artifact_save, artifact_get, artifact_list, artifact_delete)
274
275
  * - 1 episode tool (recall_work)
275
276
  */
276
- export const allPlatformTools = createPlatformTools(config);
277
+ export const allPlatformTools = createPlatformTools(config, {
278
+ planModeCallbacks: {
279
+ isPlanModeActive: () => getPermissionMode() === 'plan',
280
+ },
281
+ });
277
282
  /**
278
283
  * All 5 factory tools from @compilr-dev/factory:
279
284
  * - 3 model tools (app_model_get, app_model_update, app_model_validate)
package/dist/tools.js CHANGED
@@ -125,7 +125,12 @@ import { delegateTool } from './tools/delegate.js';
125
125
  import { delegateBackgroundTool } from './tools/delegate-background.js';
126
126
  import { delegationStatusTool } from './tools/delegation-status.js';
127
127
  import { handoffTool } from './tools/handoff.js';
128
- import { guideTool } from './tools/guide-tool.js';
128
+ import { createGuideTool } from '@compilr-dev/sdk';
129
+ import { CLI_GUIDE_ENTRIES } from './guide/cli-guide-entries.js';
130
+ const guideTool = createGuideTool({
131
+ environment: 'cli',
132
+ additionalEntries: CLI_GUIDE_ENTRIES,
133
+ });
129
134
  // DB tools for project, work item, document, plan, backlog, anchor, artifact, and episode management
130
135
  import { allDbTools, allFactoryTools } from './tools/db-tools.js';
131
136
  // Meta-tools for dynamic tool loading
@@ -0,0 +1,57 @@
1
+ /**
2
+ * App Model Overlay V2
3
+ *
4
+ * Terminal visualization of the ApplicationModel (from @compilr-dev/factory).
5
+ * Three tabs: Entities (list with detail), Relationships (map), Summary (stats).
6
+ *
7
+ * Not a replacement for the desktop SVG viewer — text-based tables and lists
8
+ * for CLI users who want to inspect the model structure.
9
+ */
10
+ import { TabbedListOverlayV2, BaseScreen } from '../../base/index.js';
11
+ interface ModelField {
12
+ name: string;
13
+ label: string;
14
+ type: string;
15
+ required: boolean;
16
+ enumValues?: readonly string[];
17
+ defaultValue?: string | number | boolean;
18
+ }
19
+ interface ModelRelationship {
20
+ type: 'belongsTo' | 'hasMany';
21
+ target: string;
22
+ fieldName?: string;
23
+ }
24
+ interface ModelEntity {
25
+ name: string;
26
+ pluralName: string;
27
+ description?: string;
28
+ icon: string;
29
+ fields: readonly ModelField[];
30
+ views: readonly string[];
31
+ relationships: readonly ModelRelationship[];
32
+ }
33
+ export interface AppModelOverlayV2Options {
34
+ /** Raw JSON content from the document store */
35
+ modelJson: string;
36
+ /** Project display name */
37
+ projectName: string;
38
+ }
39
+ export interface AppModelOverlayV2Result {
40
+ dismissed: boolean;
41
+ }
42
+ interface ModelListItem {
43
+ id: string;
44
+ tab: string;
45
+ label: string;
46
+ detail: string;
47
+ entity?: ModelEntity;
48
+ }
49
+ export declare class AppModelOverlayV2 extends TabbedListOverlayV2<ModelListItem, AppModelOverlayV2Result> {
50
+ readonly type: "inline";
51
+ readonly id = "app-model-viewer";
52
+ private readonly model;
53
+ private readonly parseError;
54
+ constructor(options: AppModelOverlayV2Options);
55
+ protected createDetailScreen(item: ModelListItem): BaseScreen | null;
56
+ }
57
+ export {};
@@ -0,0 +1,232 @@
1
+ /**
2
+ * App Model Overlay V2
3
+ *
4
+ * Terminal visualization of the ApplicationModel (from @compilr-dev/factory).
5
+ * Three tabs: Entities (list with detail), Relationships (map), Summary (stats).
6
+ *
7
+ * Not a replacement for the desktop SVG viewer — text-based tables and lists
8
+ * for CLI users who want to inspect the model structure.
9
+ */
10
+ import * as terminal from '../../terminal.js';
11
+ import { TabbedListOverlayV2, BaseScreen, stay, popScreen, isEscape, isCtrlC, isClose, renderBorder, wrapText, truncate, } from '../../base/index.js';
12
+ import { parseKeyEvent } from '../key-utils.js';
13
+ // =============================================================================
14
+ // Tab Definitions
15
+ // =============================================================================
16
+ const MODEL_TABS = [
17
+ { id: 'entities', label: 'Entities' },
18
+ { id: 'relationships', label: 'Relationships' },
19
+ { id: 'summary', label: 'Summary' },
20
+ ];
21
+ // =============================================================================
22
+ // Main Overlay
23
+ // =============================================================================
24
+ export class AppModelOverlayV2 extends TabbedListOverlayV2 {
25
+ type = 'inline';
26
+ id = 'app-model-viewer';
27
+ model = null;
28
+ parseError = null;
29
+ constructor(options) {
30
+ // Parse model and build list items
31
+ let model = null;
32
+ let parseError = null;
33
+ let items = [];
34
+ try {
35
+ model = JSON.parse(options.modelJson);
36
+ items = buildListItems(model);
37
+ }
38
+ catch (e) {
39
+ parseError = `Failed to parse model: ${e instanceof Error ? e.message : String(e)}`;
40
+ }
41
+ // Compute column widths for aligned rendering
42
+ const maxEntityName = model
43
+ ? Math.max(8, ...model.entities.map((e) => e.name.length))
44
+ : 12;
45
+ const maxRelLabel = items
46
+ .filter((i) => i.tab === 'relationships')
47
+ .reduce((max, i) => Math.max(max, i.label.length), 12);
48
+ const maxSummaryLabel = items
49
+ .filter((i) => i.tab === 'summary')
50
+ .reduce((max, i) => Math.max(max, i.label.length), 12);
51
+ super({
52
+ title: `Application Model — ${options.projectName}`,
53
+ tabs: MODEL_TABS,
54
+ items,
55
+ pageSize: 15,
56
+ filterByTab: (item, tabId) => tabId === 'all' || item.tab === tabId,
57
+ getSearchText: (item) => `${item.label} ${item.detail}`,
58
+ renderItem: (item, isSelected, s) => {
59
+ const prefix = isSelected ? s.primary('›') : ' ';
60
+ if (item.tab === 'entities') {
61
+ const entity = item.entity;
62
+ const fields = entity ? String(entity.fields.length).padStart(2) : ' 0';
63
+ const rels = entity ? String(entity.relationships.length) : '0';
64
+ const name = item.label.padEnd(maxEntityName + 2);
65
+ const desc = entity?.description ? s.muted(truncate(entity.description, 30)) : '';
66
+ return `${prefix} ${isSelected ? s.primary(name) : name} ${s.secondary(`${fields} fields · ${rels} rels`)} ${desc}`;
67
+ }
68
+ if (item.tab === 'relationships') {
69
+ const label = item.label.padEnd(maxRelLabel + 2);
70
+ return `${prefix} ${isSelected ? s.primary(label) : label} ${s.secondary(item.detail)}`;
71
+ }
72
+ // summary
73
+ const label = (item.label + ':').padEnd(maxSummaryLabel + 3);
74
+ return `${prefix} ${s.secondary(label)} ${isSelected ? s.primary(item.detail) : item.detail}`;
75
+ },
76
+ footerHints: () => '↑↓ Navigate · Enter Detail · Tab/← → Tabs · / Search · q Close',
77
+ });
78
+ this.model = model;
79
+ this.parseError = parseError;
80
+ }
81
+ createDetailScreen(item) {
82
+ if (item.entity) {
83
+ return new EntityDetailScreen(item.entity, this.getStyles());
84
+ }
85
+ return null;
86
+ }
87
+ }
88
+ // =============================================================================
89
+ // Entity Detail Screen
90
+ // =============================================================================
91
+ class EntityDetailScreen extends BaseScreen {
92
+ entity;
93
+ styles;
94
+ scrollOffset = 0;
95
+ constructor(entity, styles) {
96
+ super();
97
+ this.entity = entity;
98
+ this.styles = styles;
99
+ }
100
+ render() {
101
+ const s = this.styles;
102
+ const cols = terminal.getTerminalWidth();
103
+ const e = this.entity;
104
+ const lines = [];
105
+ // Header
106
+ lines.push(renderBorder(cols, s));
107
+ lines.push(` ${s.primaryBold(e.name)}`);
108
+ lines.push('');
109
+ if (e.description) {
110
+ for (const wrapped of wrapText(e.description, cols - 6)) {
111
+ lines.push(` ${s.secondary(wrapped)}`);
112
+ }
113
+ lines.push('');
114
+ }
115
+ // Info line
116
+ lines.push(` ${s.muted('Plural:')} ${e.pluralName} ${s.muted('Views:')} ${e.views.join(', ')}`);
117
+ lines.push('');
118
+ // Fields table
119
+ lines.push(` ${s.primaryBold('Fields')} ${s.muted(`(${String(e.fields.length)})`)}`);
120
+ lines.push(` ${s.muted('─'.repeat(Math.min(cols - 4, 60)))}`);
121
+ const nameWidth = Math.max(12, ...e.fields.map((f) => f.name.length)) + 1;
122
+ for (const field of e.fields) {
123
+ const name = field.name.padEnd(nameWidth);
124
+ const type = field.type.padEnd(10);
125
+ const req = field.required ? s.primary('*') : ' ';
126
+ const enums = field.enumValues ? s.muted(` [${field.enumValues.join(', ')}]`) : '';
127
+ const def = field.defaultValue !== undefined ? s.muted(` = ${String(field.defaultValue)}`) : '';
128
+ lines.push(` ${req} ${name} ${s.secondary(type)}${enums}${def}`);
129
+ }
130
+ // Relationships
131
+ if (e.relationships.length > 0) {
132
+ lines.push('');
133
+ lines.push(` ${s.primaryBold('Relationships')} ${s.muted(`(${String(e.relationships.length)})`)}`);
134
+ lines.push(` ${s.muted('─'.repeat(Math.min(cols - 4, 60)))}`);
135
+ for (const rel of e.relationships) {
136
+ const arrow = rel.type === 'hasMany' ? '──▸' : '◂──';
137
+ const label = rel.type === 'hasMany' ? 'has many' : 'belongs to';
138
+ const via = rel.fieldName ? s.muted(` via ${rel.fieldName}`) : '';
139
+ lines.push(` ${e.name} ${arrow} ${s.primary(rel.target)} ${s.muted(label)}${via}`);
140
+ }
141
+ }
142
+ lines.push('');
143
+ lines.push(` ${s.muted('↑↓ Scroll · Esc Back')}`);
144
+ lines.push(renderBorder(cols, s));
145
+ return lines;
146
+ }
147
+ handleKey(data) {
148
+ if (isEscape(data) || isCtrlC(data) || isClose(data)) {
149
+ return popScreen();
150
+ }
151
+ const key = parseKeyEvent(data);
152
+ if (key.name === 'up' && this.scrollOffset > 0) {
153
+ this.scrollOffset--;
154
+ return stay();
155
+ }
156
+ if (key.name === 'down') {
157
+ this.scrollOffset++;
158
+ return stay();
159
+ }
160
+ return stay();
161
+ }
162
+ getMinHeight() {
163
+ return 15;
164
+ }
165
+ }
166
+ // =============================================================================
167
+ // Build list items from model
168
+ // =============================================================================
169
+ function buildListItems(model) {
170
+ const items = [];
171
+ // Entity items
172
+ for (const entity of model.entities) {
173
+ items.push({
174
+ id: `entity-${entity.name}`,
175
+ tab: 'entities',
176
+ label: entity.name,
177
+ detail: `${String(entity.fields.length)} fields, ${String(entity.relationships.length)} relationships`,
178
+ entity,
179
+ });
180
+ }
181
+ // Relationship items (flattened from all entities)
182
+ for (const entity of model.entities) {
183
+ for (const rel of entity.relationships) {
184
+ const arrow = rel.type === 'hasMany' ? '→' : '←';
185
+ items.push({
186
+ id: `rel-${entity.name}-${rel.target}-${rel.type}`,
187
+ tab: 'relationships',
188
+ label: `${entity.name} ${arrow} ${rel.target}`,
189
+ detail: rel.type === 'hasMany' ? `has many ${rel.target}` : `belongs to ${rel.target}`,
190
+ });
191
+ }
192
+ }
193
+ // Summary items
194
+ const totalFields = model.entities.reduce((sum, e) => sum + e.fields.length, 0);
195
+ const totalRels = model.entities.reduce((sum, e) => sum + e.relationships.length, 0);
196
+ const requiredFields = model.entities.reduce((sum, e) => sum + e.fields.filter((f) => f.required).length, 0);
197
+ const enumFields = model.entities.reduce((sum, e) => sum + e.fields.filter((f) => f.type === 'enum').length, 0);
198
+ // Identity
199
+ items.push({ id: 'sum-name', tab: 'summary', label: 'Application', detail: model.identity.name }, { id: 'sum-desc', tab: 'summary', label: 'Description', detail: truncate(model.identity.description, 60) }, { id: 'sum-version', tab: 'summary', label: 'Version', detail: model.identity.version });
200
+ // Data model stats
201
+ items.push({ id: 'sum-sep1', tab: 'summary', label: '── Data Model', detail: '' }, { id: 'sum-entities', tab: 'summary', label: 'Entities', detail: String(model.entities.length) }, { id: 'sum-fields', tab: 'summary', label: 'Total Fields', detail: `${String(totalFields)} (${String(requiredFields)} required, ${String(enumFields)} enums)` }, { id: 'sum-rels', tab: 'summary', label: 'Relationships', detail: String(totalRels) });
202
+ // Configuration
203
+ const configItems = [];
204
+ if (model.layout?.shell) {
205
+ configItems.push({ id: 'sum-layout', tab: 'summary', label: 'Layout', detail: model.layout.shell });
206
+ }
207
+ if (model.techStack?.toolkit) {
208
+ configItems.push({ id: 'sum-toolkit', tab: 'summary', label: 'Toolkit', detail: model.techStack.toolkit });
209
+ }
210
+ if (model.theme?.primaryColor) {
211
+ configItems.push({ id: 'sum-color', tab: 'summary', label: 'Primary Color', detail: model.theme.primaryColor });
212
+ }
213
+ const featureList = [];
214
+ if (model.features?.dashboard)
215
+ featureList.push('Dashboard');
216
+ if (model.features?.auth)
217
+ featureList.push('Auth');
218
+ if (model.features?.userProfiles)
219
+ featureList.push('Profiles');
220
+ if (model.features?.settings)
221
+ featureList.push('Settings');
222
+ if (model.features?.darkMode)
223
+ featureList.push('Dark Mode');
224
+ if (featureList.length > 0) {
225
+ configItems.push({ id: 'sum-features', tab: 'summary', label: 'Features', detail: featureList.join(', ') });
226
+ }
227
+ if (configItems.length > 0) {
228
+ items.push({ id: 'sum-sep2', tab: 'summary', label: '── Configuration', detail: '' });
229
+ items.push(...configItems);
230
+ }
231
+ return items;
232
+ }
@@ -20,7 +20,7 @@ interface FormField {
20
20
  value: string;
21
21
  error?: string;
22
22
  }
23
- type WizardStep = 'basic' | 'tools' | 'skills' | 'tier';
23
+ type WizardStep = 'template-select' | 'basic' | 'tools' | 'skills' | 'tier' | 'prompt';
24
24
  interface FormState {
25
25
  fields: FormField[];
26
26
  activeFieldIndex: number;
@@ -35,20 +35,37 @@ interface FormState {
35
35
  skillListIndex: number;
36
36
  selectedModelTier: ModelTier;
37
37
  tierListIndex: number;
38
+ systemPromptAddition: string;
39
+ templateListIndex: number;
40
+ saveAsTemplate: boolean;
41
+ templateName: string;
42
+ }
43
+ export interface TemplateOption {
44
+ id: string;
45
+ name: string;
46
+ displayName: string;
47
+ specialty: string;
48
+ personality?: string;
49
+ systemPromptAddition?: string;
38
50
  }
39
51
  export interface CustomAgentFormOptions {
40
52
  /** Existing custom agents (for duplicate check) */
41
53
  existingCustomAgents: CustomAgentDefinition[];
42
54
  /** Current team agent IDs (for duplicate check) */
43
55
  teamAgentIds: string[];
56
+ /** Available templates to load from */
57
+ templates?: TemplateOption[];
44
58
  }
45
59
  export interface CustomAgentFormResult {
46
60
  cancelled: boolean;
61
+ /** Template name to save as (if user requested) */
62
+ saveAsTemplate?: string;
47
63
  agent?: {
48
64
  id: string;
49
65
  displayName: string;
50
66
  specialty: string;
51
67
  personality?: string;
68
+ systemPromptAddition?: string;
52
69
  toolConfig: ToolConfig;
53
70
  enabledSkills: string[];
54
71
  modelTier: ModelTier;
@@ -59,8 +76,11 @@ export declare class CustomAgentFormOverlayV2 extends BaseOverlayV2<FormState, C
59
76
  readonly id = "custom-agent-form-overlay-v2";
60
77
  private readonly existingCustomAgents;
61
78
  private readonly teamAgentIds;
79
+ private readonly templates;
62
80
  constructor(options: CustomAgentFormOptions);
63
81
  protected renderContent(context: RenderContext): string[];
82
+ private renderTemplateSelectStep;
83
+ private handleTemplateSelectKey;
64
84
  private renderBasicStep;
65
85
  private renderToolsStep;
66
86
  private renderGroupItem;
@@ -72,6 +92,8 @@ export declare class CustomAgentFormOverlayV2 extends BaseOverlayV2<FormState, C
72
92
  private handleSkillsStepKey;
73
93
  private renderTierStep;
74
94
  private handleTierStepKey;
95
+ private renderPromptStep;
96
+ private handlePromptStepKey;
75
97
  private nextField;
76
98
  private prevField;
77
99
  private validateBasicInfo;