gemkit-cli 0.2.2 → 0.3.0

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 (160) hide show
  1. package/README.md +152 -5
  2. package/dist/commands/agent/index.d.ts +9 -0
  3. package/dist/commands/agent/index.js +1329 -0
  4. package/dist/commands/cache/index.d.ts +5 -0
  5. package/dist/commands/cache/index.js +43 -0
  6. package/dist/commands/catalog/index.d.ts +2 -0
  7. package/dist/commands/catalog/index.js +57 -0
  8. package/dist/commands/config/index.d.ts +7 -0
  9. package/dist/commands/config/index.js +122 -0
  10. package/dist/commands/convert/index.d.ts +8 -0
  11. package/dist/commands/convert/index.js +391 -0
  12. package/dist/commands/doctor/index.d.ts +2 -0
  13. package/dist/commands/doctor/index.js +243 -0
  14. package/dist/commands/extension/index.d.ts +5 -0
  15. package/dist/commands/extension/index.js +52 -0
  16. package/dist/commands/index.d.ts +5 -0
  17. package/dist/commands/index.js +37 -0
  18. package/dist/commands/init/index.d.ts +6 -0
  19. package/dist/commands/init/index.js +345 -0
  20. package/dist/commands/new/index.d.ts +5 -0
  21. package/dist/commands/new/index.js +49 -0
  22. package/dist/commands/office/index.d.ts +5 -0
  23. package/dist/commands/office/index.js +283 -0
  24. package/dist/commands/paste/index.d.ts +10 -0
  25. package/dist/commands/paste/index.js +533 -0
  26. package/dist/commands/plan/index.d.ts +8 -0
  27. package/dist/commands/plan/index.js +247 -0
  28. package/dist/commands/session/index.d.ts +8 -0
  29. package/dist/commands/session/index.js +289 -0
  30. package/dist/commands/tokens/index.d.ts +6 -0
  31. package/dist/commands/tokens/index.js +148 -0
  32. package/dist/commands/update/index.d.ts +26 -0
  33. package/dist/commands/update/index.js +199 -0
  34. package/dist/commands/versions/index.d.ts +5 -0
  35. package/dist/commands/versions/index.js +39 -0
  36. package/dist/domains/agent/index.d.ts +8 -0
  37. package/dist/domains/agent/index.js +8 -0
  38. package/dist/domains/agent/mappings.d.ts +32 -0
  39. package/dist/domains/agent/mappings.js +164 -0
  40. package/dist/domains/agent/profile.d.ts +26 -0
  41. package/dist/domains/agent/profile.js +225 -0
  42. package/dist/domains/agent/pty-context.d.ts +11 -0
  43. package/dist/domains/agent/pty-context.js +83 -0
  44. package/dist/domains/agent/pty-providers.d.ts +18 -0
  45. package/dist/domains/agent/pty-providers.js +66 -0
  46. package/dist/domains/agent/pty-session.d.ts +33 -0
  47. package/dist/domains/agent/pty-session.js +82 -0
  48. package/dist/domains/agent/pty-types.d.ts +127 -0
  49. package/dist/domains/agent/pty-types.js +4 -0
  50. package/dist/domains/agent/search.d.ts +45 -0
  51. package/dist/domains/agent/search.js +614 -0
  52. package/dist/domains/agent/types.d.ts +78 -0
  53. package/dist/domains/agent/types.js +5 -0
  54. package/dist/domains/agent-office/documents-scanner.d.ts +9 -0
  55. package/dist/domains/agent-office/documents-scanner.js +143 -0
  56. package/dist/domains/agent-office/event-emitter.d.ts +43 -0
  57. package/dist/domains/agent-office/event-emitter.js +86 -0
  58. package/dist/domains/agent-office/file-watcher.d.ts +40 -0
  59. package/dist/domains/agent-office/file-watcher.js +173 -0
  60. package/dist/domains/agent-office/icons.d.ts +11 -0
  61. package/dist/domains/agent-office/icons.js +36 -0
  62. package/dist/domains/agent-office/index.d.ts +12 -0
  63. package/dist/domains/agent-office/index.js +20 -0
  64. package/dist/domains/agent-office/renderer/web/assets.d.ts +11 -0
  65. package/dist/domains/agent-office/renderer/web/assets.js +3419 -0
  66. package/dist/domains/agent-office/renderer/web/server.d.ts +42 -0
  67. package/dist/domains/agent-office/renderer/web/server.js +228 -0
  68. package/dist/domains/agent-office/renderer/web.d.ts +30 -0
  69. package/dist/domains/agent-office/renderer/web.js +111 -0
  70. package/dist/domains/agent-office/session-bridge.d.ts +23 -0
  71. package/dist/domains/agent-office/session-bridge.js +171 -0
  72. package/dist/domains/agent-office/state-machine.d.ts +5 -0
  73. package/dist/domains/agent-office/state-machine.js +82 -0
  74. package/dist/domains/agent-office/types.d.ts +91 -0
  75. package/dist/domains/agent-office/types.js +4 -0
  76. package/dist/domains/cache/index.d.ts +1 -0
  77. package/dist/domains/cache/index.js +1 -0
  78. package/dist/domains/cache/manager.d.ts +22 -0
  79. package/dist/domains/cache/manager.js +84 -0
  80. package/dist/domains/config/index.d.ts +5 -0
  81. package/dist/domains/config/index.js +5 -0
  82. package/dist/domains/config/manager.d.ts +24 -0
  83. package/dist/domains/config/manager.js +85 -0
  84. package/dist/domains/config/schema.d.ts +17 -0
  85. package/dist/domains/config/schema.js +96 -0
  86. package/dist/domains/convert/converter.d.ts +78 -0
  87. package/dist/domains/convert/converter.js +471 -0
  88. package/dist/domains/convert/index.d.ts +5 -0
  89. package/dist/domains/convert/index.js +5 -0
  90. package/dist/domains/convert/types.d.ts +88 -0
  91. package/dist/domains/convert/types.js +18 -0
  92. package/dist/domains/github/download.d.ts +12 -0
  93. package/dist/domains/github/download.js +51 -0
  94. package/dist/domains/github/index.d.ts +2 -0
  95. package/dist/domains/github/index.js +2 -0
  96. package/dist/domains/github/releases.d.ts +16 -0
  97. package/dist/domains/github/releases.js +68 -0
  98. package/dist/domains/installation/conflict.d.ts +13 -0
  99. package/dist/domains/installation/conflict.js +38 -0
  100. package/dist/domains/installation/file-sync.d.ts +16 -0
  101. package/dist/domains/installation/file-sync.js +77 -0
  102. package/dist/domains/installation/index.d.ts +3 -0
  103. package/dist/domains/installation/index.js +3 -0
  104. package/dist/domains/installation/metadata.d.ts +20 -0
  105. package/dist/domains/installation/metadata.js +52 -0
  106. package/dist/domains/plan/index.d.ts +2 -0
  107. package/dist/domains/plan/index.js +2 -0
  108. package/dist/domains/plan/resolver.d.ts +24 -0
  109. package/dist/domains/plan/resolver.js +164 -0
  110. package/dist/domains/plan/types.d.ts +13 -0
  111. package/dist/domains/plan/types.js +4 -0
  112. package/dist/domains/session/env.d.ts +51 -0
  113. package/dist/domains/session/env.js +118 -0
  114. package/dist/domains/session/index.d.ts +8 -0
  115. package/dist/domains/session/index.js +8 -0
  116. package/dist/domains/session/manager.d.ts +56 -0
  117. package/dist/domains/session/manager.js +205 -0
  118. package/dist/domains/session/paths.d.ts +6 -0
  119. package/dist/domains/session/paths.js +6 -0
  120. package/dist/domains/session/types.d.ts +121 -0
  121. package/dist/domains/session/types.js +5 -0
  122. package/dist/domains/session/writer.d.ts +82 -0
  123. package/dist/domains/session/writer.js +431 -0
  124. package/dist/domains/tokens/index.d.ts +5 -0
  125. package/dist/domains/tokens/index.js +5 -0
  126. package/dist/domains/tokens/pricing.d.ts +38 -0
  127. package/dist/domains/tokens/pricing.js +129 -0
  128. package/dist/domains/tokens/scanner.d.ts +42 -0
  129. package/dist/domains/tokens/scanner.js +168 -0
  130. package/dist/index.d.ts +5 -0
  131. package/dist/index.js +86 -57
  132. package/dist/services/aipty.d.ts +76 -0
  133. package/dist/services/aipty.js +276 -0
  134. package/dist/services/archive.d.ts +22 -0
  135. package/dist/services/archive.js +53 -0
  136. package/dist/services/auto-update.d.ts +26 -0
  137. package/dist/services/auto-update.js +117 -0
  138. package/dist/services/hash.d.ts +36 -0
  139. package/dist/services/hash.js +63 -0
  140. package/dist/services/logger.d.ts +28 -0
  141. package/dist/services/logger.js +102 -0
  142. package/dist/services/music.d.ts +67 -0
  143. package/dist/services/music.js +290 -0
  144. package/dist/services/npm.d.ts +22 -0
  145. package/dist/services/npm.js +65 -0
  146. package/dist/services/pty-client.d.ts +66 -0
  147. package/dist/services/pty-client.js +154 -0
  148. package/dist/services/pty-server.d.ts +102 -0
  149. package/dist/services/pty-server.js +613 -0
  150. package/dist/types/index.d.ts +155 -0
  151. package/dist/types/index.js +4 -0
  152. package/dist/utils/colors.d.ts +43 -0
  153. package/dist/utils/colors.js +98 -0
  154. package/dist/utils/errors.d.ts +24 -0
  155. package/dist/utils/errors.js +56 -0
  156. package/dist/utils/paths.d.ts +46 -0
  157. package/dist/utils/paths.js +89 -0
  158. package/dist/utils/platform.d.ts +11 -0
  159. package/dist/utils/platform.js +31 -0
  160. package/package.json +55 -54
@@ -0,0 +1,1329 @@
1
+ /**
2
+ * Agent command - list, info, search, spawn (NO resume)
3
+ * Spawn aligned with .gemini/extensions/spawn-agent/scripts/gemini_agent.js
4
+ *
5
+ * Uses manual subcommand routing since CAC doesn't support space-separated subcommands.
6
+ * Help is customized to show subcommand-specific options.
7
+ */
8
+ import { spawn } from 'child_process';
9
+ import { access, stat, readdir, readFile } from 'fs/promises';
10
+ import { join, basename, extname, relative } from 'path';
11
+ import { listAgentProfiles, loadAgentProfile, loadAgentProfileWithFallback } from '../../domains/agent/profile.js';
12
+ import { searchAgentSkillCombination, loadSkillContent } from '../../domains/agent/search.js';
13
+ import { mapModel, mapTools } from '../../domains/agent/mappings.js';
14
+ import { loadSession, saveSession, clearSession, isSessionActive, markFirstSendComplete } from '../../domains/agent/pty-session.js';
15
+ import { buildFirstSendPrompt } from '../../domains/agent/pty-context.js';
16
+ import { PtyClient } from '../../services/pty-client.js';
17
+ import { PtyServer } from '../../services/pty-server.js';
18
+ import { loadConfig } from '../../domains/config/manager.js';
19
+ import { readEnv } from '../../domains/session/env.js';
20
+ import { addAgent } from '../../domains/session/writer.js';
21
+ import { generateGkSessionId } from '../../services/hash.js';
22
+ import { logger } from '../../services/logger.js';
23
+ import { brand, ui, pc } from '../../utils/colors.js';
24
+ import { ElevatorMusic } from '../../services/music.js';
25
+ // ============================================================================
26
+ // HEARTBEAT MESSAGES (keeps shell alive & entertains)
27
+ // ============================================================================
28
+ const HEARTBEAT_MESSAGES = [
29
+ { icon: '💩', action: 'Pooping out some code' },
30
+ { icon: '🍳', action: 'Cooking up solutions' },
31
+ { icon: '🧠', action: 'Brain cells working overtime' },
32
+ { icon: '⚡', action: 'Neurons firing rapidly' },
33
+ { icon: '🔮', action: 'Consulting the crystal ball' },
34
+ { icon: '🎰', action: 'Rolling the dice on this one' },
35
+ { icon: '🚀', action: 'Launching into hyperthink' },
36
+ { icon: '🌪️', action: 'Brainstorming intensifies' },
37
+ { icon: '🎪', action: 'Juggling multiple thoughts' },
38
+ { icon: '🔧', action: 'Tightening the logic bolts' },
39
+ { icon: '🎨', action: 'Painting the solution' },
40
+ { icon: '🏗️', action: 'Building something awesome' },
41
+ { icon: '🔬', action: 'Analyzing under the microscope' },
42
+ { icon: '🧪', action: 'Mixing the secret sauce' },
43
+ { icon: '🎯', action: 'Aiming for perfection' },
44
+ { icon: '🧙', action: 'Casting coding spells' },
45
+ { icon: '🐙', action: 'Multitasking like an octopus' },
46
+ { icon: '🦾', action: 'Flexing the AI muscles' },
47
+ { icon: '🌈', action: 'Finding the pot of gold' },
48
+ { icon: '🎸', action: 'Rocking out some logic' }
49
+ ];
50
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
51
+ /**
52
+ * Start heartbeat to keep shell alive
53
+ */
54
+ function startHeartbeat(agentName) {
55
+ let messageIndex = 0;
56
+ let spinnerIndex = 0;
57
+ let seconds = 0;
58
+ const isTTY = process.stdout.isTTY;
59
+ const interval = setInterval(() => {
60
+ seconds++;
61
+ spinnerIndex++;
62
+ const messageChanged = seconds % 10 === 0;
63
+ if (messageChanged) {
64
+ messageIndex++;
65
+ }
66
+ const msg = HEARTBEAT_MESSAGES[messageIndex % HEARTBEAT_MESSAGES.length];
67
+ const spinner = SPINNER_FRAMES[spinnerIndex % SPINNER_FRAMES.length];
68
+ const line = `${spinner} ${msg.icon} ${msg.action}... ${seconds}s`;
69
+ if (isTTY) {
70
+ process.stdout.write(`\r\x1b[K${line}`);
71
+ }
72
+ else {
73
+ if (seconds === 1 || messageChanged) {
74
+ console.log(line);
75
+ }
76
+ }
77
+ }, 1000);
78
+ return {
79
+ interval,
80
+ stop: () => {
81
+ clearInterval(interval);
82
+ if (isTTY) {
83
+ process.stdout.write('\r\x1b[K');
84
+ }
85
+ }
86
+ };
87
+ }
88
+ /**
89
+ * Build subagent context string for prompt injection
90
+ */
91
+ function buildSubagentContext(options) {
92
+ const { agentType = 'Sub Agent', agentRole = 'unknown', parentSessionId = null, activePlan = null, projectDir = null } = options;
93
+ const lines = [
94
+ `Agent Type: ${agentType}`,
95
+ `Agent Role: ${agentRole}`,
96
+ ];
97
+ if (parentSessionId) {
98
+ lines.push(`Parent Session: ${parentSessionId.slice(0, 8)}...`);
99
+ }
100
+ if (projectDir) {
101
+ lines.push(`Project: ${projectDir}`);
102
+ }
103
+ if (activePlan) {
104
+ lines.push(`Active Plan: ${activePlan}`);
105
+ }
106
+ return lines.join('\n');
107
+ }
108
+ // ═══════════════════════════════════════════════════════════════════════════
109
+ // HELP FUNCTIONS
110
+ // ═══════════════════════════════════════════════════════════════════════════
111
+ function showMainHelp() {
112
+ console.log();
113
+ console.log(pc.bold(brand.geminiPurple('Agent Management')));
114
+ console.log();
115
+ console.log('Usage:');
116
+ console.log(` ${brand.primary('gk agent')} <subcommand> [options]`);
117
+ console.log();
118
+ console.log('Subcommands:');
119
+ console.log(` ${brand.primary('list')} List all agent profiles`);
120
+ console.log(` ${brand.primary('info')} <name> Show agent profile details`);
121
+ console.log(` ${brand.primary('search')} "<task>" Find best agent+skills for a task`);
122
+ console.log(` ${brand.primary('spawn')} Spawn a sub-agent (non-interactive)`);
123
+ console.log();
124
+ console.log('Interactive Mode:');
125
+ console.log(` ${brand.primary('start')} Start interactive session`);
126
+ console.log(` ${brand.primary('send')} "<prompt>" Send prompt to session`);
127
+ console.log(` ${brand.primary('wait')} [timeout] Wait for completion`);
128
+ console.log(` ${brand.primary('exchange')} Get structured output`);
129
+ console.log(` ${brand.primary('pending')} Check tool confirmations`);
130
+ console.log(` ${brand.primary('read')} [lines] Read raw output`);
131
+ console.log(` ${brand.primary('status')} Session status`);
132
+ console.log(` ${brand.primary('stop')} Stop session`);
133
+ console.log();
134
+ console.log('Examples:');
135
+ console.log(` ${brand.dim('gk agent list')}`);
136
+ console.log(` ${brand.dim('gk agent spawn -a researcher -p "research best practices"')}`);
137
+ console.log(` ${brand.dim('gk agent start -a researcher -s research')}`);
138
+ console.log(` ${brand.dim('gk agent send "analyze the codebase"')}`);
139
+ console.log();
140
+ console.log('Run with subcommand for specific help:');
141
+ console.log(` ${brand.dim('gk agent spawn --help')}`);
142
+ console.log(` ${brand.dim('gk agent start --help')}`);
143
+ console.log();
144
+ }
145
+ function showListHelp() {
146
+ console.log();
147
+ console.log(pc.bold(brand.geminiPurple('gk agent list')));
148
+ console.log(brand.dim('List all available agent profiles'));
149
+ console.log();
150
+ console.log('Usage:');
151
+ console.log(` ${brand.primary('gk agent list')} [options]`);
152
+ console.log();
153
+ console.log('Options:');
154
+ console.log(` ${brand.dim('--json')} Output as JSON`);
155
+ console.log();
156
+ }
157
+ function showInfoHelp() {
158
+ console.log();
159
+ console.log(pc.bold(brand.geminiPurple('gk agent info')));
160
+ console.log(brand.dim('Show detailed information about an agent profile'));
161
+ console.log();
162
+ console.log('Usage:');
163
+ console.log(` ${brand.primary('gk agent info')} <name> [options]`);
164
+ console.log();
165
+ console.log('Arguments:');
166
+ console.log(` ${brand.dim('<name>')} Agent profile name (without .md extension)`);
167
+ console.log();
168
+ console.log('Options:');
169
+ console.log(` ${brand.dim('--json')} Output as JSON`);
170
+ console.log();
171
+ console.log('Example:');
172
+ console.log(` ${brand.dim('gk agent info researcher')}`);
173
+ console.log();
174
+ }
175
+ function showSearchHelp() {
176
+ console.log();
177
+ console.log(pc.bold(brand.geminiPurple('gk agent search')));
178
+ console.log(brand.dim('Find best agent+skills combination for a task using BM25 search'));
179
+ console.log();
180
+ console.log('Usage:');
181
+ console.log(` ${brand.primary('gk agent search')} "<task>" [options]`);
182
+ console.log();
183
+ console.log('Arguments:');
184
+ console.log(` ${brand.dim('<task>')} Task description to search for`);
185
+ console.log();
186
+ console.log('Options:');
187
+ console.log(` ${brand.dim('-n, --limit <n>')} Number of results (default: 5)`);
188
+ console.log(` ${brand.dim('-i, --intent <i>')} Force intent: research|plan|execute|debug|review|test|design|docs`);
189
+ console.log(` ${brand.dim('-d, --domain <d>')} Force domain: frontend|backend|auth|payment|database|mobile|ai|etc.`);
190
+ console.log(` ${brand.dim('--max-skills <n>')} Maximum skills to include`);
191
+ console.log(` ${brand.dim('--json')} Output as JSON`);
192
+ console.log();
193
+ console.log('Examples:');
194
+ console.log(` ${brand.dim('gk agent search "implement user authentication"')}`);
195
+ console.log(` ${brand.dim('gk agent search "debug the login form" -i debug')}`);
196
+ console.log(` ${brand.dim('gk agent search "build a payment page" -d payment')}`);
197
+ console.log();
198
+ }
199
+ function showSpawnHelp() {
200
+ console.log();
201
+ console.log(pc.bold(brand.geminiPurple('gk agent spawn')));
202
+ console.log(brand.dim('Spawn a sub-agent with Gemini or Claude CLI'));
203
+ console.log();
204
+ console.log('Usage:');
205
+ console.log(` ${brand.primary('gk agent spawn')} -p "<prompt>" [options]`);
206
+ console.log();
207
+ console.log('Required:');
208
+ console.log(` ${brand.dim('-p, --prompt <text>')} Task prompt for the agent`);
209
+ console.log();
210
+ console.log('Options:');
211
+ console.log(` ${brand.dim('-a, --agent <name>')} Agent profile name`);
212
+ console.log(` ${brand.dim('-s, --skills <list>')} Comma-separated skill names to inject`);
213
+ console.log(` ${brand.dim('-c, --context <files>')} Context files (@file syntax)`);
214
+ console.log(` ${brand.dim('-m, --model <model>')} Model override (default: from config)`);
215
+ console.log(` ${brand.dim('-t, --tools <list>')} Comma-separated tools to auto-approve`);
216
+ console.log(` ${brand.dim('--cli <provider>')} CLI provider: gemini (default) or claude`);
217
+ console.log(` ${brand.dim('--music')} Play elevator music while waiting`);
218
+ console.log(` ${brand.dim('--no-music')} Disable elevator music`);
219
+ console.log(` ${brand.dim('--music-file <path>')} Custom music file path`);
220
+ console.log();
221
+ console.log('CLI Providers:');
222
+ console.log(` ${brand.dim('gemini')} Uses Gemini CLI (default). Loads from .gemini/agents/, falls back to .claude/agents/`);
223
+ console.log(` ${brand.dim('claude')} Uses Claude CLI. Loads from .claude/agents/, falls back to .gemini/agents/`);
224
+ console.log(` ${brand.dim('Models and tools are automatically mapped between providers when using fallback.')}`);
225
+ console.log();
226
+ console.log('Examples:');
227
+ console.log(` ${brand.dim('gk agent spawn -p "fix the login bug"')}`);
228
+ console.log(` ${brand.dim('gk agent spawn -a researcher -p "research React best practices"')}`);
229
+ console.log(` ${brand.dim('gk agent spawn -a code-executor -s "frontend-design" -p "build a dashboard"')}`);
230
+ console.log(` ${brand.dim('gk agent spawn -p "implement auth" -m gemini-2.5-pro --music')}`);
231
+ console.log(` ${brand.dim('gk agent spawn -a researcher -t "list_directory,read_file,glob" -p "analyze code"')}`);
232
+ console.log(` ${brand.dim('gk agent spawn --cli claude -a researcher -p "analyze codebase"')}`);
233
+ console.log();
234
+ console.log('Allowed Tools:');
235
+ console.log(` ${brand.dim('Tools can be defined in agent frontmatter (tools: tool1, tool2)')}`);
236
+ console.log(` ${brand.dim('or passed via CLI. Both sources are merged and deduplicated.')}`);
237
+ console.log(` ${brand.dim('Gemini tools: list_directory, read_file, write_file, glob, run_shell_command(*)')}`);
238
+ console.log(` ${brand.dim('Claude tools: Read, Write, Edit, Bash, Glob, Grep, WebFetch, WebSearch')}`);
239
+ console.log();
240
+ }
241
+ function showStartHelp() {
242
+ console.log();
243
+ console.log(pc.bold(brand.geminiPurple('gk agent start')));
244
+ console.log(brand.dim('Start an interactive AI session'));
245
+ console.log();
246
+ console.log('Usage:');
247
+ console.log(` ${brand.primary('gk agent start')} [options]`);
248
+ console.log();
249
+ console.log('Options:');
250
+ console.log(` ${brand.dim('-a, --agent <name>')} Agent profile name`);
251
+ console.log(` ${brand.dim('-s, --skills <list>')} Comma-separated skill names`);
252
+ console.log(` ${brand.dim('-c, --context <files>')} Context files (@file syntax)`);
253
+ console.log(` ${brand.dim('-m, --model <model>')} Model override`);
254
+ console.log(` ${brand.dim('-t, --tools <list>')} Comma-separated tools to allow`);
255
+ console.log(` ${brand.dim('--cli <provider>')} CLI provider: gemini (default) or claude`);
256
+ console.log();
257
+ console.log('Examples:');
258
+ console.log(` ${brand.dim('gk agent start -a researcher -s research')}`);
259
+ console.log(` ${brand.dim('gk agent start --cli claude -a code-executor')}`);
260
+ console.log(` ${brand.dim('gk agent start -m gemini-2.5-pro -t "read_file,write_file"')}`);
261
+ console.log();
262
+ console.log('After starting:');
263
+ console.log(` ${brand.dim('gk agent send "your prompt"')}`);
264
+ console.log(` ${brand.dim('gk agent wait')}`);
265
+ console.log(` ${brand.dim('gk agent exchange')}`);
266
+ console.log(` ${brand.dim('gk agent stop')}`);
267
+ console.log();
268
+ }
269
+ function showInteractiveHelp() {
270
+ console.log();
271
+ console.log(pc.bold(brand.geminiPurple('Interactive Mode Commands')));
272
+ console.log();
273
+ console.log('Session:');
274
+ console.log(` ${brand.primary('start')} [options] Start interactive session`);
275
+ console.log(` ${brand.primary('stop')} Stop interactive session`);
276
+ console.log(` ${brand.primary('status')} Check session status`);
277
+ console.log();
278
+ console.log('Interaction:');
279
+ console.log(` ${brand.primary('send')} "<prompt>" Send prompt to session`);
280
+ console.log(` ${brand.primary('wait')} [timeout] Wait for response completion`);
281
+ console.log(` ${brand.primary('exchange')} Get structured JSON output`);
282
+ console.log(` ${brand.primary('pending')} Check pending tool confirmations`);
283
+ console.log(` ${brand.primary('read')} [lines] Read raw output (debugging)`);
284
+ console.log();
285
+ console.log('Examples:');
286
+ console.log(` ${brand.dim('gk agent start -a researcher -s research')}`);
287
+ console.log(` ${brand.dim('gk agent send "research JWT best practices"')}`);
288
+ console.log(` ${brand.dim('gk agent wait')}`);
289
+ console.log(` ${brand.dim('gk agent pending')}`);
290
+ console.log(` ${brand.dim('gk agent send "1"')} ${brand.dim('# approve tool')}`);
291
+ console.log(` ${brand.dim('gk agent exchange')}`);
292
+ console.log(` ${brand.dim('gk agent stop')}`);
293
+ console.log();
294
+ }
295
+ // ═══════════════════════════════════════════════════════════════════════════
296
+ // COMMAND REGISTRATION
297
+ // ═══════════════════════════════════════════════════════════════════════════
298
+ export function registerAgentCommand(cli) {
299
+ cli
300
+ .command('agent [subcommand] [arg]', 'Agent management (list, info, search, spawn)')
301
+ .alias('a')
302
+ // Shared
303
+ .option('--json', '[all] Output as JSON')
304
+ // Search options
305
+ .option('-n, --limit <n>', '[search] Number of results (default: 5)', { default: 5 })
306
+ .option('-i, --intent <intent>', '[search] Force intent: research|plan|execute|debug|review|test|design|docs')
307
+ .option('-d, --domain <domain>', '[search] Force domain: frontend|backend|auth|payment|database|mobile|ai')
308
+ .option('--max-skills <n>', '[search] Maximum skills to include')
309
+ // Spawn options
310
+ .option('-a, --agent <name>', '[spawn] Agent profile name')
311
+ .option('-p, --prompt <text>', '[spawn] Task prompt (required for spawn)')
312
+ .option('-s, --skills <list>', '[spawn] Comma-separated skill names')
313
+ .option('-c, --context <files>', '[spawn] Context files (@file syntax)')
314
+ .option('-m, --model <model>', '[spawn] Model override')
315
+ .option('-t, --tools <list>', '[spawn] Comma-separated tools to auto-approve')
316
+ .option('--cli <provider>', '[spawn] CLI provider: gemini (default) or claude')
317
+ .option('--music', '[spawn] Play elevator music while waiting')
318
+ .option('--no-music', '[spawn] Disable elevator music')
319
+ .option('--music-file <path>', '[spawn] Custom music file path')
320
+ .action(async (subcommand, arg, options) => {
321
+ // Handle help for subcommands
322
+ if (options.help || options.h) {
323
+ switch (subcommand) {
324
+ case 'list':
325
+ showListHelp();
326
+ return;
327
+ case 'info':
328
+ showInfoHelp();
329
+ return;
330
+ case 'search':
331
+ showSearchHelp();
332
+ return;
333
+ case 'spawn':
334
+ showSpawnHelp();
335
+ return;
336
+ case 'start':
337
+ showStartHelp();
338
+ return;
339
+ case 'send':
340
+ case 'wait':
341
+ case 'exchange':
342
+ case 'pending':
343
+ case 'read':
344
+ case 'status':
345
+ case 'stop':
346
+ showInteractiveHelp();
347
+ return;
348
+ default:
349
+ showMainHelp();
350
+ return;
351
+ }
352
+ }
353
+ // Route to subcommand handlers
354
+ switch (subcommand) {
355
+ case 'list':
356
+ await handleList(options);
357
+ break;
358
+ case 'info':
359
+ if (!arg) {
360
+ console.log();
361
+ logger.error('Agent profile name required');
362
+ console.log(brand.dim('Usage: gk agent info <name>'));
363
+ console.log();
364
+ process.exit(1);
365
+ }
366
+ await handleInfo(arg, options);
367
+ break;
368
+ case 'search':
369
+ if (!arg) {
370
+ console.log();
371
+ logger.error('Search query required');
372
+ console.log(brand.dim('Usage: gk agent search "<task>"'));
373
+ console.log();
374
+ process.exit(1);
375
+ }
376
+ await handleSearch(arg, options);
377
+ break;
378
+ case 'spawn':
379
+ await handleSpawn(options);
380
+ break;
381
+ // Interactive mode commands
382
+ case 'start':
383
+ await handleStart(options);
384
+ break;
385
+ case 'send':
386
+ if (!arg) {
387
+ console.log();
388
+ logger.error('Prompt required');
389
+ console.log(brand.dim('Usage: gk agent send "your prompt"'));
390
+ console.log();
391
+ process.exit(1);
392
+ }
393
+ await handleSend(arg);
394
+ break;
395
+ case 'wait':
396
+ await handleWait(arg ? parseInt(arg) : 120);
397
+ break;
398
+ case 'exchange':
399
+ await handleExchange();
400
+ break;
401
+ case 'pending':
402
+ await handlePending();
403
+ break;
404
+ case 'read':
405
+ await handleRead(arg ? parseInt(arg) : 200);
406
+ break;
407
+ case 'status':
408
+ await handleInteractiveStatus();
409
+ break;
410
+ case 'stop':
411
+ await handleStop();
412
+ break;
413
+ default:
414
+ showMainHelp();
415
+ }
416
+ });
417
+ }
418
+ // ═══════════════════════════════════════════════════════════════════════════
419
+ // HANDLERS
420
+ // ═══════════════════════════════════════════════════════════════════════════
421
+ async function handleList(options) {
422
+ const profiles = listAgentProfiles();
423
+ if (options.json) {
424
+ console.log(JSON.stringify(profiles, null, 2));
425
+ return;
426
+ }
427
+ console.log();
428
+ console.log(pc.bold(brand.geminiPurple('Available Agents')));
429
+ console.log();
430
+ if (profiles.length === 0) {
431
+ logger.warn('No agent profiles found. Run "gk init" first.');
432
+ console.log();
433
+ return;
434
+ }
435
+ for (const profile of profiles) {
436
+ console.log(` ${brand.primary(profile.name)}`);
437
+ console.log(` ${brand.dim(profile.description)}`);
438
+ console.log(` ${brand.dim('Model:')} ${profile.model}`);
439
+ if (profile.skills && profile.skills.length > 0) {
440
+ console.log(` ${brand.dim('Skills:')} ${profile.skills.join(', ')}`);
441
+ }
442
+ console.log('');
443
+ }
444
+ console.log(brand.dim(` Total: ${profiles.length} agents`));
445
+ console.log();
446
+ }
447
+ async function handleInfo(name, options) {
448
+ const profile = loadAgentProfile(name);
449
+ if (!profile) {
450
+ console.log();
451
+ logger.error(`Agent profile not found: ${name}`);
452
+ console.log();
453
+ process.exit(1);
454
+ }
455
+ if (options.json) {
456
+ console.log(JSON.stringify(profile, null, 2));
457
+ return;
458
+ }
459
+ console.log();
460
+ console.log(ui.doubleLine());
461
+ console.log(pc.bold(brand.geminiPurple(`Agent: ${profile.name}`)));
462
+ console.log(ui.doubleLine());
463
+ console.log();
464
+ console.log(` ${brand.dim('Description:')} ${profile.description}`);
465
+ console.log(` ${brand.dim('Model:')} ${profile.model}`);
466
+ if (profile.skills?.length) {
467
+ console.log(` ${brand.dim('Skills:')} ${profile.skills.join(', ')}`);
468
+ }
469
+ console.log(` ${brand.dim('Path:')} ${profile.filePath}`);
470
+ console.log();
471
+ console.log(brand.dim('--- Profile Content ---'));
472
+ console.log();
473
+ console.log(brand.dim(profile.content));
474
+ console.log();
475
+ }
476
+ async function handleSearch(task, options) {
477
+ const results = searchAgentSkillCombination(task, {
478
+ top: options.limit,
479
+ forceIntent: options.intent,
480
+ forceDomain: options.domain,
481
+ maxSkills: options.maxSkills,
482
+ });
483
+ if (options.json) {
484
+ console.log(JSON.stringify(results, null, 2));
485
+ return;
486
+ }
487
+ console.log();
488
+ console.log(pc.bold(brand.geminiPurple(`Search results for: "${task}"`)));
489
+ console.log();
490
+ if (results.length === 0) {
491
+ logger.warn('No matching agents found for task.');
492
+ console.log();
493
+ return;
494
+ }
495
+ for (let i = 0; i < results.length; i++) {
496
+ const r = results[i];
497
+ const fallbackTag = r.fallback ? brand.warn(' [FALLBACK]') : '';
498
+ console.log(` ${brand.success(i + 1 + '.')} ${brand.primary(r.agent)}${fallbackTag} ${brand.dim(`(score: ${r.score})`)}`);
499
+ console.log(` ${brand.dim('Intent:')} ${r.intent} ${brand.dim('Domain:')} ${r.domain} ${brand.dim('Complexity:')} ${r.complexity}`);
500
+ if (r.skills.length > 0) {
501
+ console.log(` ${brand.dim('Skills:')} ${r.skills.join(' | ')}`);
502
+ }
503
+ if (r.description) {
504
+ console.log(` ${brand.dim(r.description)}`);
505
+ }
506
+ if (r.useWhen) {
507
+ console.log(` ${brand.dim('Use when:')} ${r.useWhen}`);
508
+ }
509
+ console.log('');
510
+ }
511
+ // Show suggested command
512
+ if (results.length > 0) {
513
+ const best = results[0];
514
+ const skillsArg = best.skills.length > 0 ? ` -s "${best.skills.join(',')}"` : '';
515
+ console.log(brand.dim(' Suggested command:'));
516
+ console.log(` ${brand.primary('gk')} agent spawn -a ${best.agent}${skillsArg} -p "${task}"`);
517
+ }
518
+ console.log();
519
+ }
520
+ async function handleSpawn(options) {
521
+ if (!options.prompt) {
522
+ console.log();
523
+ logger.error('Prompt is required. Use -p or --prompt');
524
+ console.log();
525
+ showSpawnHelp();
526
+ process.exit(1);
527
+ }
528
+ // Parse and validate CLI provider
529
+ const cliProvider = (options.cli === 'claude') ? 'claude' : 'gemini';
530
+ const config = loadConfig();
531
+ // Read .env for session info
532
+ const env = readEnv();
533
+ const parentGkSessionId = env.ACTIVE_GK_SESSION_ID || null;
534
+ const parentGeminiSessionId = env.ACTIVE_GEMINI_SESSION_ID || null;
535
+ const activePlan = env.ACTIVE_PLAN || null;
536
+ const suggestedPlan = env.SUGGESTED_PLAN || null;
537
+ const planDateFormat = env.PLAN_DATE_FORMAT || null;
538
+ const projectDir = env.PROJECT_DIR || null;
539
+ // Load agent profile if specified (with fallback between providers)
540
+ let profile = null;
541
+ if (options.agent) {
542
+ profile = loadAgentProfileWithFallback(options.agent, cliProvider);
543
+ if (!profile) {
544
+ console.log();
545
+ logger.error(`Agent profile not found: ${options.agent}`);
546
+ console.log(brand.dim(`Searched: .${cliProvider === 'claude' ? 'claude' : 'gemini'}/agents/ and fallback folder`));
547
+ console.log();
548
+ process.exit(1);
549
+ }
550
+ }
551
+ // Determine model (map to target provider if needed)
552
+ let model = options.model || profile?.model || config.spawn.defaultModel;
553
+ // Map model to target provider format
554
+ model = mapModel(model, cliProvider);
555
+ // Resolve music setting: CLI flag > config default
556
+ // CAC sets options.music=true by default when --no-music is defined, so check argv explicitly
557
+ const hasExplicitMusicFlag = process.argv.includes('--music') || process.argv.includes('--no-music');
558
+ const musicEnabled = hasExplicitMusicFlag ? options.music : config.spawn.music;
559
+ const musicFile = options.musicFile || config.spawn.musicFile;
560
+ // Build skills list
561
+ const cliSkills = options.skills?.split(',').map(s => s.trim()).filter(Boolean) || [];
562
+ const agentSkills = profile?.skills || [];
563
+ const allSkills = [...new Set([...agentSkills, ...cliSkills])];
564
+ // Build tools list (merge agent profile tools with CLI tools, deduplicated)
565
+ // Note: profile?.tools already mapped by loadAgentProfileWithFallback if from fallback path
566
+ const cliTools = options.tools?.split(',').map(s => s.trim()).filter(Boolean) || [];
567
+ const agentTools = profile?.tools || [];
568
+ const mergedTools = [...new Set([...agentTools, ...cliTools])];
569
+ // Map CLI tools to target provider (agentTools already mapped if from fallback)
570
+ const allTools = mapTools(mergedTools, cliProvider);
571
+ // Build context list - aligned with gemini_agent.js
572
+ // Parse context refs: split by comma, then by spaces, filter empty strings
573
+ const contextRefs = options.context
574
+ ? options.context.split(',').flatMap(part => part.trim().split(/\s+/).filter(s => s)).filter(s => s)
575
+ : [];
576
+ // loadContext - aligned with gemini_agent.js loadContext()
577
+ async function loadContext(contextRef) {
578
+ let fullPath = contextRef;
579
+ let found = false;
580
+ // Handle @ prefix references
581
+ if (contextRef.startsWith('@')) {
582
+ const relativePath = contextRef.substring(1);
583
+ const searchPaths = [
584
+ join(process.cwd(), '.docs', relativePath),
585
+ join(process.cwd(), '.plans', relativePath),
586
+ join(process.cwd(), 'docs', relativePath),
587
+ join(process.cwd(), 'plans', relativePath),
588
+ join(process.cwd(), relativePath)
589
+ ];
590
+ for (const searchPath of searchPaths) {
591
+ try {
592
+ await access(searchPath);
593
+ fullPath = searchPath;
594
+ found = true;
595
+ break;
596
+ }
597
+ catch {
598
+ continue;
599
+ }
600
+ }
601
+ // If @ prefix file not found, show clear error with searched paths
602
+ if (!found) {
603
+ const pathsList = searchPaths.map(p => ` - ${p}`).join('\n');
604
+ throw new Error(`Context file not found: ${contextRef}\n` +
605
+ `Searched paths:\n${pathsList}\n` +
606
+ `Tip: Place file in .docs/, .plans/, docs/, plans/, or project root`);
607
+ }
608
+ }
609
+ try {
610
+ // Check if path is a directory
611
+ const fileStat = await stat(fullPath);
612
+ if (fileStat.isDirectory()) {
613
+ return await loadContextDirectory(fullPath, contextRef);
614
+ }
615
+ // It's a file - load it directly
616
+ const content = await readFile(fullPath, 'utf-8');
617
+ const fileName = basename(fullPath);
618
+ return {
619
+ type: 'context',
620
+ name: fileName,
621
+ path: fullPath,
622
+ content: content.trim(),
623
+ originalRef: contextRef
624
+ };
625
+ }
626
+ catch (error) {
627
+ throw new Error(`Failed to load context from ${contextRef}: ${error.message}`);
628
+ }
629
+ }
630
+ // loadContextDirectory - aligned with gemini_agent.js loadContextDirectory()
631
+ async function loadContextDirectory(dirPath, originalRef) {
632
+ const maxDepth = 3;
633
+ const extensions = ['.md', '.txt', '.json', '.yaml', '.yml'];
634
+ const contexts = [];
635
+ async function walkDir(currentPath, depth = 0) {
636
+ if (depth > maxDepth)
637
+ return;
638
+ const entries = await readdir(currentPath, { withFileTypes: true });
639
+ for (const entry of entries) {
640
+ const entryPath = join(currentPath, entry.name);
641
+ // Skip hidden files and directories
642
+ if (entry.name.startsWith('.'))
643
+ continue;
644
+ if (entry.isDirectory()) {
645
+ await walkDir(entryPath, depth + 1);
646
+ }
647
+ else if (entry.isFile()) {
648
+ const ext = extname(entry.name).toLowerCase();
649
+ if (extensions.includes(ext)) {
650
+ try {
651
+ const content = await readFile(entryPath, 'utf-8');
652
+ const relativePath = relative(dirPath, entryPath);
653
+ contexts.push({
654
+ type: 'context',
655
+ name: entry.name,
656
+ path: entryPath,
657
+ relativePath: relativePath,
658
+ content: content.trim(),
659
+ originalRef: `${originalRef}/${relativePath}`
660
+ });
661
+ }
662
+ catch (err) {
663
+ console.warn(`Warning: Could not read ${entryPath}: ${err.message}`);
664
+ }
665
+ }
666
+ }
667
+ }
668
+ }
669
+ await walkDir(dirPath);
670
+ // Sort by relative path for consistent ordering
671
+ contexts.sort((a, b) => (a.relativePath || '').localeCompare(b.relativePath || ''));
672
+ return contexts;
673
+ }
674
+ // formatContext - aligned with gemini_agent.js formatContext()
675
+ function formatContext(contextItems) {
676
+ if (!contextItems || contextItems.length === 0) {
677
+ return '';
678
+ }
679
+ let contextSection = '<context>\n';
680
+ contextSection += 'The following documents provide additional context for this task:\n\n';
681
+ contextItems.forEach((ctx, index) => {
682
+ contextSection += `## Document ${index + 1}: ${ctx.name}\n`;
683
+ contextSection += `Source: ${ctx.originalRef}\n\n`;
684
+ });
685
+ contextSection += '</context>\n\n';
686
+ return contextSection;
687
+ }
688
+ // Load context files/directories - aligned with gemini_agent.js buildAgentContext()
689
+ const loadedContexts = [];
690
+ if (contextRefs && contextRefs.length > 0) {
691
+ for (const ctx of contextRefs) {
692
+ try {
693
+ if (typeof ctx === 'string') {
694
+ const loaded = await loadContext(ctx);
695
+ // Handle both single file (object) and directory (array) results
696
+ if (Array.isArray(loaded)) {
697
+ loadedContexts.push(...loaded);
698
+ }
699
+ else {
700
+ loadedContexts.push(loaded);
701
+ }
702
+ }
703
+ else {
704
+ loadedContexts.push(ctx);
705
+ }
706
+ }
707
+ catch (error) {
708
+ console.log();
709
+ logger.error(error.message);
710
+ console.log();
711
+ process.exit(1);
712
+ }
713
+ }
714
+ }
715
+ // Agent display name
716
+ const agentDisplayName = profile?.name || options.agent || 'Sub';
717
+ // Build enriched prompt
718
+ const promptParts = [];
719
+ // Add subagent context
720
+ const subagentContext = buildSubagentContext({
721
+ agentType: 'Sub Agent',
722
+ agentRole: agentDisplayName,
723
+ parentSessionId: parentGkSessionId,
724
+ activePlan: activePlan,
725
+ projectDir: projectDir
726
+ });
727
+ promptParts.push('<subagent-context>');
728
+ promptParts.push(subagentContext);
729
+ promptParts.push('</subagent-context>\n');
730
+ // Add task/prompt
731
+ promptParts.push('<task>');
732
+ promptParts.push(options.prompt);
733
+ promptParts.push('</task>\n');
734
+ // Add agent profile content
735
+ if (profile) {
736
+ promptParts.push('<agent>');
737
+ promptParts.push(`# Agent: ${profile.name}\n`);
738
+ promptParts.push('## Role & Responsibilities');
739
+ promptParts.push(profile.content);
740
+ promptParts.push('</agent>\n');
741
+ }
742
+ // Add skills content
743
+ if (allSkills.length > 0) {
744
+ promptParts.push('<skills>');
745
+ promptParts.push('You have access to the following skills and capabilities:\n');
746
+ for (let i = 0; i < allSkills.length; i++) {
747
+ const skill = allSkills[i];
748
+ const skillContent = loadSkillContent(skill);
749
+ if (skillContent) {
750
+ promptParts.push(`## Skill ${i + 1}: ${skill}`);
751
+ promptParts.push(skillContent);
752
+ promptParts.push('---\n');
753
+ }
754
+ }
755
+ promptParts.push('</skills>\n');
756
+ }
757
+ // Add context section - aligned with gemini_agent.js formatContext()
758
+ if (loadedContexts.length > 0) {
759
+ promptParts.push(formatContext(loadedContexts));
760
+ }
761
+ const enrichedPrompt = promptParts.join('\n');
762
+ // Truncate prompt for env var (150 chars)
763
+ const truncatedPrompt = options.prompt.length > 150
764
+ ? options.prompt.substring(0, 150)
765
+ : options.prompt;
766
+ console.log();
767
+ logger.info(`Spawning ${brand.primary(agentDisplayName)} agent with ${brand.primary(cliProvider)} CLI`);
768
+ logger.info(`Model: ${brand.primary(model)}`);
769
+ if (parentGkSessionId) {
770
+ logger.info(`Parent session: ${brand.dim(parentGkSessionId.slice(0, 20) + '...')}`);
771
+ }
772
+ if (activePlan) {
773
+ logger.info(`Active plan: ${brand.dim(activePlan)}`);
774
+ }
775
+ if (allSkills.length > 0) {
776
+ logger.info(`Injected skills: ${brand.dim(allSkills.join(', '))}`);
777
+ }
778
+ if (loadedContexts.length > 0) {
779
+ logger.info(`Injected context: ${brand.dim(loadedContexts.map(c => c.name).join(', '))}`);
780
+ }
781
+ if (allTools.length > 0) {
782
+ logger.info(`Allowed tools: ${brand.dim(allTools.join(', '))}`);
783
+ }
784
+ console.log(ui.line());
785
+ console.log();
786
+ // Initialize elevator music if enabled
787
+ let elevatorMusic = null;
788
+ if (musicEnabled) {
789
+ elevatorMusic = new ElevatorMusic({
790
+ audioFile: musicFile,
791
+ loop: true,
792
+ volume: 0.3
793
+ });
794
+ const started = elevatorMusic.start();
795
+ if (started) {
796
+ console.log(brand.dim('Playing elevator music while waiting...'));
797
+ console.log();
798
+ }
799
+ }
800
+ // Prepare injected info for tracking - aligned with gemini_agent.js line 911
801
+ const injectedContext = loadedContexts.map(c => c.path || c.name);
802
+ // Generate sub-agent session ID and add to session file IMMEDIATELY
803
+ // This allows dashboard to show agent right when music starts
804
+ // The ID is passed to hook via GK_SUB_SESSION_ID so hook updates same agent
805
+ const subAgentSessionId = generateGkSessionId('gemini-sub', process.pid);
806
+ if (parentGkSessionId && projectDir) {
807
+ addAgent(projectDir, parentGkSessionId, {
808
+ gkSessionId: subAgentSessionId,
809
+ pid: process.pid,
810
+ parentGkSessionId: parentGkSessionId,
811
+ agentRole: agentDisplayName,
812
+ prompt: options.prompt,
813
+ model: model,
814
+ injected: (allSkills.length > 0 || injectedContext.length > 0)
815
+ ? { skills: allSkills, context: injectedContext }
816
+ : null
817
+ });
818
+ }
819
+ // Start heartbeat
820
+ const heartbeat = startHeartbeat(agentDisplayName);
821
+ // Build spawn args and command based on CLI provider
822
+ let cliCommand;
823
+ let spawnArgs;
824
+ let spawnEnv;
825
+ if (cliProvider === 'claude') {
826
+ // Claude CLI: claude -p --model <model> --allowedTools tool1,tool2
827
+ cliCommand = 'claude';
828
+ spawnArgs = ['-p', '--model', model];
829
+ // Claude CLI expects: --allowedTools tool1,tool2 (comma-separated)
830
+ if (allTools.length > 0) {
831
+ spawnArgs.push('--allowedTools', allTools.join(','));
832
+ }
833
+ spawnEnv = {
834
+ ...process.env,
835
+ CLAUDE_TYPE: 'sub-agent',
836
+ GK_PARENT_SESSION_ID: parentGkSessionId || '',
837
+ GK_SUB_SESSION_ID: subAgentSessionId,
838
+ CLAUDE_AGENT_ROLE: agentDisplayName,
839
+ CLAUDE_AGENT_PROMPT: truncatedPrompt,
840
+ CLAUDE_AGENT_MODEL: model || '',
841
+ CLAUDE_AGENT_SKILLS: allSkills.join(','),
842
+ CLAUDE_AGENT_CONTEXT: injectedContext.join(','),
843
+ GK_ACTIVE_PLAN: activePlan || '',
844
+ GK_SUGGESTED_PLAN: suggestedPlan || '',
845
+ GK_PLAN_DATE_FORMAT: planDateFormat || ''
846
+ };
847
+ }
848
+ else {
849
+ // Gemini CLI: gemini -m <model> --allowed-tools ["tool1","tool2"]
850
+ cliCommand = 'gemini';
851
+ spawnArgs = ['-m', model];
852
+ // Gemini CLI expects: --allowed-tools ["tool1","tool2","tool3"] (JSON array)
853
+ if (allTools.length > 0) {
854
+ spawnArgs.push('--allowed-tools', JSON.stringify(allTools));
855
+ }
856
+ spawnEnv = {
857
+ ...process.env,
858
+ GEMINI_TYPE: 'sub-agent',
859
+ GEMINI_PARENT_SESSION_ID: parentGeminiSessionId || '',
860
+ GK_PARENT_SESSION_ID: parentGkSessionId || '',
861
+ GK_SUB_SESSION_ID: subAgentSessionId,
862
+ GEMINI_AGENT_ROLE: agentDisplayName,
863
+ GEMINI_AGENT_PROMPT: truncatedPrompt,
864
+ GEMINI_AGENT_MODEL: model || '',
865
+ GEMINI_AGENT_SKILLS: allSkills.join(','),
866
+ GEMINI_AGENT_CONTEXT: injectedContext.join(','),
867
+ GK_ACTIVE_PLAN: activePlan || '',
868
+ GK_SUGGESTED_PLAN: suggestedPlan || '',
869
+ GK_PLAN_DATE_FORMAT: planDateFormat || ''
870
+ };
871
+ }
872
+ // Spawn with environment variables
873
+ // Note: Sub-agent registration is handled by gk-session-init.cjs hook
874
+ // GK_SUB_SESSION_ID is passed so hook uses same ID (prevents duplicate agents)
875
+ const child = spawn(cliCommand, spawnArgs, {
876
+ stdio: ['pipe', 'pipe', 'pipe'],
877
+ shell: process.platform === 'win32',
878
+ env: spawnEnv
879
+ });
880
+ let stdout = '';
881
+ let stderr = '';
882
+ child.stdout.on('data', (data) => {
883
+ stdout += data.toString();
884
+ });
885
+ child.stderr.on('data', (data) => {
886
+ stderr += data.toString();
887
+ });
888
+ child.stdin.write(enrichedPrompt);
889
+ child.stdin.end();
890
+ child.on('close', (code) => {
891
+ heartbeat.stop();
892
+ if (elevatorMusic) {
893
+ elevatorMusic.stop();
894
+ console.log(brand.dim('Music stopped.'));
895
+ }
896
+ console.log();
897
+ if (stdout) {
898
+ console.log(stdout);
899
+ }
900
+ if (stderr) {
901
+ console.error(stderr);
902
+ }
903
+ if (code === 0) {
904
+ logger.success(`${agentDisplayName} agent completed successfully`);
905
+ }
906
+ else {
907
+ logger.error(`${agentDisplayName} agent exited with code: ${code}`);
908
+ }
909
+ console.log();
910
+ process.exit(code || 0);
911
+ });
912
+ child.on('error', (err) => {
913
+ heartbeat.stop();
914
+ if (elevatorMusic) {
915
+ elevatorMusic.stop();
916
+ }
917
+ console.log();
918
+ logger.error(`Failed to spawn agent: ${err.message}`);
919
+ console.log();
920
+ process.exit(1);
921
+ });
922
+ process.on('SIGINT', () => {
923
+ heartbeat.stop();
924
+ if (elevatorMusic) {
925
+ elevatorMusic.stop();
926
+ }
927
+ child.kill('SIGTERM');
928
+ process.exit(1);
929
+ });
930
+ }
931
+ // ═══════════════════════════════════════════════════════════════════════════
932
+ // INTERACTIVE MODE HANDLERS
933
+ // ═══════════════════════════════════════════════════════════════════════════
934
+ async function handleStart(options) {
935
+ // Check if session already running
936
+ if (isSessionActive()) {
937
+ console.log();
938
+ logger.error('Session already running. Use "gk agent stop" first.');
939
+ console.log();
940
+ process.exit(1);
941
+ }
942
+ const cliProvider = (options.cli === 'claude') ? 'claude' : 'gemini';
943
+ const config = loadConfig();
944
+ // Load agent profile with fallback
945
+ let profile = null;
946
+ if (options.agent) {
947
+ profile = loadAgentProfileWithFallback(options.agent, cliProvider);
948
+ if (!profile) {
949
+ console.log();
950
+ logger.error(`Agent profile not found: ${options.agent}`);
951
+ console.log(brand.dim(`Searched: .${cliProvider === 'claude' ? 'claude' : 'gemini'}/agents/ and fallback folder`));
952
+ console.log();
953
+ process.exit(1);
954
+ }
955
+ }
956
+ // Determine model
957
+ let model = options.model || profile?.model || config.spawn.defaultModel;
958
+ model = mapModel(model, cliProvider);
959
+ // Build tools list
960
+ const cliTools = options.tools?.split(',').map(s => s.trim()).filter(Boolean) || [];
961
+ const agentTools = profile?.tools || [];
962
+ const mergedTools = [...new Set([...agentTools, ...cliTools])];
963
+ const allTools = mapTools(mergedTools, cliProvider);
964
+ // Build skills list and load content
965
+ const cliSkills = options.skills?.split(',').map(s => s.trim()).filter(Boolean) || [];
966
+ const agentSkills = profile?.skills || [];
967
+ const allSkills = [...new Set([...agentSkills, ...cliSkills])];
968
+ const skillContents = {};
969
+ for (const skill of allSkills) {
970
+ const content = loadSkillContent(skill);
971
+ if (content)
972
+ skillContents[skill] = content;
973
+ }
974
+ // Load context files (reuse logic from handleSpawn)
975
+ const contextFiles = [];
976
+ if (options.context) {
977
+ const contextRefs = options.context.split(',').flatMap(part => part.trim().split(/\s+/).filter(s => s)).filter(s => s);
978
+ for (const ref of contextRefs) {
979
+ try {
980
+ const loaded = await loadContextFile(ref);
981
+ if (Array.isArray(loaded)) {
982
+ contextFiles.push(...loaded);
983
+ }
984
+ else {
985
+ contextFiles.push(loaded);
986
+ }
987
+ }
988
+ catch (error) {
989
+ console.log();
990
+ logger.error(error.message);
991
+ console.log();
992
+ process.exit(1);
993
+ }
994
+ }
995
+ }
996
+ // Create session state
997
+ const sessionState = {
998
+ provider: cliProvider,
999
+ model,
1000
+ port: 3377,
1001
+ pid: 0,
1002
+ isFirstSend: true,
1003
+ context: {
1004
+ agentName: profile?.name || null,
1005
+ agentContent: profile?.content || null,
1006
+ skills: allSkills,
1007
+ skillContents,
1008
+ contextFiles,
1009
+ tools: allTools
1010
+ },
1011
+ startedAt: new Date().toISOString()
1012
+ };
1013
+ // Save session state first (for context)
1014
+ saveSession(sessionState);
1015
+ console.log();
1016
+ logger.info(`Starting ${brand.primary(cliProvider)} interactive session...`);
1017
+ logger.info(`Model: ${brand.primary(model)}`);
1018
+ if (profile) {
1019
+ logger.info(`Agent: ${brand.dim(profile.name)}`);
1020
+ }
1021
+ if (allSkills.length > 0) {
1022
+ logger.info(`Skills: ${brand.dim(allSkills.join(', '))}`);
1023
+ }
1024
+ if (allTools.length > 0) {
1025
+ logger.info(`Tools: ${brand.dim(allTools.join(', '))}`);
1026
+ }
1027
+ console.log();
1028
+ // Start PTY server
1029
+ try {
1030
+ const server = new PtyServer({
1031
+ provider: cliProvider,
1032
+ model,
1033
+ tools: allTools,
1034
+ sessionState,
1035
+ debug: false
1036
+ });
1037
+ await server.start();
1038
+ // Update session with PID (server runs in same process for now)
1039
+ sessionState.pid = process.pid;
1040
+ saveSession(sessionState);
1041
+ logger.success('Session ready!');
1042
+ console.log();
1043
+ console.log('Usage:');
1044
+ console.log(` ${brand.dim('gk agent send "your prompt"')}`);
1045
+ console.log(` ${brand.dim('gk agent wait')}`);
1046
+ console.log(` ${brand.dim('gk agent exchange')}`);
1047
+ console.log(` ${brand.dim('gk agent stop')}`);
1048
+ console.log();
1049
+ // Keep process running
1050
+ process.on('SIGINT', async () => {
1051
+ console.log('\nStopping session...');
1052
+ await server.stop();
1053
+ clearSession();
1054
+ process.exit(0);
1055
+ });
1056
+ }
1057
+ catch (error) {
1058
+ clearSession();
1059
+ console.log();
1060
+ logger.error(`Failed to start session: ${error.message}`);
1061
+ console.log();
1062
+ process.exit(1);
1063
+ }
1064
+ }
1065
+ async function handleSend(prompt) {
1066
+ const session = loadSession();
1067
+ if (!session || !isSessionActive()) {
1068
+ console.log();
1069
+ logger.error('No active session. Use "gk agent start" first.');
1070
+ console.log();
1071
+ process.exit(1);
1072
+ }
1073
+ const client = new PtyClient(session.port);
1074
+ let finalPrompt = prompt;
1075
+ // First send: wrap with full context
1076
+ if (session.isFirstSend) {
1077
+ finalPrompt = buildFirstSendPrompt(prompt, session);
1078
+ markFirstSendComplete();
1079
+ }
1080
+ try {
1081
+ const result = await client.send(finalPrompt);
1082
+ if (result.ok) {
1083
+ logger.info(`Sent prompt${result.exchangeId ? ` (exchange: ${result.exchangeId.slice(0, 8)}...)` : ''}`);
1084
+ }
1085
+ else {
1086
+ logger.error(result.error || 'Failed to send');
1087
+ }
1088
+ }
1089
+ catch (err) {
1090
+ logger.error(`Failed to send: ${err.message}`);
1091
+ process.exit(1);
1092
+ }
1093
+ }
1094
+ async function handleWait(timeout) {
1095
+ const session = loadSession();
1096
+ if (!session || !isSessionActive()) {
1097
+ console.log();
1098
+ logger.error('No active session.');
1099
+ console.log();
1100
+ process.exit(1);
1101
+ }
1102
+ const client = new PtyClient(session.port);
1103
+ console.log(`Waiting for completion (timeout: ${timeout}s)...`);
1104
+ const startTime = Date.now();
1105
+ const timeoutMs = timeout * 1000;
1106
+ while (Date.now() - startTime < timeoutMs) {
1107
+ try {
1108
+ const result = await client.complete();
1109
+ if (result.complete) {
1110
+ console.log('\nComplete!');
1111
+ return;
1112
+ }
1113
+ if (result.reason === 'pending_tool') {
1114
+ console.log('\n');
1115
+ logger.warn('Tool confirmation required');
1116
+ if (result.hint) {
1117
+ console.log(brand.dim(result.hint));
1118
+ }
1119
+ console.log(brand.dim('Use: gk agent pending'));
1120
+ return;
1121
+ }
1122
+ }
1123
+ catch (err) {
1124
+ logger.error(err.message);
1125
+ return;
1126
+ }
1127
+ await sleep(2000);
1128
+ process.stdout.write('.');
1129
+ }
1130
+ console.log('');
1131
+ logger.warn('Timeout reached. AI may still be working.');
1132
+ }
1133
+ async function handleExchange() {
1134
+ const session = loadSession();
1135
+ if (!session || !isSessionActive()) {
1136
+ console.log();
1137
+ logger.error('No active session.');
1138
+ console.log();
1139
+ process.exit(1);
1140
+ }
1141
+ const client = new PtyClient(session.port);
1142
+ try {
1143
+ const exchange = await client.exchange();
1144
+ console.log(JSON.stringify(exchange, null, 2));
1145
+ }
1146
+ catch (err) {
1147
+ logger.error(err.message);
1148
+ process.exit(1);
1149
+ }
1150
+ }
1151
+ async function handlePending() {
1152
+ const session = loadSession();
1153
+ if (!session || !isSessionActive()) {
1154
+ console.log();
1155
+ logger.error('No active session.');
1156
+ console.log();
1157
+ process.exit(1);
1158
+ }
1159
+ const client = new PtyClient(session.port);
1160
+ try {
1161
+ const pending = await client.pending();
1162
+ if (pending.hasPending) {
1163
+ console.log();
1164
+ logger.warn('Tool confirmation required');
1165
+ console.log(JSON.stringify(pending, null, 2));
1166
+ console.log();
1167
+ console.log('To approve: ' + brand.primary('gk agent send "1"'));
1168
+ console.log('To reject: ' + brand.primary('gk agent send "3"'));
1169
+ console.log();
1170
+ }
1171
+ else {
1172
+ console.log('No pending confirmations');
1173
+ }
1174
+ }
1175
+ catch (err) {
1176
+ logger.error(err.message);
1177
+ process.exit(1);
1178
+ }
1179
+ }
1180
+ async function handleRead(lines) {
1181
+ const session = loadSession();
1182
+ if (!session || !isSessionActive()) {
1183
+ console.log();
1184
+ logger.error('No active session.');
1185
+ console.log();
1186
+ process.exit(1);
1187
+ }
1188
+ const client = new PtyClient(session.port);
1189
+ try {
1190
+ const output = await client.read(lines);
1191
+ console.log(output);
1192
+ }
1193
+ catch (err) {
1194
+ logger.error(err.message);
1195
+ process.exit(1);
1196
+ }
1197
+ }
1198
+ async function handleInteractiveStatus() {
1199
+ const session = loadSession();
1200
+ if (!session) {
1201
+ console.log('No session file found.');
1202
+ return;
1203
+ }
1204
+ if (!isSessionActive()) {
1205
+ console.log('Session file exists but process not running.');
1206
+ console.log('Cleaning up...');
1207
+ clearSession();
1208
+ return;
1209
+ }
1210
+ const client = new PtyClient(session.port);
1211
+ try {
1212
+ const status = await client.status();
1213
+ console.log();
1214
+ console.log(pc.bold('Session: ') + brand.success('ACTIVE'));
1215
+ console.log(`Provider: ${session.provider}`);
1216
+ console.log(`Model: ${session.model}`);
1217
+ console.log(`PID: ${session.pid}`);
1218
+ console.log(`Started: ${session.startedAt}`);
1219
+ console.log(`First send pending: ${session.isFirstSend}`);
1220
+ if (session.context.agentName) {
1221
+ console.log(`Agent: ${session.context.agentName}`);
1222
+ }
1223
+ if (session.context.skills.length > 0) {
1224
+ console.log(`Skills: ${session.context.skills.join(', ')}`);
1225
+ }
1226
+ console.log(`Output buffer: ${status.outputLength} bytes`);
1227
+ console.log();
1228
+ }
1229
+ catch (err) {
1230
+ logger.error(err.message);
1231
+ process.exit(1);
1232
+ }
1233
+ }
1234
+ async function handleStop() {
1235
+ const session = loadSession();
1236
+ if (!session) {
1237
+ console.log('No active session.');
1238
+ return;
1239
+ }
1240
+ if (isSessionActive()) {
1241
+ try {
1242
+ const client = new PtyClient(session.port);
1243
+ await client.stop();
1244
+ logger.info('Session stopping...');
1245
+ }
1246
+ catch {
1247
+ // Server may have already stopped
1248
+ }
1249
+ }
1250
+ clearSession();
1251
+ logger.success('Session stopped.');
1252
+ }
1253
+ // Helper: Load context file (simplified version)
1254
+ async function loadContextFile(contextRef) {
1255
+ let fullPath = contextRef;
1256
+ if (contextRef.startsWith('@')) {
1257
+ const relativePath = contextRef.substring(1);
1258
+ const searchPaths = [
1259
+ join(process.cwd(), '.docs', relativePath),
1260
+ join(process.cwd(), '.plans', relativePath),
1261
+ join(process.cwd(), 'docs', relativePath),
1262
+ join(process.cwd(), 'plans', relativePath),
1263
+ join(process.cwd(), relativePath)
1264
+ ];
1265
+ let found = false;
1266
+ for (const searchPath of searchPaths) {
1267
+ try {
1268
+ await access(searchPath);
1269
+ fullPath = searchPath;
1270
+ found = true;
1271
+ break;
1272
+ }
1273
+ catch {
1274
+ continue;
1275
+ }
1276
+ }
1277
+ if (!found) {
1278
+ throw new Error(`Context file not found: ${contextRef}`);
1279
+ }
1280
+ }
1281
+ const fileStat = await stat(fullPath);
1282
+ if (fileStat.isDirectory()) {
1283
+ // Load directory recursively
1284
+ const contexts = [];
1285
+ const extensions = ['.md', '.txt', '.json', '.yaml', '.yml'];
1286
+ async function walkDir(dirPath, depth = 0) {
1287
+ if (depth > 3)
1288
+ return;
1289
+ const entries = await readdir(dirPath, { withFileTypes: true });
1290
+ for (const entry of entries) {
1291
+ if (entry.name.startsWith('.'))
1292
+ continue;
1293
+ const entryPath = join(dirPath, entry.name);
1294
+ if (entry.isDirectory()) {
1295
+ await walkDir(entryPath, depth + 1);
1296
+ }
1297
+ else if (entry.isFile()) {
1298
+ const ext = extname(entry.name).toLowerCase();
1299
+ if (extensions.includes(ext)) {
1300
+ const content = await readFile(entryPath, 'utf-8');
1301
+ contexts.push({
1302
+ type: 'context',
1303
+ name: entry.name,
1304
+ path: entryPath,
1305
+ relativePath: relative(fullPath, entryPath),
1306
+ content: content.trim(),
1307
+ originalRef: `${contextRef}/${relative(fullPath, entryPath)}`
1308
+ });
1309
+ }
1310
+ }
1311
+ }
1312
+ }
1313
+ await walkDir(fullPath);
1314
+ return contexts;
1315
+ }
1316
+ // Single file
1317
+ const content = await readFile(fullPath, 'utf-8');
1318
+ return {
1319
+ type: 'context',
1320
+ name: basename(fullPath),
1321
+ path: fullPath,
1322
+ content: content.trim(),
1323
+ originalRef: contextRef
1324
+ };
1325
+ }
1326
+ // Helper: Sleep
1327
+ function sleep(ms) {
1328
+ return new Promise(resolve => setTimeout(resolve, ms));
1329
+ }