instar 1.3.203 → 1.3.205

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 (43) hide show
  1. package/dist/commands/init.js +1 -0
  2. package/dist/commands/init.js.map +1 -1
  3. package/dist/commands/server.d.ts.map +1 -1
  4. package/dist/commands/server.js +10 -1
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/commands/setup-wizard/gemini-driver.d.ts +46 -0
  7. package/dist/commands/setup-wizard/gemini-driver.d.ts.map +1 -0
  8. package/dist/commands/setup-wizard/gemini-driver.js +645 -0
  9. package/dist/commands/setup-wizard/gemini-driver.js.map +1 -0
  10. package/dist/commands/setup-wizard/model-constants.d.ts +7 -0
  11. package/dist/commands/setup-wizard/model-constants.d.ts.map +1 -1
  12. package/dist/commands/setup-wizard/model-constants.js +7 -0
  13. package/dist/commands/setup-wizard/model-constants.js.map +1 -1
  14. package/dist/commands/setup.d.ts.map +1 -1
  15. package/dist/commands/setup.js +26 -29
  16. package/dist/commands/setup.js.map +1 -1
  17. package/dist/core/LeaseCoordinator.d.ts +22 -0
  18. package/dist/core/LeaseCoordinator.d.ts.map +1 -1
  19. package/dist/core/LeaseCoordinator.js +33 -0
  20. package/dist/core/LeaseCoordinator.js.map +1 -1
  21. package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
  22. package/dist/core/MultiMachineCoordinator.js +11 -1
  23. package/dist/core/MultiMachineCoordinator.js.map +1 -1
  24. package/dist/core/PostUpdateMigrator.d.ts +2 -1
  25. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  26. package/dist/core/PostUpdateMigrator.js +104 -0
  27. package/dist/core/PostUpdateMigrator.js.map +1 -1
  28. package/dist/core/installCodexHooks.js +1 -1
  29. package/dist/core/installCodexHooks.js.map +1 -1
  30. package/dist/core/instarSettingsHooks.d.ts.map +1 -1
  31. package/dist/core/instarSettingsHooks.js +5 -0
  32. package/dist/core/instarSettingsHooks.js.map +1 -1
  33. package/dist/threadline/RelayGroundingPreamble.d.ts.map +1 -1
  34. package/dist/threadline/RelayGroundingPreamble.js +12 -0
  35. package/dist/threadline/RelayGroundingPreamble.js.map +1 -1
  36. package/package.json +1 -1
  37. package/scripts/generate-builtin-manifest.cjs +1 -0
  38. package/src/data/builtin-manifest.json +29 -20
  39. package/upgrades/1.3.204.md +26 -0
  40. package/upgrades/1.3.205.md +53 -0
  41. package/upgrades/side-effects/gemini-hybrid-setup-driver-eli16.md +40 -0
  42. package/upgrades/side-effects/gemini-hybrid-setup-driver.md +88 -0
  43. package/upgrades/side-effects/self-stop-guard.md +83 -0
@@ -0,0 +1,645 @@
1
+ /**
2
+ * Gemini driver for the hybrid wizard.
3
+ *
4
+ * Per-turn narrative: for each `narrative-then-prompt` state, instar
5
+ * spawns Gemini's canonical one-shot CLI with a tightly-constrained
6
+ * prompt asking Gemini to generate ONE warm 2-3 sentence intro paragraph. The structural
7
+ * prompt (the question text) is printed verbatim by instar from the
8
+ * state machine; Gemini never sees it. This means Gemini cannot reword
9
+ * the question, can't add or remove options, and can't decide to
10
+ * "execute the setup" — each per-turn invocation has nothing to
11
+ * execute and a single bounded text job.
12
+ *
13
+ * Action states call existing instar CLI commands directly. Gemini is
14
+ * never handed the Claude setup skill and is never asked to run shell,
15
+ * write files, configure services, or drive browser automation.
16
+ */
17
+ import { execFileSync, spawnSync } from 'node:child_process';
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import readline from 'node:readline';
21
+ import pc from 'picocolors';
22
+ import { buildFreshProjectInstall, INITIAL_STATE, resolveChoice, } from './state-machine.js';
23
+ import { GEMINI_WIZARD_MODEL } from './model-constants.js';
24
+ import { buildGeminiChildEnv, buildGeminiOneShotArgv, } from '../../providers/adapters/gemini-cli/transport/geminiSpawn.js';
25
+ /**
26
+ * Per-state narrative-prompt builders. Each returns the EXACT text we
27
+ * send to Gemini. The contract: ONE paragraph, no tools, no commands.
28
+ * The canonical one-shot argv uses `--approval-mode default`; the prompt
29
+ * itself forbids tool use and side effects, and all actual setup work
30
+ * stays in instar-owned action states.
31
+ */
32
+ const NARRATIVE_PROMPTS = {
33
+ welcome: (_a, ctx) => `
34
+ You are a warm, friendly setup wizard greeting a new user installing
35
+ instar (a persistent AI-agent toolkit) in their project at ${path.basename(ctx.projectDir)}.
36
+
37
+ OUTPUT EXACTLY ONE warm 2-3 sentence paragraph welcoming them. Do NOT
38
+ include CLI commands, file paths, or technical jargon. Do NOT use ANY
39
+ tools or run ANY commands. Do NOT ask any questions — a separate
40
+ structured prompt will follow yours.
41
+
42
+ After your paragraph, exit. Output text only.
43
+ `.trim(),
44
+ 'agent-name': (a) => `
45
+ You are continuing the instar setup wizard. The user just accepted
46
+ the privacy notice. Now you're transitioning to the identity phase
47
+ where they'll pick a name for their agent.
48
+
49
+ OUTPUT EXACTLY ONE warm 2-3 sentence paragraph introducing the
50
+ "pick a name" step. Make it feel like the start of something — they're
51
+ naming a presence, not configuring a script. Hint that the name can
52
+ be anything (made-up word, real name, project-relevant).
53
+
54
+ Do NOT include CLI commands, file paths, or examples in code blocks.
55
+ Do NOT use ANY tools. Do NOT ask the question — a structured prompt
56
+ follows. Output text only and exit.
57
+
58
+ Their answers so far: name not yet given.
59
+ `.trim(),
60
+ 'agent-role': (a) => `
61
+ The user just told you their agent will be called "${a.agentName}".
62
+ Acknowledge the name in one short sentence (warmly), then introduce
63
+ the next step: a one-sentence description of what the agent should
64
+ focus on.
65
+
66
+ OUTPUT 1-2 sentences total. No CLI, no jargon, no tools, no question
67
+ (structured prompt follows). Output text only and exit.
68
+ `.trim(),
69
+ 'user-name': (a) => `
70
+ The user named their agent "${a.agentName}" and described its focus
71
+ as: "${a.agentRole}". Now you're asking what they'd like to be called.
72
+
73
+ OUTPUT ONE short sentence transitioning into "what should ${a.agentName}
74
+ call you?". Warm, not formal. No tools, no question (structured
75
+ prompt follows). Output text only and exit.
76
+ `.trim(),
77
+ autonomy: (a) => `
78
+ The user is "${a.userName}". Their agent "${a.agentName}" will focus on:
79
+ "${a.agentRole}". Next step: pick an autonomy level.
80
+
81
+ OUTPUT 2-3 short sentences introducing the autonomy choice. Explain
82
+ that this is a starting point — they can change it anytime later by
83
+ just chatting the agent ("be more proactive", "ask before acting").
84
+ Don't re-list the options (a structured prompt with the options
85
+ follows). No tools. Output text only and exit.
86
+ `.trim(),
87
+ messaging: (a) => `
88
+ The user "${a.userName}" has set up their agent "${a.agentName}" with
89
+ ${a.autonomy} autonomy. Now they need to pick how to talk to the agent
90
+ day-to-day.
91
+
92
+ OUTPUT 2-3 short sentences introducing the messaging-channel choice.
93
+ Hint that messaging is THE interface — the user shouldn't need to
94
+ return to a terminal after this. Don't re-list the options. No tools.
95
+ Output text only and exit.
96
+ `.trim(),
97
+ };
98
+ function narrativeFor(stateId, answers, ctx) {
99
+ const builder = NARRATIVE_PROMPTS[stateId];
100
+ return builder ? builder(answers, ctx) : null;
101
+ }
102
+ /**
103
+ * Default deterministic narrative for each state, used when the
104
+ * Gemini call is suppressed or fails. Keeps the wizard usable even
105
+ * when narrative generation is unavailable.
106
+ */
107
+ const FALLBACK_NARRATIVES = {
108
+ welcome: 'Welcome to instar.',
109
+ 'agent-name': 'Let\'s start with a name for your agent.',
110
+ 'agent-role': 'Got it.',
111
+ 'user-name': 'And one more piece of identity:',
112
+ autonomy: 'A starting point — you can change this anytime.',
113
+ messaging: 'Messaging is the interface you\'ll use day-to-day.',
114
+ };
115
+ /**
116
+ * Spinner state for the composing indicator. Animates a single line
117
+ * during the gemini narrative call so the user sees feedback instead
118
+ * of a silent terminal. Cleared (via carriage-return + spaces)
119
+ * before the narrative paragraph is printed.
120
+ */
121
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
122
+ function startSpinner(label) {
123
+ let i = 0;
124
+ const tty = process.stdout.isTTY;
125
+ if (!tty) {
126
+ process.stdout.write(` ${label}\n`);
127
+ return { stop: () => { } };
128
+ }
129
+ const render = () => {
130
+ process.stdout.write(`\r ${pc.cyan(SPINNER_FRAMES[i % SPINNER_FRAMES.length])} ${pc.dim(label)}`);
131
+ i++;
132
+ };
133
+ render();
134
+ const handle = setInterval(render, 100);
135
+ return {
136
+ stop: () => {
137
+ clearInterval(handle);
138
+ // Clear the spinner line so the narrative paragraph renders cleanly.
139
+ process.stdout.write(`\r${' '.repeat(label.length + 6)}\r`);
140
+ },
141
+ };
142
+ }
143
+ /**
144
+ * Run Gemini's canonical one-shot for one narrative-generation turn. Returns the
145
+ * text body of Gemini's response (stdout), or null on failure /
146
+ * timeout. Bounded to a short per-turn timeout so a flaky network
147
+ * doesn't stall the wizard.
148
+ *
149
+ * Shows a spinner while the call is running so the user sees
150
+ * visible feedback during the latency window (avoids the
151
+ * "is-it-frozen?" reflex that triggers buffered-Enter bugs).
152
+ */
153
+ function runGeminiNarrative(geminiPath, prompt, options) {
154
+ const spinner = startSpinner('composing…');
155
+ try {
156
+ const result = spawnSync(geminiPath, buildGeminiOneShotArgv(GEMINI_WIZARD_MODEL, prompt), {
157
+ cwd: options.instarRoot,
158
+ timeout: options.timeoutMs,
159
+ stdio: ['ignore', 'pipe', 'pipe'],
160
+ encoding: 'utf-8',
161
+ env: buildGeminiWizardEnv(),
162
+ });
163
+ if (result.status !== 0)
164
+ return null;
165
+ // Strip any trailing trace blocks that gemini one-shot output may print. We want
166
+ // the last contiguous block of plain-English text.
167
+ const stdout = (result.stdout || '').trim();
168
+ // Heuristic: take everything after the last "--------" or "user\n" /
169
+ // "gemini\n" marker if present (gemini output sometimes prefixes
170
+ // session metadata).
171
+ const markers = ['\n--------\n', '\nuser\n', '\ngemini\n'];
172
+ let body = stdout;
173
+ for (const m of markers) {
174
+ const idx = body.lastIndexOf(m);
175
+ if (idx >= 0 && idx < body.length - 20)
176
+ body = body.slice(idx + m.length).trim();
177
+ }
178
+ return body || null;
179
+ }
180
+ catch {
181
+ return null;
182
+ }
183
+ finally {
184
+ spinner.stop();
185
+ }
186
+ }
187
+ function buildGeminiWizardEnv(parentEnv = process.env) {
188
+ const env = buildGeminiChildEnv(parentEnv);
189
+ env.GEMINI_CLI_TRUST_WORKSPACE = 'true';
190
+ if (parentEnv.ASDF_NODEJS_VERSION) {
191
+ env.ASDF_NODEJS_VERSION = parentEnv.ASDF_NODEJS_VERSION;
192
+ }
193
+ return env;
194
+ }
195
+ /**
196
+ * Prompt the user with readline. Returns the trimmed answer (may be
197
+ * empty string for "user pressed enter").
198
+ */
199
+ async function askUser(promptText) {
200
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
201
+ return new Promise((resolve) => {
202
+ rl.question(promptText, (answer) => {
203
+ rl.close();
204
+ resolve(answer || '');
205
+ });
206
+ });
207
+ }
208
+ /**
209
+ * Render a state to the user: narrative paragraph (LLM-generated) +
210
+ * structural prompt (verbatim). Then read the answer, validating it
211
+ * against the state's `validate` function. On invalid input, prints
212
+ * the validator's friendly retry message and re-asks WITHOUT
213
+ * regenerating the narrative paragraph (cheap loop, no extra gemini
214
+ * exec calls). Returns the first answer that passes validation.
215
+ *
216
+ * This is the structural answer to the buffered-Enter bug from
217
+ * v1.2.13 — the user could press Enter during the silent gemini
218
+ * narrative window, that Enter would buffer in stdin, and the next
219
+ * readline would consume it as an empty answer to a required field
220
+ * (silently defaulting agent-name to "agent"). With validate +
221
+ * retry-loop, an empty submission to a required field surfaces a
222
+ * friendly reprompt instead of accepting the default.
223
+ */
224
+ async function renderNarrativeState(state, answers, options) {
225
+ // Generate narrative once per state entry (re-asks on validation
226
+ // failure skip this and just re-print the question).
227
+ let narrative = null;
228
+ if (options.enableNarrative !== false) {
229
+ const prompt = narrativeFor(state.id, answers, { projectDir: options.projectDir });
230
+ if (prompt) {
231
+ narrative = runGeminiNarrative(options.geminiPath, prompt, {
232
+ instarRoot: options.instarRoot,
233
+ timeoutMs: 30_000,
234
+ });
235
+ }
236
+ }
237
+ console.log();
238
+ console.log(pc.cyan(narrative ?? FALLBACK_NARRATIVES[state.id] ?? ''));
239
+ console.log();
240
+ console.log(state.prompt);
241
+ console.log();
242
+ const placeholderHint = state.input.kind === 'text' && state.input.placeholder
243
+ ? pc.dim(` (${state.input.placeholder})`)
244
+ : '';
245
+ if (placeholderHint)
246
+ console.log(placeholderHint);
247
+ // Retry loop: at most 5 attempts so a wedged input doesn't trap
248
+ // the wizard forever. After 5 invalid attempts we let the answer
249
+ // through; the state machine's `next` function still has its own
250
+ // resolveChoice fallback so the wizard doesn't crash.
251
+ for (let attempt = 0; attempt < 5; attempt++) {
252
+ const answer = await askUser(' > ');
253
+ if (!state.validate) {
254
+ echoChoice(state, answer);
255
+ return answer;
256
+ }
257
+ const validationMsg = state.validate(answer);
258
+ if (validationMsg === null) {
259
+ echoChoice(state, answer);
260
+ return answer;
261
+ }
262
+ console.log();
263
+ console.log(pc.yellow(` ${validationMsg}`));
264
+ console.log();
265
+ }
266
+ // Last-resort: take whatever was last typed even if invalid. The
267
+ // state machine's `next` should still produce a usable transition.
268
+ const answer = await askUser(' > ');
269
+ echoChoice(state, answer);
270
+ return answer;
271
+ }
272
+ /**
273
+ * For choice prompts, echo the resolved label back to the user as a
274
+ * confirmation line ("→ Proactive"). For text prompts, no-op — the
275
+ * answer is the answer.
276
+ *
277
+ * This closes the v1.2.14 confusion where typing "Proactive" or
278
+ * "Telegram" got correctly interpreted by resolveChoice but the
279
+ * wizard never showed the user what selection it understood.
280
+ */
281
+ function echoChoice(state, answer) {
282
+ if (state.input.kind !== 'choice')
283
+ return;
284
+ const matched = resolveChoice(answer, state.input.choices);
285
+ if (!matched)
286
+ return;
287
+ const choice = state.input.choices.find((c) => c.value === matched);
288
+ if (!choice)
289
+ return;
290
+ console.log(pc.dim(` → ${choice.label}`));
291
+ }
292
+ /**
293
+ * Execute an action state — calls the appropriate instar CLI command
294
+ * or hands off to an agentic session.
295
+ */
296
+ async function runAction(action, answers, options) {
297
+ console.log();
298
+ console.log(pc.dim(` → ${action.description}...`));
299
+ switch (action.kind) {
300
+ case 'init': {
301
+ // We assume instar is on PATH (the bareword `npx instar` flow
302
+ // got us here). Use the installed binary.
303
+ try {
304
+ execFileSync('npx', [
305
+ 'instar',
306
+ 'init',
307
+ '--dir', options.projectDir,
308
+ '--framework', 'gemini-cli',
309
+ ], { stdio: 'inherit' });
310
+ }
311
+ catch {
312
+ console.log(pc.yellow(' (init returned non-zero; continuing)'));
313
+ }
314
+ return {};
315
+ }
316
+ case 'add-user': {
317
+ const id = (answers.userName || 'user').toLowerCase().replace(/[^a-z0-9]/g, '') || 'user';
318
+ const name = answers.userName || 'User';
319
+ try {
320
+ // `instar user add` reads project from cwd — does NOT accept
321
+ // -d/--dir (pre-fix passed it and the CLI errored "unknown
322
+ // option '-d'"). Set cwd on the spawn instead.
323
+ execFileSync('npx', ['instar', 'user', 'add', '--id', id, '--name', name], { stdio: 'inherit', cwd: options.projectDir });
324
+ }
325
+ catch {
326
+ // non-fatal
327
+ }
328
+ return {};
329
+ }
330
+ case 'start-server': {
331
+ try {
332
+ // server start accepts -d, but pass cwd too for consistency.
333
+ execFileSync('npx', ['instar', 'server', 'start', '-d', options.projectDir], { stdio: 'inherit', cwd: options.projectDir });
334
+ return { serverStarted: true };
335
+ }
336
+ catch {
337
+ return { serverStarted: false };
338
+ }
339
+ }
340
+ case 'install-autostart': {
341
+ try {
342
+ execFileSync('npx', ['instar', 'autostart', 'install', '-d', options.projectDir], { stdio: 'inherit', cwd: options.projectDir });
343
+ }
344
+ catch {
345
+ // non-fatal
346
+ }
347
+ return {};
348
+ }
349
+ case 'setup-telegram-agentic': {
350
+ // Gemini is bounded to narrative-only setup turns. Telegram setup
351
+ // stays instar-native: readline for user input, Telegram Bot API for
352
+ // verification, and direct config writes owned by this process.
353
+ return await runTelegramSetup(options);
354
+ }
355
+ case 'setup-whatsapp-agentic':
356
+ case 'setup-slack-agentic': {
357
+ // Not yet ported to the hybrid wizard. Fall back to a clear
358
+ // pointer: setup completes, user can configure later via
359
+ // `instar add <channel>`.
360
+ console.log();
361
+ console.log(pc.yellow(' This channel will be configured after setup.'));
362
+ console.log(pc.dim(' (Hybrid wizard will gain agentic setup for it in a follow-up release.)'));
363
+ return {};
364
+ }
365
+ case 'send-greeting': {
366
+ // The "magic moment" — agent's first words to the user in the
367
+ // Lifeline topic, in the agent's voice, after the server is
368
+ // alive. SKILL.md devotes Phase 5b to this; the v1.2.20 audit
369
+ // surfaced that this action was a no-op.
370
+ //
371
+ // Reads token + chatId + lifelineTopicId from the just-written
372
+ // config.json, and sends a 2-3 sentence personalized greeting via
373
+ // sendMessage.
374
+ // Silently skips if any of the three fields are missing
375
+ // (Telegram wasn't configured, or the user took the manual
376
+ // backstop without lifelineTopicId).
377
+ return await runSendLifelineGreeting(answers, options);
378
+ }
379
+ case 'github-backup': {
380
+ // Out of scope for v1.2.12 minimum. Future PR.
381
+ return {};
382
+ }
383
+ }
384
+ }
385
+ /**
386
+ * Send the agent's personal "first hello" to the Lifeline topic
387
+ * after the server has started. This is SKILL.md's "magic moment"
388
+ * — the agent's first words to the user, in voice, using their
389
+ * actual name + role + the user's name.
390
+ *
391
+ * Reads token + chatId + lifelineTopicId from the just-written
392
+ * config.json. Silently no-ops if any are missing (Telegram wasn't
393
+ * configured, or the manual backstop didn't write lifelineTopicId).
394
+ *
395
+ * Exported for testing.
396
+ */
397
+ export async function runSendLifelineGreeting(answers, options) {
398
+ const configPath = path.join(options.projectDir, '.instar', 'config.json');
399
+ let token = '';
400
+ let chatId = '';
401
+ let lifelineTopicId = 0;
402
+ try {
403
+ const raw = fs.readFileSync(configPath, 'utf-8');
404
+ const config = JSON.parse(raw);
405
+ const tg = (config.messaging || []).find((m) => m.type === 'telegram');
406
+ if (!tg?.config)
407
+ return {};
408
+ token = tg.config.token ?? '';
409
+ chatId = tg.config.chatId ?? '';
410
+ lifelineTopicId = tg.config.lifelineTopicId ?? 0;
411
+ }
412
+ catch {
413
+ return {};
414
+ }
415
+ if (!token || !chatId || !lifelineTopicId) {
416
+ // No Telegram configured, or partial config (manual backstop
417
+ // didn't capture chat id). Silent skip.
418
+ return {};
419
+ }
420
+ const agentName = (answers.agentName || 'your agent').trim();
421
+ const userName = (answers.userName || 'there').trim();
422
+ const autonomy = answers.autonomy || 'proactive';
423
+ const autonomyBlurb = autonomy === 'guided'
424
+ ? 'I\'ll check with you before doing things.'
425
+ : autonomy === 'autonomous'
426
+ ? 'I\'ll own outcomes end-to-end and report back when something needs you.'
427
+ : 'I\'ll take initiative on obvious next steps and ask when uncertain.';
428
+ const greeting = `Hey ${userName}, ${agentName} here — server's up and I'm online.\n\n${autonomyBlurb}\n\nAnything we set up just now — name, focus, autonomy, messaging — you can change anytime just by chatting me. What would you like to work on first?`;
429
+ try {
430
+ // RULE 3: EXEMPT — this is a fire-and-forget POST to send a
431
+ // wizard-completion greeting, NOT a state-detection probe.
432
+ // Failure is silently swallowed (non-fatal) so there's no
433
+ // detection-result branching to canary against.
434
+ const res = await fetch(`https://api.telegram.org/bot${encodeURIComponent(token)}/sendMessage`, {
435
+ method: 'POST',
436
+ headers: { 'Content-Type': 'application/json' },
437
+ body: JSON.stringify({
438
+ chat_id: chatId,
439
+ message_thread_id: lifelineTopicId,
440
+ text: greeting,
441
+ }),
442
+ });
443
+ const data = (await res.json());
444
+ if (data.ok) {
445
+ console.log();
446
+ console.log(pc.green(` ✓ ${agentName} said hello in the Lifeline topic.`));
447
+ }
448
+ }
449
+ catch {
450
+ // Non-fatal — server is up, agent will reach out on its own
451
+ // schedule. Don't block the wizard's completion.
452
+ }
453
+ return {};
454
+ }
455
+ /**
456
+ * Telegram setup — instar-native readline backstop (v1.2.15 flow).
457
+ *
458
+ * Drives the entire setup from instar
459
+ * with readline + the Telegram Bot API. No LLM session is involved.
460
+ * See spec wizard-telegram-native.md for the original rationale.
461
+ */
462
+ async function runTelegramSetup(options) {
463
+ console.log();
464
+ console.log(pc.bold(' Telegram setup'));
465
+ console.log(pc.dim(' This takes about 2 minutes. I\'ll walk you through each step.'));
466
+ console.log();
467
+ console.log(pc.bold(' Step 1 of 3 — Create a bot'));
468
+ console.log(' • Open https://web.telegram.org/a/ in your browser');
469
+ console.log(' • Search for @BotFather and start a chat');
470
+ console.log(' • Send /newbot and follow the prompts:');
471
+ console.log(' – Display name (e.g. "Instar Codey")');
472
+ console.log(' – Username (must end with "bot", e.g. "instar_codey_bot")');
473
+ console.log(' • BotFather will reply with a token like 1234567890:AAH...');
474
+ console.log();
475
+ let token = '';
476
+ let botUsername = '';
477
+ for (let attempt = 0; attempt < 5; attempt++) {
478
+ const input = (await askUser(' Paste the bot token here: ')).trim();
479
+ if (!input) {
480
+ console.log(pc.yellow(' That looked blank — paste the token from BotFather (the long string).'));
481
+ continue;
482
+ }
483
+ console.log(pc.dim(' Verifying token with Telegram…'));
484
+ const verified = await telegramGetMe(input);
485
+ if (!verified.ok) {
486
+ console.log(pc.yellow(` Telegram rejected that token: ${verified.error}. Try pasting it again.`));
487
+ continue;
488
+ }
489
+ token = input;
490
+ botUsername = verified.username;
491
+ console.log(pc.green(` ✓ Bot verified: @${botUsername}`));
492
+ break;
493
+ }
494
+ if (!token) {
495
+ console.log(pc.yellow(' Skipping Telegram for now. You can finish setup by chatting your agent later.'));
496
+ return { telegramConfigured: false };
497
+ }
498
+ console.log();
499
+ console.log(pc.bold(' Step 2 of 3 — Connect a chat'));
500
+ console.log(` • In Telegram, create a new group (or open an existing one)`);
501
+ console.log(` • Add @${botUsername} as a member`);
502
+ console.log(' • Send any message in the group ("hi" works)');
503
+ console.log();
504
+ await askUser(' Press Enter once you\'ve done that > ');
505
+ let chatId = '';
506
+ let chatName = '';
507
+ for (let attempt = 0; attempt < 4; attempt++) {
508
+ console.log(pc.dim(' Fetching the chat ID from Telegram…'));
509
+ const updates = await telegramGetUpdates(token);
510
+ if (!updates.ok) {
511
+ console.log(pc.yellow(` Couldn't reach Telegram: ${updates.error}.`));
512
+ const retry = (await askUser(' Press Enter to try again, type "skip" to skip > ')).toLowerCase().trim();
513
+ if (retry === 'skip')
514
+ break;
515
+ continue;
516
+ }
517
+ // Prefer group/supergroup chats. Fall back to most recent chat
518
+ // of any type — that's still useful (private DM with the bot
519
+ // works too).
520
+ const groupHit = updates.chats.find((c) => c.type === 'group' || c.type === 'supergroup');
521
+ const hit = groupHit ?? updates.chats[updates.chats.length - 1];
522
+ if (hit) {
523
+ chatId = hit.id;
524
+ chatName = hit.name;
525
+ console.log(pc.green(` ✓ Chat found: "${chatName}" (id ${chatId})`));
526
+ break;
527
+ }
528
+ console.log(pc.yellow(' No recent messages found yet. Make sure you sent a message in the group AFTER adding the bot.'));
529
+ const retry = (await askUser(' Press Enter to try again, type "skip" to skip > ')).toLowerCase().trim();
530
+ if (retry === 'skip')
531
+ break;
532
+ }
533
+ if (!chatId) {
534
+ console.log(pc.yellow(' Skipping chat-ID detection. Your bot token is saved; you can add the chat ID later.'));
535
+ // Still save the token — partial config is better than none.
536
+ writeTelegramConfig(options.projectDir, { token, chatId: '' });
537
+ return { telegramConfigured: false };
538
+ }
539
+ console.log();
540
+ console.log(pc.bold(' Step 3 of 3 — Saving config'));
541
+ const written = writeTelegramConfig(options.projectDir, { token, chatId });
542
+ if (!written) {
543
+ console.log(pc.red(' Couldn\'t write to .instar/config.json. Your bot is created but not wired up yet.'));
544
+ return { telegramConfigured: false };
545
+ }
546
+ console.log(pc.green(' ✓ Telegram is set up.'));
547
+ console.log(pc.dim(` Your agent will message you in "${chatName}" once the server starts.`));
548
+ return { telegramConfigured: true };
549
+ }
550
+ async function telegramGetMe(token) {
551
+ try {
552
+ const url = `https://api.telegram.org/bot${encodeURIComponent(token)}/getMe`;
553
+ const res = await fetch(url);
554
+ const data = (await res.json());
555
+ if (!data.ok)
556
+ return { ok: false, username: '', error: data.description || 'invalid token' };
557
+ return { ok: true, username: data.result?.username || 'unknown', error: '' };
558
+ }
559
+ catch (err) {
560
+ return { ok: false, username: '', error: err instanceof Error ? err.message : String(err) };
561
+ }
562
+ }
563
+ async function telegramGetUpdates(token) {
564
+ try {
565
+ const url = `https://api.telegram.org/bot${encodeURIComponent(token)}/getUpdates`;
566
+ const res = await fetch(url);
567
+ const data = (await res.json());
568
+ if (!data.ok)
569
+ return { ok: false, chats: [], error: data.description || 'getUpdates failed' };
570
+ const chats = (data.result || [])
571
+ .map((u) => u.message?.chat)
572
+ .filter((c) => !!c && typeof c.id === 'number')
573
+ .map((c) => ({
574
+ id: String(c.id),
575
+ type: c.type || 'private',
576
+ name: c.title || c.first_name || c.username || `chat ${c.id}`,
577
+ }));
578
+ return { ok: true, chats, error: '' };
579
+ }
580
+ catch (err) {
581
+ return { ok: false, chats: [], error: err instanceof Error ? err.message : String(err) };
582
+ }
583
+ }
584
+ /**
585
+ * Persist a Telegram messaging entry into .instar/config.json's
586
+ * messaging[]. Replaces any existing telegram entry. Returns true on
587
+ * success.
588
+ */
589
+ function writeTelegramConfig(projectDir, params) {
590
+ const configPath = path.join(projectDir, '.instar', 'config.json');
591
+ try {
592
+ const raw = fs.readFileSync(configPath, 'utf-8');
593
+ const config = JSON.parse(raw);
594
+ config.messaging = (config.messaging || []).filter((m) => m.type !== 'telegram');
595
+ config.messaging.push({
596
+ type: 'telegram',
597
+ enabled: true,
598
+ config: {
599
+ token: params.token,
600
+ chatId: params.chatId,
601
+ pollIntervalMs: 2000,
602
+ stallTimeoutMinutes: 5,
603
+ },
604
+ });
605
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
606
+ return true;
607
+ }
608
+ catch {
609
+ return false;
610
+ }
611
+ }
612
+ /**
613
+ * Entry point — runs the wizard from the initial state to completion.
614
+ */
615
+ export async function runGeminiWizard(options) {
616
+ const states = buildFreshProjectInstall();
617
+ const answers = {};
618
+ let currentId = INITIAL_STATE;
619
+ let safety = 30; // guard against infinite loops via bad transitions
620
+ while (safety-- > 0) {
621
+ const state = states[currentId];
622
+ if (!state) {
623
+ console.log(pc.red(` Wizard reached an unknown state: ${currentId}`));
624
+ return;
625
+ }
626
+ if (state.kind === 'terminal') {
627
+ console.log();
628
+ console.log(pc.green(state.farewell));
629
+ console.log();
630
+ return;
631
+ }
632
+ if (state.kind === 'action') {
633
+ const updates = await runAction(state.action, answers, options);
634
+ Object.assign(answers, updates);
635
+ currentId = state.next(answers);
636
+ continue;
637
+ }
638
+ // narrative-then-prompt
639
+ const answer = await renderNarrativeState(state, answers, options);
640
+ const { state: nextId, updates } = state.next(answer, answers);
641
+ Object.assign(answers, updates);
642
+ currentId = nextId;
643
+ }
644
+ }
645
+ //# sourceMappingURL=gemini-driver.js.map