@kitnai/cli 0.1.35 → 0.1.36

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.
package/dist/index.js CHANGED
@@ -282,8 +282,8 @@ var init_config = __esm({
282
282
  // src/installers/tsconfig-patcher.ts
283
283
  import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
284
284
  import { join as join4 } from "path";
285
- function stripJsonc(text3) {
286
- return text3.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([}\]])/g, "$1");
285
+ function stripJsonc(text4) {
286
+ return text4.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,\s*([}\]])/g, "$1");
287
287
  }
288
288
  function patchTsconfig(tsconfigContent, paths, removePrefixes) {
289
289
  const config2 = JSON.parse(stripJsonc(tsconfigContent));
@@ -423,7 +423,7 @@ async function resolveDependencies(names, fetchItem) {
423
423
  const visited = /* @__PURE__ */ new Set();
424
424
  const items = /* @__PURE__ */ new Map();
425
425
  const edges = [];
426
- async function resolve(name) {
426
+ async function resolve2(name) {
427
427
  if (visited.has(name)) return;
428
428
  visited.add(name);
429
429
  const item = await fetchItem(name);
@@ -431,11 +431,11 @@ async function resolveDependencies(names, fetchItem) {
431
431
  const deps = item.registryDependencies ?? [];
432
432
  for (const dep of deps) {
433
433
  edges.push([dep, name]);
434
- await resolve(dep);
434
+ await resolve2(dep);
435
435
  }
436
436
  }
437
437
  for (const name of names) {
438
- await resolve(name);
438
+ await resolve2(name);
439
439
  }
440
440
  return topologicalSort(items, edges);
441
441
  }
@@ -3448,14 +3448,61 @@ var init_registry = __esm({
3448
3448
  // src/commands/chat.ts
3449
3449
  var chat_exports = {};
3450
3450
  __export(chat_exports, {
3451
- buildRequestPayload: () => buildRequestPayload,
3451
+ applyCompaction: () => applyCompaction,
3452
+ buildServicePayload: () => buildServicePayload,
3452
3453
  chatCommand: () => chatCommand,
3453
3454
  fetchGlobalRegistries: () => fetchGlobalRegistries,
3455
+ formatElapsed: () => formatElapsed,
3454
3456
  formatPlan: () => formatPlan,
3455
- resolveServiceUrl: () => resolveServiceUrl
3457
+ formatSessionStats: () => formatSessionStats,
3458
+ formatStepLabel: () => formatStepLabel,
3459
+ formatTokens: () => formatTokens,
3460
+ handleAskUser: () => handleAskUser,
3461
+ handleCreatePlan: () => handleCreatePlan,
3462
+ handleListFiles: () => handleListFiles,
3463
+ handleReadFile: () => handleReadFile,
3464
+ handleUpdateEnv: () => handleUpdateEnv,
3465
+ handleUpdateEnvDirect: () => handleUpdateEnvDirect,
3466
+ handleWriteFile: () => handleWriteFile,
3467
+ hasToolCalls: () => hasToolCalls,
3468
+ resolveServiceUrl: () => resolveServiceUrl,
3469
+ shouldCompact: () => shouldCompact
3456
3470
  });
3457
3471
  import * as p16 from "@clack/prompts";
3458
3472
  import pc15 from "picocolors";
3473
+ import { readFile as fsReadFile, writeFile as fsWriteFile, mkdir as mkdir8, readdir as readdir2 } from "fs/promises";
3474
+ import { join as join15, dirname as dirname7, resolve } from "path";
3475
+ function buildServicePayload(messages, metadata) {
3476
+ return { messages, metadata };
3477
+ }
3478
+ function hasToolCalls(response) {
3479
+ return !!(response.message.toolCalls && response.message.toolCalls.length > 0);
3480
+ }
3481
+ function formatTokens(count) {
3482
+ if (count >= 1e3) return `${(count / 1e3).toFixed(1)}k`;
3483
+ return `${count}`;
3484
+ }
3485
+ function formatElapsed(ms) {
3486
+ const seconds = Math.floor(ms / 1e3);
3487
+ if (seconds < 60) return `${seconds}s`;
3488
+ const minutes = Math.floor(seconds / 60);
3489
+ const remaining = seconds % 60;
3490
+ return `${minutes}m ${remaining}s`;
3491
+ }
3492
+ function formatSessionStats(elapsedMs, totalTokens) {
3493
+ return `Session: ${formatElapsed(elapsedMs)} | ${formatTokens(totalTokens)} tokens`;
3494
+ }
3495
+ function shouldCompact(lastPromptTokens, threshold) {
3496
+ return lastPromptTokens >= threshold;
3497
+ }
3498
+ function applyCompaction(messages, summary, keepRecent = 2) {
3499
+ const recent = messages.slice(-keepRecent);
3500
+ return [
3501
+ { role: "user", content: `[Context from earlier in conversation]
3502
+ ${summary}` },
3503
+ ...recent
3504
+ ];
3505
+ }
3459
3506
  async function fetchGlobalRegistries(configuredNamespaces) {
3460
3507
  let directory;
3461
3508
  try {
@@ -3496,9 +3543,6 @@ async function resolveServiceUrl(urlOverride, chatServiceConfig) {
3496
3543
  if (chatServiceConfig?.url) return chatServiceConfig.url;
3497
3544
  return DEFAULT_SERVICE_URL;
3498
3545
  }
3499
- function buildRequestPayload(message, metadata) {
3500
- return { message, metadata };
3501
- }
3502
3546
  function formatPlan(plan) {
3503
3547
  const lines = [plan.summary, ""];
3504
3548
  for (let i = 0; i < plan.steps.length; i++) {
@@ -3523,6 +3567,246 @@ function formatStepLabel(step) {
3523
3567
  return `Unlink ${pc15.red(step.toolName)} from ${pc15.cyan(step.agentName)}`;
3524
3568
  case "registry-add":
3525
3569
  return `Add registry ${pc15.magenta(step.namespace)}`;
3570
+ case "update":
3571
+ return `Update ${pc15.yellow(step.component)}`;
3572
+ }
3573
+ }
3574
+ async function executeStep(step) {
3575
+ switch (step.action) {
3576
+ case "registry-add": {
3577
+ const { registryAddCommand: registryAddCommand2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
3578
+ await registryAddCommand2(step.namespace, step.url, { overwrite: true });
3579
+ break;
3580
+ }
3581
+ case "add": {
3582
+ const { addCommand: addCommand2 } = await Promise.resolve().then(() => (init_add(), add_exports));
3583
+ await addCommand2([step.component], { yes: true });
3584
+ break;
3585
+ }
3586
+ case "create": {
3587
+ const { createCommand: createCommand2 } = await Promise.resolve().then(() => (init_create(), create_exports));
3588
+ await createCommand2(step.type, step.name);
3589
+ break;
3590
+ }
3591
+ case "link": {
3592
+ const { linkCommand: linkCommand2 } = await Promise.resolve().then(() => (init_link(), link_exports));
3593
+ await linkCommand2("tool", step.toolName, { to: step.agentName });
3594
+ break;
3595
+ }
3596
+ case "remove": {
3597
+ const { removeCommand: removeCommand2 } = await Promise.resolve().then(() => (init_remove(), remove_exports));
3598
+ await removeCommand2(step.component);
3599
+ break;
3600
+ }
3601
+ case "unlink": {
3602
+ const { unlinkCommand: unlinkCommand2 } = await Promise.resolve().then(() => (init_unlink(), unlink_exports));
3603
+ await unlinkCommand2("tool", step.toolName, { from: step.agentName });
3604
+ break;
3605
+ }
3606
+ case "update": {
3607
+ const { addCommand: addCommand2 } = await Promise.resolve().then(() => (init_add(), add_exports));
3608
+ await addCommand2([step.component], { overwrite: true, yes: true });
3609
+ break;
3610
+ }
3611
+ }
3612
+ }
3613
+ async function handleAskUser(input) {
3614
+ const responses = [];
3615
+ for (const item of input.items) {
3616
+ switch (item.type) {
3617
+ case "info":
3618
+ p16.log.info(item.text);
3619
+ break;
3620
+ case "warning":
3621
+ p16.log.warn(item.text);
3622
+ break;
3623
+ case "confirmation": {
3624
+ const confirmed = await p16.confirm({ message: item.text });
3625
+ if (p16.isCancel(confirmed)) return "User cancelled.";
3626
+ responses.push(confirmed ? "Yes" : "No");
3627
+ break;
3628
+ }
3629
+ case "option": {
3630
+ if (!item.choices?.length) {
3631
+ responses.push("No choices provided.");
3632
+ break;
3633
+ }
3634
+ const selected = await p16.select({
3635
+ message: item.text,
3636
+ options: item.choices.map((c) => ({ value: c, label: c }))
3637
+ });
3638
+ if (p16.isCancel(selected)) return "User cancelled.";
3639
+ responses.push(`User selected: ${selected}`);
3640
+ break;
3641
+ }
3642
+ case "question": {
3643
+ const answer = await p16.text({ message: item.text, placeholder: item.context });
3644
+ if (p16.isCancel(answer)) return "User cancelled.";
3645
+ responses.push(`User answered: ${answer}`);
3646
+ break;
3647
+ }
3648
+ }
3649
+ }
3650
+ return responses.join("\n");
3651
+ }
3652
+ async function handleCreatePlan(plan, cwd) {
3653
+ p16.log.message(formatPlan(plan));
3654
+ const steps = plan.steps;
3655
+ let selectedSteps;
3656
+ if (steps.length === 1) {
3657
+ const confirm7 = await p16.confirm({ message: `Run: ${formatStepLabel(steps[0])}?` });
3658
+ if (p16.isCancel(confirm7) || !confirm7) return "User cancelled the plan.";
3659
+ selectedSteps = steps;
3660
+ } else {
3661
+ const action = await p16.select({
3662
+ message: "How would you like to proceed?",
3663
+ options: [
3664
+ { value: "all", label: "Yes, run all steps" },
3665
+ { value: "select", label: "Select which steps to run" },
3666
+ { value: "cancel", label: "Cancel" }
3667
+ ]
3668
+ });
3669
+ if (p16.isCancel(action) || action === "cancel") return "User cancelled the plan.";
3670
+ if (action === "select") {
3671
+ const choices = await p16.multiselect({
3672
+ message: "Select steps to run:",
3673
+ options: steps.map((step, i) => ({ value: i, label: `${formatStepLabel(step)} - ${step.reason}` }))
3674
+ });
3675
+ if (p16.isCancel(choices)) return "User cancelled the plan.";
3676
+ selectedSteps = choices.map((i) => steps[i]);
3677
+ } else {
3678
+ selectedSteps = steps;
3679
+ }
3680
+ }
3681
+ const results = [];
3682
+ const s = p16.spinner();
3683
+ for (const step of selectedSteps) {
3684
+ s.start(`Running: ${formatStepLabel(step)}...`);
3685
+ try {
3686
+ await executeStep(step);
3687
+ s.stop(pc15.green(`Done: ${formatStepLabel(step)}`));
3688
+ results.push(`Completed: ${step.action} ${step.component ?? step.name ?? ""}`);
3689
+ } catch (err) {
3690
+ s.stop(pc15.red(`Failed: ${formatStepLabel(step)}`));
3691
+ results.push(`Failed: ${step.action} ${step.component ?? step.name ?? ""} \u2014 ${err.message}`);
3692
+ }
3693
+ }
3694
+ return results.join("\n");
3695
+ }
3696
+ async function handleWriteFile(input, cwd) {
3697
+ const fullPath = resolve(cwd, input.path);
3698
+ if (!fullPath.startsWith(resolve(cwd))) {
3699
+ return `Rejected: path '${input.path}' would escape project directory`;
3700
+ }
3701
+ await mkdir8(dirname7(fullPath), { recursive: true });
3702
+ await fsWriteFile(fullPath, input.content, "utf-8");
3703
+ p16.log.success(`Wrote ${pc15.cyan(input.path)}${input.description ? ` \u2014 ${input.description}` : ""}`);
3704
+ return `Wrote ${input.path}`;
3705
+ }
3706
+ async function handleReadFile(input, cwd) {
3707
+ const fullPath = resolve(cwd, input.path);
3708
+ if (!fullPath.startsWith(resolve(cwd))) {
3709
+ return `Rejected: path '${input.path}' would escape project directory`;
3710
+ }
3711
+ try {
3712
+ return await fsReadFile(fullPath, "utf-8");
3713
+ } catch {
3714
+ return `File not found: ${input.path}`;
3715
+ }
3716
+ }
3717
+ async function handleListFiles(input, cwd) {
3718
+ const searchDir = input.directory ? join15(cwd, input.directory) : cwd;
3719
+ try {
3720
+ const entries = await readdir2(searchDir, { recursive: true });
3721
+ const filtered = entries.filter((e) => {
3722
+ if (input.pattern.startsWith("*.")) {
3723
+ const ext = input.pattern.slice(1);
3724
+ return String(e).endsWith(ext);
3725
+ }
3726
+ return true;
3727
+ });
3728
+ return filtered.length > 0 ? `Files found:
3729
+ ${filtered.join("\n")}` : "No files found matching the pattern.";
3730
+ } catch {
3731
+ return `Directory not found: ${input.directory ?? "."}`;
3732
+ }
3733
+ }
3734
+ async function handleUpdateEnv(input, cwd) {
3735
+ const value = await p16.password({ message: `Enter ${input.key} (${input.description}):` });
3736
+ if (p16.isCancel(value) || !value) return "User cancelled.";
3737
+ return handleUpdateEnvDirect(input, cwd, value);
3738
+ }
3739
+ async function handleUpdateEnvDirect(input, cwd, value) {
3740
+ const envPath = join15(cwd, ".env");
3741
+ let existing = "";
3742
+ try {
3743
+ existing = await fsReadFile(envPath, "utf-8");
3744
+ } catch {
3745
+ }
3746
+ const lines = existing.split("\n");
3747
+ const keyIndex = lines.findIndex((l) => l.startsWith(`${input.key}=`));
3748
+ let newContent;
3749
+ if (keyIndex >= 0) {
3750
+ lines[keyIndex] = `${input.key}=${value}`;
3751
+ newContent = lines.join("\n");
3752
+ } else {
3753
+ newContent = existing + (existing && !existing.endsWith("\n") ? "\n" : "") + `${input.key}=${value}
3754
+ `;
3755
+ }
3756
+ await fsWriteFile(envPath, newContent, "utf-8");
3757
+ p16.log.success(`Set ${pc15.cyan(input.key)} in .env`);
3758
+ return `Successfully set ${input.key} in .env`;
3759
+ }
3760
+ async function handleToolCalls(toolCalls, cwd) {
3761
+ const results = [];
3762
+ for (const call of toolCalls) {
3763
+ let result;
3764
+ try {
3765
+ switch (call.name) {
3766
+ case "askUser":
3767
+ result = await handleAskUser(call.input);
3768
+ break;
3769
+ case "createPlan":
3770
+ result = await handleCreatePlan(call.input, cwd);
3771
+ break;
3772
+ case "writeFile":
3773
+ result = await handleWriteFile(call.input, cwd);
3774
+ break;
3775
+ case "readFile":
3776
+ result = await handleReadFile(call.input, cwd);
3777
+ break;
3778
+ case "listFiles":
3779
+ result = await handleListFiles(call.input, cwd);
3780
+ break;
3781
+ case "updateEnv":
3782
+ result = await handleUpdateEnv(call.input, cwd);
3783
+ break;
3784
+ default:
3785
+ result = `Unknown tool: ${call.name}`;
3786
+ }
3787
+ } catch (err) {
3788
+ result = `Error executing ${call.name}: ${err.message ?? "Unknown error"}`;
3789
+ }
3790
+ results.push({ toolCallId: call.id, result });
3791
+ }
3792
+ return results;
3793
+ }
3794
+ async function compactConversation(messages, serviceUrl) {
3795
+ try {
3796
+ const headers = { "Content-Type": "application/json" };
3797
+ if (process.env.KITN_API_KEY) headers["Authorization"] = `Bearer ${process.env.KITN_API_KEY}`;
3798
+ const res = await fetch(`${serviceUrl}/api/chat/compact`, {
3799
+ method: "POST",
3800
+ headers,
3801
+ body: JSON.stringify({ messages })
3802
+ });
3803
+ if (res.ok) {
3804
+ const data = await res.json();
3805
+ const compacted = applyCompaction(messages, data.summary);
3806
+ messages.length = 0;
3807
+ messages.push(...compacted);
3808
+ }
3809
+ } catch {
3526
3810
  }
3527
3811
  }
3528
3812
  async function chatCommand(message, opts) {
@@ -3532,11 +3816,18 @@ async function chatCommand(message, opts) {
3532
3816
  p16.log.error("No kitn.json found. Run `kitn init` first.");
3533
3817
  process.exit(1);
3534
3818
  }
3819
+ p16.intro(pc15.bold("kitn assistant"));
3535
3820
  if (!message) {
3536
- p16.log.error('Please provide a message. Usage: kitn chat "add a weather tool"');
3537
- process.exit(1);
3821
+ const input = await p16.text({
3822
+ message: "What would you like to do?",
3823
+ placeholder: "e.g., I want to build a weather agent"
3824
+ });
3825
+ if (p16.isCancel(input) || !input) {
3826
+ p16.cancel("Cancelled.");
3827
+ return;
3828
+ }
3829
+ message = input;
3538
3830
  }
3539
- p16.intro(pc15.bold("kitn assistant"));
3540
3831
  const s = p16.spinner();
3541
3832
  s.start("Gathering project context...");
3542
3833
  let registryIndex;
@@ -3568,141 +3859,70 @@ async function chatCommand(message, opts) {
3568
3859
  globalRegistryIndex = globalEntries.length > 0 ? globalEntries : void 0;
3569
3860
  } catch {
3570
3861
  s.stop(pc15.red("Failed to gather context"));
3571
- p16.log.error("Could not read project context. Check your kitn.json and network connection.");
3862
+ p16.log.error("Could not read project context.");
3572
3863
  process.exit(1);
3573
3864
  }
3574
3865
  s.stop("Context gathered");
3575
- s.start("Thinking...");
3866
+ const messages = [{ role: "user", content: message }];
3576
3867
  const serviceUrl = await resolveServiceUrl(opts?.url, config2.chatService);
3577
3868
  const metadata = { registryIndex, installed };
3578
3869
  if (globalRegistryIndex) metadata.globalRegistryIndex = globalRegistryIndex;
3579
- const payload = buildRequestPayload(message, metadata);
3580
- let response;
3581
- try {
3582
- const headers = {
3583
- "Content-Type": "application/json"
3584
- };
3585
- if (process.env.KITN_API_KEY) {
3586
- headers["Authorization"] = `Bearer ${process.env.KITN_API_KEY}`;
3587
- }
3588
- response = await fetch(`${serviceUrl}/api/chat`, {
3589
- method: "POST",
3590
- headers,
3591
- body: JSON.stringify(payload)
3592
- });
3593
- } catch (err) {
3594
- s.stop(pc15.red("Connection failed"));
3595
- p16.log.error(`Could not reach chat service at ${serviceUrl}. ${err.message ?? ""}`);
3596
- process.exit(1);
3597
- }
3598
- if (!response.ok) {
3599
- s.stop(pc15.red("Request failed"));
3600
- p16.log.error(`Chat service returned ${response.status}: ${response.statusText}`);
3601
- process.exit(1);
3602
- }
3603
- let data;
3604
- try {
3605
- data = await response.json();
3606
- } catch {
3607
- s.stop(pc15.red("Invalid response"));
3608
- p16.log.error("Chat service returned an invalid response.");
3609
- process.exit(1);
3610
- }
3611
- s.stop("Done");
3612
- if (data.rejected) {
3613
- p16.log.warn(data.text ?? "Request was rejected by the assistant.");
3614
- p16.outro("Try rephrasing your request.");
3615
- return;
3616
- }
3617
- if (!data.plan) {
3618
- p16.log.info(data.text ?? "No actionable plan returned.");
3619
- p16.outro("Nothing to do.");
3620
- return;
3621
- }
3622
- p16.log.message(formatPlan(data.plan));
3623
- const steps = data.plan.steps;
3624
- let selectedSteps;
3625
- if (steps.length === 1) {
3626
- const confirm7 = await p16.confirm({
3627
- message: `Run: ${formatStepLabel(steps[0])}?`
3628
- });
3629
- if (p16.isCancel(confirm7) || !confirm7) {
3630
- p16.cancel("Cancelled.");
3631
- return;
3632
- }
3633
- selectedSteps = steps;
3634
- } else {
3635
- const action = await p16.select({
3636
- message: "How would you like to proceed?",
3637
- options: [
3638
- { value: "all", label: "Yes, run all steps" },
3639
- { value: "select", label: "Select which steps to run" },
3640
- { value: "cancel", label: "Cancel" }
3641
- ]
3642
- });
3643
- if (p16.isCancel(action) || action === "cancel") {
3644
- p16.cancel("Cancelled.");
3645
- return;
3646
- }
3647
- if (action === "select") {
3648
- const choices = await p16.multiselect({
3649
- message: "Select steps to run:",
3650
- options: steps.map((step, i) => ({
3651
- value: i,
3652
- label: `${formatStepLabel(step)} - ${step.reason}`
3653
- }))
3654
- });
3655
- if (p16.isCancel(choices)) {
3656
- p16.cancel("Cancelled.");
3657
- return;
3658
- }
3659
- selectedSteps = choices.map((i) => steps[i]);
3660
- } else {
3661
- selectedSteps = steps;
3662
- }
3663
- }
3664
- for (const step of selectedSteps) {
3665
- s.start(`Running: ${formatStepLabel(step)}...`);
3870
+ let totalTokens = 0;
3871
+ let lastInputTokens = 0;
3872
+ const sessionStart = Date.now();
3873
+ const MAX_PROMPT_TOKENS = 1e5;
3874
+ while (true) {
3875
+ const turnStart = Date.now();
3876
+ if (shouldCompact(lastInputTokens, MAX_PROMPT_TOKENS)) {
3877
+ s.start("Compacting conversation...");
3878
+ await compactConversation(messages, serviceUrl);
3879
+ s.stop(pc15.dim("Conversation compacted."));
3880
+ }
3881
+ s.start("Thinking...");
3882
+ let response;
3666
3883
  try {
3667
- switch (step.action) {
3668
- case "registry-add": {
3669
- const { registryAddCommand: registryAddCommand2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
3670
- await registryAddCommand2(step.namespace, step.url, { overwrite: true });
3671
- break;
3672
- }
3673
- case "add": {
3674
- const { addCommand: addCommand2 } = await Promise.resolve().then(() => (init_add(), add_exports));
3675
- await addCommand2([step.component], { yes: true });
3676
- break;
3677
- }
3678
- case "create": {
3679
- const { createCommand: createCommand2 } = await Promise.resolve().then(() => (init_create(), create_exports));
3680
- await createCommand2(step.type, step.name);
3681
- break;
3682
- }
3683
- case "link": {
3684
- const { linkCommand: linkCommand2 } = await Promise.resolve().then(() => (init_link(), link_exports));
3685
- await linkCommand2("tool", step.toolName, { to: step.agentName });
3686
- break;
3687
- }
3688
- case "remove": {
3689
- const { removeCommand: removeCommand2 } = await Promise.resolve().then(() => (init_remove(), remove_exports));
3690
- await removeCommand2(step.component);
3691
- break;
3692
- }
3693
- case "unlink": {
3694
- const { unlinkCommand: unlinkCommand2 } = await Promise.resolve().then(() => (init_unlink(), unlink_exports));
3695
- await unlinkCommand2("tool", step.toolName, { from: step.agentName });
3696
- break;
3697
- }
3884
+ const headers = { "Content-Type": "application/json" };
3885
+ if (process.env.KITN_API_KEY) headers["Authorization"] = `Bearer ${process.env.KITN_API_KEY}`;
3886
+ const res = await fetch(`${serviceUrl}/api/chat`, {
3887
+ method: "POST",
3888
+ headers,
3889
+ body: JSON.stringify(buildServicePayload(messages, metadata))
3890
+ });
3891
+ if (!res.ok) {
3892
+ s.stop(pc15.red("Request failed"));
3893
+ p16.log.error(`Chat service returned ${res.status}: ${res.statusText}`);
3894
+ break;
3698
3895
  }
3699
- s.stop(pc15.green(`Done: ${formatStepLabel(step)}`));
3896
+ response = await res.json();
3700
3897
  } catch (err) {
3701
- s.stop(pc15.red(`Failed: ${formatStepLabel(step)}`));
3702
- p16.log.error(err.message ?? "Unknown error");
3898
+ s.stop(pc15.red("Connection failed"));
3899
+ p16.log.error(`Could not reach chat service at ${serviceUrl}. ${err.message ?? ""}`);
3900
+ break;
3703
3901
  }
3902
+ const elapsed = Date.now() - turnStart;
3903
+ totalTokens += response.usage.outputTokens;
3904
+ lastInputTokens = response.usage.inputTokens;
3905
+ if (response.rejected) {
3906
+ s.stop("Done");
3907
+ p16.log.warn(response.text ?? "Request was rejected.");
3908
+ break;
3909
+ }
3910
+ if (!hasToolCalls(response)) {
3911
+ s.stop(`Done ${pc15.dim(`(${formatElapsed(elapsed)} | ${formatTokens(response.usage.outputTokens)} tokens)`)}`);
3912
+ if (response.message.content) p16.log.message(response.message.content);
3913
+ break;
3914
+ }
3915
+ s.stop(`${pc15.dim(`(${formatElapsed(elapsed)} | ${formatTokens(response.usage.outputTokens)} tokens)`)}`);
3916
+ messages.push({
3917
+ role: "assistant",
3918
+ content: response.message.content,
3919
+ toolCalls: response.message.toolCalls
3920
+ });
3921
+ const toolResults = await handleToolCalls(response.message.toolCalls, cwd);
3922
+ messages.push({ role: "tool", toolResults });
3704
3923
  }
3705
- p16.outro(pc15.green("All done! Run your dev server to test the new components."));
3924
+ const totalElapsed = Date.now() - sessionStart;
3925
+ p16.outro(pc15.green("Done! ") + pc15.dim(formatSessionStats(totalElapsed, totalTokens)));
3706
3926
  }
3707
3927
  var DEFAULT_SERVICE_URL, GLOBAL_REGISTRY_URL;
3708
3928
  var init_chat = __esm({
@@ -3719,7 +3939,7 @@ var init_chat = __esm({
3719
3939
  // src/index.ts
3720
3940
  init_update_check();
3721
3941
  import { Command } from "commander";
3722
- var VERSION = true ? "0.1.35" : "0.0.0-dev";
3942
+ var VERSION = true ? "0.1.36" : "0.0.0-dev";
3723
3943
  var printUpdateNotice = startUpdateCheck(VERSION);
3724
3944
  var program = new Command().name("kitn").description("Install AI agent components from the kitn registry").version(VERSION);
3725
3945
  program.command("init").description("Initialize kitn in your project").option("-r, --runtime <runtime>", "runtime to use (bun, node, deno)").option("-f, --framework <framework>", "HTTP framework (hono, hono-openapi, elysia)").option("-b, --base <path>", "base directory for components (default: src/ai)").option("-y, --yes", "accept all defaults without prompting").action(async (opts) => {
@@ -3770,7 +3990,7 @@ program.command("rules").description("Regenerate AI coding tool rules files").ac
3770
3990
  const { rulesCommand: rulesCommand2 } = await Promise.resolve().then(() => (init_rules(), rules_exports));
3771
3991
  await rulesCommand2();
3772
3992
  });
3773
- program.command("chat").description("AI-powered scaffolding assistant \u2014 describe what you need in plain English").argument("<message>", 'what you want to build (e.g. "I want a weather agent")').option("-u, --url <url>", "chat service URL (overrides config and default)").action(async (message, opts) => {
3993
+ program.command("chat").description("AI-powered scaffolding assistant").argument("[message]", "What you want to do").option("-u, --url <url>", "Chat service URL").action(async (message, opts) => {
3774
3994
  const { chatCommand: chatCommand2 } = await Promise.resolve().then(() => (init_chat(), chat_exports));
3775
3995
  await chatCommand2(message, opts);
3776
3996
  });