@serkanalgur/opencode-slim 1.0.0 → 1.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;;;;;AAoSjD,wBAA8C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;;;;;AAqYjD,wBAA8C"}
package/dist/index.js CHANGED
@@ -13663,6 +13663,251 @@ function getNudgeMessage(reason, tokenCount, maxTokens) {
13663
13663
  }
13664
13664
  }
13665
13665
 
13666
+ // lib/types.ts
13667
+ var COST_PROFILES = {
13668
+ // Anthropic
13669
+ "anthropic/claude-sonnet-4-20250514": {
13670
+ inputPricePer1k: 3e-3,
13671
+ outputPricePer1k: 0.015,
13672
+ cacheReadPricePer1k: 3e-4,
13673
+ cacheWritePricePer1k: 375e-5
13674
+ },
13675
+ "anthropic/claude-3-5-sonnet-20241022": {
13676
+ inputPricePer1k: 3e-3,
13677
+ outputPricePer1k: 0.015,
13678
+ cacheReadPricePer1k: 3e-4,
13679
+ cacheWritePricePer1k: 375e-5
13680
+ },
13681
+ // OpenAI
13682
+ "openai/gpt-4o": {
13683
+ inputPricePer1k: 25e-4,
13684
+ outputPricePer1k: 0.01,
13685
+ cacheReadPricePer1k: 125e-5,
13686
+ cacheWritePricePer1k: 25e-4
13687
+ },
13688
+ "openai/gpt-4o-mini": {
13689
+ inputPricePer1k: 15e-5,
13690
+ outputPricePer1k: 6e-4,
13691
+ cacheReadPricePer1k: 75e-6,
13692
+ cacheWritePricePer1k: 15e-5
13693
+ },
13694
+ // Default
13695
+ "default": {
13696
+ inputPricePer1k: 3e-3,
13697
+ outputPricePer1k: 0.015,
13698
+ cacheReadPricePer1k: 3e-4,
13699
+ cacheWritePricePer1k: 375e-5
13700
+ }
13701
+ };
13702
+
13703
+ // lib/tui.ts
13704
+ async function buildPanelData(sessionId, messages, state, config2, modelId) {
13705
+ const modelContextLimit = state.modelContextLimit || 2e5;
13706
+ const maxTokens = resolveTokenLimit(config2.compress.maxContextLimit, modelContextLimit);
13707
+ let currentTokens = 0;
13708
+ const tokensByRole = { user: 0, assistant: 0, tools: 0, system: 0 };
13709
+ let userMessages = 0;
13710
+ let assistantMessages = 0;
13711
+ let toolCalls = 0;
13712
+ let toolResults = 0;
13713
+ for (const msg of messages) {
13714
+ const role = msg.info.role;
13715
+ const text = getMessageText(msg);
13716
+ const toolContent = getToolResultContent(msg);
13717
+ const msgTokens = await countTokens(text + toolContent);
13718
+ currentTokens += msgTokens;
13719
+ if (role === "user") {
13720
+ tokensByRole.user += msgTokens;
13721
+ userMessages++;
13722
+ } else if (role === "assistant") {
13723
+ tokensByRole.assistant += msgTokens;
13724
+ assistantMessages++;
13725
+ }
13726
+ for (const part of msg.parts) {
13727
+ if (part.type === "tool") {
13728
+ const toolPart = part;
13729
+ if (toolPart.state?.status === "completed") {
13730
+ toolResults++;
13731
+ } else {
13732
+ toolCalls++;
13733
+ }
13734
+ }
13735
+ }
13736
+ }
13737
+ tokensByRole.tools = tokensByRole.user + tokensByRole.assistant - tokensByRole.user - tokensByRole.assistant;
13738
+ tokensByRole.system = Math.max(0, currentTokens - tokensByRole.user - tokensByRole.assistant - tokensByRole.tools);
13739
+ const usagePercent = currentTokens / maxTokens * 100;
13740
+ let status = "healthy";
13741
+ if (usagePercent > 90) status = "critical";
13742
+ else if (usagePercent > 70) status = "warning";
13743
+ const compressionCount = state.compressionCount;
13744
+ const averageRatio = state.averageCompressionRatio;
13745
+ let totalTokensSaved = 0;
13746
+ for (const record2 of state.compressionHistory) {
13747
+ if (record2.success) {
13748
+ totalTokensSaved += record2.inputTokens - record2.outputTokens;
13749
+ }
13750
+ }
13751
+ const lastCompression = state.compressionHistory.length > 0 ? state.compressionHistory[state.compressionHistory.length - 1] : null;
13752
+ const profile = COST_PROFILES[modelId || "default"] || COST_PROFILES.default;
13753
+ const estimatedCost = currentTokens / 1e3 * profile.inputPricePer1k;
13754
+ const costSaved = totalTokensSaved / 1e3 * profile.inputPricePer1k;
13755
+ const topicMap = /* @__PURE__ */ new Map();
13756
+ for (const msg of messages) {
13757
+ const text = getMessageText(msg);
13758
+ const topics2 = extractTopics(text);
13759
+ for (const topic of topics2) {
13760
+ const existing = topicMap.get(topic) || { count: 0, tokens: 0 };
13761
+ existing.count++;
13762
+ existing.tokens += await countTokens(text);
13763
+ topicMap.set(topic, existing);
13764
+ }
13765
+ }
13766
+ const topics = Array.from(topicMap.entries()).map(([topic, data]) => ({ topic, ...data })).sort((a, b) => b.tokens - a.tokens).slice(0, 10);
13767
+ const recommendations = generateRecommendations(
13768
+ usagePercent,
13769
+ compressionCount,
13770
+ averageRatio,
13771
+ messages.length,
13772
+ config2
13773
+ );
13774
+ return {
13775
+ sessionId,
13776
+ timestamp: Date.now(),
13777
+ currentTokens,
13778
+ maxTokens,
13779
+ usagePercent,
13780
+ status,
13781
+ messageCount: messages.length,
13782
+ userMessages,
13783
+ assistantMessages,
13784
+ toolCalls,
13785
+ toolResults,
13786
+ tokensByRole,
13787
+ compressionCount,
13788
+ averageRatio,
13789
+ totalTokensSaved,
13790
+ lastCompression,
13791
+ estimatedCost,
13792
+ costSaved,
13793
+ model: modelId || "unknown",
13794
+ topics,
13795
+ recommendations
13796
+ };
13797
+ }
13798
+ var TOPIC_KEYWORDS = {
13799
+ "authentication": ["auth", "login", "password", "token", "session", "jwt"],
13800
+ "database": ["database", "db", "sql", "query", "migration", "schema"],
13801
+ "api": ["api", "endpoint", "route", "request", "response", "http"],
13802
+ "testing": ["test", "spec", "assert", "expect", "describe", "jest"],
13803
+ "configuration": ["config", "settings", "env", "environment", "variable"],
13804
+ "deployment": ["deploy", "docker", "kubernetes", "ci", "cd", "pipeline"],
13805
+ "ui": ["ui", "component", "render", "display", "style", "css"],
13806
+ "error": ["error", "exception", "catch", "throw", "debug", "fix"],
13807
+ "performance": ["performance", "optimize", "cache", "speed", "slow"],
13808
+ "security": ["security", "encrypt", "decrypt", "hash", "sanitize"]
13809
+ };
13810
+ function extractTopics(text) {
13811
+ const lower = text.toLowerCase();
13812
+ const topics = [];
13813
+ for (const [topic, keywords] of Object.entries(TOPIC_KEYWORDS)) {
13814
+ if (keywords.some((kw) => lower.includes(kw))) {
13815
+ topics.push(topic);
13816
+ }
13817
+ }
13818
+ return topics.length > 0 ? topics : ["general"];
13819
+ }
13820
+ function generateRecommendations(usagePercent, compressionCount, averageRatio, messageCount, config2) {
13821
+ const recs = [];
13822
+ if (usagePercent > 80) {
13823
+ recs.push("Context usage is high. Consider compressing older messages.");
13824
+ }
13825
+ if (usagePercent > 90) {
13826
+ recs.push("Context nearly full! Run compress immediately to avoid truncation.");
13827
+ }
13828
+ if (compressionCount === 0 && messageCount > 20) {
13829
+ recs.push("No compressions yet with many messages. Consider running compress.");
13830
+ }
13831
+ if (averageRatio < 0.3 && compressionCount > 0) {
13832
+ recs.push("Compression ratio is low. Summaries may be too verbose.");
13833
+ }
13834
+ if (messageCount > 50 && usagePercent < 50) {
13835
+ recs.push("Many messages but low usage. Deduplication may help further.");
13836
+ }
13837
+ if (recs.length === 0) {
13838
+ recs.push("Context is healthy. No action needed.");
13839
+ }
13840
+ return recs;
13841
+ }
13842
+ function renderPanel(data) {
13843
+ const lines = [];
13844
+ lines.push("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
13845
+ lines.push("\u2502 SLIM CONTEXT PANEL \u2502");
13846
+ lines.push("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
13847
+ const statusIcon = data.status === "healthy" ? "\u{1F7E2}" : data.status === "warning" ? "\u{1F7E1}" : "\u{1F534}";
13848
+ lines.push(`\u2502 Status: ${statusIcon} ${data.status.toUpperCase().padEnd(10)} \u2502`);
13849
+ lines.push("");
13850
+ const barLength = 30;
13851
+ const filledLength = Math.round(data.usagePercent / 100 * barLength);
13852
+ const emptyLength = barLength - filledLength;
13853
+ const bar = "\u2588".repeat(filledLength) + "\u2591".repeat(emptyLength);
13854
+ lines.push(`\u2502 Context: [${bar}] ${data.usagePercent.toFixed(1)}%`);
13855
+ lines.push(`\u2502 ${formatTokens(data.currentTokens)} / ${formatTokens(data.maxTokens)} tokens`);
13856
+ lines.push("");
13857
+ lines.push("\u2502 Messages:");
13858
+ lines.push(`\u2502 User: ${data.userMessages} Assistant: ${data.assistantMessages}`);
13859
+ lines.push(`\u2502 Tool calls: ${data.toolCalls} Results: ${data.toolResults}`);
13860
+ lines.push("");
13861
+ lines.push("\u2502 Token Distribution:");
13862
+ lines.push(`\u2502 User: ${formatTokens(data.tokensByRole.user)}`);
13863
+ lines.push(`\u2502 Assistant: ${formatTokens(data.tokensByRole.assistant)}`);
13864
+ lines.push("");
13865
+ lines.push("\u2502 Compression Stats:");
13866
+ lines.push(`\u2502 Count: ${data.compressionCount}`);
13867
+ lines.push(`\u2502 Avg ratio: ${(data.averageRatio * 100).toFixed(1)}%`);
13868
+ lines.push(`\u2502 Tokens saved: ${formatTokens(data.totalTokensSaved)}`);
13869
+ if (data.lastCompression) {
13870
+ const ago = Date.now() - data.lastCompression.timestamp;
13871
+ lines.push(`\u2502 Last: ${formatTimeAgo(ago)} ago`);
13872
+ }
13873
+ lines.push("");
13874
+ lines.push("\u2502 Cost Estimate:");
13875
+ lines.push(`\u2502 Current: $${data.estimatedCost.toFixed(4)}`);
13876
+ lines.push(`\u2502 Saved: $${data.costSaved.toFixed(4)}`);
13877
+ lines.push(`\u2502 Model: ${data.model}`);
13878
+ lines.push("");
13879
+ if (data.topics.length > 0) {
13880
+ lines.push("\u2502 Top Topics:");
13881
+ for (const topic of data.topics.slice(0, 5)) {
13882
+ lines.push(`\u2502 ${topic.topic}: ${topic.count} msgs (${formatTokens(topic.tokens)})`);
13883
+ }
13884
+ lines.push("");
13885
+ }
13886
+ lines.push("\u2502 Recommendations:");
13887
+ for (const rec of data.recommendations) {
13888
+ lines.push(`\u2502 \u2022 ${rec}`);
13889
+ }
13890
+ lines.push("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
13891
+ return lines.join("\n");
13892
+ }
13893
+ function formatTokens(tokens) {
13894
+ if (tokens >= 1e6) {
13895
+ return `${(tokens / 1e6).toFixed(1)}M`;
13896
+ }
13897
+ if (tokens >= 1e3) {
13898
+ return `${(tokens / 1e3).toFixed(1)}K`;
13899
+ }
13900
+ return String(tokens);
13901
+ }
13902
+ function formatTimeAgo(ms) {
13903
+ const seconds = Math.floor(ms / 1e3);
13904
+ if (seconds < 60) return `${seconds}s`;
13905
+ const minutes = Math.floor(seconds / 60);
13906
+ if (minutes < 60) return `${minutes}m`;
13907
+ const hours = Math.floor(minutes / 60);
13908
+ return `${hours}h`;
13909
+ }
13910
+
13666
13911
  // index.ts
13667
13912
  var sessionStates = /* @__PURE__ */ new Map();
13668
13913
  var sessionConfigs = /* @__PURE__ */ new Map();
@@ -13682,7 +13927,12 @@ var server = async (ctx) => {
13682
13927
  const compressTool = tool({
13683
13928
  description: getCompressToolDescription(),
13684
13929
  args: {
13685
- focus: external_exports.string().describe("Description of what content should be compressed")
13930
+ focus: external_exports.string().describe("What to compress (e.g., 'old exploration', 'completed tasks')"),
13931
+ mode: external_exports.enum(["auto", "range", "topic"]).default("auto").describe("Compression mode"),
13932
+ start: external_exports.number().optional().describe("Start message index (for range mode)"),
13933
+ end: external_exports.number().optional().describe("End message index (for range mode)"),
13934
+ topic: external_exports.string().optional().describe("Topic to compress (for topic mode)"),
13935
+ keepRecent: external_exports.number().default(5).describe("Number of recent messages to always keep")
13686
13936
  },
13687
13937
  async execute(args, context) {
13688
13938
  const config2 = getConfig(context.sessionID);
@@ -13699,17 +13949,42 @@ var server = async (ctx) => {
13699
13949
  info: m.info,
13700
13950
  parts: m.parts
13701
13951
  }));
13952
+ let targetIndices = [];
13702
13953
  let inputTokens = 0;
13703
- for (const msg of messageWithParts) {
13704
- const text = getMessageText(msg) + getToolResultContent(msg);
13705
- inputTokens += await countTokens(text);
13954
+ if (args.mode === "range" && args.start !== void 0 && args.end !== void 0) {
13955
+ const start = Math.max(0, args.start);
13956
+ const end = Math.min(messageWithParts.length, args.end);
13957
+ for (let i = start; i < end; i++) {
13958
+ targetIndices.push(i);
13959
+ const text = getMessageText(messageWithParts[i]) + getToolResultContent(messageWithParts[i]);
13960
+ inputTokens += await countTokens(text);
13961
+ }
13962
+ } else if (args.mode === "topic" && args.topic) {
13963
+ const topicLower = args.topic.toLowerCase();
13964
+ for (let i = 0; i < messageWithParts.length - args.keepRecent; i++) {
13965
+ const msg = messageWithParts[i];
13966
+ const text = getMessageText(msg) + getToolResultContent(msg);
13967
+ if (text.toLowerCase().includes(topicLower)) {
13968
+ targetIndices.push(i);
13969
+ inputTokens += await countTokens(text);
13970
+ }
13971
+ }
13972
+ } else {
13973
+ const keepRecent = args.keepRecent;
13974
+ for (let i = 0; i < messageWithParts.length - keepRecent; i++) {
13975
+ const msg = messageWithParts[i];
13976
+ const text = getMessageText(msg) + getToolResultContent(msg);
13977
+ const tokens = await countTokens(text);
13978
+ if (tokens < 100) continue;
13979
+ targetIndices.push(i);
13980
+ inputTokens += tokens;
13981
+ }
13706
13982
  }
13707
- const targets = messageWithParts.length > 10 ? [{ start: 0, end: messageWithParts.length - 5, reason: "user_requested", estimatedTokens: inputTokens }] : [];
13708
- if (targets.length === 0) {
13983
+ if (targetIndices.length === 0) {
13709
13984
  return "Nothing to compress - context is already efficient";
13710
13985
  }
13711
- const compressedMessages = messageWithParts.slice(targets[0].start, targets[0].end);
13712
- const summary = buildCompressionSummary(compressedMessages, args.focus);
13986
+ const targetMessages = targetIndices.map((i) => messageWithParts[i]);
13987
+ const summary = buildCompressionSummary(targetMessages, args.focus);
13713
13988
  const outputTokens = await countTokens(summary);
13714
13989
  const ratio = inputTokens > 0 ? 1 - outputTokens / inputTokens : 0;
13715
13990
  addCompressionRecord(
@@ -13719,19 +13994,20 @@ var server = async (ctx) => {
13719
13994
  inputTokens,
13720
13995
  outputTokens,
13721
13996
  ratio,
13722
- messageCount: compressedMessages.length,
13997
+ messageCount: targetMessages.length,
13723
13998
  success: true
13724
13999
  },
13725
14000
  config2.adaptive.learningRate
13726
14001
  );
13727
14002
  saveSessionState(state, config2.persistence.directory);
13728
14003
  return {
13729
- title: `Compressed ${compressedMessages.length} messages`,
14004
+ title: `Compressed ${targetMessages.length} messages`,
13730
14005
  output: summary,
13731
14006
  metadata: {
13732
14007
  inputTokens,
13733
14008
  outputTokens,
13734
14009
  ratio: Math.round(ratio * 100) + "%",
14010
+ mode: args.mode,
13735
14011
  focus: args.focus
13736
14012
  }
13737
14013
  };
@@ -13740,6 +14016,55 @@ var server = async (ctx) => {
13740
14016
  }
13741
14017
  }
13742
14018
  });
14019
+ const panelTool = tool({
14020
+ description: `Display a rich context usage panel showing:
14021
+ - Current token usage vs model limit
14022
+ - Message breakdown (user/assistant/tools)
14023
+ - Token distribution by role
14024
+ - Compression history and savings
14025
+ - Cost estimate
14026
+ - Topic distribution
14027
+ - Smart recommendations`,
14028
+ args: {},
14029
+ async execute(_args, context) {
14030
+ const config2 = getConfig(context.sessionID);
14031
+ const state = getState(context.sessionID, config2);
14032
+ try {
14033
+ const response = await ctx.client.session.messages({
14034
+ path: { id: context.sessionID }
14035
+ });
14036
+ if (!response.data || response.error) {
14037
+ return "Failed to fetch messages";
14038
+ }
14039
+ const messageList = response.data;
14040
+ const messageWithParts = messageList.map((m) => ({
14041
+ info: m.info,
14042
+ parts: m.parts
14043
+ }));
14044
+ const modelId = context.model?.id || "unknown";
14045
+ const panelData = await buildPanelData(
14046
+ context.sessionID,
14047
+ messageWithParts,
14048
+ state,
14049
+ config2,
14050
+ modelId
14051
+ );
14052
+ const panel = renderPanel(panelData);
14053
+ return {
14054
+ title: "Context Panel",
14055
+ output: panel,
14056
+ metadata: {
14057
+ usagePercent: panelData.usagePercent,
14058
+ status: panelData.status,
14059
+ currentTokens: panelData.currentTokens,
14060
+ maxTokens: panelData.maxTokens
14061
+ }
14062
+ };
14063
+ } catch (error45) {
14064
+ return `Error generating panel: ${error45 instanceof Error ? error45.message : "Unknown error"}`;
14065
+ }
14066
+ }
14067
+ });
13743
14068
  return {
13744
14069
  config: async (opencodeConfig) => {
13745
14070
  if (!opencodeConfig.permission) {
@@ -13747,9 +14072,11 @@ var server = async (ctx) => {
13747
14072
  }
13748
14073
  ;
13749
14074
  opencodeConfig.permission.compress = globalConfig2.compress.permission;
14075
+ opencodeConfig.permission.panel = "allow";
13750
14076
  },
13751
14077
  tool: {
13752
- compress: compressTool
14078
+ compress: compressTool,
14079
+ panel: panelTool
13753
14080
  },
13754
14081
  "experimental.chat.system.transform": async (input, output) => {
13755
14082
  const config2 = getConfig(input.sessionID || "");