atom-lite 0.5.1 → 0.6.2

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 (59) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +40 -107
  3. package/dist/agent/loop.d.ts +6 -0
  4. package/dist/agent/loop.js +45 -11
  5. package/dist/agent/loop.js.map +1 -1
  6. package/dist/agent/system-prompt.d.ts +1 -0
  7. package/dist/agent/system-prompt.js +23 -0
  8. package/dist/agent/system-prompt.js.map +1 -0
  9. package/dist/agent/types.d.ts +11 -0
  10. package/dist/cli/app.js +203 -41
  11. package/dist/cli/app.js.map +1 -1
  12. package/dist/cli/diff-view.d.ts +18 -0
  13. package/dist/cli/diff-view.js +66 -0
  14. package/dist/cli/diff-view.js.map +1 -0
  15. package/dist/cli/panel.js +8 -8
  16. package/dist/cli/panel.js.map +1 -1
  17. package/dist/cli/status-line.d.ts +2 -0
  18. package/dist/cli/status-line.js +9 -1
  19. package/dist/cli/status-line.js.map +1 -1
  20. package/dist/cli/tool-box.d.ts +1 -1
  21. package/dist/cli/tool-box.js +44 -39
  22. package/dist/cli/tool-box.js.map +1 -1
  23. package/dist/config/defaults.d.ts +4 -5
  24. package/dist/config/defaults.js +6 -3
  25. package/dist/config/defaults.js.map +1 -1
  26. package/dist/config/load.js +29 -3
  27. package/dist/config/load.js.map +1 -1
  28. package/dist/config/schema.d.ts +10 -14
  29. package/dist/config/schema.js +4 -5
  30. package/dist/config/schema.js.map +1 -1
  31. package/dist/context/manager.js +1 -1
  32. package/dist/context/manager.js.map +1 -1
  33. package/dist/runtime/modes.d.ts +3 -0
  34. package/dist/runtime/modes.js +8 -0
  35. package/dist/runtime/modes.js.map +1 -0
  36. package/dist/runtime/tool-executor.js +10 -0
  37. package/dist/runtime/tool-executor.js.map +1 -1
  38. package/dist/skills/registry.js +8 -1
  39. package/dist/skills/registry.js.map +1 -1
  40. package/dist/tools/bash.js +1 -1
  41. package/dist/tools/bash.js.map +1 -1
  42. package/dist/tools/glob.d.ts +2 -0
  43. package/dist/tools/glob.js +97 -0
  44. package/dist/tools/glob.js.map +1 -0
  45. package/dist/tools/grep.d.ts +2 -0
  46. package/dist/tools/grep.js +290 -0
  47. package/dist/tools/grep.js.map +1 -0
  48. package/dist/tools/index.d.ts +2 -0
  49. package/dist/tools/index.js +2 -0
  50. package/dist/tools/index.js.map +1 -1
  51. package/dist/tools/types.d.ts +2 -0
  52. package/dist/types.d.ts +0 -3
  53. package/dist/types.js +1 -4
  54. package/dist/types.js.map +1 -1
  55. package/package.json +3 -2
  56. package/skills/atom-lite-extensions/SKILL.md +240 -0
  57. package/skills/caveman/SKILL.md +89 -0
  58. package/skills/code-review/SKILL.md +46 -0
  59. package/skills/optimization/SKILL.md +51 -0
package/dist/cli/app.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { loadConfig } from "../config/load.js";
2
+ import { DEFAULT_SYSTEM_PROMPT } from "../agent/system-prompt.js";
2
3
  import { defaultAgentDir, userConfigDir } from "../config/paths.js";
3
4
  import { saveProviderSelection, saveTheme, } from "../config/write.js";
4
5
  import { resolveApiKey, saveApiKey } from "../config/credentials.js";
5
6
  import { join } from "node:path";
6
7
  import { writeFileSync, existsSync } from "node:fs";
7
- import { execSync } from "node:child_process";
8
+ import { execSync, spawn } from "node:child_process";
9
+ import { defaultShell, terminateProcessTree } from "../security/process.js";
8
10
  import { openSearchablePicker } from "./picker.js";
9
11
  import { openCommandPalette } from "./palette.js";
10
12
  import { bareCommandName, createCommandRegistry } from "./commands.js";
@@ -13,7 +15,6 @@ import { SecretInput } from "./secret-input.js";
13
15
  import { StatusLine } from "./status-line.js";
14
16
  import { Activity } from "./activity.js";
15
17
  import { ToolBox } from "./tool-box.js";
16
- import { ApprovalPrompt } from "./approval-prompt.js";
17
18
  import { Banner } from "./banner.js";
18
19
  import { formatContextUsage, knownContextWindow, } from "../providers/context-window.js";
19
20
  import { StreamText } from "./stream-text.js";
@@ -27,7 +28,7 @@ import { ProjectTrustPrompt } from "./trust-prompt.js";
27
28
  import { formatSessionList, formatSessionSearch, searchSessions, sessionTitleFromEvents, } from "../sessions/search.js";
28
29
  import { providerSwitchNotice, sessionHistoryLines, sessionSelectionFromEvents, } from "../sessions/history.js";
29
30
  import { blockAtLine, createMouseCapture, handleMouseEvent, parseSgrMouse, shouldEnableMouse, } from "./mouse.js";
30
- import { readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool } from "../tools/index.js";
31
+ import { readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool, grepTool, globTool } from "../tools/index.js";
31
32
  import { McpManager, formatMcpStatus, formatMcpDoctor, createMcpTools } from "../mcp/index.js";
32
33
  import { createProvider } from "../providers/factory.js";
33
34
  import { availableProviderNames, getPreset, listModelsForProvider, materializeProviderConfig, probeApiKey, providerLabel, PROVIDER_PRESETS, } from "../providers/catalog.js";
@@ -124,7 +125,7 @@ export async function run(args) {
124
125
  modelByProvider.set(provider.id, modelOverride);
125
126
  const useTools = !positional.includes("__notools__");
126
127
  const builtInTools = useTools
127
- ? [readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool]
128
+ ? [readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool, grepTool, globTool]
128
129
  : [];
129
130
  const interactive = process.stdin.isTTY === true && !jsonMode;
130
131
  // one-shot task from positional or stdin
@@ -244,7 +245,7 @@ export async function run(args) {
244
245
  return results;
245
246
  };
246
247
  const contextManager = new ContextManager({
247
- systemPrompt: "You are a coding assistant.",
248
+ systemPrompt: DEFAULT_SYSTEM_PROMPT,
248
249
  maxInputTokens: config.context.maxInputTokens ?? null,
249
250
  reserveOutputTokens: config.context.reserveOutputTokens,
250
251
  compactAtPercent: config.context.compactAtPercent,
@@ -256,7 +257,7 @@ export async function run(args) {
256
257
  });
257
258
  let sessionTrusted = false;
258
259
  const approvals = {
259
- mode: config.runtime.approvalMode ?? "ask",
260
+ mode: "ask",
260
261
  // Installed below once the interactive UI helpers exist.
261
262
  handler: undefined,
262
263
  isTrusted: () => sessionTrusted,
@@ -271,6 +272,9 @@ export async function run(args) {
271
272
  workspace: process.cwd(),
272
273
  tools: allTools,
273
274
  approvals,
275
+ mode: config.runtime.mode,
276
+ maxTokensPerSession: config.runtime.maxTokensPerSession,
277
+ maxTokensPerTurn: config.runtime.maxTokensPerTurn,
274
278
  contextManager,
275
279
  extensionDispatch,
276
280
  bashConcurrency: config.runtime.bashConcurrency,
@@ -359,10 +363,12 @@ export async function run(args) {
359
363
  const header = new Text(theme.tokens.dim(headerLabel()), 1, 0);
360
364
  const statusBar = new StatusLine(theme);
361
365
  const activity = new Activity({ theme, onChange: () => updateStatus() });
366
+ let currentMode = config.runtime.mode;
362
367
  statusBar.setParts({
363
368
  left: `${provider.id}/${provider.model ?? "default"}`,
364
369
  tokens: "tokens: ?",
365
370
  effort: "effort: medium",
371
+ mode: currentMode,
366
372
  activity: activity.text(),
367
373
  });
368
374
  const selectListTheme = {
@@ -397,18 +403,59 @@ export async function run(args) {
397
403
  return;
398
404
  commandRegistry = createCommandRegistry({ skills: cachedSkillRegistry().list() });
399
405
  registrySkillSignature = sig;
400
- input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd()));
406
+ input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd(), fdPath));
401
407
  }
402
- input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd()));
408
+ const fdPath = (() => {
409
+ for (const bin of ["fd", "fdfind"]) {
410
+ try {
411
+ const found = execSync(`command -v ${bin}`, {
412
+ encoding: "utf8",
413
+ stdio: ["ignore", "pipe", "ignore"],
414
+ }).trim();
415
+ if (found)
416
+ return found;
417
+ }
418
+ catch {
419
+ // ignore
420
+ }
421
+ }
422
+ return null;
423
+ })();
424
+ input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd(), fdPath));
425
+ // Skills should autocomplete (fill the input) rather than execute immediately
426
+ // when selected from the autocomplete list; Enter on a typed command still
427
+ // runs them. Convert a confirming Enter into a Tab completion for skills.
428
+ const originalHandleInput = input.handleInput.bind(input);
429
+ input.handleInput = (data) => {
430
+ try {
431
+ if ((data === "\r" || data === "\n") &&
432
+ input.autocompleteState &&
433
+ input.autocompleteList) {
434
+ const selected = input.autocompleteList.getSelectedItem?.();
435
+ const name = selected?.value ?? selected?.name ?? selected?.label;
436
+ const cmd = name ? commandRegistry.get(String(name).replace(/^\//, "")) : undefined;
437
+ if (cmd?.bareInvokable) {
438
+ originalHandleInput("\t");
439
+ return;
440
+ }
441
+ }
442
+ }
443
+ catch {
444
+ // Fall through to default handling on any introspection failure.
445
+ }
446
+ originalHandleInput(data);
447
+ };
403
448
  const root = new VStack([]);
404
- root.addChild(output, { grow: 1 });
405
- root.addChild(new Spacer(1));
406
- root.addChild(input);
407
- root.addChild(statusBar);
449
+ // Keep fixed chrome (spacer, input, status, header, dropdown) from being
450
+ // collapsed when the transcript grows; only the output region may shrink.
451
+ root.addChild(output, { grow: 1, shrink: 1, minSize: 0 });
452
+ root.addChild(new Spacer(1), { grow: 0, shrink: 0, minSize: 1 });
453
+ root.addChild(input, { grow: 0, shrink: 0 });
454
+ root.addChild(statusBar, { grow: 0, shrink: 0 });
408
455
  // folder:branch info below the status line, dimmed
409
- root.addChild(header);
456
+ root.addChild(header, { grow: 0, shrink: 0 });
410
457
  const dropdownContainer = new Container();
411
- root.addChild(dropdownContainer);
458
+ root.addChild(dropdownContainer, { grow: 0, shrink: 0 });
412
459
  tui.setLayoutRoot(root);
413
460
  tui.setFocus(input);
414
461
  if (!noColor) {
@@ -536,6 +583,7 @@ export async function run(args) {
536
583
  streamText.finalize();
537
584
  }
538
585
  let activeTurnController = null;
586
+ let shellMode = false;
539
587
  const messageQueue = [];
540
588
  const steeringQueue = [];
541
589
  runtime.setSteeringQueue(steeringQueue);
@@ -549,7 +597,11 @@ export async function run(args) {
549
597
  await mcpManager.syncTools();
550
598
  runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
551
599
  }
552
- await runtime.runAgentTurn(prompt, session, controller.signal);
600
+ const turnResult = await runtime.runAgentTurn(prompt, session, controller.signal);
601
+ if (turnResult === "TOKEN_BUDGET_REACHED") {
602
+ appendLine("token budget reached; stopping", theme.tokens.warning);
603
+ tui.requestRender();
604
+ }
553
605
  }
554
606
  catch (err) {
555
607
  if (err instanceof Error && err.message === "USER_CANCELLED") {
@@ -569,12 +621,94 @@ export async function run(args) {
569
621
  }
570
622
  }
571
623
  }
624
+ /** Run a shell command directly (shell mode / `!` prefix). */
625
+ async function runShellCommand(command) {
626
+ const shell = defaultShell();
627
+ const flag = process.platform === "win32" ? "/c" : "-c";
628
+ const start = Date.now();
629
+ const cap = 30000;
630
+ let stdout = "";
631
+ let stderr = "";
632
+ let truncated = false;
633
+ const append = (target, chunk) => {
634
+ const s = chunk.toString("utf8");
635
+ let accepted = s;
636
+ if (target === "out") {
637
+ if (stdout.length + s.length > cap) {
638
+ truncated = true;
639
+ accepted = s.slice(0, Math.max(0, cap - stdout.length));
640
+ stdout += accepted;
641
+ }
642
+ else {
643
+ stdout += s;
644
+ }
645
+ }
646
+ else {
647
+ if (stderr.length + s.length > cap) {
648
+ truncated = true;
649
+ accepted = s.slice(0, Math.max(0, cap - stderr.length));
650
+ stderr += accepted;
651
+ }
652
+ else {
653
+ stderr += s;
654
+ }
655
+ }
656
+ if (accepted) {
657
+ tui.requestRender();
658
+ }
659
+ };
660
+ try {
661
+ const child = spawn(shell, [flag, command], {
662
+ cwd: process.cwd(),
663
+ env: process.env,
664
+ detached: process.platform !== "win32",
665
+ });
666
+ child.stdout?.on("data", (c) => append("out", c));
667
+ child.stderr?.on("data", (c) => append("err", c));
668
+ const exit = await new Promise((resolve) => {
669
+ const timer = setTimeout(() => {
670
+ terminateProcessTree(child, "SIGTERM");
671
+ setTimeout(() => terminateProcessTree(child, "SIGKILL"), 1000).unref();
672
+ resolve({ code: null, signal: "SIGTERM" });
673
+ }, 120000);
674
+ timer.unref();
675
+ child.on("exit", (code, signal) => {
676
+ clearTimeout(timer);
677
+ resolve({ code, signal });
678
+ });
679
+ child.on("error", (err) => {
680
+ clearTimeout(timer);
681
+ resolve({ code: null, signal: null });
682
+ stderr = `COMMAND_FAILED: ${err instanceof Error ? err.message : String(err)}`;
683
+ });
684
+ });
685
+ const parts = [];
686
+ parts.push(`exit_code: ${exit.code ?? "null"}`);
687
+ if (exit.signal)
688
+ parts.push(`signal: ${exit.signal}`);
689
+ if (truncated)
690
+ parts.push("OUTPUT_TRUNCATED");
691
+ parts.push(`duration_ms: ${Date.now() - start}`);
692
+ if (stdout)
693
+ parts.push(`stdout:\n${stdout}`);
694
+ if (stderr)
695
+ parts.push(`stderr:\n${stderr}`);
696
+ const content = parts.join("\n");
697
+ appendLine(content, exit.code === 0 && !exit.signal && !stderr
698
+ ? theme.tokens.success
699
+ : theme.tokens.error);
700
+ }
701
+ catch (err) {
702
+ appendLine(`shell error: ${err instanceof Error ? err.message : String(err)}`, theme.tokens.error);
703
+ }
704
+ }
572
705
  function updateStatus() {
573
706
  const usage = runtime.getContextUsage();
574
707
  statusBar.setParts({
575
708
  left: `${provider.id}/${currentModelLabel()}`,
576
709
  tokens: `tokens: ${formatContextUsage(usage.used, usage.window)}`,
577
710
  effort: `effort: ${effortLevel}`,
711
+ mode: currentMode,
578
712
  activity: activity.text(),
579
713
  });
580
714
  tui.requestRender();
@@ -1085,13 +1219,15 @@ export async function run(args) {
1085
1219
  appendLine(`skill not found: ${name}`);
1086
1220
  return false;
1087
1221
  }
1222
+ if (extra) {
1223
+ appendLine(`> ${extra}`, theme.tokens.user);
1224
+ }
1088
1225
  session.appendMessage({ role: "system", content });
1089
1226
  if (typeof session.appendEvent === "function") {
1090
1227
  session.appendEvent({ type: "skill_loaded", name });
1091
1228
  }
1092
- appendLine(`Loaded skill: ${name}`);
1229
+ appendLine(`Loaded skill: ${name}`, theme.tokens.dim);
1093
1230
  if (extra) {
1094
- appendLine(`> ${extra}`);
1095
1231
  await runTurn(extra);
1096
1232
  }
1097
1233
  return true;
@@ -1359,6 +1495,7 @@ export async function run(args) {
1359
1495
  for (const box of toolBoxes) {
1360
1496
  box.setExpanded(expandTools);
1361
1497
  }
1498
+ transcript.invalidate();
1362
1499
  tui.requestRender();
1363
1500
  }
1364
1501
  async function reloadExtensions() {
@@ -1482,6 +1619,7 @@ export async function run(args) {
1482
1619
  if (!(child instanceof ToolBox))
1483
1620
  return false;
1484
1621
  child.toggleExpanded();
1622
+ transcript.invalidate();
1485
1623
  return true;
1486
1624
  }
1487
1625
  tui.addInputListener((data) => {
@@ -1516,11 +1654,28 @@ export async function run(args) {
1516
1654
  toggleTools();
1517
1655
  return { consume: true };
1518
1656
  }
1519
- // Escape cancels the active turn.
1520
- if (matchesKey(data, Key.escape) && !uiModalOpen && activeTurnController) {
1521
- activeTurnController.abort();
1657
+ // Tab toggles plan/yolo mode.
1658
+ if (matchesKey(data, Key.tab) && !uiModalOpen) {
1659
+ currentMode = currentMode === "plan" ? "yolo" : "plan";
1660
+ runtime.setMode(currentMode);
1661
+ updateStatus();
1662
+ appendLine(`mode: ${currentMode}`, theme.tokens.dim);
1663
+ tui.requestRender();
1522
1664
  return { consume: true };
1523
1665
  }
1666
+ // Escape cancels the active turn, or exits shell mode.
1667
+ if (matchesKey(data, Key.escape) && !uiModalOpen) {
1668
+ if (activeTurnController) {
1669
+ activeTurnController.abort();
1670
+ return { consume: true };
1671
+ }
1672
+ if (shellMode) {
1673
+ shellMode = false;
1674
+ appendLine("shell mode: off", theme.tokens.dim);
1675
+ tui.requestRender();
1676
+ return { consume: true };
1677
+ }
1678
+ }
1524
1679
  return undefined;
1525
1680
  });
1526
1681
  runtime["onEvent"] = (e) => {
@@ -1560,6 +1715,10 @@ export async function run(args) {
1560
1715
  else if (e.type === "context_usage") {
1561
1716
  updateStatus();
1562
1717
  }
1718
+ else if (e.type === "budget_warning") {
1719
+ appendLine(`budget warning (${e.scope}): ${e.used}/${e.limit} tokens`, theme.tokens.warning);
1720
+ tui.requestRender();
1721
+ }
1563
1722
  else if (e.type === "model_switched") {
1564
1723
  appendLine(`switched model to ${e.provider}/${e.model}`);
1565
1724
  if (typeof session.appendEvent === "function") {
@@ -1581,6 +1740,7 @@ export async function run(args) {
1581
1740
  if (box) {
1582
1741
  activity.setPhase("working", e.name);
1583
1742
  box.appendLive(e.text);
1743
+ transcript.invalidate();
1584
1744
  tui.requestRender();
1585
1745
  }
1586
1746
  }
@@ -1806,6 +1966,27 @@ export async function run(args) {
1806
1966
  tui.requestRender();
1807
1967
  return;
1808
1968
  }
1969
+ // Shell mode: every line is a shell command. `!` alone (or as first char)
1970
+ // enters shell mode; Escape exits it.
1971
+ if (shellMode) {
1972
+ const cmd = trimmed.startsWith("!") ? trimmed.slice(1).trim() : trimmed;
1973
+ appendLine(`$ ${cmd}`, theme.tokens.user);
1974
+ await runShellCommand(cmd);
1975
+ tui.requestRender();
1976
+ return;
1977
+ }
1978
+ // `!` as first character enters shell mode.
1979
+ if (trimmed === "!" || trimmed.startsWith("! ")) {
1980
+ shellMode = true;
1981
+ const rest = trimmed.slice(1).trim();
1982
+ appendLine("shell mode: on (Escape to exit)", theme.tokens.dim);
1983
+ if (rest) {
1984
+ appendLine(`$ ${rest}`, theme.tokens.user);
1985
+ await runShellCommand(rest);
1986
+ }
1987
+ tui.requestRender();
1988
+ return;
1989
+ }
1809
1990
  refreshCommandRegistry();
1810
1991
  if (trimmed === "/steer" || trimmed.startsWith("/steer ")) {
1811
1992
  const text = trimmed.slice("/steer".length).trim();
@@ -1849,27 +2030,8 @@ export async function run(args) {
1849
2030
  appendLine(`> ${trimmed}`, theme.tokens.user);
1850
2031
  await runTurn(trimmed);
1851
2032
  };
1852
- approvals.handler = interactive
1853
- ? (req) => new Promise((resolve) => {
1854
- uiModalOpen = true;
1855
- closeOverlay();
1856
- dropdownContainer.clear();
1857
- activity.setPhase("waiting");
1858
- const prompt = new ApprovalPrompt(req, (decision) => {
1859
- uiModalOpen = false;
1860
- dropdownContainer.clear();
1861
- tui.setFocus(input);
1862
- activity.setPhase("working");
1863
- if (decision === "trust-session") {
1864
- appendLine("trusted this session: shell commands will run without approval");
1865
- }
1866
- resolve(decision);
1867
- });
1868
- dropdownContainer.addChild(prompt);
1869
- tui.setFocus(prompt);
1870
- tui.requestRender();
1871
- })
1872
- : undefined;
2033
+ // Approval prompts are superseded by plan/yolo modes.
2034
+ approvals.handler = undefined;
1873
2035
  process.on("SIGTERM", () => {
1874
2036
  void mcpManager.close().finally(() => {
1875
2037
  tui.stop();