cliskill 1.1.0 → 1.1.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.
@@ -28,8 +28,8 @@ var providerSchema = z.object({
28
28
  apiKey: z.string().optional(),
29
29
  /** Default model to use */
30
30
  model: z.string().min(1),
31
- /** API format: "openai-compatible" | "messages-compatible" | "glm-compatible" | "custom" */
32
- format: z.enum(["openai-compatible", "messages-compatible", "glm-compatible", "custom"]),
31
+ /** API format: "openai-compatible" | "messages-compatible" | "glm-compatible" | "anthropic-compatible" | "custom" */
32
+ format: z.enum(["openai-compatible", "messages-compatible", "glm-compatible", "anthropic-compatible", "custom"]),
33
33
  /** Custom headers to send with each request */
34
34
  headers: z.record(z.string()).optional(),
35
35
  /** Request timeout in milliseconds */
@@ -269,6 +269,36 @@ function applyEnvironmentVariables(config) {
269
269
  }];
270
270
  }
271
271
  }
272
+ const anthropicApiKey = process.env.ANTHROPIC_API_KEY || process.env.CLISKILL_ANTHROPIC_API_KEY;
273
+ const anthropicBaseUrl = process.env.ANTHROPIC_BASE_URL || process.env.CLISKILL_ANTHROPIC_BASE_URL;
274
+ const anthropicModel = process.env.CLISKILL_ANTHROPIC_MODEL;
275
+ if (anthropicApiKey || anthropicBaseUrl) {
276
+ const existingAnthropic = result.providers.find((p) => p.format === "anthropic-compatible");
277
+ if (existingAnthropic) {
278
+ const updated = { ...existingAnthropic };
279
+ if (anthropicApiKey) updated.apiKey = anthropicApiKey;
280
+ if (anthropicBaseUrl) updated.baseUrl = anthropicBaseUrl;
281
+ if (anthropicModel) updated.model = anthropicModel;
282
+ result.providers = [
283
+ updated,
284
+ ...result.providers.filter((p) => p.format !== "anthropic-compatible")
285
+ ];
286
+ } else {
287
+ const anthropicProvider = {
288
+ name: "anthropic",
289
+ baseUrl: anthropicBaseUrl ?? "https://api.anthropic.com",
290
+ model: anthropicModel ?? "claude-sonnet-4-20250514",
291
+ format: "anthropic-compatible",
292
+ apiKey: anthropicApiKey,
293
+ timeout: 18e4,
294
+ maxTokens: 65536
295
+ };
296
+ result.providers = [...result.providers, anthropicProvider];
297
+ if (!result.defaultProvider) {
298
+ result.defaultProvider = "anthropic";
299
+ }
300
+ }
301
+ }
272
302
  const envAutoMode = process.env.CLISKILL_AUTO_MODE;
273
303
  if (envAutoMode) {
274
304
  result.autoMode = {
@@ -457,6 +487,81 @@ var BaseAdapter = class {
457
487
 
458
488
  // src/connect/generic-adapter.ts
459
489
  import { randomUUID } from "crypto";
490
+
491
+ // src/connect/network-errors.ts
492
+ var NETWORK_ERROR_PATTERNS = [
493
+ "ECONNRESET",
494
+ "ECONNABORTED",
495
+ "ETIMEDOUT",
496
+ "EHOSTUNREACH",
497
+ "ENETUNREACH",
498
+ "WSAETIMEDOUT",
499
+ "WSAECONNRESET",
500
+ "WSAECONNABORTED",
501
+ "fetch failed",
502
+ "network"
503
+ ];
504
+ var ABORT_PATTERNS = [
505
+ "AbortError",
506
+ "abort",
507
+ "Abort",
508
+ "timeout",
509
+ "Timeout"
510
+ ];
511
+ function isAbortOrNetworkError(err) {
512
+ const name = err.name ?? "";
513
+ const msg = err.message ?? "";
514
+ const code = err.code ?? "";
515
+ if (name === "AbortError") return true;
516
+ const combined = `${name} ${msg} ${code}`;
517
+ for (const pattern of ABORT_PATTERNS) {
518
+ if (combined.includes(pattern)) return true;
519
+ }
520
+ for (const pattern of NETWORK_ERROR_PATTERNS) {
521
+ if (combined.includes(pattern)) return true;
522
+ }
523
+ return false;
524
+ }
525
+
526
+ // src/connect/sse-parser.ts
527
+ async function* readStreamLines(body) {
528
+ const reader = body.getReader();
529
+ const decoder = new TextDecoder();
530
+ let buffer = "";
531
+ let scanFrom = 0;
532
+ try {
533
+ while (true) {
534
+ let done;
535
+ let value;
536
+ try {
537
+ const result = await reader.read();
538
+ done = result.done;
539
+ value = result.value;
540
+ } catch (err) {
541
+ if (isAbortOrNetworkError(err)) break;
542
+ throw err;
543
+ }
544
+ if (done) break;
545
+ buffer += decoder.decode(value, { stream: true });
546
+ let nlIdx;
547
+ while ((nlIdx = buffer.indexOf("\n", scanFrom)) !== -1) {
548
+ yield buffer.substring(scanFrom, nlIdx);
549
+ scanFrom = nlIdx + 1;
550
+ }
551
+ if (scanFrom > 0) {
552
+ buffer = buffer.substring(scanFrom);
553
+ scanFrom = 0;
554
+ }
555
+ }
556
+ if (buffer.length > 0) {
557
+ yield buffer;
558
+ }
559
+ } finally {
560
+ reader.releaseLock();
561
+ }
562
+ }
563
+
564
+ // src/connect/generic-adapter.ts
460
565
  var GenericCompatAdapter = class extends BaseAdapter {
461
566
  constructor(config, name) {
462
567
  super();
@@ -593,38 +698,15 @@ var GenericCompatAdapter = class extends BaseAdapter {
593
698
  };
594
699
  }
595
700
  async *parseSSEStream(body) {
596
- const reader = body.getReader();
597
- const decoder = new TextDecoder();
598
- let buffer = "";
599
- try {
600
- while (true) {
601
- let done;
602
- let value;
603
- try {
604
- const result = await reader.read();
605
- done = result.done;
606
- value = result.value;
607
- } catch (err) {
608
- if (isAbortOrNetworkError(err)) break;
609
- throw err;
610
- }
611
- if (done) break;
612
- buffer += decoder.decode(value, { stream: true });
613
- const lines = buffer.split("\n");
614
- buffer = lines.pop() ?? "";
615
- for (const line of lines) {
616
- const trimmed = line.trim();
617
- if (!trimmed || trimmed === "data: [DONE]") continue;
618
- if (!trimmed.startsWith("data: ")) continue;
619
- try {
620
- const json = JSON.parse(trimmed.slice(6));
621
- yield* this.processChunk(json);
622
- } catch {
623
- }
624
- }
701
+ for await (const line of readStreamLines(body)) {
702
+ const trimmed = line.trim();
703
+ if (!trimmed || trimmed === "data: [DONE]") continue;
704
+ if (!trimmed.startsWith("data: ")) continue;
705
+ try {
706
+ const json = JSON.parse(trimmed.slice(6));
707
+ yield* this.processChunk(json);
708
+ } catch {
625
709
  }
626
- } finally {
627
- reader.releaseLock();
628
710
  }
629
711
  }
630
712
  /** Track active tool calls: index → { id, name, args } for emitting tool_use_end */
@@ -672,11 +754,252 @@ var GenericCompatAdapter = class extends BaseAdapter {
672
754
  }
673
755
  }
674
756
  };
675
- function isAbortOrNetworkError(err) {
676
- const name = err.name ?? "";
677
- const msg = err.message ?? "";
678
- return name === "AbortError" || msg.includes("abort") || msg.includes("Abort") || msg.includes("timeout") || msg.includes("Timeout") || msg.includes("fetch failed") || msg.includes("ECONNRESET") || msg.includes("network");
679
- }
757
+
758
+ // src/connect/anthropic-adapter.ts
759
+ var ANTHROPIC_VERSION = "2023-06-01";
760
+ var MESSAGES_PATH = "/v1/messages";
761
+ var AnthropicAdapter = class extends BaseAdapter {
762
+ constructor(config, name) {
763
+ super();
764
+ this.config = config;
765
+ this.name = name ?? `anthropic:${config.model}`;
766
+ }
767
+ config;
768
+ name;
769
+ async complete(request) {
770
+ const url = this.buildUrl(MESSAGES_PATH);
771
+ const body = this.buildRequestBody(request, false);
772
+ const response = await this.fetchWithRetry(url, () => ({
773
+ method: "POST",
774
+ headers: this.buildHeaders(),
775
+ body: JSON.stringify(body),
776
+ signal: this.buildSignal(request.signal)
777
+ }));
778
+ if (!response.ok) {
779
+ const text = await response.text().catch(() => "unknown error");
780
+ throw new Error(`Anthropic error (${response.status}): ${text}`);
781
+ }
782
+ const data = await response.json();
783
+ return this.mapResponse(data);
784
+ }
785
+ async *stream(request) {
786
+ const url = this.buildUrl(MESSAGES_PATH);
787
+ const body = this.buildRequestBody(request, true);
788
+ const response = await this.fetchWithRetry(url, () => ({
789
+ method: "POST",
790
+ headers: this.buildHeaders(),
791
+ body: JSON.stringify(body),
792
+ signal: this.buildStreamingSignal(request.signal)
793
+ }));
794
+ if (!response.ok) {
795
+ const text = await response.text().catch(() => "unknown error");
796
+ throw new Error(`Anthropic error (${response.status}): ${text}`);
797
+ }
798
+ if (!response.body) throw new Error("No response body for streaming");
799
+ yield* this.parseSSEStream(response.body);
800
+ }
801
+ async validate() {
802
+ try {
803
+ const url = this.buildUrl("/v1/models");
804
+ const response = await fetch(url, {
805
+ headers: this.buildHeaders(),
806
+ signal: AbortSignal.timeout(1e4)
807
+ });
808
+ if (!response.ok) return { ok: false, error: `HTTP ${response.status}` };
809
+ return { ok: true };
810
+ } catch (err) {
811
+ return { ok: false, error: err.message };
812
+ }
813
+ }
814
+ dispose() {
815
+ }
816
+ buildHeaders() {
817
+ const headers = {
818
+ "Content-Type": "application/json",
819
+ "anthropic-version": ANTHROPIC_VERSION,
820
+ ...this.config.headers
821
+ };
822
+ if (this.config.apiKey) {
823
+ headers["x-api-key"] = this.config.apiKey;
824
+ }
825
+ return headers;
826
+ }
827
+ buildRequestBody(request, stream) {
828
+ const messages = [];
829
+ for (const msg of request.messages) {
830
+ const mapped = this.mapMessage(msg);
831
+ if (mapped) messages.push(...mapped);
832
+ }
833
+ const body = {
834
+ model: this.config.model,
835
+ messages,
836
+ max_tokens: request.maxTokens ?? this.config.maxTokens,
837
+ temperature: request.temperature,
838
+ stream
839
+ };
840
+ if (request.systemPrompt) {
841
+ body.system = request.systemPrompt;
842
+ }
843
+ if (request.stopSequences && request.stopSequences.length > 0) {
844
+ body.stop_sequences = request.stopSequences;
845
+ }
846
+ if (request.tools && request.tools.length > 0) {
847
+ body.tools = request.tools.map(this.mapTool);
848
+ }
849
+ return body;
850
+ }
851
+ mapMessage(msg) {
852
+ const textParts = msg.content.filter((b) => b.type === "text");
853
+ const toolUseParts = msg.content.filter((b) => b.type === "tool_use");
854
+ const toolResultParts = msg.content.filter((b) => b.type === "tool_result");
855
+ if (msg.role === "system") {
856
+ return null;
857
+ }
858
+ if (msg.role === "assistant") {
859
+ const blocks = [];
860
+ const textContent2 = textParts.map((t) => t.text).join("");
861
+ if (textContent2) {
862
+ blocks.push({ type: "text", text: textContent2 });
863
+ }
864
+ for (const tc of toolUseParts) {
865
+ blocks.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.input });
866
+ }
867
+ return [{ role: "assistant", content: blocks.length > 0 ? blocks : "" }];
868
+ }
869
+ if (toolResultParts.length > 0) {
870
+ const blocks = [];
871
+ for (const result of toolResultParts) {
872
+ const sanitizedContent = result.content.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "") || "(empty output)";
873
+ blocks.push({ type: "tool_result", tool_use_id: result.toolUseId, content: sanitizedContent });
874
+ }
875
+ const textContent2 = textParts.map((t) => t.text).join("");
876
+ if (textContent2) {
877
+ blocks.unshift({ type: "text", text: textContent2 });
878
+ }
879
+ return [{ role: "user", content: blocks }];
880
+ }
881
+ const textContent = textParts.map((t) => t.text).join("");
882
+ return [{ role: msg.role, content: textContent || "" }];
883
+ }
884
+ mapTool(tool) {
885
+ return {
886
+ name: tool.name,
887
+ description: tool.description,
888
+ input_schema: tool.inputSchema
889
+ };
890
+ }
891
+ mapResponse(data) {
892
+ const contentBlocks = [];
893
+ for (const block of data.content) {
894
+ if (block.type === "text") {
895
+ contentBlocks.push({ type: "text", text: block.text });
896
+ } else if (block.type === "tool_use") {
897
+ contentBlocks.push({
898
+ type: "tool_use",
899
+ id: block.id,
900
+ name: block.name,
901
+ input: block.input
902
+ });
903
+ }
904
+ }
905
+ return {
906
+ message: { role: "assistant", content: contentBlocks },
907
+ stopReason: data.stop_reason ?? "end_turn",
908
+ usage: {
909
+ inputTokens: data.usage?.input_tokens ?? 0,
910
+ outputTokens: data.usage?.output_tokens ?? 0,
911
+ cacheReadTokens: data.usage?.cache_read_input_tokens,
912
+ cacheWriteTokens: data.usage?.cache_creation_input_tokens
913
+ },
914
+ model: data.model
915
+ };
916
+ }
917
+ async *parseSSEStream(body) {
918
+ let currentModel = "";
919
+ let currentToolId = "";
920
+ let currentToolName = "";
921
+ let toolInputBuffer = "";
922
+ for await (const line of readStreamLines(body)) {
923
+ const trimmed = line.trim();
924
+ if (!trimmed || trimmed.startsWith("event:")) continue;
925
+ if (!trimmed.startsWith("data:")) continue;
926
+ const jsonStr = trimmed.slice(6).trim();
927
+ if (!jsonStr) continue;
928
+ let event;
929
+ try {
930
+ event = JSON.parse(jsonStr);
931
+ } catch {
932
+ continue;
933
+ }
934
+ switch (event.type) {
935
+ case "message_start": {
936
+ if (event.message) {
937
+ currentModel = event.message.model ?? "";
938
+ yield { type: "message_start", model: currentModel };
939
+ }
940
+ break;
941
+ }
942
+ case "content_block_start": {
943
+ const block = event.content_block;
944
+ if (block?.type === "tool_use") {
945
+ currentToolId = block.id ?? "";
946
+ currentToolName = block.name ?? "";
947
+ toolInputBuffer = "";
948
+ yield { type: "tool_use_start", id: currentToolId, name: currentToolName };
949
+ }
950
+ break;
951
+ }
952
+ case "content_block_delta": {
953
+ const delta = event.delta;
954
+ if (!delta) break;
955
+ if (delta.type === "text_delta" && delta.text) {
956
+ yield { type: "text_delta", text: delta.text };
957
+ } else if (delta.type === "input_json_delta" && delta.partial_json) {
958
+ toolInputBuffer += delta.partial_json;
959
+ yield { type: "tool_use_delta", id: currentToolId, inputDelta: delta.partial_json };
960
+ }
961
+ break;
962
+ }
963
+ case "content_block_stop": {
964
+ if (currentToolId && currentToolName) {
965
+ let input = {};
966
+ try {
967
+ input = JSON.parse(toolInputBuffer || "{}");
968
+ } catch {
969
+ }
970
+ yield { type: "tool_use_end", id: currentToolId, name: currentToolName, input };
971
+ currentToolId = "";
972
+ currentToolName = "";
973
+ toolInputBuffer = "";
974
+ }
975
+ break;
976
+ }
977
+ case "message_delta": {
978
+ const delta = event.delta;
979
+ if (delta?.stop_reason) {
980
+ const usage = event.usage ?? { output_tokens: 0 };
981
+ yield {
982
+ type: "message_end",
983
+ stopReason: delta.stop_reason,
984
+ usage: { inputTokens: 0, outputTokens: usage.output_tokens }
985
+ };
986
+ }
987
+ break;
988
+ }
989
+ case "message_stop": {
990
+ break;
991
+ }
992
+ case "ping": {
993
+ break;
994
+ }
995
+ case "error": {
996
+ const errorData = event;
997
+ throw new Error(`Anthropic stream error: ${errorData.error?.message ?? "unknown"}`);
998
+ }
999
+ }
1000
+ }
1001
+ }
1002
+ };
680
1003
 
681
1004
  // src/connect/glm-adapter.ts
682
1005
  import { randomUUID as randomUUID2 } from "crypto";
@@ -831,38 +1154,15 @@ var GLMAdapter = class extends BaseAdapter {
831
1154
  };
832
1155
  }
833
1156
  async *parseSSEStream(body) {
834
- const reader = body.getReader();
835
- const decoder = new TextDecoder();
836
- let buffer = "";
837
- try {
838
- while (true) {
839
- let done;
840
- let value;
841
- try {
842
- const result = await reader.read();
843
- done = result.done;
844
- value = result.value;
845
- } catch (err) {
846
- if (isAbortOrNetworkError2(err)) break;
847
- throw err;
848
- }
849
- if (done) break;
850
- buffer += decoder.decode(value, { stream: true });
851
- const lines = buffer.split("\n");
852
- buffer = lines.pop() ?? "";
853
- for (const line of lines) {
854
- const trimmed = line.trim();
855
- if (!trimmed || trimmed === "data: [DONE]") continue;
856
- if (!trimmed.startsWith("data: ")) continue;
857
- try {
858
- const json = JSON.parse(trimmed.slice(6));
859
- yield* this.processChunk(json);
860
- } catch {
861
- }
862
- }
1157
+ for await (const line of readStreamLines(body)) {
1158
+ const trimmed = line.trim();
1159
+ if (!trimmed || trimmed === "data: [DONE]") continue;
1160
+ if (!trimmed.startsWith("data: ")) continue;
1161
+ try {
1162
+ const json = JSON.parse(trimmed.slice(6));
1163
+ yield* this.processChunk(json);
1164
+ } catch {
863
1165
  }
864
- } finally {
865
- reader.releaseLock();
866
1166
  }
867
1167
  }
868
1168
  /** Track active tool calls: index → { id, name, args } for emitting tool_use_end */
@@ -917,11 +1217,6 @@ var GLMAdapter = class extends BaseAdapter {
917
1217
  }
918
1218
  }
919
1219
  };
920
- function isAbortOrNetworkError2(err) {
921
- const name = err.name ?? "";
922
- const msg = err.message ?? "";
923
- return name === "AbortError" || msg.includes("abort") || msg.includes("Abort") || msg.includes("timeout") || msg.includes("Timeout") || msg.includes("fetch failed") || msg.includes("ECONNRESET") || msg.includes("network");
924
- }
925
1220
 
926
1221
  // src/connect/registry.ts
927
1222
  var AdapterRegistry = class {
@@ -955,6 +1250,8 @@ var AdapterRegistry = class {
955
1250
  return new GenericCompatAdapter(config);
956
1251
  case "glm-compatible":
957
1252
  return new GLMAdapter(config);
1253
+ case "anthropic-compatible":
1254
+ return new AnthropicAdapter(config);
958
1255
  case "custom":
959
1256
  throw new Error(`Custom provider format requires a custom adapter for "${config.name}".`);
960
1257
  default:
@@ -5540,6 +5837,32 @@ var EnhancedMemoryStore = class {
5540
5837
  }
5541
5838
  return result;
5542
5839
  }
5840
+ /**
5841
+ * Get a formatted summary of all memories for injection into system prompt.
5842
+ * Returns empty string if no memories stored.
5843
+ */
5844
+ getSummary() {
5845
+ if (this.entries.size === 0) return "";
5846
+ const sections = [];
5847
+ for (const [topic, entries] of this.entries) {
5848
+ const latest = entries[entries.length - 1];
5849
+ if (!latest) continue;
5850
+ const age = Date.now() - latest.timestamp;
5851
+ const ageDays = Math.floor(age / (1e3 * 60 * 60 * 24));
5852
+ const ageStr = ageDays === 0 ? "today" : ageDays === 1 ? "yesterday" : `${ageDays}d ago`;
5853
+ sections.push(`- **${topic}**: ${latest.content} (${ageStr})`);
5854
+ }
5855
+ if (sections.length === 0) return "";
5856
+ return [
5857
+ "<global-memory>",
5858
+ "The following memories were saved in previous sessions and persist across conversations:",
5859
+ "",
5860
+ ...sections,
5861
+ "",
5862
+ "Use the memory tool to save new facts or search/recall existing memories.",
5863
+ "</global-memory>"
5864
+ ].join("\n");
5865
+ }
5543
5866
  async persist(topic) {
5544
5867
  const entries = this.entries.get(topic);
5545
5868
  if (!entries || entries.length === 0) {
@@ -5555,6 +5878,25 @@ var EnhancedMemoryStore = class {
5555
5878
  }
5556
5879
  };
5557
5880
 
5881
+ // src/memory/global-memory.ts
5882
+ var cachedSummary = null;
5883
+ async function loadGlobalMemorySummary() {
5884
+ if (cachedSummary !== null) return cachedSummary;
5885
+ try {
5886
+ const dir = getGlobalMemoryDir();
5887
+ const store = new EnhancedMemoryStore(dir);
5888
+ await store.init();
5889
+ cachedSummary = store.getSummary();
5890
+ return cachedSummary;
5891
+ } catch {
5892
+ cachedSummary = "";
5893
+ return "";
5894
+ }
5895
+ }
5896
+ function invalidateGlobalMemoryCache() {
5897
+ cachedSummary = null;
5898
+ }
5899
+
5558
5900
  // src/tools/builtins/memory-tool.ts
5559
5901
  var memoryInputSchema = z19.object({
5560
5902
  action: z19.enum(["save", "recall", "search", "list", "delete"]).describe(
@@ -5590,6 +5932,7 @@ var MemoryTool = class extends BaseTool {
5590
5932
  return "Error: topic and content are required for save action";
5591
5933
  }
5592
5934
  await store.addMemory(input.topic, input.content, input.tags ?? [], "agent");
5935
+ invalidateGlobalMemoryCache();
5593
5936
  return `\u2705 Saved to "${input.topic}": ${input.content.slice(0, 100)}${input.content.length > 100 ? "..." : ""}`;
5594
5937
  }
5595
5938
  case "recall": {
@@ -5637,6 +5980,7 @@ var MemoryTool = class extends BaseTool {
5637
5980
  entry.entry.lastAccessed = 0;
5638
5981
  }
5639
5982
  await store.prune();
5983
+ invalidateGlobalMemoryCache();
5640
5984
  return `\u{1F5D1}\uFE0F Deleted ${topicEntries.length} entries from "${input.topic}"`;
5641
5985
  }
5642
5986
  default:
@@ -6401,25 +6745,166 @@ ${m.content}`;
6401
6745
  ].join("\n");
6402
6746
  }
6403
6747
 
6404
- // src/core/loop.ts
6405
- var MAX_TURNS = 50;
6406
- var MAX_CONSECUTIVE_TOOL_ERRORS = 3;
6407
- var MAX_TOOL_OUTPUT_CHARS = 5e4;
6408
- var MIN_OUTPUT_TOKENS = 16384;
6409
- var MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3;
6410
- var ESCALATED_MAX_TOKENS = 65536;
6411
- function sanitizeToolOutput(output, maxChars = MAX_TOOL_OUTPUT_CHARS) {
6412
- const sanitized = output.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
6413
- if (sanitized.length <= maxChars) return sanitized;
6414
- const truncated = sanitized.slice(0, maxChars);
6415
- const omitted = sanitized.length - maxChars;
6416
- return `${truncated}
6417
-
6418
- ... [truncated ${omitted} characters]`;
6748
+ // src/services/session-recovery.ts
6749
+ import { readFile as readFile9, readdir as readdir5 } from "fs/promises";
6750
+ import { join as join10 } from "path";
6751
+ import { existsSync as existsSync5 } from "fs";
6752
+ async function recoverSession(filePath) {
6753
+ const raw = await readFile9(filePath, "utf-8");
6754
+ const lines = raw.split("\n").filter((line) => line.trim().length > 0);
6755
+ const entries = [];
6756
+ for (const line of lines) {
6757
+ try {
6758
+ const entry = JSON.parse(line);
6759
+ if (entry.type && entry.content !== void 0) {
6760
+ entries.push(entry);
6761
+ }
6762
+ } catch {
6763
+ }
6764
+ }
6765
+ const messages = reconstructMessages(entries);
6766
+ const firstEntry = entries[0];
6767
+ const timestamp = firstEntry?.timestamp ?? 0;
6768
+ return {
6769
+ sessionId: filePath,
6770
+ filePath,
6771
+ timestamp,
6772
+ messages,
6773
+ entryCount: entries.length
6774
+ };
6419
6775
  }
6420
- async function* runAgentLoop(userMessage, deps) {
6421
- const { adapter, toolRegistry, config, systemPrompt, cwd: cwd2, abortSignal, onPermissionRequest } = deps;
6422
- const autoMode = deps.autoModeManager ?? new AutoModeManager(config.autoMode);
6776
+ async function listSessions(limit = 20) {
6777
+ const dir = getSessionsDir();
6778
+ if (!existsSync5(dir)) return [];
6779
+ const files = await readdir5(dir);
6780
+ const sessionFiles = files.filter((f) => f.startsWith("session-") && f.endsWith(".jsonl"));
6781
+ const sessions = [];
6782
+ for (const fileName of sessionFiles) {
6783
+ const filePath = join10(dir, fileName);
6784
+ try {
6785
+ const raw = await readFile9(filePath, "utf-8");
6786
+ const lines = raw.split("\n").filter((line) => line.trim().length > 0);
6787
+ let lastActivity = 0;
6788
+ let entryCount = 0;
6789
+ for (const line of lines) {
6790
+ try {
6791
+ const entry = JSON.parse(line);
6792
+ if (entry.timestamp && entry.timestamp > lastActivity) {
6793
+ lastActivity = entry.timestamp;
6794
+ }
6795
+ entryCount++;
6796
+ } catch {
6797
+ }
6798
+ }
6799
+ const dateStr = fileName.replace("session-", "").replace(".jsonl", "");
6800
+ const fileTimestamp = Date.parse(dateStr) || lastActivity;
6801
+ sessions.push({
6802
+ fileName,
6803
+ filePath,
6804
+ timestamp: fileTimestamp,
6805
+ entryCount,
6806
+ lastActivity
6807
+ });
6808
+ } catch {
6809
+ }
6810
+ }
6811
+ sessions.sort((a, b) => b.lastActivity - a.lastActivity);
6812
+ return sessions.slice(0, limit);
6813
+ }
6814
+ async function findLatestSession() {
6815
+ const sessions = await listSessions(1);
6816
+ return sessions[0] ?? null;
6817
+ }
6818
+ async function getLastSessionSummary() {
6819
+ try {
6820
+ const latest = await findLatestSession();
6821
+ if (!latest) return "";
6822
+ const raw = await readFile9(latest.filePath, "utf-8");
6823
+ const lines = raw.split("\n").filter((line) => line.trim().length > 0);
6824
+ const userMessages = [];
6825
+ const assistantTopics = [];
6826
+ for (const line of lines) {
6827
+ try {
6828
+ const entry = JSON.parse(line);
6829
+ if (entry.type === "user" && entry.content.trim()) {
6830
+ userMessages.push(entry.content.trim());
6831
+ } else if (entry.type === "assistant" && entry.content.trim()) {
6832
+ const preview = entry.content.trim().slice(0, 120);
6833
+ assistantTopics.push(preview);
6834
+ }
6835
+ } catch {
6836
+ }
6837
+ }
6838
+ if (userMessages.length === 0) return "";
6839
+ const date = new Date(latest.lastActivity).toLocaleString();
6840
+ const lastUserMsg = userMessages[userMessages.length - 1];
6841
+ const lastAssistant = assistantTopics.length > 0 ? assistantTopics[assistantTopics.length - 1] : "N/A";
6842
+ return [
6843
+ "<last-session-context>",
6844
+ `Previous session (${date}, ${lines.length} entries):`,
6845
+ ` Last user message: ${lastUserMsg.slice(0, 200)}`,
6846
+ ` Last assistant response: ${lastAssistant.slice(0, 200)}`,
6847
+ ` Total user messages in session: ${userMessages.length}`,
6848
+ "",
6849
+ "Use the memory tool to recall specific details from past interactions.",
6850
+ "</last-session-context>"
6851
+ ].join("\n");
6852
+ } catch {
6853
+ return "";
6854
+ }
6855
+ }
6856
+ function reconstructMessages(entries) {
6857
+ const messages = [];
6858
+ let i = 0;
6859
+ while (i < entries.length) {
6860
+ const entry = entries[i];
6861
+ if (entry.type === "user") {
6862
+ const content = [{ type: "text", text: entry.content }];
6863
+ messages.push({ role: "user", content });
6864
+ i++;
6865
+ } else if (entry.type === "assistant") {
6866
+ const content = [{ type: "text", text: entry.content }];
6867
+ messages.push({ role: "assistant", content });
6868
+ i++;
6869
+ } else if (entry.type === "tool_use") {
6870
+ const lastMsg = messages[messages.length - 1];
6871
+ if (lastMsg && lastMsg.role === "assistant") {
6872
+ }
6873
+ i++;
6874
+ } else if (entry.type === "tool_result") {
6875
+ const content = [{
6876
+ type: "tool_result",
6877
+ toolUseId: `recovered-${i}`,
6878
+ content: entry.content
6879
+ }];
6880
+ messages.push({ role: "user", content });
6881
+ i++;
6882
+ } else {
6883
+ i++;
6884
+ }
6885
+ }
6886
+ return messages;
6887
+ }
6888
+
6889
+ // src/core/loop.ts
6890
+ var MAX_TURNS = 50;
6891
+ var MAX_CONSECUTIVE_TOOL_ERRORS = 3;
6892
+ var MAX_TOOL_OUTPUT_CHARS = 5e4;
6893
+ var MIN_OUTPUT_TOKENS = 16384;
6894
+ var MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3;
6895
+ var ESCALATED_MAX_TOKENS = 65536;
6896
+ function sanitizeToolOutput(output, maxChars = MAX_TOOL_OUTPUT_CHARS) {
6897
+ const sanitized = output.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
6898
+ if (sanitized.length <= maxChars) return sanitized;
6899
+ const truncated = sanitized.slice(0, maxChars);
6900
+ const omitted = sanitized.length - maxChars;
6901
+ return `${truncated}
6902
+
6903
+ ... [truncated ${omitted} characters]`;
6904
+ }
6905
+ async function* runAgentLoop(userMessage, deps) {
6906
+ const { adapter, toolRegistry, config, systemPrompt, cwd: cwd2, abortSignal, onPermissionRequest } = deps;
6907
+ const autoMode = deps.autoModeManager ?? new AutoModeManager(config.autoMode);
6423
6908
  const fastMode = deps.fastModeManager ?? new FastModeManager(config.fastMode);
6424
6909
  const contextWindow = deps.contextWindowManager ?? new ContextWindowManager(config.contextWindow);
6425
6910
  const compactor = new ContextCompactor({
@@ -6463,7 +6948,9 @@ ${result.summary}` }]
6463
6948
  });
6464
6949
  const toolDefs = toolRegistry.getToolDefinitions();
6465
6950
  const projectMemory = await loadProjectMemoryPrompt(cwd2);
6466
- const effectiveSystemPrompt = systemPrompt + fastMode.getSystemPromptModifier() + (projectMemory ? "\n\n" + projectMemory : "");
6951
+ const globalMemory = await loadGlobalMemorySummary();
6952
+ const lastSessionSummary = await getLastSessionSummary();
6953
+ const effectiveSystemPrompt = systemPrompt + fastMode.getSystemPromptModifier() + (projectMemory ? "\n\n" + projectMemory : "") + (globalMemory ? "\n\n" + globalMemory : "") + (lastSessionSummary ? "\n\n" + lastSessionSummary : "");
6467
6954
  const toolContext = {
6468
6955
  cwd: cwd2,
6469
6956
  abortSignal,
@@ -6949,7 +7436,7 @@ function createMissingToolResults(toolCalls, errorMessage) {
6949
7436
  import { useState, useEffect, useCallback, useRef, useMemo, memo } from "react";
6950
7437
  import { Box, Text, render, useInput, useApp, useStdout } from "ink";
6951
7438
  import { readdirSync } from "fs";
6952
- import { join as join10, basename as basename2, dirname as dirname3 } from "path";
7439
+ import { join as join11, basename as basename2, dirname as dirname3 } from "path";
6953
7440
  import { exec as exec2, execSync as execSync2 } from "child_process";
6954
7441
  import { jsx, jsxs } from "react/jsx-runtime";
6955
7442
  var C = {
@@ -7543,10 +8030,10 @@ function InkApp({ model, toolCount, onSubmit, onCancel }) {
7543
8030
  const selected = currentFiles[selectedIdx];
7544
8031
  if (selected) {
7545
8032
  if (selected.isDir) {
7546
- setFileCwd(join10(fileCwd, selected.name));
8033
+ setFileCwd(join11(fileCwd, selected.name));
7547
8034
  setSelectedIdx(0);
7548
8035
  } else {
7549
- openInEditor(join10(fileCwd, selected.name));
8036
+ openInEditor(join11(fileCwd, selected.name));
7550
8037
  }
7551
8038
  }
7552
8039
  return;
@@ -7841,109 +8328,208 @@ function buildSystemPrompt() {
7841
8328
  // src/mcp/client.ts
7842
8329
  import { spawn as spawn2 } from "child_process";
7843
8330
  import { createInterface } from "readline";
7844
- var REQUEST_TIMEOUT = 3e4;
8331
+ var DEFAULT_REQUEST_TIMEOUT = 3e4;
8332
+ var CONNECT_TIMEOUT = 3e4;
8333
+ var MCP_PROTOCOL_VERSION = "2024-11-05";
7845
8334
  var MCPClient = class {
7846
8335
  config;
7847
8336
  process = null;
7848
8337
  rl = null;
7849
- connected = false;
8338
+ connectionState = "disconnected";
7850
8339
  nextId = 1;
7851
8340
  pendingRequests = /* @__PURE__ */ new Map();
7852
- buffer = "";
8341
+ serverCapabilities = {};
8342
+ serverInfo;
8343
+ serverInstructions;
8344
+ connectReject = null;
7853
8345
  constructor(config) {
7854
8346
  this.config = config;
7855
8347
  }
7856
8348
  async connect() {
7857
- if (this.connected) return;
7858
- const env = {
7859
- ...process.env,
7860
- ...this.config.env
7861
- };
7862
- this.process = spawn2(this.config.command, this.config.args, {
7863
- stdio: ["pipe", "pipe", "pipe"],
7864
- env
7865
- });
7866
- this.process.on("error", (err) => {
7867
- this.cleanup();
7868
- throw err;
7869
- });
7870
- this.process.on("exit", () => {
7871
- this.cleanup();
7872
- });
7873
- if (!this.process.stdout) {
7874
- throw new Error("MCP server stdout is not available");
8349
+ if (this.connectionState === "connected") return;
8350
+ if (this.connectionState === "connecting") {
8351
+ throw new Error(`MCP server "${this.config.name}" is already connecting`);
7875
8352
  }
7876
- this.rl = createInterface({ input: this.process.stdout });
7877
- this.rl.on("line", (line) => {
7878
- this.handleLine(line);
7879
- });
7880
- if (this.process.stderr) {
7881
- this.process.stderr.on("data", () => {
7882
- });
8353
+ this.connectionState = "connecting";
8354
+ try {
8355
+ await this.spawnProcess();
8356
+ await this.performHandshake();
8357
+ this.connectionState = "connected";
8358
+ } catch (error) {
8359
+ this.cleanup();
8360
+ this.connectionState = "failed";
8361
+ throw error;
7883
8362
  }
7884
- await this.sendRequest("initialize", {
7885
- protocolVersion: "2024-11-05",
7886
- capabilities: {},
7887
- clientInfo: { name: "cliskill", version: "1.0.0" }
7888
- });
7889
- this.sendNotification("notifications/initialized", {});
7890
- this.connected = true;
7891
8363
  }
7892
8364
  async disconnect() {
7893
- if (!this.process) return;
8365
+ if (this.connectionState === "disconnected") return;
7894
8366
  for (const [, pending] of this.pendingRequests) {
7895
8367
  clearTimeout(pending.timer);
7896
8368
  pending.reject(new Error("Connection closed"));
7897
8369
  }
7898
8370
  this.pendingRequests.clear();
7899
- this.process.kill("SIGTERM");
8371
+ if (this.process != null && !this.process.killed) {
8372
+ try {
8373
+ this.process.kill("SIGTERM");
8374
+ await new Promise((resolve10) => {
8375
+ if (!this.process) {
8376
+ resolve10();
8377
+ return;
8378
+ }
8379
+ const forceKillTimer = setTimeout(() => {
8380
+ try {
8381
+ this.process?.kill("SIGKILL");
8382
+ } catch {
8383
+ }
8384
+ resolve10();
8385
+ }, 5e3);
8386
+ this.process.once("exit", () => {
8387
+ clearTimeout(forceKillTimer);
8388
+ resolve10();
8389
+ });
8390
+ });
8391
+ } catch {
8392
+ }
8393
+ }
7900
8394
  this.cleanup();
7901
8395
  }
7902
8396
  async listTools() {
7903
8397
  const response = await this.sendRequest("tools/list", {});
8398
+ this.assertNoError(response);
7904
8399
  const result = response.result;
7905
8400
  return result?.tools ?? [];
7906
8401
  }
7907
8402
  async callTool(name, args) {
7908
8403
  const response = await this.sendRequest("tools/call", { name, arguments: args });
7909
- return response.result;
8404
+ this.assertNoError(response);
8405
+ const result = response.result;
8406
+ return result ?? { content: [{ type: "text", text: "No result from MCP server" }] };
7910
8407
  }
7911
8408
  async listResources() {
7912
8409
  const response = await this.sendRequest("resources/list", {});
8410
+ this.assertNoError(response);
7913
8411
  const result = response.result;
7914
8412
  return result?.resources ?? [];
7915
8413
  }
7916
8414
  async readResource(uri) {
7917
8415
  const response = await this.sendRequest("resources/read", { uri });
8416
+ this.assertNoError(response);
7918
8417
  return response.result;
7919
8418
  }
7920
8419
  async listPrompts() {
7921
8420
  const response = await this.sendRequest("prompts/list", {});
8421
+ this.assertNoError(response);
7922
8422
  const result = response.result;
7923
8423
  return result?.prompts ?? [];
7924
8424
  }
7925
8425
  isConnected() {
7926
- return this.connected;
8426
+ return this.connectionState === "connected";
8427
+ }
8428
+ getState() {
8429
+ return this.connectionState;
8430
+ }
8431
+ getServerInfo() {
8432
+ return this.serverInfo;
8433
+ }
8434
+ getServerInstructions() {
8435
+ return this.serverInstructions;
8436
+ }
8437
+ getCapabilities() {
8438
+ return this.serverCapabilities;
8439
+ }
8440
+ getConfig() {
8441
+ return this.config;
8442
+ }
8443
+ async spawnProcess() {
8444
+ const env = {
8445
+ ...process.env,
8446
+ ...this.config.env
8447
+ };
8448
+ this.process = spawn2(this.config.command, this.config.args, {
8449
+ stdio: ["pipe", "pipe", "pipe"],
8450
+ env
8451
+ });
8452
+ this.process.on("error", (err) => {
8453
+ this.handleProcessError(err);
8454
+ });
8455
+ this.process.on("exit", (code, signal) => {
8456
+ if (this.connectionState === "connected" || this.connectionState === "connecting") {
8457
+ this.handleProcessExit(code, signal);
8458
+ }
8459
+ });
8460
+ if (!this.process.stdout) {
8461
+ throw new Error(`MCP server "${this.config.name}": stdout is not available`);
8462
+ }
8463
+ if (!this.process.stdin) {
8464
+ throw new Error(`MCP server "${this.config.name}": stdin is not available`);
8465
+ }
8466
+ this.rl = createInterface({ input: this.process.stdout });
8467
+ this.rl.on("line", (line) => {
8468
+ this.handleLine(line);
8469
+ });
8470
+ if (this.process.stderr) {
8471
+ this.process.stderr.on("data", (data) => {
8472
+ const text = data.toString().trim();
8473
+ if (text) {
8474
+ console.error(`[MCP:${this.config.name}:stderr] ${text}`);
8475
+ }
8476
+ });
8477
+ }
8478
+ }
8479
+ async performHandshake() {
8480
+ const connectPromise = this.sendRequest("initialize", {
8481
+ protocolVersion: MCP_PROTOCOL_VERSION,
8482
+ capabilities: {},
8483
+ clientInfo: { name: "cliskill", version: "1.0.0" }
8484
+ });
8485
+ const timeoutPromise = new Promise((_, reject) => {
8486
+ const timer = setTimeout(() => {
8487
+ reject(new Error(
8488
+ `MCP server "${this.config.name}" connection timed out after ${CONNECT_TIMEOUT}ms`
8489
+ ));
8490
+ }, CONNECT_TIMEOUT);
8491
+ timer.unref();
8492
+ });
8493
+ const response = await Promise.race([connectPromise, timeoutPromise]);
8494
+ this.assertNoError(response);
8495
+ const initResult = response.result;
8496
+ if (!initResult) {
8497
+ throw new Error(`MCP server "${this.config.name}" returned empty initialize result`);
8498
+ }
8499
+ if (initResult.protocolVersion !== MCP_PROTOCOL_VERSION) {
8500
+ throw new Error(
8501
+ `MCP server "${this.config.name}" uses incompatible protocol version: ${initResult.protocolVersion} (expected ${MCP_PROTOCOL_VERSION})`
8502
+ );
8503
+ }
8504
+ this.serverCapabilities = initResult.capabilities ?? {};
8505
+ this.serverInfo = initResult.serverInfo;
8506
+ this.serverInstructions = initResult.instructions;
8507
+ this.sendNotification("notifications/initialized", {});
7927
8508
  }
7928
8509
  sendRequest(method, params) {
7929
8510
  return new Promise((resolve10, reject) => {
7930
8511
  if (!this.process?.stdin) {
7931
- reject(new Error("MCP server not connected"));
8512
+ reject(new Error(`MCP server "${this.config.name}" is not connected`));
7932
8513
  return;
7933
8514
  }
7934
8515
  const id = this.nextId++;
7935
8516
  const request = { jsonrpc: "2.0", id, method, params };
7936
8517
  const timer = setTimeout(() => {
7937
8518
  this.pendingRequests.delete(id);
7938
- reject(new Error(`Request timeout: ${method} (id=${id})`));
7939
- }, REQUEST_TIMEOUT);
8519
+ reject(new Error(
8520
+ `Request timeout: ${method} (id=${id}) on server "${this.config.name}"`
8521
+ ));
8522
+ }, DEFAULT_REQUEST_TIMEOUT);
8523
+ timer.unref();
7940
8524
  this.pendingRequests.set(id, { resolve: resolve10, reject, timer });
7941
8525
  const data = JSON.stringify(request) + "\n";
7942
8526
  this.process.stdin.write(data, (err) => {
7943
8527
  if (err) {
7944
8528
  clearTimeout(timer);
7945
8529
  this.pendingRequests.delete(id);
7946
- reject(err);
8530
+ reject(new Error(
8531
+ `Failed to send request to MCP server "${this.config.name}": ${err.message}`
8532
+ ));
7947
8533
  }
7948
8534
  });
7949
8535
  });
@@ -7952,19 +8538,23 @@ var MCPClient = class {
7952
8538
  if (!this.process?.stdin) return;
7953
8539
  const notification = { jsonrpc: "2.0", method, params };
7954
8540
  const data = JSON.stringify(notification) + "\n";
7955
- this.process.stdin.write(data);
8541
+ this.process.stdin.write(data, (err) => {
8542
+ if (err) {
8543
+ console.error(
8544
+ `[MCP:${this.config.name}] Failed to send notification "${method}": ${err.message}`
8545
+ );
8546
+ }
8547
+ });
7956
8548
  }
7957
8549
  handleLine(line) {
7958
- this.buffer += line;
8550
+ if (!line.trim()) return;
7959
8551
  let response;
7960
8552
  try {
7961
- response = JSON.parse(this.buffer);
8553
+ response = JSON.parse(line);
7962
8554
  } catch {
7963
8555
  return;
7964
- } finally {
7965
- this.buffer = "";
7966
8556
  }
7967
- if (response.id !== void 0 && response.id !== null) {
8557
+ if (response.id != null) {
7968
8558
  const pending = this.pendingRequests.get(response.id);
7969
8559
  if (pending) {
7970
8560
  clearTimeout(pending.timer);
@@ -7973,12 +8563,47 @@ var MCPClient = class {
7973
8563
  }
7974
8564
  }
7975
8565
  }
8566
+ handleProcessError(err) {
8567
+ const message = `MCP server "${this.config.name}" process error: ${err.message}`;
8568
+ console.error(`[MCP] ${message}`);
8569
+ for (const [, pending] of this.pendingRequests) {
8570
+ clearTimeout(pending.timer);
8571
+ pending.reject(new Error(message));
8572
+ }
8573
+ this.pendingRequests.clear();
8574
+ if (this.connectReject) {
8575
+ this.connectReject(new Error(message));
8576
+ this.connectReject = null;
8577
+ }
8578
+ this.cleanup();
8579
+ }
8580
+ handleProcessExit(code, signal) {
8581
+ const reason = signal ? `terminated by signal ${signal}` : `exited with code ${code}`;
8582
+ const message = `MCP server "${this.config.name}" ${reason}`;
8583
+ for (const [, pending] of this.pendingRequests) {
8584
+ clearTimeout(pending.timer);
8585
+ pending.reject(new Error(message));
8586
+ }
8587
+ this.pendingRequests.clear();
8588
+ this.cleanup();
8589
+ }
8590
+ assertNoError(response) {
8591
+ if (response.error) {
8592
+ const { code, message, data } = response.error;
8593
+ const detail = data ? ` | data: ${JSON.stringify(data)}` : "";
8594
+ throw new Error(
8595
+ `MCP server "${this.config.name}" returned error [${code}]: ${message}${detail}`
8596
+ );
8597
+ }
8598
+ }
7976
8599
  cleanup() {
7977
- this.connected = false;
8600
+ this.connectionState = "disconnected";
7978
8601
  this.rl?.close();
7979
8602
  this.rl = null;
7980
8603
  this.process = null;
7981
- this.buffer = "";
8604
+ this.serverCapabilities = {};
8605
+ this.serverInfo = void 0;
8606
+ this.serverInstructions = void 0;
7982
8607
  }
7983
8608
  };
7984
8609
 
@@ -7986,28 +8611,51 @@ var MCPClient = class {
7986
8611
  var MCPConnectionManager = class {
7987
8612
  clients = /* @__PURE__ */ new Map();
7988
8613
  async addServer(config) {
8614
+ if (this.clients.has(config.name)) {
8615
+ await this.removeServer(config.name);
8616
+ }
7989
8617
  const client = new MCPClient(config);
7990
- await client.connect();
7991
- this.clients.set(config.name, client);
8618
+ try {
8619
+ await client.connect();
8620
+ this.clients.set(config.name, client);
8621
+ } catch (error) {
8622
+ const message = error instanceof Error ? error.message : String(error);
8623
+ console.error(`[MCP] Failed to connect server "${config.name}": ${message}`);
8624
+ try {
8625
+ await client.disconnect();
8626
+ } catch {
8627
+ }
8628
+ throw error;
8629
+ }
7992
8630
  }
7993
8631
  async removeServer(name) {
7994
8632
  const client = this.clients.get(name);
7995
- if (client) {
8633
+ if (!client) return;
8634
+ try {
7996
8635
  await client.disconnect();
8636
+ } catch (error) {
8637
+ const message = error instanceof Error ? error.message : String(error);
8638
+ console.error(`[MCP] Error disconnecting server "${name}": ${message}`);
8639
+ } finally {
7997
8640
  this.clients.delete(name);
7998
8641
  }
7999
8642
  }
8000
8643
  getConnectedServers() {
8001
8644
  return Array.from(this.clients.entries()).filter(([, client]) => client.isConnected()).map(([name]) => name);
8002
8645
  }
8646
+ getClient(name) {
8647
+ return this.clients.get(name);
8648
+ }
8003
8649
  async getAllTools() {
8004
8650
  const allTools = [];
8005
- for (const [, client] of this.clients) {
8651
+ for (const [name, client] of this.clients) {
8006
8652
  if (!client.isConnected()) continue;
8007
8653
  try {
8008
8654
  const tools = await client.listTools();
8009
8655
  allTools.push(...tools);
8010
- } catch {
8656
+ } catch (error) {
8657
+ const message = error instanceof Error ? error.message : String(error);
8658
+ console.error(`[MCP] Failed to list tools from "${name}": ${message}`);
8011
8659
  }
8012
8660
  }
8013
8661
  return allTools;
@@ -8027,17 +8675,40 @@ var MCPConnectionManager = class {
8027
8675
  if (!client || !client.isConnected()) {
8028
8676
  return [];
8029
8677
  }
8030
- return client.listTools();
8678
+ try {
8679
+ return await client.listTools();
8680
+ } catch (error) {
8681
+ const message = error instanceof Error ? error.message : String(error);
8682
+ console.error(`[MCP] Failed to list tools from "${serverName}": ${message}`);
8683
+ return [];
8684
+ }
8031
8685
  }
8032
8686
  async disconnectAll() {
8033
- for (const [, client] of this.clients) {
8034
- try {
8035
- await client.disconnect();
8036
- } catch {
8687
+ const disconnectPromises = Array.from(this.clients.entries()).map(
8688
+ async ([name, client]) => {
8689
+ try {
8690
+ await client.disconnect();
8691
+ } catch (error) {
8692
+ const message = error instanceof Error ? error.message : String(error);
8693
+ console.error(`[MCP] Error disconnecting server "${name}": ${message}`);
8694
+ }
8037
8695
  }
8038
- }
8696
+ );
8697
+ await Promise.allSettled(disconnectPromises);
8039
8698
  this.clients.clear();
8040
8699
  }
8700
+ /** Register graceful shutdown handlers for SIGINT/SIGTERM. */
8701
+ registerShutdownHandlers() {
8702
+ const handler = async () => {
8703
+ await this.disconnectAll();
8704
+ };
8705
+ process.on("SIGINT", handler);
8706
+ process.on("SIGTERM", handler);
8707
+ return () => {
8708
+ process.off("SIGINT", handler);
8709
+ process.off("SIGTERM", handler);
8710
+ };
8711
+ }
8041
8712
  };
8042
8713
 
8043
8714
  // src/mcp/mcp-tool-adapter.ts
@@ -8046,19 +8717,23 @@ var MCPToolAdapter = class {
8046
8717
  name;
8047
8718
  description;
8048
8719
  inputSchema;
8049
- riskLevel = "safe";
8720
+ riskLevel;
8050
8721
  concurrencySafe = true;
8051
- readOnly = true;
8722
+ readOnly;
8052
8723
  serverName;
8724
+ originalToolName;
8053
8725
  mcpManager;
8054
8726
  rawInputSchema;
8055
- constructor(serverName, toolName, description, inputSchema2, mcpManager) {
8727
+ constructor(serverName, toolName, description, inputSchema2, mcpManager, annotations) {
8728
+ this.serverName = serverName;
8729
+ this.originalToolName = toolName;
8056
8730
  this.name = `mcp_${serverName}_${toolName}`;
8057
8731
  this.description = description;
8058
- this.serverName = serverName;
8059
8732
  this.mcpManager = mcpManager;
8060
8733
  this.rawInputSchema = inputSchema2;
8061
8734
  this.inputSchema = this.buildZodSchema(inputSchema2);
8735
+ this.readOnly = annotations?.readOnlyHint === true;
8736
+ this.riskLevel = this.deriveRiskLevel(annotations);
8062
8737
  }
8063
8738
  async execute(input, context) {
8064
8739
  const allowed = await context.checkPermission(this.name, JSON.stringify(input));
@@ -8066,8 +8741,16 @@ var MCPToolAdapter = class {
8066
8741
  return `Error: Permission denied for MCP tool: ${this.name}`;
8067
8742
  }
8068
8743
  try {
8069
- const result = await this.mcpManager.callTool(this.serverName, this.getOriginalName(), input);
8070
- return typeof result === "string" ? result : JSON.stringify(result, null, 2);
8744
+ const result = await this.mcpManager.callTool(
8745
+ this.serverName,
8746
+ this.originalToolName,
8747
+ input
8748
+ );
8749
+ if (result.isError) {
8750
+ const errorText = result.content.filter((block) => block.type === "text").map((block) => block.text ?? "").join("\n");
8751
+ return `Error from MCP tool ${this.name}: ${errorText || "Unknown error"}`;
8752
+ }
8753
+ return this.formatResult(result);
8071
8754
  } catch (err) {
8072
8755
  return `Error calling MCP tool ${this.name}: ${err.message}`;
8073
8756
  }
@@ -8080,12 +8763,29 @@ var MCPToolAdapter = class {
8080
8763
  };
8081
8764
  }
8082
8765
  getOriginalName() {
8083
- const parts = this.name.split("_");
8084
- return parts.slice(2).join("_");
8766
+ return this.originalToolName;
8085
8767
  }
8086
8768
  getServerName() {
8087
8769
  return this.serverName;
8088
8770
  }
8771
+ deriveRiskLevel(annotations) {
8772
+ if (annotations?.destructiveHint === true) return "destructive";
8773
+ if (annotations?.readOnlyHint === true) return "readonly";
8774
+ return "safe";
8775
+ }
8776
+ formatResult(result) {
8777
+ const parts = [];
8778
+ for (const block of result.content) {
8779
+ if (block.type === "text" && block.text != null) {
8780
+ parts.push(block.text);
8781
+ } else if (block.type === "image" && block.data != null) {
8782
+ parts.push(`[Image: ${block.mimeType ?? "unknown"}, ${block.data.length} bytes base64]`);
8783
+ } else if (block.type === "resource" && block.text != null) {
8784
+ parts.push(block.text);
8785
+ }
8786
+ }
8787
+ return parts.length > 0 ? parts.join("\n") : JSON.stringify(result, null, 2);
8788
+ }
8089
8789
  buildZodSchema(jsonSchema) {
8090
8790
  const properties = jsonSchema.properties ?? {};
8091
8791
  const required = new Set(jsonSchema.required ?? []);
@@ -8101,10 +8801,21 @@ var MCPToolAdapter = class {
8101
8801
  switch (type) {
8102
8802
  case "string":
8103
8803
  if (prop.enum) return z25.enum(prop.enum);
8804
+ if (prop.format === "date-time") return z25.string().datetime();
8805
+ if (typeof prop.minLength === "number" || typeof prop.maxLength === "number") {
8806
+ let schema = z25.string();
8807
+ if (typeof prop.minLength === "number") schema = schema.min(prop.minLength);
8808
+ if (typeof prop.maxLength === "number") schema = schema.max(prop.maxLength);
8809
+ return schema;
8810
+ }
8104
8811
  return z25.string();
8105
8812
  case "number":
8106
- case "integer":
8107
- return z25.number();
8813
+ case "integer": {
8814
+ let schema = z25.number();
8815
+ if (typeof prop.minimum === "number") schema = schema.min(prop.minimum);
8816
+ if (typeof prop.maximum === "number") schema = schema.max(prop.maximum);
8817
+ return schema;
8818
+ }
8108
8819
  case "boolean":
8109
8820
  return z25.boolean();
8110
8821
  case "array":
@@ -8133,120 +8844,20 @@ async function registerMCPTools(mcpManager, registerFn) {
8133
8844
  tool.name,
8134
8845
  tool.description ?? `MCP tool: ${tool.name}`,
8135
8846
  tool.inputSchema,
8136
- mcpManager
8847
+ mcpManager,
8848
+ tool.annotations
8137
8849
  );
8138
8850
  registerFn(adapter);
8139
8851
  registered.push(adapter.name);
8140
8852
  }
8141
- } catch {
8853
+ } catch (error) {
8854
+ const message = error instanceof Error ? error.message : String(error);
8855
+ console.error(`[MCP] Failed to register tools from "${serverName}": ${message}`);
8142
8856
  }
8143
8857
  }
8144
8858
  return registered;
8145
8859
  }
8146
8860
 
8147
- // src/services/session-recovery.ts
8148
- import { readFile as readFile9, readdir as readdir5 } from "fs/promises";
8149
- import { join as join11 } from "path";
8150
- import { existsSync as existsSync5 } from "fs";
8151
- async function recoverSession(filePath) {
8152
- const raw = await readFile9(filePath, "utf-8");
8153
- const lines = raw.split("\n").filter((line) => line.trim().length > 0);
8154
- const entries = [];
8155
- for (const line of lines) {
8156
- try {
8157
- const entry = JSON.parse(line);
8158
- if (entry.type && entry.content !== void 0) {
8159
- entries.push(entry);
8160
- }
8161
- } catch {
8162
- }
8163
- }
8164
- const messages = reconstructMessages(entries);
8165
- const firstEntry = entries[0];
8166
- const timestamp = firstEntry?.timestamp ?? 0;
8167
- return {
8168
- sessionId: filePath,
8169
- filePath,
8170
- timestamp,
8171
- messages,
8172
- entryCount: entries.length
8173
- };
8174
- }
8175
- async function listSessions(limit = 20) {
8176
- const dir = getSessionsDir();
8177
- if (!existsSync5(dir)) return [];
8178
- const files = await readdir5(dir);
8179
- const sessionFiles = files.filter((f) => f.startsWith("session-") && f.endsWith(".jsonl"));
8180
- const sessions = [];
8181
- for (const fileName of sessionFiles) {
8182
- const filePath = join11(dir, fileName);
8183
- try {
8184
- const raw = await readFile9(filePath, "utf-8");
8185
- const lines = raw.split("\n").filter((line) => line.trim().length > 0);
8186
- let lastActivity = 0;
8187
- let entryCount = 0;
8188
- for (const line of lines) {
8189
- try {
8190
- const entry = JSON.parse(line);
8191
- if (entry.timestamp && entry.timestamp > lastActivity) {
8192
- lastActivity = entry.timestamp;
8193
- }
8194
- entryCount++;
8195
- } catch {
8196
- }
8197
- }
8198
- const dateStr = fileName.replace("session-", "").replace(".jsonl", "");
8199
- const fileTimestamp = Date.parse(dateStr) || lastActivity;
8200
- sessions.push({
8201
- fileName,
8202
- filePath,
8203
- timestamp: fileTimestamp,
8204
- entryCount,
8205
- lastActivity
8206
- });
8207
- } catch {
8208
- }
8209
- }
8210
- sessions.sort((a, b) => b.lastActivity - a.lastActivity);
8211
- return sessions.slice(0, limit);
8212
- }
8213
- async function findLatestSession() {
8214
- const sessions = await listSessions(1);
8215
- return sessions[0] ?? null;
8216
- }
8217
- function reconstructMessages(entries) {
8218
- const messages = [];
8219
- let i = 0;
8220
- while (i < entries.length) {
8221
- const entry = entries[i];
8222
- if (entry.type === "user") {
8223
- const content = [{ type: "text", text: entry.content }];
8224
- messages.push({ role: "user", content });
8225
- i++;
8226
- } else if (entry.type === "assistant") {
8227
- const content = [{ type: "text", text: entry.content }];
8228
- messages.push({ role: "assistant", content });
8229
- i++;
8230
- } else if (entry.type === "tool_use") {
8231
- const lastMsg = messages[messages.length - 1];
8232
- if (lastMsg && lastMsg.role === "assistant") {
8233
- }
8234
- i++;
8235
- } else if (entry.type === "tool_result") {
8236
- const content = [{
8237
- type: "tool_result",
8238
- toolUseId: `recovered-${i}`,
8239
- content: entry.content
8240
- }];
8241
- messages.push({ role: "user", content });
8242
- i++;
8243
- } else {
8244
- i++;
8245
- }
8246
- }
8247
- return messages;
8248
- }
8249
-
8250
8861
  // src/ui/repl.ts
8251
8862
  var SessionSaver = class {
8252
8863
  filePath;
@@ -8311,7 +8922,7 @@ async function connectMcpServers(config, toolRegistry) {
8311
8922
  console.error(`MCP server "${serverConfig.name}" connection failed: ${err.message}`);
8312
8923
  }
8313
8924
  }
8314
- if (connectedCount === 0) return mcpManager;
8925
+ if (connectedCount === 0) return null;
8315
8926
  const registeredTools = await registerMCPTools(mcpManager, (tool) => {
8316
8927
  try {
8317
8928
  toolRegistry.register(tool);
@@ -8321,6 +8932,7 @@ async function connectMcpServers(config, toolRegistry) {
8321
8932
  if (registeredTools.length > 0) {
8322
8933
  console.log(` MCP: ${connectedCount} server(s) connected, ${registeredTools.length} tool(s) loaded`);
8323
8934
  }
8935
+ mcpManager.registerShutdownHandlers();
8324
8936
  return mcpManager;
8325
8937
  }
8326
8938
  async function runTuiRepl(config) {
@@ -10564,6 +11176,13 @@ var PROVIDER_OPTIONS = [
10564
11176
  requiresApiKey: true,
10565
11177
  format: "openai-compatible"
10566
11178
  },
11179
+ {
11180
+ id: "anthropic",
11181
+ label: "Anthropic Claude",
11182
+ defaultBaseUrl: "https://api.anthropic.com",
11183
+ requiresApiKey: true,
11184
+ format: "anthropic-compatible"
11185
+ },
10567
11186
  {
10568
11187
  id: "ollama",
10569
11188
  label: "Ollama (local)",
@@ -10763,13 +11382,18 @@ import { useState as useState7, useEffect as useEffect2 } from "react";
10763
11382
  import { Box as Box10, Text as Text10, useInput as useInput6 } from "ink";
10764
11383
 
10765
11384
  // src/connect/fetch-models.ts
10766
- async function fetchModelsFromApi(baseUrl, apiKey) {
11385
+ async function fetchModelsFromApi(baseUrl, apiKey, format) {
10767
11386
  const url = baseUrl.replace(/\/$/, "") + "/models";
10768
11387
  const headers = {
10769
11388
  "Content-Type": "application/json"
10770
11389
  };
10771
11390
  if (apiKey) {
10772
- headers["Authorization"] = `Bearer ${apiKey}`;
11391
+ if (format === "anthropic-compatible") {
11392
+ headers["x-api-key"] = apiKey;
11393
+ headers["anthropic-version"] = "2023-06-01";
11394
+ } else {
11395
+ headers["Authorization"] = `Bearer ${apiKey}`;
11396
+ }
10773
11397
  }
10774
11398
  const response = await fetch(url, {
10775
11399
  method: "GET",
@@ -10787,9 +11411,12 @@ async function fetchModelsFromApi(baseUrl, apiKey) {
10787
11411
  }
10788
11412
  var CATEGORY_PATTERNS = [
10789
11413
  { pattern: /o[1-4](?:-|$|-mini)/i, category: "reasoning" },
11414
+ { pattern: /claude-opus|claude-[34]\.(\d)+/i, category: "reasoning" },
10790
11415
  { pattern: /reason|think|deep/i, category: "reasoning" },
11416
+ { pattern: /claude-sonnet/i, category: "code" },
10791
11417
  { pattern: /code|codestral|deepseek-coder|qwen.*coder|starcoder/i, category: "code" },
10792
11418
  { pattern: /mini|flash|fast|turbo|haiku|lite/i, category: "fast" },
11419
+ { pattern: /claude-haiku/i, category: "fast" },
10793
11420
  { pattern: /creative|dall/i, category: "creative" }
10794
11421
  ];
10795
11422
  function classifyModelName(modelName) {
@@ -10865,7 +11492,7 @@ function ModelStep() {
10865
11492
  setPhase("fallback");
10866
11493
  return;
10867
11494
  }
10868
- fetchModelsFromApi(state.baseUrl, state.apiKey || void 0).then((fetched) => {
11495
+ fetchModelsFromApi(state.baseUrl, state.apiKey || void 0, state.provider?.format).then((fetched) => {
10869
11496
  if (fetched.length === 0) {
10870
11497
  setError("No models found at this endpoint");
10871
11498
  setPhase("fallback");
@@ -11553,4 +12180,4 @@ export {
11553
12180
  MCPConnectionManager,
11554
12181
  runCli
11555
12182
  };
11556
- //# sourceMappingURL=chunk-LHS6LLBW.js.map
12183
+ //# sourceMappingURL=chunk-SDHXIBKZ.js.map