@moxxy/cli 0.0.12 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +278 -112
  2. package/bin/moxxy +10 -0
  3. package/package.json +36 -53
  4. package/src/api-client.js +286 -0
  5. package/src/cli.js +349 -0
  6. package/src/commands/agent.js +413 -0
  7. package/src/commands/auth.js +326 -0
  8. package/src/commands/channel.js +285 -0
  9. package/src/commands/doctor.js +261 -0
  10. package/src/commands/events.js +80 -0
  11. package/src/commands/gateway.js +428 -0
  12. package/src/commands/heartbeat.js +145 -0
  13. package/src/commands/init.js +954 -0
  14. package/src/commands/mcp.js +278 -0
  15. package/src/commands/plugin.js +583 -0
  16. package/src/commands/provider.js +1934 -0
  17. package/src/commands/settings.js +224 -0
  18. package/src/commands/skill.js +125 -0
  19. package/src/commands/template.js +237 -0
  20. package/src/commands/uninstall.js +196 -0
  21. package/src/commands/update.js +406 -0
  22. package/src/commands/vault.js +219 -0
  23. package/src/help.js +392 -0
  24. package/src/lib/plugin-registry.js +98 -0
  25. package/src/platform.js +40 -0
  26. package/src/sse-client.js +79 -0
  27. package/src/tui/action-wizards.js +130 -0
  28. package/src/tui/app.jsx +859 -0
  29. package/src/tui/components/action-picker.jsx +86 -0
  30. package/src/tui/components/chat-panel.jsx +120 -0
  31. package/src/tui/components/footer.jsx +13 -0
  32. package/src/tui/components/header.jsx +45 -0
  33. package/src/tui/components/input-area.jsx +384 -0
  34. package/src/tui/components/messages/ask-message.jsx +13 -0
  35. package/src/tui/components/messages/assistant-message.jsx +165 -0
  36. package/src/tui/components/messages/channel-message.jsx +18 -0
  37. package/src/tui/components/messages/event-message.jsx +22 -0
  38. package/src/tui/components/messages/hive-status.jsx +34 -0
  39. package/src/tui/components/messages/skill-message.jsx +31 -0
  40. package/src/tui/components/messages/system-message.jsx +12 -0
  41. package/src/tui/components/messages/thinking.jsx +25 -0
  42. package/src/tui/components/messages/tool-group.jsx +62 -0
  43. package/src/tui/components/messages/tool-message.jsx +66 -0
  44. package/src/tui/components/messages/user-message.jsx +12 -0
  45. package/src/tui/components/model-picker.jsx +138 -0
  46. package/src/tui/components/multiline-input.jsx +72 -0
  47. package/src/tui/events-handler.js +730 -0
  48. package/src/tui/helpers.js +59 -0
  49. package/src/tui/hooks/use-command-handler.js +451 -0
  50. package/src/tui/index.jsx +55 -0
  51. package/src/tui/input-utils.js +26 -0
  52. package/src/tui/markdown-renderer.js +66 -0
  53. package/src/tui/mcp-wizard.js +136 -0
  54. package/src/tui/model-picker.js +174 -0
  55. package/src/tui/slash-commands.js +26 -0
  56. package/src/tui/store.js +12 -0
  57. package/src/tui/theme.js +17 -0
  58. package/src/ui.js +109 -0
  59. package/bin/moxxy.js +0 -2
  60. package/dist/chunk-23LZYKQ6.mjs +0 -1131
  61. package/dist/chunk-2FZEA3NG.mjs +0 -457
  62. package/dist/chunk-3KDPLS22.mjs +0 -1131
  63. package/dist/chunk-3QRJTRBT.mjs +0 -1102
  64. package/dist/chunk-6DZX6EAA.mjs +0 -37
  65. package/dist/chunk-A4WRDUNY.mjs +0 -1242
  66. package/dist/chunk-C46NSEKG.mjs +0 -211
  67. package/dist/chunk-CAUXONEF.mjs +0 -1131
  68. package/dist/chunk-CPL5V56X.mjs +0 -1131
  69. package/dist/chunk-CTBVTTBG.mjs +0 -440
  70. package/dist/chunk-FHHLXTEZ.mjs +0 -1121
  71. package/dist/chunk-FXY3GPVA.mjs +0 -1126
  72. package/dist/chunk-GSNMMI3H.mjs +0 -530
  73. package/dist/chunk-HHOAOGUS.mjs +0 -1242
  74. package/dist/chunk-ITBO7BKI.mjs +0 -1243
  75. package/dist/chunk-J33O35WX.mjs +0 -532
  76. package/dist/chunk-N5JTPB6U.mjs +0 -820
  77. package/dist/chunk-NGVL4Q5C.mjs +0 -1102
  78. package/dist/chunk-Q2OCMNYI.mjs +0 -1131
  79. package/dist/chunk-QDVRLN6D.mjs +0 -1121
  80. package/dist/chunk-QO2JONHP.mjs +0 -1131
  81. package/dist/chunk-RVAPILHA.mjs +0 -1242
  82. package/dist/chunk-S7YBOV7E.mjs +0 -1131
  83. package/dist/chunk-SHIG6Y5L.mjs +0 -1074
  84. package/dist/chunk-SOFST2PV.mjs +0 -1242
  85. package/dist/chunk-SUNUYS6G.mjs +0 -1243
  86. package/dist/chunk-TMZWETMH.mjs +0 -1242
  87. package/dist/chunk-TYD7NMMI.mjs +0 -581
  88. package/dist/chunk-TYQ3YS42.mjs +0 -1068
  89. package/dist/chunk-UALWCJ7F.mjs +0 -1131
  90. package/dist/chunk-UQZKODNW.mjs +0 -1124
  91. package/dist/chunk-USC6R2ON.mjs +0 -1242
  92. package/dist/chunk-W32EQCVC.mjs +0 -823
  93. package/dist/chunk-WMB5ENMC.mjs +0 -1242
  94. package/dist/chunk-WNHA5JAP.mjs +0 -1242
  95. package/dist/cli-2AIWTL6F.mjs +0 -8
  96. package/dist/cli-2QKJ5UUL.mjs +0 -8
  97. package/dist/cli-4RIS6DQX.mjs +0 -8
  98. package/dist/cli-5RH4VBBL.mjs +0 -7
  99. package/dist/cli-7MK4YGOP.mjs +0 -7
  100. package/dist/cli-B4KH6MZI.mjs +0 -8
  101. package/dist/cli-CGO2LZ6Z.mjs +0 -8
  102. package/dist/cli-CVP26EL2.mjs +0 -8
  103. package/dist/cli-DDRVVNAV.mjs +0 -8
  104. package/dist/cli-E7U56QVQ.mjs +0 -8
  105. package/dist/cli-EQNRMLL3.mjs +0 -8
  106. package/dist/cli-F5RUHHH4.mjs +0 -8
  107. package/dist/cli-LX6FFSEF.mjs +0 -8
  108. package/dist/cli-LY74GWKR.mjs +0 -6
  109. package/dist/cli-MAT3ZJHI.mjs +0 -8
  110. package/dist/cli-NJXXTQYF.mjs +0 -8
  111. package/dist/cli-O4ZGFAZG.mjs +0 -8
  112. package/dist/cli-ORVLI3UQ.mjs +0 -8
  113. package/dist/cli-PV43ZVKA.mjs +0 -8
  114. package/dist/cli-REVD6ISM.mjs +0 -8
  115. package/dist/cli-TBX76KQX.mjs +0 -8
  116. package/dist/cli-THCGF7SQ.mjs +0 -8
  117. package/dist/cli-TLX5ENVM.mjs +0 -8
  118. package/dist/cli-TMNI5ZYE.mjs +0 -8
  119. package/dist/cli-TNJHCBQA.mjs +0 -6
  120. package/dist/cli-TUX22CZP.mjs +0 -8
  121. package/dist/cli-XJVH7EEP.mjs +0 -8
  122. package/dist/cli-XXOW4VXJ.mjs +0 -8
  123. package/dist/cli-XZ5RESNB.mjs +0 -6
  124. package/dist/cli-YCBYZ76Q.mjs +0 -8
  125. package/dist/cli-ZLMQCU7X.mjs +0 -8
  126. package/dist/dist-2VGKJRBH.mjs +0 -6820
  127. package/dist/dist-37BNX4QG.mjs +0 -7081
  128. package/dist/dist-7LTHRYKA.mjs +0 -11569
  129. package/dist/dist-7XJPQW5C.mjs +0 -6950
  130. package/dist/dist-AYMVOW7T.mjs +0 -7123
  131. package/dist/dist-BHUWCDRS.mjs +0 -7132
  132. package/dist/dist-FAXRJMEN.mjs +0 -6812
  133. package/dist/dist-HQGANM3P.mjs +0 -6976
  134. package/dist/dist-KATLOZQV.mjs +0 -7054
  135. package/dist/dist-KLSB6YHV.mjs +0 -6964
  136. package/dist/dist-LKIOZQ42.mjs +0 -17
  137. package/dist/dist-UYA4RJUH.mjs +0 -2792
  138. package/dist/dist-ZYHCBILM.mjs +0 -6993
  139. package/dist/index.d.mts +0 -23
  140. package/dist/index.d.ts +0 -23
  141. package/dist/index.js +0 -25531
  142. package/dist/index.mjs +0 -18
  143. package/dist/src-APP5P3UD.mjs +0 -1386
  144. package/dist/src-D5HMDDVE.mjs +0 -1324
  145. package/dist/src-EK3WD4AU.mjs +0 -1327
  146. package/dist/src-LSZFLMFN.mjs +0 -1400
  147. package/dist/src-T77DFTFP.mjs +0 -1407
  148. package/dist/src-WIOCZRAC.mjs +0 -1397
  149. package/dist/src-YK6CHCMW.mjs +0 -1400
@@ -0,0 +1,224 @@
1
+ import { p, isInteractive, showResult } from '../ui.js';
2
+ import { parseFlags } from './auth.js';
3
+ import { getMoxxyHome } from './init.js';
4
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ const VALID_NETWORK_MODES = ['safe', 'unsafe'];
8
+
9
+ function settingsPath() {
10
+ return join(getMoxxyHome(), 'settings.yaml');
11
+ }
12
+
13
+ function loadSettings() {
14
+ try {
15
+ const raw = readFileSync(settingsPath(), 'utf-8');
16
+ if (!raw.trim()) return {};
17
+ const settings = {};
18
+ for (const line of raw.split('\n')) {
19
+ const match = line.match(/^(\w+):\s*(.+)$/);
20
+ if (match) settings[match[1]] = match[2].trim();
21
+ }
22
+ return settings;
23
+ } catch {
24
+ return {};
25
+ }
26
+ }
27
+
28
+ function saveSettings(settings) {
29
+ const dir = getMoxxyHome();
30
+ mkdirSync(dir, { recursive: true });
31
+ const lines = Object.entries(settings).map(([k, v]) => `${k}: ${v}`);
32
+ writeFileSync(settingsPath(), lines.join('\n') + '\n');
33
+ }
34
+
35
+ export function parseSettingsCommand(args) {
36
+ const [action, ...rest] = args;
37
+ const flags = parseFlags(rest);
38
+ return { action, flags };
39
+ }
40
+
41
+ async function settingsGet(flags) {
42
+ const settings = loadSettings();
43
+ const key = flags.key;
44
+
45
+ if (flags.json) {
46
+ console.log(JSON.stringify(key ? { key, value: settings[key] ?? null } : settings, null, 2));
47
+ return;
48
+ }
49
+
50
+ if (key) {
51
+ const value = settings[key] ?? '(not set)';
52
+ p.log.info(`${key}: ${value}`);
53
+ } else {
54
+ const entries = Object.entries(settings);
55
+ if (entries.length === 0) {
56
+ p.log.info('No settings configured. Using defaults.');
57
+ p.log.info(' network_mode: safe (default)');
58
+ } else {
59
+ for (const [k, v] of entries) {
60
+ p.log.info(`${k}: ${v}`);
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ async function settingsSet(flags) {
67
+ const key = flags.key;
68
+ const value = flags.value;
69
+
70
+ if (!key) throw new Error('Required: --key');
71
+ if (value === undefined) throw new Error('Required: --value');
72
+
73
+ // Validate known keys
74
+ if (key === 'network_mode' && !VALID_NETWORK_MODES.includes(value)) {
75
+ throw new Error(`Invalid network_mode '${value}'. Must be one of: ${VALID_NETWORK_MODES.join(', ')}`);
76
+ }
77
+
78
+ const settings = loadSettings();
79
+ settings[key] = value;
80
+ saveSettings(settings);
81
+
82
+ if (flags.json) {
83
+ console.log(JSON.stringify({ status: 'updated', key, value }, null, 2));
84
+ } else {
85
+ showResult({ status: 'updated', key, value }, p);
86
+ }
87
+ }
88
+
89
+ async function settingsNetworkMode(flags) {
90
+ const settings = loadSettings();
91
+ const current = settings.network_mode || 'safe';
92
+
93
+ // No value provided — show current or toggle interactively
94
+ if (!flags.mode && !flags._positional) {
95
+ if (isInteractive()) {
96
+ const selected = await p.select({
97
+ message: `Network mode (currently: ${current})`,
98
+ options: [
99
+ { value: 'safe', label: 'Safe', hint: 'agent asks user before accessing non-allowlisted domains' },
100
+ { value: 'unsafe', label: 'Unsafe', hint: 'domain allowlist is bypassed entirely' },
101
+ ],
102
+ initialValue: current,
103
+ });
104
+ if (p.isCancel(selected)) return;
105
+ settings.network_mode = selected;
106
+ saveSettings(settings);
107
+ p.log.success(`Network mode set to: ${selected}`);
108
+ } else {
109
+ if (flags.json) {
110
+ console.log(JSON.stringify({ network_mode: current }, null, 2));
111
+ } else {
112
+ p.log.info(`network_mode: ${current}`);
113
+ }
114
+ }
115
+ return;
116
+ }
117
+
118
+ const mode = flags.mode || flags._positional;
119
+ if (!VALID_NETWORK_MODES.includes(mode)) {
120
+ throw new Error(`Invalid mode '${mode}'. Must be one of: ${VALID_NETWORK_MODES.join(', ')}`);
121
+ }
122
+
123
+ settings.network_mode = mode;
124
+ saveSettings(settings);
125
+
126
+ if (flags.json) {
127
+ console.log(JSON.stringify({ status: 'updated', network_mode: mode }, null, 2));
128
+ } else {
129
+ p.log.success(`Network mode set to: ${mode}`);
130
+ }
131
+ }
132
+
133
+ async function settingsBrowserRendering(flags) {
134
+ const settings = loadSettings();
135
+ const current = settings.browser_rendering === 'true';
136
+
137
+ // No value provided — show current or toggle interactively
138
+ if (!flags._positional) {
139
+ if (isInteractive()) {
140
+ const selected = await p.select({
141
+ message: `Browser rendering (currently: ${current ? 'enabled' : 'disabled'})`,
142
+ options: [
143
+ { value: 'true', label: 'Enabled', hint: 'agents can render JS-heavy pages via headless Chrome' },
144
+ { value: 'false', label: 'Disabled', hint: 'agents use HTTP-only browsing' },
145
+ ],
146
+ initialValue: current ? 'true' : 'false',
147
+ });
148
+ if (p.isCancel(selected)) return;
149
+ settings.browser_rendering = selected;
150
+ saveSettings(settings);
151
+ p.log.success(`Browser rendering ${selected === 'true' ? 'enabled' : 'disabled'}.`);
152
+ } else {
153
+ if (flags.json) {
154
+ console.log(JSON.stringify({ browser_rendering: current }, null, 2));
155
+ } else {
156
+ p.log.info(`browser_rendering: ${current}`);
157
+ }
158
+ }
159
+ return;
160
+ }
161
+
162
+ const val = flags._positional;
163
+ if (!['true', 'false', 'on', 'off', 'enable', 'disable'].includes(val)) {
164
+ throw new Error(`Invalid value '${val}'. Use: true/false, on/off, or enable/disable`);
165
+ }
166
+
167
+ const enabled = ['true', 'on', 'enable'].includes(val);
168
+ settings.browser_rendering = String(enabled);
169
+ saveSettings(settings);
170
+
171
+ if (flags.json) {
172
+ console.log(JSON.stringify({ status: 'updated', browser_rendering: enabled }, null, 2));
173
+ } else {
174
+ p.log.success(`Browser rendering ${enabled ? 'enabled' : 'disabled'}.`);
175
+ }
176
+ }
177
+
178
+ export async function runSettings(_client, args) {
179
+ const { action, flags } = parseSettingsCommand(args);
180
+
181
+ // Collect first positional arg after the action for convenience
182
+ // e.g. `moxxy settings network-mode unsafe`
183
+ const restArgs = args.slice(1).filter(a => !a.startsWith('--'));
184
+ if (restArgs.length > 0 && !flags._positional) {
185
+ flags._positional = restArgs[0];
186
+ }
187
+
188
+ switch (action) {
189
+ case 'get':
190
+ await settingsGet(flags);
191
+ break;
192
+ case 'set':
193
+ await settingsSet(flags);
194
+ break;
195
+ case 'network-mode':
196
+ await settingsNetworkMode(flags);
197
+ break;
198
+ case 'browser-rendering':
199
+ await settingsBrowserRendering(flags);
200
+ break;
201
+ default:
202
+ if (isInteractive() && !action) {
203
+ // Interactive: show settings menu
204
+ const selected = await p.select({
205
+ message: 'Settings',
206
+ options: [
207
+ { value: 'network-mode', label: 'Network mode', hint: 'safe / unsafe domain access' },
208
+ { value: 'browser-rendering', label: 'Browser rendering', hint: 'headless Chrome for JS-heavy sites' },
209
+ { value: 'get', label: 'View all settings', hint: 'show current configuration' },
210
+ ],
211
+ });
212
+ if (p.isCancel(selected)) return;
213
+ await runSettings(_client, [selected]);
214
+ } else {
215
+ throw new Error(
216
+ 'Usage: moxxy settings <action>\n' +
217
+ ' network-mode [safe|unsafe] Get or set network mode\n' +
218
+ ' browser-rendering [true|false] Enable/disable headless Chrome rendering\n' +
219
+ ' get [--key <k>] View settings\n' +
220
+ ' set --key <k> --value <v> Set a setting'
221
+ );
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Skill commands: create/remove/list.
3
+ */
4
+ import { parseFlags } from './auth.js';
5
+ import { isInteractive, handleCancel, withSpinner, showResult, pickAgent, pickSkill, p } from '../ui.js';
6
+
7
+ export async function runSkill(client, args) {
8
+ let [action, ...rest] = args;
9
+ const flags = parseFlags(rest);
10
+
11
+ // Interactive sub-menu when no valid action
12
+ if (!['create', 'remove', 'list'].includes(action) && isInteractive()) {
13
+ action = await p.select({
14
+ message: 'Skill action',
15
+ options: [
16
+ { value: 'create', label: 'Create skill', hint: 'install a skill on an agent' },
17
+ { value: 'remove', label: 'Remove skill', hint: 'remove a skill from an agent' },
18
+ { value: 'list', label: 'List skills', hint: 'list agent skills' },
19
+ ],
20
+ });
21
+ handleCancel(action);
22
+ }
23
+
24
+ switch (action) {
25
+ case 'create': {
26
+ let agentId = flags.agent;
27
+
28
+ // Interactive wizard when missing agent
29
+ if (!agentId && isInteractive()) {
30
+ agentId = await pickAgent(client, 'Select agent for skill');
31
+
32
+ const content = flags.content || handleCancel(await p.text({
33
+ message: 'Skill content (SKILL.md with YAML frontmatter)',
34
+ placeholder: 'Paste skill content...',
35
+ validate: (val) => { if (!val) return 'Content is required'; },
36
+ }));
37
+
38
+ const body = { content };
39
+ const result = await withSpinner('Creating skill...', () =>
40
+ client.request(`/v1/agents/${encodeURIComponent(agentId)}/skills/install`, 'POST', body), 'Skill created.');
41
+
42
+ showResult('Skill Created', {
43
+ Agent: agentId,
44
+ Name: result.name,
45
+ Version: result.version,
46
+ });
47
+
48
+ return result;
49
+ }
50
+
51
+ if (!agentId) throw new Error('Required: --agent');
52
+ const body = {
53
+ content: flags.content || '',
54
+ };
55
+ const result = await client.request(`/v1/agents/${encodeURIComponent(agentId)}/skills/install`, 'POST', body);
56
+ console.log(JSON.stringify(result, null, 2));
57
+ return result;
58
+ }
59
+
60
+ case 'remove': {
61
+ let agentId = flags.agent;
62
+ let skillId = flags.skill;
63
+
64
+ if ((!agentId || !skillId) && isInteractive()) {
65
+ if (!agentId) {
66
+ agentId = await pickAgent(client, 'Select agent');
67
+ }
68
+ if (!skillId) {
69
+ skillId = await pickSkill(client, agentId, 'Select skill to remove');
70
+ }
71
+
72
+ const confirmed = await p.confirm({
73
+ message: 'Remove this skill?',
74
+ initialValue: false,
75
+ });
76
+ handleCancel(confirmed);
77
+ if (!confirmed) {
78
+ p.log.info('Cancelled.');
79
+ return;
80
+ }
81
+ }
82
+
83
+ if (!agentId || !skillId) throw new Error('Required: --agent, --skill');
84
+ const result = await client.deleteSkill(agentId, skillId);
85
+ if (isInteractive()) {
86
+ p.log.success(`Skill ${skillId} removed.`);
87
+ } else {
88
+ console.log(`Skill ${skillId} removed.`);
89
+ }
90
+ return result;
91
+ }
92
+
93
+ case 'list': {
94
+ let agentId = flags.agent;
95
+ if (!agentId && isInteractive()) {
96
+ agentId = await pickAgent(client, 'Select agent to list skills');
97
+ }
98
+ if (!agentId) throw new Error('Required: --agent');
99
+
100
+ const skills = isInteractive()
101
+ ? await withSpinner('Fetching skills...', () =>
102
+ client.listSkills(agentId), 'Skills loaded.')
103
+ : await client.listSkills(agentId);
104
+
105
+ if (isInteractive()) {
106
+ if (Array.isArray(skills) && skills.length > 0) {
107
+ for (const s of skills) {
108
+ p.log.info(`${s.name} v${s.version} (${s.slug || s.name})`);
109
+ }
110
+ } else {
111
+ p.log.warn('No skills found for this agent.');
112
+ }
113
+ } else {
114
+ console.log(JSON.stringify(skills, null, 2));
115
+ }
116
+ return skills;
117
+ }
118
+
119
+ default: {
120
+ const { showHelp } = await import('../help.js');
121
+ showHelp('skill', p);
122
+ break;
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Template commands: list/get/create/update/remove/assign.
3
+ */
4
+ import { parseFlags } from './auth.js';
5
+ import { isInteractive, handleCancel, withSpinner, showResult, pickAgent, p } from '../ui.js';
6
+
7
+ async function pickTemplate(client, message = 'Select template') {
8
+ const templates = await client.listTemplates();
9
+ if (!templates || templates.length === 0) {
10
+ throw new Error('No templates found. Create one first.');
11
+ }
12
+ const options = templates.map(t => ({
13
+ value: t.slug,
14
+ label: t.name,
15
+ hint: t.description,
16
+ }));
17
+ const selected = await p.select({ message, options });
18
+ handleCancel(selected);
19
+ return selected;
20
+ }
21
+
22
+ export async function runTemplate(client, args) {
23
+ let [action, ...rest] = args;
24
+ const flags = parseFlags(rest);
25
+
26
+ // Interactive sub-menu when no valid action
27
+ if (!['list', 'get', 'create', 'update', 'remove', 'assign'].includes(action) && isInteractive()) {
28
+ action = await p.select({
29
+ message: 'Template action',
30
+ options: [
31
+ { value: 'list', label: 'List templates', hint: 'list all templates' },
32
+ { value: 'get', label: 'Get template', hint: 'view template details' },
33
+ { value: 'create', label: 'Create template', hint: 'create a new template' },
34
+ { value: 'update', label: 'Update template', hint: 'update an existing template' },
35
+ { value: 'remove', label: 'Remove template', hint: 'delete a template' },
36
+ { value: 'assign', label: 'Assign template', hint: 'assign a template to an agent' },
37
+ ],
38
+ });
39
+ handleCancel(action);
40
+ }
41
+
42
+ switch (action) {
43
+ case 'list': {
44
+ const templates = isInteractive()
45
+ ? await withSpinner('Fetching templates...', () =>
46
+ client.listTemplates(), 'Templates loaded.')
47
+ : await client.listTemplates();
48
+
49
+ if (isInteractive()) {
50
+ if (Array.isArray(templates) && templates.length > 0) {
51
+ for (const t of templates) {
52
+ const tags = t.tags && t.tags.length > 0 ? ` [${t.tags.join(', ')}]` : '';
53
+ p.log.info(`${t.name} v${t.version} (${t.slug})${tags}`);
54
+ }
55
+ } else {
56
+ p.log.warn('No templates found.');
57
+ }
58
+ } else {
59
+ console.log(JSON.stringify(templates, null, 2));
60
+ }
61
+ return templates;
62
+ }
63
+
64
+ case 'get': {
65
+ let slug = flags.slug || rest[0];
66
+ if (!slug && isInteractive()) {
67
+ slug = await pickTemplate(client, 'Select template to view');
68
+ }
69
+ if (!slug) throw new Error('Required: --slug or positional argument');
70
+
71
+ const template = isInteractive()
72
+ ? await withSpinner('Fetching template...', () =>
73
+ client.getTemplate(slug), 'Template loaded.')
74
+ : await client.getTemplate(slug);
75
+
76
+ if (isInteractive()) {
77
+ showResult('Template', {
78
+ Name: template.name,
79
+ Slug: template.slug,
80
+ Version: template.version,
81
+ Description: template.description,
82
+ Tags: (template.tags || []).join(', ') || '(none)',
83
+ });
84
+ if (template.body) {
85
+ p.log.info(`\nContent:\n${template.body}`);
86
+ }
87
+ } else {
88
+ console.log(JSON.stringify(template, null, 2));
89
+ }
90
+ return template;
91
+ }
92
+
93
+ case 'create': {
94
+ let content = flags.content;
95
+
96
+ if (!content && isInteractive()) {
97
+ content = handleCancel(await p.text({
98
+ message: 'Template content (TEMPLATE.md with YAML frontmatter)',
99
+ placeholder: 'Paste template content...',
100
+ validate: (val) => { if (!val) return 'Content is required'; },
101
+ }));
102
+ }
103
+ if (!content) throw new Error('Required: --content');
104
+
105
+ const result = isInteractive()
106
+ ? await withSpinner('Creating template...', () =>
107
+ client.createTemplate(content), 'Template created.')
108
+ : await client.createTemplate(content);
109
+
110
+ if (isInteractive()) {
111
+ showResult('Template Created', {
112
+ Name: result.name,
113
+ Slug: result.slug,
114
+ Version: result.version,
115
+ });
116
+ } else {
117
+ console.log(JSON.stringify(result, null, 2));
118
+ }
119
+ return result;
120
+ }
121
+
122
+ case 'update': {
123
+ let slug = flags.slug;
124
+ let content = flags.content;
125
+
126
+ if ((!slug || !content) && isInteractive()) {
127
+ if (!slug) {
128
+ slug = await pickTemplate(client, 'Select template to update');
129
+ }
130
+ if (!content) {
131
+ content = handleCancel(await p.text({
132
+ message: 'New template content',
133
+ placeholder: 'Paste updated content...',
134
+ validate: (val) => { if (!val) return 'Content is required'; },
135
+ }));
136
+ }
137
+ }
138
+ if (!slug || !content) throw new Error('Required: --slug, --content');
139
+
140
+ const result = isInteractive()
141
+ ? await withSpinner('Updating template...', () =>
142
+ client.updateTemplate(slug, content), 'Template updated.')
143
+ : await client.updateTemplate(slug, content);
144
+
145
+ if (isInteractive()) {
146
+ showResult('Template Updated', {
147
+ Slug: slug,
148
+ Name: result.name,
149
+ Version: result.version,
150
+ });
151
+ } else {
152
+ console.log(JSON.stringify(result, null, 2));
153
+ }
154
+ return result;
155
+ }
156
+
157
+ case 'remove': {
158
+ let slug = flags.slug || rest[0];
159
+
160
+ if (!slug && isInteractive()) {
161
+ slug = await pickTemplate(client, 'Select template to remove');
162
+ const confirmed = await p.confirm({
163
+ message: `Remove template "${slug}"?`,
164
+ initialValue: false,
165
+ });
166
+ handleCancel(confirmed);
167
+ if (!confirmed) {
168
+ p.log.info('Cancelled.');
169
+ return;
170
+ }
171
+ }
172
+ if (!slug) throw new Error('Required: --slug or positional argument');
173
+
174
+ await (isInteractive()
175
+ ? withSpinner('Removing template...', () =>
176
+ client.deleteTemplate(slug), 'Template removed.')
177
+ : client.deleteTemplate(slug));
178
+
179
+ if (isInteractive()) {
180
+ p.log.success(`Template ${slug} removed.`);
181
+ } else {
182
+ console.log(`Template ${slug} removed.`);
183
+ }
184
+ return;
185
+ }
186
+
187
+ case 'assign': {
188
+ let agentId = flags.agent;
189
+ let slug = flags.slug || flags.template;
190
+
191
+ if ((!agentId || slug === undefined) && isInteractive()) {
192
+ if (!agentId) {
193
+ agentId = await pickAgent(client, 'Select agent');
194
+ }
195
+
196
+ const clearOrAssign = await p.select({
197
+ message: 'Action',
198
+ options: [
199
+ { value: 'assign', label: 'Assign template', hint: 'assign a template to this agent' },
200
+ { value: 'clear', label: 'Clear template', hint: 'remove template assignment' },
201
+ ],
202
+ });
203
+ handleCancel(clearOrAssign);
204
+
205
+ if (clearOrAssign === 'clear') {
206
+ slug = null;
207
+ } else {
208
+ slug = await pickTemplate(client, 'Select template to assign');
209
+ }
210
+ }
211
+
212
+ if (!agentId) throw new Error('Required: --agent');
213
+
214
+ const result = isInteractive()
215
+ ? await withSpinner('Updating agent template...', () =>
216
+ client.setAgentTemplate(agentId, slug), 'Agent template updated.')
217
+ : await client.setAgentTemplate(agentId, slug);
218
+
219
+ if (isInteractive()) {
220
+ if (slug) {
221
+ p.log.success(`Template "${slug}" assigned to agent "${agentId}".`);
222
+ } else {
223
+ p.log.success(`Template cleared from agent "${agentId}".`);
224
+ }
225
+ } else {
226
+ console.log(JSON.stringify(result, null, 2));
227
+ }
228
+ return result;
229
+ }
230
+
231
+ default: {
232
+ const { showHelp } = await import('../help.js');
233
+ showHelp('template', p);
234
+ break;
235
+ }
236
+ }
237
+ }