@kmugalkhod/lightcode 0.3.0 → 0.5.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 (5) hide show
  1. package/README.md +20 -1
  2. package/cli.js +138 -39
  3. package/lightcode.cjs +63 -18
  4. package/package.json +2 -1
  5. package/server.js +408 -65
package/README.md CHANGED
@@ -6,6 +6,19 @@ permission-gated tool suite (files, shell, git, web, MCP), and tiered context
6
6
  optimization that keeps long sessions inside the model's context window
7
7
  without ever losing history.
8
8
 
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g @kmugalkhod/lightcode
13
+ lightcode
14
+ ```
15
+
16
+ Works on macOS (Apple Silicon and Intel), Linux, and Windows. Bun ships with
17
+ the package, so there's nothing else to install — the first `lightcode` run
18
+ uses the bundled runtime automatically (and reuses a system Bun if you already
19
+ have one). The published package is scoped as `@kmugalkhod/lightcode`; the
20
+ command it installs is `lightcode`.
21
+
9
22
  ## Quick start (from source)
10
23
 
11
24
  ```bash
@@ -13,6 +26,11 @@ bun install
13
26
  bun run cli:dev # starts the TUI; it boots the server automatically
14
27
  ```
15
28
 
29
+ Lightcode is aware of the directory you launch it in: each turn it reads the
30
+ project's shape (top-level files, git branch/status) and any `AGENTS.md` /
31
+ `CLAUDE.md` / `README.md`, so "review my code" or "explain this" inspects your
32
+ actual files instead of asking you to paste them.
33
+
16
34
  On first run Lightcode walks you through provider setup (Anthropic,
17
35
  OpenRouter, OpenCode Zen, or any OpenAI-compatible endpoint) and stores the
18
36
  API key in `~/.lightcode/credentials.json`. Environment variables such as
@@ -44,7 +62,8 @@ variables. Notable keys:
44
62
  | `context.pruneAtFraction` | Tool-output pruning threshold (default 0.6) |
45
63
  | `context.contextWindowOverride` | Override the model context window |
46
64
  | `maxRetries` | Provider-call retries for transient errors (default 5) |
47
- | `autoContinue.stallTimeoutSeconds` | Abort+retry a byte-silent stream (default 180) |
65
+ | `autoContinue.stallTimeoutSeconds` | Abort+retry a byte-silent stream (default 300; raise for very slow reasoning models) |
66
+ | `autoContinue.maxErrorRetries` | Auto-resends before surfacing a stream error (default 8) |
48
67
 
49
68
  The local companion server listens on `127.0.0.1:4983` (uncommon on purpose —
50
69
  port 3000 belongs to the apps you build). Override with `PORT` for the server
package/cli.js CHANGED
@@ -124,7 +124,7 @@ var init_logger = __esm(() => {
124
124
  });
125
125
 
126
126
  // packages/shared/src/version.ts
127
- var lightcodeVersion = "0.3.0";
127
+ var lightcodeVersion = "0.5.0";
128
128
 
129
129
  // packages/shared/src/index.ts
130
130
  var productName = "Lightcode";
@@ -226,7 +226,8 @@ async function ensureServerRunning() {
226
226
  stderr: logFile,
227
227
  env: {
228
228
  ...process.env,
229
- PORT: port
229
+ PORT: port,
230
+ LIGHTCODE_PARENT_PID: String(process.pid)
230
231
  }
231
232
  });
232
233
  for (let attempt = 0;attempt < startupPollAttempts; attempt += 1) {
@@ -998,7 +999,7 @@ var init_slash_page_menu = __esm(() => {
998
999
  });
999
1000
 
1000
1001
  // packages/ai/src/constants.ts
1001
- var MAX_TOOL_TEXT_OUTPUT_CHARS = 12000, MAX_TOOL_LIST_ENTRIES = 200, MAX_TOOL_SEARCH_RESULTS = 200, ANTHROPIC_TOOL_OPTIONAL_PARAMETER_BUDGET = 24;
1002
+ var MAX_TOOL_TEXT_OUTPUT_CHARS = 12000, DEFAULT_TOOL_TEXT_OUTPUT_CHARS = 6000, MAX_TOOL_LIST_ENTRIES = 200, MAX_TOOL_SEARCH_RESULTS = 200, ANTHROPIC_TOOL_OPTIONAL_PARAMETER_BUDGET = 24;
1002
1003
 
1003
1004
  // packages/ai/src/coding-agent-modes.ts
1004
1005
  import { z } from "zod";
@@ -1594,13 +1595,13 @@ var init_base_schemas = __esm(() => {
1594
1595
 
1595
1596
  // packages/ai/src/bash/schema.ts
1596
1597
  import { z as z5 } from "zod";
1597
- var bashDescription = "Run a shell command from workspace root with a timeout. Commands are classified before execution and may require approval.", bashInputSchema, bashProviderInputSchema, bashOutputSchema;
1598
+ var bashDescription = "Run a shell command from workspace root with a timeout. Bound noisy output at the source (head/tail/grep/wc, or write to a file then read a range) rather than dumping everything \u2014 output is truncated head+tail and wastes tokens. Commands are classified before execution and may require approval.", bashInputSchema, bashProviderInputSchema, bashOutputSchema;
1598
1599
  var init_schema = __esm(() => {
1599
1600
  init_base_schemas();
1600
1601
  bashInputSchema = z5.object({
1601
1602
  command: z5.string().min(1).max(4000),
1602
1603
  timeoutMs: integerRangeSchema(1000, 120000, "timeoutMs").optional().default(30000),
1603
- maxOutputChars: integerRangeSchema(200, MAX_TOOL_TEXT_OUTPUT_CHARS, "maxOutputChars").optional().default(MAX_TOOL_TEXT_OUTPUT_CHARS)
1604
+ maxOutputChars: integerRangeSchema(200, MAX_TOOL_TEXT_OUTPUT_CHARS, "maxOutputChars").optional().default(DEFAULT_TOOL_TEXT_OUTPUT_CHARS)
1604
1605
  });
1605
1606
  bashProviderInputSchema = z5.object({
1606
1607
  command: z5.string().min(1).max(4000)
@@ -1710,7 +1711,7 @@ var init_schema5 = __esm(() => {
1710
1711
  path: boundedPathSchema.optional(),
1711
1712
  staged: z9.boolean().optional().default(false),
1712
1713
  statOnly: z9.boolean().optional().default(false),
1713
- maxChars: integerRangeSchema(200, MAX_TOOL_TEXT_OUTPUT_CHARS, "maxChars").optional().default(MAX_TOOL_TEXT_OUTPUT_CHARS)
1714
+ maxChars: integerRangeSchema(200, MAX_TOOL_TEXT_OUTPUT_CHARS, "maxChars").optional().default(DEFAULT_TOOL_TEXT_OUTPUT_CHARS)
1714
1715
  });
1715
1716
  gitDiffProviderInputSchema = z9.object({
1716
1717
  path: boundedPathSchema.optional(),
@@ -1725,7 +1726,7 @@ var init_schema5 = __esm(() => {
1725
1726
  gitShowInputSchema = z9.object({
1726
1727
  revision: z9.string().min(1).max(240),
1727
1728
  path: boundedPathSchema.optional(),
1728
- maxChars: integerRangeSchema(200, MAX_TOOL_TEXT_OUTPUT_CHARS, "maxChars").optional().default(MAX_TOOL_TEXT_OUTPUT_CHARS)
1729
+ maxChars: integerRangeSchema(200, MAX_TOOL_TEXT_OUTPUT_CHARS, "maxChars").optional().default(DEFAULT_TOOL_TEXT_OUTPUT_CHARS)
1729
1730
  });
1730
1731
  gitShowProviderInputSchema = z9.object({
1731
1732
  revision: z9.string().min(1).max(240),
@@ -1822,7 +1823,7 @@ var init_schema6 = __esm(() => {
1822
1823
 
1823
1824
  // packages/ai/src/read-file/schema.ts
1824
1825
  import { z as z11 } from "zod";
1825
- var readFileDescription = "Read file content from the working directory with optional line range limits.", readFileInputSchema, readFileProviderInputSchema, readFileOutputSchema;
1826
+ var readFileDescription = "Read file content from the working directory. Prefer a startLine/endLine range to read only the region you need instead of the whole file \u2014 large files are truncated (head+tail) and waste tokens. Raise maxChars only when you truly need more.", readFileInputSchema, readFileProviderInputSchema, readFileOutputSchema;
1826
1827
  var init_schema7 = __esm(() => {
1827
1828
  init_base_schemas();
1828
1829
  readFileInputSchema = z11.object({
@@ -2076,12 +2077,12 @@ var init_schema13 = __esm(() => {
2076
2077
 
2077
2078
  // packages/ai/src/web-fetch/schema.ts
2078
2079
  import { z as z19 } from "zod";
2079
- var webFetchDescription = "Fetch a URL and return normalized, bounded text with source metadata. Requires network permission.", webFetchInputSchema, webFetchProviderInputSchema, webFetchErrorSchema, webFetchOutputSchema;
2080
+ var webFetchDescription = "Fetch a URL and return normalized, bounded text with source metadata. Keep maxChars modest and request more only if the answer wasn't in the returned text. Requires network permission.", webFetchInputSchema, webFetchProviderInputSchema, webFetchErrorSchema, webFetchOutputSchema;
2080
2081
  var init_schema14 = __esm(() => {
2081
2082
  init_base_schemas();
2082
2083
  webFetchInputSchema = z19.object({
2083
2084
  url: z19.string().url().max(4096),
2084
- maxChars: integerRangeSchema(200, MAX_TOOL_TEXT_OUTPUT_CHARS, "maxChars").optional().default(MAX_TOOL_TEXT_OUTPUT_CHARS),
2085
+ maxChars: integerRangeSchema(200, MAX_TOOL_TEXT_OUTPUT_CHARS, "maxChars").optional().default(DEFAULT_TOOL_TEXT_OUTPUT_CHARS),
2085
2086
  timeoutMs: integerRangeSchema(1000, 30000, "timeoutMs").optional().default(1e4)
2086
2087
  });
2087
2088
  webFetchProviderInputSchema = z19.object({
@@ -2466,7 +2467,8 @@ var init_agent_tools = __esm(() => {
2466
2467
  permissionMode: permissionModeSchema.optional(),
2467
2468
  allowedTools: z21.array(codingAgentToolNameSchema).optional(),
2468
2469
  permissionRules: permissionRulesSchema.optional(),
2469
- sandbox: sandboxConfigSchema.optional()
2470
+ sandbox: sandboxConfigSchema.optional(),
2471
+ environmentContext: z21.string().optional()
2470
2472
  });
2471
2473
  });
2472
2474
 
@@ -2841,7 +2843,8 @@ function buildCodingAgentSystemPrompt({
2841
2843
  cwd,
2842
2844
  override,
2843
2845
  mode = defaultCodingAgentMode,
2844
- includeToolDiscipline = false
2846
+ includeToolDiscipline = false,
2847
+ environmentContext
2845
2848
  }) {
2846
2849
  const promptTemplate = override?.trim() ? override : defaultCodingAgentSystemPrompt;
2847
2850
  const normalizedPrompt = normalizePromptTemplate(promptTemplate, cwd);
@@ -2852,16 +2855,18 @@ function buildCodingAgentSystemPrompt({
2852
2855
  ` + `Mode purpose: ${modeDefinition.purpose}
2853
2856
  ` + `${modeDefinition.instructions}` + (includeToolDiscipline ? `
2854
2857
 
2855
- ${toolCallingDisciplineAddendum}` : "");
2858
+ ${toolCallingDisciplineAddendum}` : "") + (environmentContext?.trim() ? `
2859
+
2860
+ ${environmentContext.trim()}` : "");
2856
2861
  }
2857
2862
  var cwdPromptPlaceholder = "{cwd}", defaultCodingAgentSystemPrompt, toolCallingDisciplineAddendum;
2858
2863
  var init_coding_agent_prompt = __esm(() => {
2859
2864
  init_coding_agent_modes();
2860
- defaultCodingAgentSystemPrompt = "You are a basic coding agent. Use tools for filesystem and codebase tasks instead of guessing. " + "Respect the user's intent, explain changes clearly, and prefer incremental, auditable actions. " + "You can only interact with files under this working directory: {cwd}. " + "Use glob_search for path discovery, grep for text search, structured git tools for repository inspection, and bash only when dedicated tools are not enough. " + "Use todo_write to keep multi-step implementation work visible and current. " + "For risky or uncertain operations, inspect context first and be explicit about assumptions. " + "While working, emit brief progress notes in natural language before major tool actions and after important findings. " + `Keep them short, human, and concrete. Vary wording naturally and avoid repetitive templates or rigid labels.
2865
+ defaultCodingAgentSystemPrompt = "You are a coding agent working inside the user's project at this working directory: {cwd}. " + 'You can already see the project \u2014 proactively read it. When the user asks you to review, explain, or work on "my code"/"this", inspect the working directory yourself with list_files, glob_search, grep, and read_file; NEVER ask the user to paste or share code that lives in this directory \u2014 open it. ' + "Use tools for filesystem and codebase tasks instead of guessing, and only interact with files under the working directory. " + "Prefer dedicated tools \u2014 glob_search for paths, grep for text search, structured git tools for repository inspection \u2014 and bash only when those are not enough. " + `Use todo_write to keep multi-step work visible and current, inspect context before risky operations, and emit a brief, natural progress note before major tool actions and after important findings.
2861
2866
 
2862
- ` + "You are an agent \u2014 keep working until the user's request is fully resolved before ending your turn. " + "Never stop with a partial answer. If you say you will do something, do it in the same turn by calling a tool. " + "Use todo_write to track multi-step work and do not finish while any todo is pending or in_progress. " + `Only end your turn when the task is complete or you are blocked on input that only the user can provide.
2867
+ ` + `You are an agent \u2014 keep working until the request is fully resolved. Never stop with a partial answer; if you say you will do something, do it in the same turn by calling a tool. Do not finish while any todo is pending or in_progress \u2014 end your turn only when the task is complete or you are blocked on input that only the user can provide.
2863
2868
 
2864
- ` + "Long-running processes: never run dev servers, watchers, or other non-exiting commands as plain foreground bash \u2014 " + "the bash tool enforces a timeout and the command will be killed. Start them detached with output redirected to a " + 'log file (e.g. on Windows: start /B cmd /c "npm run dev > dev.log 2>&1"; on Unix: nohup npm run dev > dev.log 2>&1 &), ' + "wait briefly, then verify by reading the log file or with a bounded probe like curl --max-time 5. " + "Dev servers do not always get their default port \u2014 read the actual port from the startup log instead of assuming it. " + "Never kill processes or free ports you did not open yourself; other tools (including this agent's own backend) may be using them.";
2869
+ ` + "Long-running processes (dev servers, watchers): never run them as plain foreground bash \u2014 the bash tool enforces a timeout and will kill them. Start them detached with output redirected to a log " + '(Windows: start /B cmd /c "npm run dev > dev.log 2>&1"; Unix: nohup npm run dev > dev.log 2>&1 &), then read the log to confirm startup and the actual port (do not assume the default). ' + "Never kill processes or free ports you did not open yourself.";
2865
2870
  toolCallingDisciplineAddendum = "Invoke tools only through the function-calling interface. " + "Never write tool calls as XML or plain text such as <tool_call>, <function_call>, <invoke>, or bare JSON in your message. " + "Tool arguments must be valid JSON matching the tool's schema. " + "Call one tool at a time and wait for its result before deciding the next step.";
2866
2871
  });
2867
2872
 
@@ -3180,6 +3185,17 @@ function selectCodingAgentIntentTools({
3180
3185
  return [];
3181
3186
  }
3182
3187
  const selectedTools = [];
3188
+ const hasContinuationIntent = includesAny(normalizedText, [
3189
+ "continue",
3190
+ "proceed",
3191
+ "go on",
3192
+ "go ahead",
3193
+ "keep going",
3194
+ "carry on",
3195
+ "resume",
3196
+ "finish",
3197
+ "next step"
3198
+ ]);
3183
3199
  const hasCodeIntent = includesAny(normalizedText, [
3184
3200
  "file",
3185
3201
  "folder",
@@ -3226,7 +3242,44 @@ function selectCodingAgentIntentTools({
3226
3242
  "analyze",
3227
3243
  "check"
3228
3244
  ]);
3229
- const hasWriteIntent = mode === "build" && includesAny(normalizedText, [
3245
+ const workspaceReferents = [
3246
+ "this",
3247
+ "these",
3248
+ "that",
3249
+ "here",
3250
+ "code",
3251
+ "codebase",
3252
+ "repo",
3253
+ "repository",
3254
+ "project",
3255
+ "file",
3256
+ "files",
3257
+ "directory",
3258
+ "folder",
3259
+ "app",
3260
+ "application",
3261
+ "module",
3262
+ "my"
3263
+ ];
3264
+ const strongReviewIntent = includesAny(normalizedText, [
3265
+ "review",
3266
+ "audit",
3267
+ "walk through",
3268
+ "walkthrough",
3269
+ "code review"
3270
+ ]);
3271
+ const softReviewIntent = includesAny(normalizedText, [
3272
+ "explain",
3273
+ "understand",
3274
+ "summarize",
3275
+ "summarise",
3276
+ "overview",
3277
+ "describe",
3278
+ "explore",
3279
+ "look at"
3280
+ ]) && includesAny(normalizedText, workspaceReferents);
3281
+ const hasReviewIntent = strongReviewIntent || softReviewIntent;
3282
+ const hasWriteIntent = mode === "build" && (hasContinuationIntent || includesAny(normalizedText, [
3230
3283
  "write",
3231
3284
  "edit",
3232
3285
  "change",
@@ -3237,8 +3290,8 @@ function selectCodingAgentIntentTools({
3237
3290
  "delete",
3238
3291
  "remove",
3239
3292
  "rename"
3240
- ]);
3241
- const hasShellIntent = mode === "build" && includesAny(normalizedText, [
3293
+ ]));
3294
+ const hasShellIntent = mode === "build" && (hasContinuationIntent || includesAny(normalizedText, [
3242
3295
  "run",
3243
3296
  "command",
3244
3297
  "terminal",
@@ -3250,7 +3303,7 @@ function selectCodingAgentIntentTools({
3250
3303
  "run test",
3251
3304
  "run tests",
3252
3305
  "typecheck"
3253
- ]);
3306
+ ]));
3254
3307
  const hasGitIntent = includesAny(normalizedText, [
3255
3308
  "git",
3256
3309
  "diff",
@@ -3298,7 +3351,7 @@ function selectCodingAgentIntentTools({
3298
3351
  "mcp tool",
3299
3352
  "mcp server"
3300
3353
  ]);
3301
- if (hasCodeIntent || hasReadIntent && !hasGitIntent && !hasWebIntent && !explicitToolSearch) {
3354
+ if (hasCodeIntent || hasContinuationIntent || hasReviewIntent || hasReadIntent && !hasGitIntent && !hasWebIntent && !explicitToolSearch) {
3302
3355
  for (const toolName of baseReadTools) {
3303
3356
  pushUniqueTool(selectedTools, toolName);
3304
3357
  }
@@ -3552,7 +3605,8 @@ function createCodingAgent({
3552
3605
  cwd: options.cwd,
3553
3606
  override: promptOverride,
3554
3607
  mode,
3555
- includeToolDiscipline
3608
+ includeToolDiscipline,
3609
+ environmentContext: options.environmentContext
3556
3610
  })
3557
3611
  };
3558
3612
  }
@@ -3646,6 +3700,10 @@ var init_config3 = __esm(() => {
3646
3700
  preserveRecentMessages: z26.number().int().min(2).max(100).optional(),
3647
3701
  contextWindowOverride: z26.number().int().min(8000).max(1e7).optional(),
3648
3702
  summaryMaxChars: z26.number().int().min(200).max(50000).optional(),
3703
+ aggressivePruneWhenUncached: z26.boolean().optional(),
3704
+ uncachedPruneAtFraction: z26.number().min(0.2).max(0.9).optional(),
3705
+ uncachedPruneMinOutputChars: z26.number().int().min(200).max(12000).optional(),
3706
+ uncachedQuantizeUserTurns: z26.number().int().min(1).max(20).optional(),
3649
3707
  maxInputTokens: z26.number().int().min(1).max(1e7).optional()
3650
3708
  }).strict();
3651
3709
  resolvedContextOptimizerConfigSchema = z26.object({
@@ -3654,15 +3712,23 @@ var init_config3 = __esm(() => {
3654
3712
  pruneAtFraction: z26.number().min(0.2).max(0.9),
3655
3713
  preserveRecentMessages: z26.number().int().min(2).max(100),
3656
3714
  contextWindowOverride: z26.number().int().min(8000).max(1e7).nullable(),
3657
- summaryMaxChars: z26.number().int().min(200).max(50000)
3715
+ summaryMaxChars: z26.number().int().min(200).max(50000),
3716
+ aggressivePruneWhenUncached: z26.boolean(),
3717
+ uncachedPruneAtFraction: z26.number().min(0.2).max(0.9),
3718
+ uncachedPruneMinOutputChars: z26.number().int().min(200).max(12000),
3719
+ uncachedQuantizeUserTurns: z26.number().int().min(1).max(20)
3658
3720
  });
3659
3721
  defaultContextOptimizerConfig = {
3660
3722
  autoCompact: true,
3661
- compactAtFraction: 0.8,
3723
+ compactAtFraction: 0.7,
3662
3724
  pruneAtFraction: 0.6,
3663
3725
  preserveRecentMessages: 6,
3664
3726
  contextWindowOverride: null,
3665
- summaryMaxChars: 4000
3727
+ summaryMaxChars: 4000,
3728
+ aggressivePruneWhenUncached: true,
3729
+ uncachedPruneAtFraction: 0.45,
3730
+ uncachedPruneMinOutputChars: 600,
3731
+ uncachedQuantizeUserTurns: 1
3666
3732
  };
3667
3733
  });
3668
3734
 
@@ -4128,9 +4194,13 @@ function buildProviderView(options) {
4128
4194
  }
4129
4195
  let estimate = estimateContextTokens(providerMessages);
4130
4196
  let tier1 = null;
4131
- if (estimate.tokens > config.pruneAtFraction * contextWindow) {
4197
+ const uncachedActive = options.cacheActive === false && config.aggressivePruneWhenUncached;
4198
+ const pruneFraction = uncachedActive ? config.uncachedPruneAtFraction : config.pruneAtFraction;
4199
+ if (estimate.tokens > pruneFraction * contextWindow) {
4132
4200
  tier1 = pruneToolOutputs(providerMessages, {
4133
- preserveRecentMessages: config.preserveRecentMessages
4201
+ preserveRecentMessages: config.preserveRecentMessages,
4202
+ minOutputChars: uncachedActive ? config.uncachedPruneMinOutputChars : undefined,
4203
+ quantizeUserTurns: uncachedActive ? config.uncachedQuantizeUserTurns : undefined
4134
4204
  });
4135
4205
  if (tier1.savedChars > 0) {
4136
4206
  providerMessages = tier1.messages;
@@ -4314,7 +4384,11 @@ function readEnvConfig(env) {
4314
4384
  contextWindowOverride: parseNumberEnv(env.LIGHTCODE_CONTEXT_WINDOW, "LIGHTCODE_CONTEXT_WINDOW"),
4315
4385
  maxInputTokens: parseNumberEnv(env.LIGHTCODE_CONTEXT_MAX_INPUT_TOKENS, "LIGHTCODE_CONTEXT_MAX_INPUT_TOKENS"),
4316
4386
  preserveRecentMessages: parseNumberEnv(env.LIGHTCODE_CONTEXT_PRESERVE_RECENT_MESSAGES, "LIGHTCODE_CONTEXT_PRESERVE_RECENT_MESSAGES"),
4317
- summaryMaxChars: parseNumberEnv(env.LIGHTCODE_CONTEXT_SUMMARY_MAX_CHARS, "LIGHTCODE_CONTEXT_SUMMARY_MAX_CHARS")
4387
+ summaryMaxChars: parseNumberEnv(env.LIGHTCODE_CONTEXT_SUMMARY_MAX_CHARS, "LIGHTCODE_CONTEXT_SUMMARY_MAX_CHARS"),
4388
+ aggressivePruneWhenUncached: parseBooleanEnv(env.LIGHTCODE_CONTEXT_AGGRESSIVE_PRUNE_UNCACHED, "LIGHTCODE_CONTEXT_AGGRESSIVE_PRUNE_UNCACHED"),
4389
+ uncachedPruneAtFraction: parseNumberEnv(env.LIGHTCODE_CONTEXT_UNCACHED_PRUNE_AT_FRACTION, "LIGHTCODE_CONTEXT_UNCACHED_PRUNE_AT_FRACTION"),
4390
+ uncachedPruneMinOutputChars: parseNumberEnv(env.LIGHTCODE_CONTEXT_UNCACHED_PRUNE_MIN_OUTPUT_CHARS, "LIGHTCODE_CONTEXT_UNCACHED_PRUNE_MIN_OUTPUT_CHARS"),
4391
+ uncachedQuantizeUserTurns: parseNumberEnv(env.LIGHTCODE_CONTEXT_UNCACHED_QUANTIZE_USER_TURNS, "LIGHTCODE_CONTEXT_UNCACHED_QUANTIZE_USER_TURNS")
4318
4392
  };
4319
4393
  const compactContextConfig = Object.fromEntries(Object.entries(contextConfig).filter(([, value]) => value !== undefined));
4320
4394
  const headroomConfig = {
@@ -4441,8 +4515,8 @@ var init_lightcode_config = __esm(() => {
4441
4515
  defaultAutoContinueConfig = {
4442
4516
  enabled: true,
4443
4517
  maxAutoContinues: 50,
4444
- maxErrorRetries: 5,
4445
- stallTimeoutSeconds: 180
4518
+ maxErrorRetries: 8,
4519
+ stallTimeoutSeconds: 300
4446
4520
  };
4447
4521
  headroomConfigSchema = z28.object({
4448
4522
  enabled: z28.boolean().optional(),
@@ -5949,19 +6023,29 @@ var init_resolve_within_workspace = __esm(() => {
5949
6023
  });
5950
6024
 
5951
6025
  // packages/ai/src/common/output-utils.ts
5952
- function truncateText(text, maxChars = MAX_TOOL_TEXT_OUTPUT_CHARS) {
6026
+ function omissionMarker(omitted) {
6027
+ return `
6028
+
6029
+ ... [${omitted} characters omitted; re-read a line range with read_file or raise maxChars] ...
6030
+
6031
+ `;
6032
+ }
6033
+ function truncateText(text, maxChars = DEFAULT_TOOL_TEXT_OUTPUT_CHARS) {
5953
6034
  if (text.length <= maxChars) {
5954
6035
  return {
5955
6036
  text,
5956
6037
  truncated: false
5957
6038
  };
5958
6039
  }
5959
- const suffix = `
5960
-
5961
- ... output truncated to ${maxChars} characters ...`;
5962
- const allowedLength = Math.max(0, maxChars - suffix.length);
6040
+ const markerReserve = omissionMarker(text.length).length;
6041
+ const usable = Math.max(0, maxChars - markerReserve);
6042
+ const headChars = Math.floor(usable * 0.7);
6043
+ const tailChars = usable - headChars;
6044
+ const head = text.slice(0, headChars);
6045
+ const tail = tailChars > 0 ? text.slice(text.length - tailChars) : "";
6046
+ const omitted = text.length - head.length - tail.length;
5963
6047
  return {
5964
- text: `${text.slice(0, allowedLength)}${suffix}`,
6048
+ text: `${head}${omissionMarker(omitted)}${tail}`,
5965
6049
  truncated: true
5966
6050
  };
5967
6051
  }
@@ -6971,7 +7055,7 @@ async function executeReadFile(input, workspaceContext = getDefaultWorkspaceCont
6971
7055
  const { start, end } = clampLineRange(lines.length, parsedInput.startLine, parsedInput.endLine);
6972
7056
  const selectedContent = lines.slice(start - 1, end).join(`
6973
7057
  `);
6974
- const maxChars = parsedInput.maxChars ?? MAX_TOOL_TEXT_OUTPUT_CHARS;
7058
+ const maxChars = parsedInput.maxChars ?? DEFAULT_TOOL_TEXT_OUTPUT_CHARS;
6975
7059
  const truncated = truncateText(selectedContent, maxChars);
6976
7060
  return readFileOutputSchema.parse({
6977
7061
  path: relativePath,
@@ -13568,7 +13652,12 @@ function OnboardingScreen() {
13568
13652
  ...finalBaseUrl ? { baseUrl: finalBaseUrl } : {}
13569
13653
  });
13570
13654
  const restarted = await restartOwnedServer();
13571
- setServerRestarted(restarted);
13655
+ let reloaded = false;
13656
+ try {
13657
+ const response = await client.config.reload.$post();
13658
+ reloaded = response.ok;
13659
+ } catch {}
13660
+ setServerRestarted(restarted || reloaded);
13572
13661
  bumpConfigRefresh();
13573
13662
  setStep("done");
13574
13663
  } catch (error) {
@@ -13779,6 +13868,7 @@ var providerChoices;
13779
13868
  var init_onboarding_screen = __esm(() => {
13780
13869
  init_src2();
13781
13870
  init_chat_text_area();
13871
+ init_client();
13782
13872
  init_server_launcher();
13783
13873
  init_app_state();
13784
13874
  init_cli_theme();
@@ -14743,10 +14833,19 @@ var renderer = await createCliRenderer({
14743
14833
  });
14744
14834
  if (serverLaunch.ownedProcess) {
14745
14835
  const ownedProcess = serverLaunch.ownedProcess;
14746
- process.on("exit", () => {
14836
+ const killOwnedServer = () => {
14747
14837
  try {
14748
14838
  ownedProcess.kill();
14749
14839
  } catch {}
14840
+ };
14841
+ process.on("exit", killOwnedServer);
14842
+ process.on("SIGINT", () => {
14843
+ killOwnedServer();
14844
+ process.exit(0);
14845
+ });
14846
+ process.on("SIGTERM", () => {
14847
+ killOwnedServer();
14848
+ process.exit(0);
14750
14849
  });
14751
14850
  }
14752
14851
  createRoot(renderer).render(/* @__PURE__ */ jsxDEV33(App2, {}, undefined, false, undefined, this));
package/lightcode.cjs CHANGED
@@ -8,22 +8,67 @@ const path = require("path");
8
8
  const fs = require("fs");
9
9
  const os = require("os");
10
10
 
11
- // Check if Bun is available
12
- try {
13
- const result = spawnSync("bun", ["--version"], { encoding: "utf8" });
14
- if (result.status !== 0) {
15
- throw new Error("Bun not found");
11
+ // Resolve a usable Bun (>= 1.3.0). Prefer the Bun bundled as a dependency of
12
+ // this package (so a plain `npm install -g` works with no separate Bun
13
+ // install), then fall back to a Bun already on PATH.
14
+ function isUsableBun(bunCmd) {
15
+ try {
16
+ const result = spawnSync(bunCmd, ["--version"], { encoding: "utf8" });
17
+ if (result.status !== 0 || !result.stdout) {
18
+ return false;
19
+ }
20
+ const [major, minor] = result.stdout.trim().split(".").map(Number);
21
+ return major > 1 || (major === 1 && minor >= 3);
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ function resolveBun() {
28
+ // __dirname is the realpath of this launcher even when invoked via npm's
29
+ // global bin symlink (process.argv[1] would be the symlink path on macOS/Linux).
30
+ const launcherDir = __dirname;
31
+ const candidates = [];
32
+
33
+ // Bun shipped as a dependency, inside this package's own node_modules. The
34
+ // npm "bun" package names its executable bin/bun.exe on EVERY platform
35
+ // (macOS and Linux included), so read the real path from its "bin" field
36
+ // rather than guessing a filename.
37
+ try {
38
+ const bunPkgPath = require.resolve("bun/package.json", {
39
+ paths: [launcherDir],
40
+ });
41
+ const bunDir = path.dirname(bunPkgPath);
42
+ const bin = JSON.parse(fs.readFileSync(bunPkgPath, "utf8")).bin;
43
+ const rel = typeof bin === "string" ? bin : bin && bin.bun;
44
+ if (rel) {
45
+ candidates.push(path.join(bunDir, rel));
46
+ }
47
+ } catch {}
48
+
49
+ // npm's bin shim (symlink) and conventional names, as fallbacks.
50
+ const shim = process.platform === "win32" ? "bun.exe" : "bun";
51
+ candidates.push(path.join(launcherDir, "node_modules", ".bin", shim));
52
+ candidates.push(path.join(launcherDir, "node_modules", "bun", "bin", "bun.exe"));
53
+ candidates.push(path.join(launcherDir, "node_modules", "bun", "bin", "bun"));
54
+
55
+ for (const candidate of candidates) {
56
+ if (fs.existsSync(candidate) && isUsableBun(candidate)) {
57
+ return candidate;
58
+ }
16
59
  }
17
- const version = result.stdout.trim();
18
- const [major, minor] = version.split(".").map(Number);
19
- // Accept Bun >= 1.3.0
20
- if (major < 1 || (major === 1 && minor < 3)) {
21
- console.error("Lightcode requires Bun >= 1.3.0. Found: " + version);
22
- console.error("Install the latest Bun from https://bun.sh");
23
- process.exit(1);
60
+
61
+ // Fall back to a system Bun on PATH.
62
+ if (isUsableBun("bun")) {
63
+ return "bun";
24
64
  }
25
- } catch {
26
- console.error("Lightcode requires Bun >= 1.3.0, which is not installed.");
65
+
66
+ return null;
67
+ }
68
+
69
+ const bunPath = resolveBun();
70
+ if (!bunPath) {
71
+ console.error("Lightcode requires Bun >= 1.3.0, which could not be found.");
27
72
  console.error("Install Bun from https://bun.sh or:");
28
73
  console.error(" curl -fsSL https://bun.sh/install | bash");
29
74
  console.error(" # on Windows (PowerShell):");
@@ -84,9 +129,9 @@ if (process.platform === "win32" && !process.env.NODE_EXTRA_CA_CERTS) {
84
129
  }
85
130
  }
86
131
 
87
- // Get the directory of this launcher and find cli.js next to it
88
- const launcherDir = path.dirname(process.argv[1]);
89
- const cliPath = path.join(launcherDir, "cli.js");
132
+ // Get the directory of this launcher and find cli.js next to it. Use __dirname
133
+ // (realpath-resolved) so this works when run via npm's global bin symlink.
134
+ const cliPath = path.join(__dirname, "cli.js");
90
135
 
91
136
  // Ensure cli.js exists
92
137
  if (!fs.existsSync(cliPath)) {
@@ -97,7 +142,7 @@ if (!fs.existsSync(cliPath)) {
97
142
  // Exec Bun with the cli.js script using spawnSync for proper argument handling
98
143
  try {
99
144
  const args = process.argv.slice(2);
100
- const result = spawnSync("bun", [cliPath, ...args], {
145
+ const result = spawnSync(bunPath, [cliPath, ...args], {
101
146
  stdio: "inherit",
102
147
  env: process.env,
103
148
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kmugalkhod/lightcode",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Lightcode — a personal AI coding agent with a terminal UI and local session storage.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,6 +18,7 @@
18
18
  "LICENSE"
19
19
  ],
20
20
  "dependencies": {
21
+ "bun": "1.3.13",
21
22
  "@ai-sdk/anthropic": "3.0.77",
22
23
  "@ai-sdk/openai-compatible": "2.0.48",
23
24
  "@ai-sdk/react": "3.0.184",