@within-7/minto 0.3.6 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{cli.js → cli.cjs} +25 -23
- package/dist/commands/agents/AgentsCommand.js +459 -655
- package/dist/commands/agents/AgentsCommand.js.map +2 -2
- package/dist/commands/agents/types.js +1 -0
- package/dist/commands/agents/types.js.map +2 -2
- package/dist/commands/agents/utils/fileOperations.js +96 -36
- package/dist/commands/agents/utils/fileOperations.js.map +3 -3
- package/dist/commands/agents/utils/index.js +3 -1
- package/dist/commands/agents/utils/index.js.map +2 -2
- package/dist/commands/context.js +54 -23
- package/dist/commands/context.js.map +2 -2
- package/dist/commands/export.js +673 -93
- package/dist/commands/export.js.map +2 -2
- package/dist/commands/language.js +110 -0
- package/dist/commands/language.js.map +7 -0
- package/dist/commands/mcp-interactive.js +419 -217
- package/dist/commands/mcp-interactive.js.map +2 -2
- package/dist/commands/model.js +415 -66
- package/dist/commands/model.js.map +2 -2
- package/dist/commands/new.js +56 -0
- package/dist/commands/new.js.map +7 -0
- package/dist/commands/permissions.js +75 -49
- package/dist/commands/permissions.js.map +2 -2
- package/dist/commands/plugin.js +882 -185
- package/dist/commands/plugin.js.map +3 -3
- package/dist/commands/resume.js +251 -16
- package/dist/commands/resume.js.map +2 -2
- package/dist/commands/sandbox.js +168 -70
- package/dist/commands/sandbox.js.map +2 -2
- package/dist/commands/sessions.js +224 -0
- package/dist/commands/sessions.js.map +7 -0
- package/dist/commands/setup.js +596 -109
- package/dist/commands/setup.js.map +2 -2
- package/dist/commands/stats.js +292 -0
- package/dist/commands/stats.js.map +7 -0
- package/dist/commands/status.js +75 -7
- package/dist/commands/status.js.map +2 -2
- package/dist/commands/undo.js +154 -180
- package/dist/commands/undo.js.map +2 -2
- package/dist/commands.js +6 -0
- package/dist/commands.js.map +2 -2
- package/dist/components/AskUserQuestionDialog/AskUserQuestionDialog.js +3 -2
- package/dist/components/AskUserQuestionDialog/AskUserQuestionDialog.js.map +2 -2
- package/dist/components/Config.js +9 -8
- package/dist/components/Config.js.map +2 -2
- package/dist/components/HeaderBar.js +2 -1
- package/dist/components/HeaderBar.js.map +2 -2
- package/dist/components/Help.js +166 -32
- package/dist/components/Help.js.map +2 -2
- package/dist/components/HotkeyHelpPanel.js +46 -44
- package/dist/components/HotkeyHelpPanel.js.map +2 -2
- package/dist/components/InfoPanel/InfoPanel.js +123 -0
- package/dist/components/InfoPanel/InfoPanel.js.map +7 -0
- package/dist/components/InfoPanel/index.js +5 -0
- package/dist/components/InfoPanel/index.js.map +7 -0
- package/dist/components/InfoPanel/types.js +1 -0
- package/dist/components/InfoPanel/types.js.map +7 -0
- package/dist/components/Logo.js +5 -2
- package/dist/components/Logo.js.map +2 -2
- package/dist/components/MCPServerApprovalDialog.js +6 -5
- package/dist/components/MCPServerApprovalDialog.js.map +2 -2
- package/dist/components/MCPServerMultiselectDialog.js +5 -4
- package/dist/components/MCPServerMultiselectDialog.js.map +2 -2
- package/dist/components/MessageSelector.js +4 -3
- package/dist/components/MessageSelector.js.map +2 -2
- package/dist/components/ModelConfig.js +13 -12
- package/dist/components/ModelConfig.js.map +2 -2
- package/dist/components/ModelListManager.js +4 -3
- package/dist/components/ModelListManager.js.map +2 -2
- package/dist/components/ModelSelector/BrandTextInput.js +43 -0
- package/dist/components/ModelSelector/BrandTextInput.js.map +7 -0
- package/dist/components/ModelSelector/ModelSelector.js +419 -501
- package/dist/components/ModelSelector/ModelSelector.js.map +2 -2
- package/dist/components/ModelSelector/WizardContainer.js +45 -0
- package/dist/components/ModelSelector/WizardContainer.js.map +7 -0
- package/dist/components/ModelSelector/index.js +1 -3
- package/dist/components/ModelSelector/index.js.map +2 -2
- package/dist/components/PromptInput.js +77 -44
- package/dist/components/PromptInput.js.map +2 -2
- package/dist/components/SensitiveFileWarning.js +12 -8
- package/dist/components/SensitiveFileWarning.js.map +2 -2
- package/dist/components/SimpleSelector/SimpleSelector.js +154 -0
- package/dist/components/SimpleSelector/SimpleSelector.js.map +7 -0
- package/dist/components/SimpleSelector/index.js +5 -0
- package/dist/components/SimpleSelector/index.js.map +7 -0
- package/dist/components/SimpleSelector/types.js +1 -0
- package/dist/components/SimpleSelector/types.js.map +7 -0
- package/dist/components/StatusOverlayContent.js +21 -0
- package/dist/components/StatusOverlayContent.js.map +7 -0
- package/dist/components/TabbedListView/ScrollableList.js +117 -0
- package/dist/components/TabbedListView/ScrollableList.js.map +7 -0
- package/dist/components/TabbedListView/SearchInput.js +23 -0
- package/dist/components/TabbedListView/SearchInput.js.map +7 -0
- package/dist/components/TabbedListView/TabBar.js +20 -0
- package/dist/components/TabbedListView/TabBar.js.map +7 -0
- package/dist/components/TabbedListView/TabbedListView.js +246 -0
- package/dist/components/TabbedListView/TabbedListView.js.map +7 -0
- package/dist/components/TabbedListView/index.js +11 -0
- package/dist/components/TabbedListView/index.js.map +7 -0
- package/dist/components/TabbedListView/types.js +1 -0
- package/dist/components/TabbedListView/types.js.map +7 -0
- package/dist/components/TodoChangeBlock.js +6 -5
- package/dist/components/TodoChangeBlock.js.map +3 -3
- package/dist/components/TodoPanel.js +6 -3
- package/dist/components/TodoPanel.js.map +3 -3
- package/dist/components/TrustDialog.js +6 -5
- package/dist/components/TrustDialog.js.map +2 -2
- package/dist/components/messages/UserToolResultMessage/UserToolCanceledMessage.js +2 -1
- package/dist/components/messages/UserToolResultMessage/UserToolCanceledMessage.js.map +2 -2
- package/dist/constants/macros.js +1 -1
- package/dist/constants/macros.js.map +1 -1
- package/dist/constants/product.js +2 -2
- package/dist/constants/product.js.map +1 -1
- package/dist/constants/prompts.js +17 -0
- package/dist/constants/prompts.js.map +2 -2
- package/dist/constants/toolInputExamples.js +5 -1
- package/dist/constants/toolInputExamples.js.map +2 -2
- package/dist/core/backupHook.js +29 -0
- package/dist/core/backupHook.js.map +7 -0
- package/dist/core/config/defaults.js +8 -2
- package/dist/core/config/defaults.js.map +2 -2
- package/dist/core/config/schema.js +14 -2
- package/dist/core/config/schema.js.map +2 -2
- package/dist/core/costTracker.js +0 -16
- package/dist/core/costTracker.js.map +2 -2
- package/dist/core/tokenStatsManager.js +5 -0
- package/dist/core/tokenStatsManager.js.map +2 -2
- package/dist/cost-tracker.js +0 -16
- package/dist/cost-tracker.js.map +2 -2
- package/dist/entrypoints/bootstrap.js +56 -0
- package/dist/entrypoints/bootstrap.js.map +7 -0
- package/dist/entrypoints/cli.js +164 -23
- package/dist/entrypoints/cli.js.map +3 -3
- package/dist/history.js +75 -15
- package/dist/history.js.map +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/index.js.map +2 -2
- package/dist/i18n/locales/en.js +582 -1
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/zh-CN.js +582 -1
- package/dist/i18n/locales/zh-CN.js.map +2 -2
- package/dist/i18n/types.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +2 -2
- package/dist/messages.js +11 -0
- package/dist/messages.js.map +2 -2
- package/dist/permissions.js.map +2 -2
- package/dist/query.js +9 -0
- package/dist/query.js.map +2 -2
- package/dist/screens/REPL.js +45 -7
- package/dist/screens/REPL.js.map +2 -2
- package/dist/services/customCommands.js +44 -16
- package/dist/services/customCommands.js.map +2 -2
- package/dist/services/plugins/lspServers.js +1 -1
- package/dist/services/plugins/lspServers.js.map +2 -2
- package/dist/services/plugins/pluginRuntime.js +2 -1
- package/dist/services/plugins/pluginRuntime.js.map +2 -2
- package/dist/services/plugins/pluginValidation.js +10 -3
- package/dist/services/plugins/pluginValidation.js.map +2 -2
- package/dist/services/plugins/skillMarketplace.js +16 -8
- package/dist/services/plugins/skillMarketplace.js.map +2 -2
- package/dist/services/systemReminder.js +17 -6
- package/dist/services/systemReminder.js.map +2 -2
- package/dist/tools/FileEditTool/FileEditTool.js +7 -0
- package/dist/tools/FileEditTool/FileEditTool.js.map +2 -2
- package/dist/tools/FileWriteTool/FileWriteTool.js +7 -0
- package/dist/tools/FileWriteTool/FileWriteTool.js.map +2 -2
- package/dist/tools/MultiEditTool/MultiEditTool.js +7 -0
- package/dist/tools/MultiEditTool/MultiEditTool.js.map +2 -2
- package/dist/tools/NotebookEditTool/NotebookEditTool.js +2 -0
- package/dist/tools/NotebookEditTool/NotebookEditTool.js.map +2 -2
- package/dist/tools/TaskTool/TaskTool.js +179 -1
- package/dist/tools/TaskTool/TaskTool.js.map +2 -2
- package/dist/tools/TodoWriteTool/prompt.js +21 -0
- package/dist/tools/TodoWriteTool/prompt.js.map +2 -2
- package/dist/tools/URLFetcherTool/prompt.js +14 -9
- package/dist/tools/URLFetcherTool/prompt.js.map +2 -2
- package/dist/tools/WebSearchTool/prompt.js +12 -6
- package/dist/tools/WebSearchTool/prompt.js.map +2 -2
- package/dist/types/PermissionMode.js +30 -1
- package/dist/types/PermissionMode.js.map +2 -2
- package/dist/types/plugin.js +2 -4
- package/dist/types/plugin.js.map +2 -2
- package/dist/utils/agentHookExecutor.js +103 -0
- package/dist/utils/agentHookExecutor.js.map +7 -0
- package/dist/utils/agentLoader.js +272 -32
- package/dist/utils/agentLoader.js.map +2 -2
- package/dist/utils/agentMemory.js +134 -0
- package/dist/utils/agentMemory.js.map +7 -0
- package/dist/utils/claudeCodeSync.js +439 -0
- package/dist/utils/claudeCodeSync.js.map +7 -0
- package/dist/utils/config.js +52 -24
- package/dist/utils/config.js.map +2 -2
- package/dist/utils/configPaths.js +199 -0
- package/dist/utils/configPaths.js.map +7 -0
- package/dist/utils/execFileNoThrow.js +2 -1
- package/dist/utils/execFileNoThrow.js.map +2 -2
- package/dist/utils/historyManager.js +234 -0
- package/dist/utils/historyManager.js.map +7 -0
- package/dist/utils/marketplaceManager.js +80 -43
- package/dist/utils/marketplaceManager.js.map +2 -2
- package/dist/utils/messages.js +13 -8
- package/dist/utils/messages.js.map +2 -2
- package/dist/utils/migration/index.js +37 -0
- package/dist/utils/migration/index.js.map +7 -0
- package/dist/utils/migration/migrateHistory.js +273 -0
- package/dist/utils/migration/migrateHistory.js.map +7 -0
- package/dist/utils/migration/migrateTodos.js +323 -0
- package/dist/utils/migration/migrateTodos.js.map +7 -0
- package/dist/utils/pasteCache.js +309 -0
- package/dist/utils/pasteCache.js.map +7 -0
- package/dist/utils/pluginInstaller.js +34 -24
- package/dist/utils/pluginInstaller.js.map +2 -2
- package/dist/utils/pluginLoader.js +54 -28
- package/dist/utils/pluginLoader.js.map +2 -2
- package/dist/utils/repoFetcher.js +110 -0
- package/dist/utils/repoFetcher.js.map +7 -0
- package/dist/utils/sessionIndex.js +192 -0
- package/dist/utils/sessionIndex.js.map +7 -0
- package/dist/utils/sessionTracker.js +170 -0
- package/dist/utils/sessionTracker.js.map +7 -0
- package/dist/utils/skillLoader.js +103 -5
- package/dist/utils/skillLoader.js.map +2 -2
- package/dist/utils/stats.js +417 -0
- package/dist/utils/stats.js.map +7 -0
- package/dist/utils/stringSubstitution.js +106 -0
- package/dist/utils/stringSubstitution.js.map +7 -0
- package/dist/utils/teamConfig.js +156 -14
- package/dist/utils/teamConfig.js.map +2 -2
- package/dist/utils/terminal.js +1 -1
- package/dist/utils/terminal.js.map +2 -2
- package/dist/utils/todoStorage.js +51 -19
- package/dist/utils/todoStorage.js.map +2 -2
- package/dist/utils/tooling/safeRender.js.map +2 -2
- package/dist/version.js +2 -2
- package/dist/version.js.map +1 -1
- package/package.json +71 -28
- package/scripts/{postinstall.js → postinstall.cjs} +1 -1
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
import { CONFIG_PATHS } from "./configPaths.js";
|
|
4
|
+
import { debug as debugLogger } from "./debugLogger.js";
|
|
5
|
+
const STATS_VERSION = 1;
|
|
6
|
+
const MAX_DAILY_RECORDS = 90;
|
|
7
|
+
const SAVE_DEBOUNCE_MS = 5e3;
|
|
8
|
+
const CURRENT_SESSION_ID = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
9
|
+
class StatsManager {
|
|
10
|
+
static instance = null;
|
|
11
|
+
stats;
|
|
12
|
+
isDirty = false;
|
|
13
|
+
saveTimeout = null;
|
|
14
|
+
sessionStartTime = null;
|
|
15
|
+
constructor() {
|
|
16
|
+
this.stats = this.loadStats();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get singleton instance
|
|
20
|
+
*/
|
|
21
|
+
static getInstance() {
|
|
22
|
+
if (!StatsManager.instance) {
|
|
23
|
+
StatsManager.instance = new StatsManager();
|
|
24
|
+
}
|
|
25
|
+
return StatsManager.instance;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Load stats from disk or create default
|
|
29
|
+
*/
|
|
30
|
+
loadStats() {
|
|
31
|
+
const statsPath = CONFIG_PATHS.activityFile;
|
|
32
|
+
try {
|
|
33
|
+
if (existsSync(statsPath)) {
|
|
34
|
+
const data = readFileSync(statsPath, "utf-8");
|
|
35
|
+
const parsed = JSON.parse(data);
|
|
36
|
+
if (parsed.version !== STATS_VERSION) {
|
|
37
|
+
return this.migrateStats(parsed);
|
|
38
|
+
}
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
debugLogger.warn("STATS_LOAD_ERROR", {
|
|
43
|
+
error: error instanceof Error ? error.message : String(error)
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return this.createDefaultStats();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create default empty stats structure
|
|
50
|
+
*/
|
|
51
|
+
createDefaultStats() {
|
|
52
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
53
|
+
return {
|
|
54
|
+
version: STATS_VERSION,
|
|
55
|
+
lastUpdated: now,
|
|
56
|
+
dailyActivity: [],
|
|
57
|
+
modelUsage: {},
|
|
58
|
+
toolUsage: {},
|
|
59
|
+
lifetime: {
|
|
60
|
+
totalMessages: 0,
|
|
61
|
+
totalSessions: 0,
|
|
62
|
+
totalToolCalls: 0,
|
|
63
|
+
totalTokens: 0,
|
|
64
|
+
totalCost: 0,
|
|
65
|
+
firstUsed: now
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Migrate older stats versions
|
|
71
|
+
*/
|
|
72
|
+
migrateStats(oldStats) {
|
|
73
|
+
debugLogger.info("STATS_MIGRATION", {
|
|
74
|
+
fromVersion: oldStats.version,
|
|
75
|
+
toVersion: STATS_VERSION
|
|
76
|
+
});
|
|
77
|
+
return this.createDefaultStats();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get today's date string in YYYY-MM-DD format
|
|
81
|
+
*/
|
|
82
|
+
getTodayString() {
|
|
83
|
+
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get or create today's activity record
|
|
87
|
+
*/
|
|
88
|
+
getOrCreateTodayActivity() {
|
|
89
|
+
const today = this.getTodayString();
|
|
90
|
+
let activity = this.stats.dailyActivity.find((a) => a.date === today);
|
|
91
|
+
if (!activity) {
|
|
92
|
+
activity = {
|
|
93
|
+
date: today,
|
|
94
|
+
messageCount: 0,
|
|
95
|
+
sessionCount: 0,
|
|
96
|
+
toolCallCount: 0,
|
|
97
|
+
tokensUsed: 0,
|
|
98
|
+
costEstimate: 0
|
|
99
|
+
};
|
|
100
|
+
this.stats.dailyActivity.push(activity);
|
|
101
|
+
if (this.stats.dailyActivity.length > MAX_DAILY_RECORDS) {
|
|
102
|
+
this.stats.dailyActivity = this.stats.dailyActivity.sort((a, b) => b.date.localeCompare(a.date)).slice(0, MAX_DAILY_RECORDS);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return activity;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Schedule a debounced save
|
|
109
|
+
*/
|
|
110
|
+
scheduleSave() {
|
|
111
|
+
this.isDirty = true;
|
|
112
|
+
if (this.saveTimeout) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.saveTimeout = setTimeout(() => {
|
|
116
|
+
this.saveStatsSync();
|
|
117
|
+
this.saveTimeout = null;
|
|
118
|
+
}, SAVE_DEBOUNCE_MS);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Save stats to disk (synchronous, for debounced writes)
|
|
122
|
+
*/
|
|
123
|
+
saveStatsSync() {
|
|
124
|
+
if (!this.isDirty) return;
|
|
125
|
+
const statsPath = CONFIG_PATHS.activityFile;
|
|
126
|
+
try {
|
|
127
|
+
const dir = dirname(statsPath);
|
|
128
|
+
if (!existsSync(dir)) {
|
|
129
|
+
mkdirSync(dir, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
this.stats.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
132
|
+
writeFileSync(statsPath, JSON.stringify(this.stats, null, 2), "utf-8");
|
|
133
|
+
this.isDirty = false;
|
|
134
|
+
debugLogger.trace("STATS_SAVED", { path: statsPath });
|
|
135
|
+
} catch (error) {
|
|
136
|
+
debugLogger.error("STATS_SAVE_ERROR", {
|
|
137
|
+
error: error instanceof Error ? error.message : String(error)
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ============================================
|
|
142
|
+
// Public API - Recording Methods
|
|
143
|
+
// ============================================
|
|
144
|
+
/**
|
|
145
|
+
* Record a message (API call)
|
|
146
|
+
*
|
|
147
|
+
* Call this after each API call completes.
|
|
148
|
+
*
|
|
149
|
+
* @param model - Model name used
|
|
150
|
+
* @param inputTokens - Number of input tokens
|
|
151
|
+
* @param outputTokens - Number of output tokens
|
|
152
|
+
* @param costUSD - Estimated cost in USD (optional)
|
|
153
|
+
*/
|
|
154
|
+
recordMessage(model, inputTokens, outputTokens, costUSD = 0) {
|
|
155
|
+
const totalTokens = inputTokens + outputTokens;
|
|
156
|
+
const today = this.getOrCreateTodayActivity();
|
|
157
|
+
today.messageCount += 1;
|
|
158
|
+
today.tokensUsed += totalTokens;
|
|
159
|
+
today.costEstimate += costUSD;
|
|
160
|
+
if (!this.stats.modelUsage[model]) {
|
|
161
|
+
this.stats.modelUsage[model] = {
|
|
162
|
+
model,
|
|
163
|
+
calls: 0,
|
|
164
|
+
tokens: 0,
|
|
165
|
+
inputTokens: 0,
|
|
166
|
+
outputTokens: 0,
|
|
167
|
+
costEstimate: 0
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const modelStats = this.stats.modelUsage[model];
|
|
171
|
+
modelStats.calls += 1;
|
|
172
|
+
modelStats.tokens += totalTokens;
|
|
173
|
+
modelStats.inputTokens += inputTokens;
|
|
174
|
+
modelStats.outputTokens += outputTokens;
|
|
175
|
+
modelStats.costEstimate += costUSD;
|
|
176
|
+
this.stats.lifetime.totalMessages += 1;
|
|
177
|
+
this.stats.lifetime.totalTokens += totalTokens;
|
|
178
|
+
this.stats.lifetime.totalCost += costUSD;
|
|
179
|
+
if (this.stats.currentSession) {
|
|
180
|
+
this.stats.currentSession.messageCount += 1;
|
|
181
|
+
this.stats.currentSession.model = model;
|
|
182
|
+
}
|
|
183
|
+
this.scheduleSave();
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Record a tool call
|
|
187
|
+
*
|
|
188
|
+
* Call this when a tool execution completes.
|
|
189
|
+
*
|
|
190
|
+
* @param toolName - Name of the tool
|
|
191
|
+
* @param isError - Whether the execution resulted in an error
|
|
192
|
+
*/
|
|
193
|
+
recordToolCall(toolName, isError = false) {
|
|
194
|
+
const today = this.getOrCreateTodayActivity();
|
|
195
|
+
today.toolCallCount += 1;
|
|
196
|
+
if (!this.stats.toolUsage[toolName]) {
|
|
197
|
+
this.stats.toolUsage[toolName] = {
|
|
198
|
+
toolName,
|
|
199
|
+
calls: 0,
|
|
200
|
+
successCount: 0,
|
|
201
|
+
errorCount: 0
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const toolStats = this.stats.toolUsage[toolName];
|
|
205
|
+
toolStats.calls += 1;
|
|
206
|
+
if (isError) {
|
|
207
|
+
toolStats.errorCount += 1;
|
|
208
|
+
} else {
|
|
209
|
+
toolStats.successCount += 1;
|
|
210
|
+
}
|
|
211
|
+
this.stats.lifetime.totalToolCalls += 1;
|
|
212
|
+
this.scheduleSave();
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Record session start
|
|
216
|
+
*
|
|
217
|
+
* Call this when a new REPL session starts.
|
|
218
|
+
*/
|
|
219
|
+
recordSessionStart() {
|
|
220
|
+
this.sessionStartTime = Date.now();
|
|
221
|
+
const today = this.getOrCreateTodayActivity();
|
|
222
|
+
today.sessionCount += 1;
|
|
223
|
+
this.stats.lifetime.totalSessions += 1;
|
|
224
|
+
this.stats.currentSession = {
|
|
225
|
+
sessionId: CURRENT_SESSION_ID,
|
|
226
|
+
startTime: this.sessionStartTime,
|
|
227
|
+
messageCount: 0
|
|
228
|
+
};
|
|
229
|
+
this.scheduleSave();
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Record session end
|
|
233
|
+
*
|
|
234
|
+
* Call this when a REPL session ends.
|
|
235
|
+
*
|
|
236
|
+
* @param durationMs - Session duration in milliseconds (optional, auto-calculated if sessionStart was called)
|
|
237
|
+
*/
|
|
238
|
+
recordSessionEnd(durationMs) {
|
|
239
|
+
const endTime = Date.now();
|
|
240
|
+
if (this.stats.currentSession) {
|
|
241
|
+
this.stats.currentSession.endTime = endTime;
|
|
242
|
+
this.stats.currentSession.durationMs = durationMs ?? (this.sessionStartTime ? endTime - this.sessionStartTime : 0);
|
|
243
|
+
}
|
|
244
|
+
this.sessionStartTime = null;
|
|
245
|
+
this.isDirty = true;
|
|
246
|
+
this.saveStatsSync();
|
|
247
|
+
}
|
|
248
|
+
// ============================================
|
|
249
|
+
// Public API - Query Methods
|
|
250
|
+
// ============================================
|
|
251
|
+
/**
|
|
252
|
+
* Get the complete stats data
|
|
253
|
+
*/
|
|
254
|
+
getStats() {
|
|
255
|
+
return { ...this.stats };
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Save stats (use for manual saves, e.g., before exit)
|
|
259
|
+
*/
|
|
260
|
+
saveStats(stats) {
|
|
261
|
+
this.stats = stats;
|
|
262
|
+
this.isDirty = true;
|
|
263
|
+
this.saveStatsSync();
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get today's activity statistics
|
|
267
|
+
*/
|
|
268
|
+
getTodayStats() {
|
|
269
|
+
const today = this.getTodayString();
|
|
270
|
+
return this.stats.dailyActivity.find((a) => a.date === today) ?? null;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Get statistics for the last N days
|
|
274
|
+
*
|
|
275
|
+
* @param days - Number of days to retrieve
|
|
276
|
+
*/
|
|
277
|
+
getStatsForDays(days) {
|
|
278
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
279
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
280
|
+
const cutoffString = cutoffDate.toISOString().split("T")[0];
|
|
281
|
+
return this.stats.dailyActivity.filter((a) => a.date >= cutoffString).sort((a, b) => b.date.localeCompare(a.date));
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Get model usage statistics
|
|
285
|
+
*/
|
|
286
|
+
getModelUsageStats() {
|
|
287
|
+
return Object.values(this.stats.modelUsage).sort(
|
|
288
|
+
(a, b) => b.calls - a.calls
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Get tool usage statistics
|
|
293
|
+
*/
|
|
294
|
+
getToolUsageStats() {
|
|
295
|
+
return Object.values(this.stats.toolUsage).sort((a, b) => b.calls - a.calls);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Get lifetime statistics summary
|
|
299
|
+
*/
|
|
300
|
+
getLifetimeStats() {
|
|
301
|
+
return { ...this.stats.lifetime };
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Estimate cost for a given model and token count
|
|
305
|
+
*
|
|
306
|
+
* Simple cost estimation based on known model pricing.
|
|
307
|
+
* Returns 0 for unknown models.
|
|
308
|
+
*
|
|
309
|
+
* @param model - Model name
|
|
310
|
+
* @param inputTokens - Number of input tokens
|
|
311
|
+
* @param outputTokens - Number of output tokens
|
|
312
|
+
*/
|
|
313
|
+
estimateCost(model, inputTokens, outputTokens) {
|
|
314
|
+
const pricing = {
|
|
315
|
+
// Claude models
|
|
316
|
+
"claude-3-opus": { input: 15, output: 75 },
|
|
317
|
+
"claude-3-sonnet": { input: 3, output: 15 },
|
|
318
|
+
"claude-3-haiku": { input: 0.25, output: 1.25 },
|
|
319
|
+
"claude-sonnet-4": { input: 3, output: 15 },
|
|
320
|
+
// GPT models
|
|
321
|
+
"gpt-4": { input: 30, output: 60 },
|
|
322
|
+
"gpt-4-turbo": { input: 10, output: 30 },
|
|
323
|
+
"gpt-4o": { input: 5, output: 15 },
|
|
324
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
325
|
+
"gpt-3.5-turbo": { input: 0.5, output: 1.5 },
|
|
326
|
+
// Default for unknown models
|
|
327
|
+
default: { input: 3, output: 15 }
|
|
328
|
+
};
|
|
329
|
+
let modelPricing = pricing.default;
|
|
330
|
+
for (const [key, price] of Object.entries(pricing)) {
|
|
331
|
+
if (key !== "default" && model.toLowerCase().includes(key)) {
|
|
332
|
+
modelPricing = price;
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return inputTokens / 1e6 * modelPricing.input + outputTokens / 1e6 * modelPricing.output;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Reset statistics (for testing)
|
|
340
|
+
*/
|
|
341
|
+
resetForTests() {
|
|
342
|
+
if (process.env.NODE_ENV !== "test") {
|
|
343
|
+
throw new Error("resetForTests can only be called in tests");
|
|
344
|
+
}
|
|
345
|
+
this.stats = this.createDefaultStats();
|
|
346
|
+
this.isDirty = false;
|
|
347
|
+
this.sessionStartTime = null;
|
|
348
|
+
if (this.saveTimeout) {
|
|
349
|
+
clearTimeout(this.saveTimeout);
|
|
350
|
+
this.saveTimeout = null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Force immediate save (for graceful shutdown)
|
|
355
|
+
*/
|
|
356
|
+
forceImmediateSave() {
|
|
357
|
+
if (this.saveTimeout) {
|
|
358
|
+
clearTimeout(this.saveTimeout);
|
|
359
|
+
this.saveTimeout = null;
|
|
360
|
+
}
|
|
361
|
+
this.isDirty = true;
|
|
362
|
+
this.saveStatsSync();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const statsManager = StatsManager.getInstance();
|
|
366
|
+
function getStats() {
|
|
367
|
+
return statsManager.getStats();
|
|
368
|
+
}
|
|
369
|
+
function saveStats(stats) {
|
|
370
|
+
statsManager.saveStats(stats);
|
|
371
|
+
}
|
|
372
|
+
function recordMessage(model, inputTokens, outputTokens, costUSD) {
|
|
373
|
+
statsManager.recordMessage(model, inputTokens, outputTokens, costUSD);
|
|
374
|
+
}
|
|
375
|
+
function recordToolCall(toolName, isError) {
|
|
376
|
+
statsManager.recordToolCall(toolName, isError);
|
|
377
|
+
}
|
|
378
|
+
function recordSessionStart() {
|
|
379
|
+
statsManager.recordSessionStart();
|
|
380
|
+
}
|
|
381
|
+
function recordSessionEnd(durationMs) {
|
|
382
|
+
statsManager.recordSessionEnd(durationMs);
|
|
383
|
+
}
|
|
384
|
+
function getTodayStats() {
|
|
385
|
+
return statsManager.getTodayStats();
|
|
386
|
+
}
|
|
387
|
+
function getStatsForDays(days) {
|
|
388
|
+
return statsManager.getStatsForDays(days);
|
|
389
|
+
}
|
|
390
|
+
function getModelUsageStats() {
|
|
391
|
+
return statsManager.getModelUsageStats();
|
|
392
|
+
}
|
|
393
|
+
function getToolUsageStats() {
|
|
394
|
+
return statsManager.getToolUsageStats();
|
|
395
|
+
}
|
|
396
|
+
function estimateCost(model, inputTokens, outputTokens) {
|
|
397
|
+
return statsManager.estimateCost(model, inputTokens, outputTokens);
|
|
398
|
+
}
|
|
399
|
+
function forceImmediateSave() {
|
|
400
|
+
statsManager.forceImmediateSave();
|
|
401
|
+
}
|
|
402
|
+
export {
|
|
403
|
+
estimateCost,
|
|
404
|
+
forceImmediateSave,
|
|
405
|
+
getModelUsageStats,
|
|
406
|
+
getStats,
|
|
407
|
+
getStatsForDays,
|
|
408
|
+
getTodayStats,
|
|
409
|
+
getToolUsageStats,
|
|
410
|
+
recordMessage,
|
|
411
|
+
recordSessionEnd,
|
|
412
|
+
recordSessionStart,
|
|
413
|
+
recordToolCall,
|
|
414
|
+
saveStats,
|
|
415
|
+
statsManager
|
|
416
|
+
};
|
|
417
|
+
//# sourceMappingURL=stats.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/utils/stats.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Usage Statistics Manager\n *\n * Tracks and persists usage statistics across sessions.\n * Integrates with token tracking, tool execution, and session lifecycle.\n *\n * Data is stored at ~/.minto/stats/activity.json\n *\n * Features:\n * - Daily activity aggregation\n * - Model usage breakdown\n * - Tool call tracking\n * - Session duration tracking\n * - Cost estimation\n *\n * Performance:\n * - Uses in-memory cache with periodic file persistence\n * - Debounced writes to avoid excessive disk I/O\n * - Non-blocking async saves that don't block main flow\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'\nimport { dirname } from 'path'\nimport { CONFIG_PATHS } from './configPaths'\nimport { debug as debugLogger } from './debugLogger'\n\n// ============================================\n// Type Definitions\n// ============================================\n\n/**\n * Daily activity statistics\n */\nexport interface DailyActivity {\n /** Date in YYYY-MM-DD format */\n date: string\n /** Number of messages sent/received */\n messageCount: number\n /** Number of sessions started */\n sessionCount: number\n /** Number of tool calls executed */\n toolCallCount: number\n /** Total tokens used (input + output) */\n tokensUsed: number\n /** Estimated cost in USD */\n costEstimate: number\n}\n\n/**\n * Model-specific usage statistics\n */\nexport interface ModelUsage {\n /** Model name/identifier */\n model: string\n /** Number of API calls to this model */\n calls: number\n /** Total tokens used by this model */\n tokens: number\n /** Input tokens */\n inputTokens: number\n /** Output tokens */\n outputTokens: number\n /** Estimated cost for this model */\n costEstimate: number\n}\n\n/**\n * Tool-specific usage statistics\n */\nexport interface ToolUsage {\n /** Tool name */\n toolName: string\n /** Number of times this tool was called */\n calls: number\n /** Number of successful executions */\n successCount: number\n /** Number of failed executions */\n errorCount: number\n}\n\n/**\n * Session information\n */\nexport interface SessionInfo {\n /** Session ID */\n sessionId: string\n /** Start timestamp */\n startTime: number\n /** End timestamp (if completed) */\n endTime?: number\n /** Duration in milliseconds */\n durationMs?: number\n /** Number of messages in this session */\n messageCount: number\n /** Model used */\n model?: string\n}\n\n/**\n * Complete statistics data structure\n */\nexport interface StatsData {\n /** Schema version for future migrations */\n version: number\n /** Last update timestamp */\n lastUpdated: string\n /** Daily activity records (last 90 days) */\n dailyActivity: DailyActivity[]\n /** Model usage breakdown */\n modelUsage: Record<string, ModelUsage>\n /** Tool usage breakdown */\n toolUsage: Record<string, ToolUsage>\n /** Current session info */\n currentSession?: SessionInfo\n /** Total lifetime statistics */\n lifetime: {\n totalMessages: number\n totalSessions: number\n totalToolCalls: number\n totalTokens: number\n totalCost: number\n firstUsed: string\n }\n}\n\n// ============================================\n// Constants\n// ============================================\n\nconst STATS_VERSION = 1\nconst MAX_DAILY_RECORDS = 90 // Keep 90 days of history\nconst SAVE_DEBOUNCE_MS = 5000 // Debounce file writes to 5 seconds\nconst CURRENT_SESSION_ID = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`\n\n// ============================================\n// Stats Manager Implementation\n// ============================================\n\n/**\n * StatsManager - Singleton class for managing usage statistics\n */\nclass StatsManager {\n private static instance: StatsManager | null = null\n private stats: StatsData\n private isDirty: boolean = false\n private saveTimeout: NodeJS.Timeout | null = null\n private sessionStartTime: number | null = null\n\n private constructor() {\n this.stats = this.loadStats()\n }\n\n /**\n * Get singleton instance\n */\n static getInstance(): StatsManager {\n if (!StatsManager.instance) {\n StatsManager.instance = new StatsManager()\n }\n return StatsManager.instance\n }\n\n /**\n * Load stats from disk or create default\n */\n private loadStats(): StatsData {\n const statsPath = CONFIG_PATHS.activityFile\n\n try {\n if (existsSync(statsPath)) {\n const data = readFileSync(statsPath, 'utf-8')\n const parsed = JSON.parse(data) as StatsData\n\n // Validate version and migrate if needed\n if (parsed.version !== STATS_VERSION) {\n return this.migrateStats(parsed)\n }\n\n return parsed\n }\n } catch (error) {\n debugLogger.warn('STATS_LOAD_ERROR', {\n error: error instanceof Error ? error.message : String(error),\n })\n }\n\n return this.createDefaultStats()\n }\n\n /**\n * Create default empty stats structure\n */\n private createDefaultStats(): StatsData {\n const now = new Date().toISOString()\n return {\n version: STATS_VERSION,\n lastUpdated: now,\n dailyActivity: [],\n modelUsage: {},\n toolUsage: {},\n lifetime: {\n totalMessages: 0,\n totalSessions: 0,\n totalToolCalls: 0,\n totalTokens: 0,\n totalCost: 0,\n firstUsed: now,\n },\n }\n }\n\n /**\n * Migrate older stats versions\n */\n private migrateStats(oldStats: any): StatsData {\n // For now, just create fresh stats\n // Future versions can add migration logic here\n debugLogger.info('STATS_MIGRATION', {\n fromVersion: oldStats.version,\n toVersion: STATS_VERSION,\n })\n return this.createDefaultStats()\n }\n\n /**\n * Get today's date string in YYYY-MM-DD format\n */\n private getTodayString(): string {\n return new Date().toISOString().split('T')[0]!\n }\n\n /**\n * Get or create today's activity record\n */\n private getOrCreateTodayActivity(): DailyActivity {\n const today = this.getTodayString()\n let activity = this.stats.dailyActivity.find(a => a.date === today)\n\n if (!activity) {\n activity = {\n date: today,\n messageCount: 0,\n sessionCount: 0,\n toolCallCount: 0,\n tokensUsed: 0,\n costEstimate: 0,\n }\n this.stats.dailyActivity.push(activity)\n\n // Prune old records\n if (this.stats.dailyActivity.length > MAX_DAILY_RECORDS) {\n this.stats.dailyActivity = this.stats.dailyActivity\n .sort((a, b) => b.date.localeCompare(a.date))\n .slice(0, MAX_DAILY_RECORDS)\n }\n }\n\n return activity\n }\n\n /**\n * Schedule a debounced save\n */\n private scheduleSave(): void {\n this.isDirty = true\n\n if (this.saveTimeout) {\n return // Already scheduled\n }\n\n this.saveTimeout = setTimeout(() => {\n this.saveStatsSync()\n this.saveTimeout = null\n }, SAVE_DEBOUNCE_MS)\n }\n\n /**\n * Save stats to disk (synchronous, for debounced writes)\n */\n private saveStatsSync(): void {\n if (!this.isDirty) return\n\n const statsPath = CONFIG_PATHS.activityFile\n\n try {\n // Ensure directory exists\n const dir = dirname(statsPath)\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true })\n }\n\n this.stats.lastUpdated = new Date().toISOString()\n writeFileSync(statsPath, JSON.stringify(this.stats, null, 2), 'utf-8')\n this.isDirty = false\n\n debugLogger.trace('STATS_SAVED', { path: statsPath })\n } catch (error) {\n debugLogger.error('STATS_SAVE_ERROR', {\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n\n // ============================================\n // Public API - Recording Methods\n // ============================================\n\n /**\n * Record a message (API call)\n *\n * Call this after each API call completes.\n *\n * @param model - Model name used\n * @param inputTokens - Number of input tokens\n * @param outputTokens - Number of output tokens\n * @param costUSD - Estimated cost in USD (optional)\n */\n recordMessage(\n model: string,\n inputTokens: number,\n outputTokens: number,\n costUSD: number = 0,\n ): void {\n const totalTokens = inputTokens + outputTokens\n\n // Update daily activity\n const today = this.getOrCreateTodayActivity()\n today.messageCount += 1\n today.tokensUsed += totalTokens\n today.costEstimate += costUSD\n\n // Update model usage\n if (!this.stats.modelUsage[model]) {\n this.stats.modelUsage[model] = {\n model,\n calls: 0,\n tokens: 0,\n inputTokens: 0,\n outputTokens: 0,\n costEstimate: 0,\n }\n }\n const modelStats = this.stats.modelUsage[model]!\n modelStats.calls += 1\n modelStats.tokens += totalTokens\n modelStats.inputTokens += inputTokens\n modelStats.outputTokens += outputTokens\n modelStats.costEstimate += costUSD\n\n // Update lifetime stats\n this.stats.lifetime.totalMessages += 1\n this.stats.lifetime.totalTokens += totalTokens\n this.stats.lifetime.totalCost += costUSD\n\n // Update current session if active\n if (this.stats.currentSession) {\n this.stats.currentSession.messageCount += 1\n this.stats.currentSession.model = model\n }\n\n this.scheduleSave()\n }\n\n /**\n * Record a tool call\n *\n * Call this when a tool execution completes.\n *\n * @param toolName - Name of the tool\n * @param isError - Whether the execution resulted in an error\n */\n recordToolCall(toolName: string, isError: boolean = false): void {\n // Update daily activity\n const today = this.getOrCreateTodayActivity()\n today.toolCallCount += 1\n\n // Update tool usage\n if (!this.stats.toolUsage[toolName]) {\n this.stats.toolUsage[toolName] = {\n toolName,\n calls: 0,\n successCount: 0,\n errorCount: 0,\n }\n }\n const toolStats = this.stats.toolUsage[toolName]!\n toolStats.calls += 1\n if (isError) {\n toolStats.errorCount += 1\n } else {\n toolStats.successCount += 1\n }\n\n // Update lifetime stats\n this.stats.lifetime.totalToolCalls += 1\n\n this.scheduleSave()\n }\n\n /**\n * Record session start\n *\n * Call this when a new REPL session starts.\n */\n recordSessionStart(): void {\n this.sessionStartTime = Date.now()\n\n // Update daily activity\n const today = this.getOrCreateTodayActivity()\n today.sessionCount += 1\n\n // Update lifetime stats\n this.stats.lifetime.totalSessions += 1\n\n // Set current session\n this.stats.currentSession = {\n sessionId: CURRENT_SESSION_ID,\n startTime: this.sessionStartTime,\n messageCount: 0,\n }\n\n this.scheduleSave()\n }\n\n /**\n * Record session end\n *\n * Call this when a REPL session ends.\n *\n * @param durationMs - Session duration in milliseconds (optional, auto-calculated if sessionStart was called)\n */\n recordSessionEnd(durationMs?: number): void {\n const endTime = Date.now()\n\n if (this.stats.currentSession) {\n this.stats.currentSession.endTime = endTime\n this.stats.currentSession.durationMs =\n durationMs ??\n (this.sessionStartTime ? endTime - this.sessionStartTime : 0)\n }\n\n this.sessionStartTime = null\n\n // Force immediate save on session end\n this.isDirty = true\n this.saveStatsSync()\n }\n\n // ============================================\n // Public API - Query Methods\n // ============================================\n\n /**\n * Get the complete stats data\n */\n getStats(): StatsData {\n return { ...this.stats }\n }\n\n /**\n * Save stats (use for manual saves, e.g., before exit)\n */\n saveStats(stats: StatsData): void {\n this.stats = stats\n this.isDirty = true\n this.saveStatsSync()\n }\n\n /**\n * Get today's activity statistics\n */\n getTodayStats(): DailyActivity | null {\n const today = this.getTodayString()\n return this.stats.dailyActivity.find(a => a.date === today) ?? null\n }\n\n /**\n * Get statistics for the last N days\n *\n * @param days - Number of days to retrieve\n */\n getStatsForDays(days: number): DailyActivity[] {\n const cutoffDate = new Date()\n cutoffDate.setDate(cutoffDate.getDate() - days)\n const cutoffString = cutoffDate.toISOString().split('T')[0]!\n\n return this.stats.dailyActivity\n .filter(a => a.date >= cutoffString)\n .sort((a, b) => b.date.localeCompare(a.date))\n }\n\n /**\n * Get model usage statistics\n */\n getModelUsageStats(): ModelUsage[] {\n return Object.values(this.stats.modelUsage).sort(\n (a, b) => b.calls - a.calls,\n )\n }\n\n /**\n * Get tool usage statistics\n */\n getToolUsageStats(): ToolUsage[] {\n return Object.values(this.stats.toolUsage).sort((a, b) => b.calls - a.calls)\n }\n\n /**\n * Get lifetime statistics summary\n */\n getLifetimeStats(): StatsData['lifetime'] {\n return { ...this.stats.lifetime }\n }\n\n /**\n * Estimate cost for a given model and token count\n *\n * Simple cost estimation based on known model pricing.\n * Returns 0 for unknown models.\n *\n * @param model - Model name\n * @param inputTokens - Number of input tokens\n * @param outputTokens - Number of output tokens\n */\n estimateCost(\n model: string,\n inputTokens: number,\n outputTokens: number,\n ): number {\n // Default pricing (USD per million tokens)\n const pricing: Record<string, { input: number; output: number }> = {\n // Claude models\n 'claude-3-opus': { input: 15, output: 75 },\n 'claude-3-sonnet': { input: 3, output: 15 },\n 'claude-3-haiku': { input: 0.25, output: 1.25 },\n 'claude-sonnet-4': { input: 3, output: 15 },\n // GPT models\n 'gpt-4': { input: 30, output: 60 },\n 'gpt-4-turbo': { input: 10, output: 30 },\n 'gpt-4o': { input: 5, output: 15 },\n 'gpt-4o-mini': { input: 0.15, output: 0.6 },\n 'gpt-3.5-turbo': { input: 0.5, output: 1.5 },\n // Default for unknown models\n default: { input: 3, output: 15 },\n }\n\n // Find matching pricing (partial match)\n let modelPricing = pricing.default!\n for (const [key, price] of Object.entries(pricing)) {\n if (key !== 'default' && model.toLowerCase().includes(key)) {\n modelPricing = price\n break\n }\n }\n\n return (\n (inputTokens / 1_000_000) * modelPricing.input +\n (outputTokens / 1_000_000) * modelPricing.output\n )\n }\n\n /**\n * Reset statistics (for testing)\n */\n resetForTests(): void {\n if (process.env.NODE_ENV !== 'test') {\n throw new Error('resetForTests can only be called in tests')\n }\n this.stats = this.createDefaultStats()\n this.isDirty = false\n this.sessionStartTime = null\n if (this.saveTimeout) {\n clearTimeout(this.saveTimeout)\n this.saveTimeout = null\n }\n }\n\n /**\n * Force immediate save (for graceful shutdown)\n */\n forceImmediateSave(): void {\n if (this.saveTimeout) {\n clearTimeout(this.saveTimeout)\n this.saveTimeout = null\n }\n this.isDirty = true\n this.saveStatsSync()\n }\n}\n\n// ============================================\n// Exported Singleton and Convenience Functions\n// ============================================\n\nexport const statsManager = StatsManager.getInstance()\n\n/**\n * Get complete statistics data\n */\nexport function getStats(): StatsData {\n return statsManager.getStats()\n}\n\n/**\n * Save statistics data\n */\nexport function saveStats(stats: StatsData): void {\n statsManager.saveStats(stats)\n}\n\n/**\n * Record a message (API call)\n */\nexport function recordMessage(\n model: string,\n inputTokens: number,\n outputTokens: number,\n costUSD?: number,\n): void {\n statsManager.recordMessage(model, inputTokens, outputTokens, costUSD)\n}\n\n/**\n * Record a tool call\n */\nexport function recordToolCall(toolName: string, isError?: boolean): void {\n statsManager.recordToolCall(toolName, isError)\n}\n\n/**\n * Record session start\n */\nexport function recordSessionStart(): void {\n statsManager.recordSessionStart()\n}\n\n/**\n * Record session end\n */\nexport function recordSessionEnd(durationMs?: number): void {\n statsManager.recordSessionEnd(durationMs)\n}\n\n/**\n * Get today's statistics\n */\nexport function getTodayStats(): DailyActivity | null {\n return statsManager.getTodayStats()\n}\n\n/**\n * Get statistics for the last N days\n */\nexport function getStatsForDays(days: number): DailyActivity[] {\n return statsManager.getStatsForDays(days)\n}\n\n/**\n * Get model usage statistics\n */\nexport function getModelUsageStats(): ModelUsage[] {\n return statsManager.getModelUsageStats()\n}\n\n/**\n * Get tool usage statistics\n */\nexport function getToolUsageStats(): ToolUsage[] {\n return statsManager.getToolUsageStats()\n}\n\n/**\n * Estimate cost for model and tokens\n */\nexport function estimateCost(\n model: string,\n inputTokens: number,\n outputTokens: number,\n): number {\n return statsManager.estimateCost(model, inputTokens, outputTokens)\n}\n\n/**\n * Force immediate save (for graceful shutdown)\n */\nexport function forceImmediateSave(): void {\n statsManager.forceImmediateSave()\n}\n"],
|
|
5
|
+
"mappings": "AAqBA,SAAS,YAAY,cAAc,eAAe,iBAAiB;AACnE,SAAS,eAAe;AACxB,SAAS,oBAAoB;AAC7B,SAAS,SAAS,mBAAmB;AAyGrC,MAAM,gBAAgB;AACtB,MAAM,oBAAoB;AAC1B,MAAM,mBAAmB;AACzB,MAAM,qBAAqB,WAAW,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAS1F,MAAM,aAAa;AAAA,EACjB,OAAe,WAAgC;AAAA,EACvC;AAAA,EACA,UAAmB;AAAA,EACnB,cAAqC;AAAA,EACrC,mBAAkC;AAAA,EAElC,cAAc;AACpB,SAAK,QAAQ,KAAK,UAAU;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,cAA4B;AACjC,QAAI,CAAC,aAAa,UAAU;AAC1B,mBAAa,WAAW,IAAI,aAAa;AAAA,IAC3C;AACA,WAAO,aAAa;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKQ,YAAuB;AAC7B,UAAM,YAAY,aAAa;AAE/B,QAAI;AACF,UAAI,WAAW,SAAS,GAAG;AACzB,cAAM,OAAO,aAAa,WAAW,OAAO;AAC5C,cAAM,SAAS,KAAK,MAAM,IAAI;AAG9B,YAAI,OAAO,YAAY,eAAe;AACpC,iBAAO,KAAK,aAAa,MAAM;AAAA,QACjC;AAEA,eAAO;AAAA,MACT;AAAA,IACF,SAAS,OAAO;AACd,kBAAY,KAAK,oBAAoB;AAAA,QACnC,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,MAC9D,CAAC;AAAA,IACH;AAEA,WAAO,KAAK,mBAAmB;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKQ,qBAAgC;AACtC,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,aAAa;AAAA,MACb,eAAe,CAAC;AAAA,MAChB,YAAY,CAAC;AAAA,MACb,WAAW,CAAC;AAAA,MACZ,UAAU;AAAA,QACR,eAAe;AAAA,QACf,eAAe;AAAA,QACf,gBAAgB;AAAA,QAChB,aAAa;AAAA,QACb,WAAW;AAAA,QACX,WAAW;AAAA,MACb;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,aAAa,UAA0B;AAG7C,gBAAY,KAAK,mBAAmB;AAAA,MAClC,aAAa,SAAS;AAAA,MACtB,WAAW;AAAA,IACb,CAAC;AACD,WAAO,KAAK,mBAAmB;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKQ,iBAAyB;AAC/B,YAAO,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA,EAKQ,2BAA0C;AAChD,UAAM,QAAQ,KAAK,eAAe;AAClC,QAAI,WAAW,KAAK,MAAM,cAAc,KAAK,OAAK,EAAE,SAAS,KAAK;AAElE,QAAI,CAAC,UAAU;AACb,iBAAW;AAAA,QACT,MAAM;AAAA,QACN,cAAc;AAAA,QACd,cAAc;AAAA,QACd,eAAe;AAAA,QACf,YAAY;AAAA,QACZ,cAAc;AAAA,MAChB;AACA,WAAK,MAAM,cAAc,KAAK,QAAQ;AAGtC,UAAI,KAAK,MAAM,cAAc,SAAS,mBAAmB;AACvD,aAAK,MAAM,gBAAgB,KAAK,MAAM,cACnC,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC,EAC3C,MAAM,GAAG,iBAAiB;AAAA,MAC/B;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAqB;AAC3B,SAAK,UAAU;AAEf,QAAI,KAAK,aAAa;AACpB;AAAA,IACF;AAEA,SAAK,cAAc,WAAW,MAAM;AAClC,WAAK,cAAc;AACnB,WAAK,cAAc;AAAA,IACrB,GAAG,gBAAgB;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA,EAKQ,gBAAsB;AAC5B,QAAI,CAAC,KAAK,QAAS;AAEnB,UAAM,YAAY,aAAa;AAE/B,QAAI;AAEF,YAAM,MAAM,QAAQ,SAAS;AAC7B,UAAI,CAAC,WAAW,GAAG,GAAG;AACpB,kBAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,MACpC;AAEA,WAAK,MAAM,eAAc,oBAAI,KAAK,GAAE,YAAY;AAChD,oBAAc,WAAW,KAAK,UAAU,KAAK,OAAO,MAAM,CAAC,GAAG,OAAO;AACrE,WAAK,UAAU;AAEf,kBAAY,MAAM,eAAe,EAAE,MAAM,UAAU,CAAC;AAAA,IACtD,SAAS,OAAO;AACd,kBAAY,MAAM,oBAAoB;AAAA,QACpC,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,MAC9D,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,cACE,OACA,aACA,cACA,UAAkB,GACZ;AACN,UAAM,cAAc,cAAc;AAGlC,UAAM,QAAQ,KAAK,yBAAyB;AAC5C,UAAM,gBAAgB;AACtB,UAAM,cAAc;AACpB,UAAM,gBAAgB;AAGtB,QAAI,CAAC,KAAK,MAAM,WAAW,KAAK,GAAG;AACjC,WAAK,MAAM,WAAW,KAAK,IAAI;AAAA,QAC7B;AAAA,QACA,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,aAAa;AAAA,QACb,cAAc;AAAA,QACd,cAAc;AAAA,MAChB;AAAA,IACF;AACA,UAAM,aAAa,KAAK,MAAM,WAAW,KAAK;AAC9C,eAAW,SAAS;AACpB,eAAW,UAAU;AACrB,eAAW,eAAe;AAC1B,eAAW,gBAAgB;AAC3B,eAAW,gBAAgB;AAG3B,SAAK,MAAM,SAAS,iBAAiB;AACrC,SAAK,MAAM,SAAS,eAAe;AACnC,SAAK,MAAM,SAAS,aAAa;AAGjC,QAAI,KAAK,MAAM,gBAAgB;AAC7B,WAAK,MAAM,eAAe,gBAAgB;AAC1C,WAAK,MAAM,eAAe,QAAQ;AAAA,IACpC;AAEA,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,eAAe,UAAkB,UAAmB,OAAa;AAE/D,UAAM,QAAQ,KAAK,yBAAyB;AAC5C,UAAM,iBAAiB;AAGvB,QAAI,CAAC,KAAK,MAAM,UAAU,QAAQ,GAAG;AACnC,WAAK,MAAM,UAAU,QAAQ,IAAI;AAAA,QAC/B;AAAA,QACA,OAAO;AAAA,QACP,cAAc;AAAA,QACd,YAAY;AAAA,MACd;AAAA,IACF;AACA,UAAM,YAAY,KAAK,MAAM,UAAU,QAAQ;AAC/C,cAAU,SAAS;AACnB,QAAI,SAAS;AACX,gBAAU,cAAc;AAAA,IAC1B,OAAO;AACL,gBAAU,gBAAgB;AAAA,IAC5B;AAGA,SAAK,MAAM,SAAS,kBAAkB;AAEtC,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,qBAA2B;AACzB,SAAK,mBAAmB,KAAK,IAAI;AAGjC,UAAM,QAAQ,KAAK,yBAAyB;AAC5C,UAAM,gBAAgB;AAGtB,SAAK,MAAM,SAAS,iBAAiB;AAGrC,SAAK,MAAM,iBAAiB;AAAA,MAC1B,WAAW;AAAA,MACX,WAAW,KAAK;AAAA,MAChB,cAAc;AAAA,IAChB;AAEA,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,iBAAiB,YAA2B;AAC1C,UAAM,UAAU,KAAK,IAAI;AAEzB,QAAI,KAAK,MAAM,gBAAgB;AAC7B,WAAK,MAAM,eAAe,UAAU;AACpC,WAAK,MAAM,eAAe,aACxB,eACC,KAAK,mBAAmB,UAAU,KAAK,mBAAmB;AAAA,IAC/D;AAEA,SAAK,mBAAmB;AAGxB,SAAK,UAAU;AACf,SAAK,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,WAAsB;AACpB,WAAO,EAAE,GAAG,KAAK,MAAM;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU,OAAwB;AAChC,SAAK,QAAQ;AACb,SAAK,UAAU;AACf,SAAK,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAsC;AACpC,UAAM,QAAQ,KAAK,eAAe;AAClC,WAAO,KAAK,MAAM,cAAc,KAAK,OAAK,EAAE,SAAS,KAAK,KAAK;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,gBAAgB,MAA+B;AAC7C,UAAM,aAAa,oBAAI,KAAK;AAC5B,eAAW,QAAQ,WAAW,QAAQ,IAAI,IAAI;AAC9C,UAAM,eAAe,WAAW,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AAE1D,WAAO,KAAK,MAAM,cACf,OAAO,OAAK,EAAE,QAAQ,YAAY,EAClC,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA,EAKA,qBAAmC;AACjC,WAAO,OAAO,OAAO,KAAK,MAAM,UAAU,EAAE;AAAA,MAC1C,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE;AAAA,IACxB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,oBAAiC;AAC/B,WAAO,OAAO,OAAO,KAAK,MAAM,SAAS,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAAA,EAC7E;AAAA;AAAA;AAAA;AAAA,EAKA,mBAA0C;AACxC,WAAO,EAAE,GAAG,KAAK,MAAM,SAAS;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,aACE,OACA,aACA,cACQ;AAER,UAAM,UAA6D;AAAA;AAAA,MAEjE,iBAAiB,EAAE,OAAO,IAAI,QAAQ,GAAG;AAAA,MACzC,mBAAmB,EAAE,OAAO,GAAG,QAAQ,GAAG;AAAA,MAC1C,kBAAkB,EAAE,OAAO,MAAM,QAAQ,KAAK;AAAA,MAC9C,mBAAmB,EAAE,OAAO,GAAG,QAAQ,GAAG;AAAA;AAAA,MAE1C,SAAS,EAAE,OAAO,IAAI,QAAQ,GAAG;AAAA,MACjC,eAAe,EAAE,OAAO,IAAI,QAAQ,GAAG;AAAA,MACvC,UAAU,EAAE,OAAO,GAAG,QAAQ,GAAG;AAAA,MACjC,eAAe,EAAE,OAAO,MAAM,QAAQ,IAAI;AAAA,MAC1C,iBAAiB,EAAE,OAAO,KAAK,QAAQ,IAAI;AAAA;AAAA,MAE3C,SAAS,EAAE,OAAO,GAAG,QAAQ,GAAG;AAAA,IAClC;AAGA,QAAI,eAAe,QAAQ;AAC3B,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAClD,UAAI,QAAQ,aAAa,MAAM,YAAY,EAAE,SAAS,GAAG,GAAG;AAC1D,uBAAe;AACf;AAAA,MACF;AAAA,IACF;AAEA,WACG,cAAc,MAAa,aAAa,QACxC,eAAe,MAAa,aAAa;AAAA,EAE9C;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAsB;AACpB,QAAI,QAAQ,IAAI,aAAa,QAAQ;AACnC,YAAM,IAAI,MAAM,2CAA2C;AAAA,IAC7D;AACA,SAAK,QAAQ,KAAK,mBAAmB;AACrC,SAAK,UAAU;AACf,SAAK,mBAAmB;AACxB,QAAI,KAAK,aAAa;AACpB,mBAAa,KAAK,WAAW;AAC7B,WAAK,cAAc;AAAA,IACrB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,qBAA2B;AACzB,QAAI,KAAK,aAAa;AACpB,mBAAa,KAAK,WAAW;AAC7B,WAAK,cAAc;AAAA,IACrB;AACA,SAAK,UAAU;AACf,SAAK,cAAc;AAAA,EACrB;AACF;AAMO,MAAM,eAAe,aAAa,YAAY;AAK9C,SAAS,WAAsB;AACpC,SAAO,aAAa,SAAS;AAC/B;AAKO,SAAS,UAAU,OAAwB;AAChD,eAAa,UAAU,KAAK;AAC9B;AAKO,SAAS,cACd,OACA,aACA,cACA,SACM;AACN,eAAa,cAAc,OAAO,aAAa,cAAc,OAAO;AACtE;AAKO,SAAS,eAAe,UAAkB,SAAyB;AACxE,eAAa,eAAe,UAAU,OAAO;AAC/C;AAKO,SAAS,qBAA2B;AACzC,eAAa,mBAAmB;AAClC;AAKO,SAAS,iBAAiB,YAA2B;AAC1D,eAAa,iBAAiB,UAAU;AAC1C;AAKO,SAAS,gBAAsC;AACpD,SAAO,aAAa,cAAc;AACpC;AAKO,SAAS,gBAAgB,MAA+B;AAC7D,SAAO,aAAa,gBAAgB,IAAI;AAC1C;AAKO,SAAS,qBAAmC;AACjD,SAAO,aAAa,mBAAmB;AACzC;AAKO,SAAS,oBAAiC;AAC/C,SAAO,aAAa,kBAAkB;AACxC;AAKO,SAAS,aACd,OACA,aACA,cACQ;AACR,SAAO,aAAa,aAAa,OAAO,aAAa,YAAY;AACnE;AAKO,SAAS,qBAA2B;AACzC,eAAa,mBAAmB;AAClC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
const DEFAULT_CONTEXT = {
|
|
5
|
+
sessionId: "",
|
|
6
|
+
cwd: process.cwd(),
|
|
7
|
+
env: {},
|
|
8
|
+
allowDynamicCommands: false,
|
|
9
|
+
commandTimeout: 1e4
|
|
10
|
+
};
|
|
11
|
+
async function substituteVariables(content, args, context = {}) {
|
|
12
|
+
const ctx = { ...DEFAULT_CONTEXT, ...context };
|
|
13
|
+
let result = content;
|
|
14
|
+
const argValues = args.trim() ? args.trim().split(/\s+/) : [];
|
|
15
|
+
result = result.replace(/\$ARGUMENTS(?!\[)/g, args);
|
|
16
|
+
result = result.replace(/\$ARGUMENTS\[(\d+)\]/g, (_, indexStr) => {
|
|
17
|
+
const index = parseInt(indexStr, 10);
|
|
18
|
+
return argValues[index] ?? "";
|
|
19
|
+
});
|
|
20
|
+
result = result.replace(/\$(\d+)(?!\d)/g, (_, indexStr) => {
|
|
21
|
+
const index = parseInt(indexStr, 10);
|
|
22
|
+
return argValues[index] ?? "";
|
|
23
|
+
});
|
|
24
|
+
result = result.replace(/\$\{([A-Z_][A-Z0-9_]*)\}/gi, (_, varName) => {
|
|
25
|
+
switch (varName.toUpperCase()) {
|
|
26
|
+
case "CLAUDE_SESSION_ID":
|
|
27
|
+
case "SESSION_ID":
|
|
28
|
+
return ctx.sessionId || "";
|
|
29
|
+
case "CWD":
|
|
30
|
+
case "PWD":
|
|
31
|
+
return ctx.cwd;
|
|
32
|
+
default:
|
|
33
|
+
return ctx.env[varName] ?? process.env[varName] ?? "";
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
if (ctx.allowDynamicCommands) {
|
|
37
|
+
const commandPattern = /!\`([^`]+)\`/g;
|
|
38
|
+
const matches = [...result.matchAll(commandPattern)];
|
|
39
|
+
for (const match of matches) {
|
|
40
|
+
const command = match[1];
|
|
41
|
+
try {
|
|
42
|
+
const { stdout } = await execAsync(command, {
|
|
43
|
+
timeout: ctx.commandTimeout,
|
|
44
|
+
cwd: ctx.cwd
|
|
45
|
+
});
|
|
46
|
+
result = result.replace(match[0], stdout.trim());
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
49
|
+
result = result.replace(match[0], `(error: ${errorMessage})`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
result = result.replace(/!\`([^`]+)\`/g, "(dynamic commands disabled)");
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
function hasSubstitutionPatterns(content) {
|
|
58
|
+
return /\$(?:ARGUMENTS|\d+|\{[A-Z_][A-Z0-9_]*\})|!\`/.test(content);
|
|
59
|
+
}
|
|
60
|
+
function extractSubstitutionPatterns(content) {
|
|
61
|
+
const patterns = {
|
|
62
|
+
arguments: [],
|
|
63
|
+
indexedArgs: [],
|
|
64
|
+
variables: [],
|
|
65
|
+
commands: []
|
|
66
|
+
};
|
|
67
|
+
if (/\$ARGUMENTS(?!\[)/.test(content)) {
|
|
68
|
+
patterns.arguments.push("$ARGUMENTS");
|
|
69
|
+
}
|
|
70
|
+
const argIndexMatches = content.matchAll(/\$ARGUMENTS\[(\d+)\]/g);
|
|
71
|
+
for (const match of argIndexMatches) {
|
|
72
|
+
patterns.indexedArgs.push(parseInt(match[1], 10));
|
|
73
|
+
}
|
|
74
|
+
const shortIndexMatches = content.matchAll(/\$(\d+)(?!\d)/g);
|
|
75
|
+
for (const match of shortIndexMatches) {
|
|
76
|
+
patterns.indexedArgs.push(parseInt(match[1], 10));
|
|
77
|
+
}
|
|
78
|
+
const varMatches = content.matchAll(/\$\{([A-Z_][A-Z0-9_]*)\}/gi);
|
|
79
|
+
for (const match of varMatches) {
|
|
80
|
+
patterns.variables.push(match[1]);
|
|
81
|
+
}
|
|
82
|
+
const cmdMatches = content.matchAll(/!\`([^`]+)\`/g);
|
|
83
|
+
for (const match of cmdMatches) {
|
|
84
|
+
patterns.commands.push(match[1]);
|
|
85
|
+
}
|
|
86
|
+
patterns.indexedArgs = [...new Set(patterns.indexedArgs)].sort(
|
|
87
|
+
(a, b) => a - b
|
|
88
|
+
);
|
|
89
|
+
patterns.variables = [...new Set(patterns.variables)];
|
|
90
|
+
return patterns;
|
|
91
|
+
}
|
|
92
|
+
function validateArguments(args, patterns) {
|
|
93
|
+
const argValues = args.trim() ? args.trim().split(/\s+/) : [];
|
|
94
|
+
const maxIndex = Math.max(...patterns.indexedArgs, -1);
|
|
95
|
+
if (maxIndex >= 0 && argValues.length <= maxIndex) {
|
|
96
|
+
return `Expected at least ${maxIndex + 1} argument(s), got ${argValues.length}`;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
extractSubstitutionPatterns,
|
|
102
|
+
hasSubstitutionPatterns,
|
|
103
|
+
substituteVariables,
|
|
104
|
+
validateArguments
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=stringSubstitution.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/utils/stringSubstitution.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * String Substitution Engine\n *\n * Implements Claude Code skill specification for variable substitution.\n * Supports the following patterns:\n *\n * - $ARGUMENTS - All arguments as a single string\n * - $ARGUMENTS[N] - Indexed argument (0-based)\n * - $N - Shorthand for $ARGUMENTS[N]\n * - ${VARIABLE_NAME} - Environment variable or context variable\n * - !`command` - Dynamic command execution (with security controls)\n */\n\nimport { exec } from 'child_process'\nimport { promisify } from 'util'\n\nconst execAsync = promisify(exec)\n\n/**\n * Context for string substitution\n */\nexport interface SubstitutionContext {\n sessionId?: string\n cwd?: string\n env?: Record<string, string>\n /**\n * Allow dynamic command execution (!`command`)\n * Default: false (security consideration)\n */\n allowDynamicCommands?: boolean\n /**\n * Timeout for dynamic command execution in milliseconds\n * Default: 10000 (10 seconds)\n */\n commandTimeout?: number\n}\n\n/**\n * Default substitution context\n */\nconst DEFAULT_CONTEXT: Required<SubstitutionContext> = {\n sessionId: '',\n cwd: process.cwd(),\n env: {},\n allowDynamicCommands: false,\n commandTimeout: 10000,\n}\n\n/**\n * Substitute variables in a string\n *\n * @param content - The content to substitute variables in\n * @param args - Arguments string (space-separated)\n * @param context - Substitution context\n * @returns The content with variables substituted\n */\nexport async function substituteVariables(\n content: string,\n args: string,\n context: SubstitutionContext = {},\n): Promise<string> {\n const ctx = { ...DEFAULT_CONTEXT, ...context }\n let result = content\n const argValues = args.trim() ? args.trim().split(/\\s+/) : []\n\n // 1. $ARGUMENTS - All arguments as a single string\n // Use negative lookahead to avoid matching $ARGUMENTS[N]\n result = result.replace(/\\$ARGUMENTS(?!\\[)/g, args)\n\n // 2. $ARGUMENTS[N] - Indexed argument (0-based)\n result = result.replace(/\\$ARGUMENTS\\[(\\d+)\\]/g, (_, indexStr) => {\n const index = parseInt(indexStr, 10)\n return argValues[index] ?? ''\n })\n\n // 3. $N - Shorthand for indexed argument (0-based)\n // Use negative lookahead to avoid matching multi-digit or $ARGUMENTS\n result = result.replace(/\\$(\\d+)(?!\\d)/g, (_, indexStr) => {\n const index = parseInt(indexStr, 10)\n return argValues[index] ?? ''\n })\n\n // 4. ${VARIABLE_NAME} - Context and environment variables\n result = result.replace(/\\$\\{([A-Z_][A-Z0-9_]*)\\}/gi, (_, varName) => {\n // Built-in context variables (Claude Code spec)\n switch (varName.toUpperCase()) {\n case 'CLAUDE_SESSION_ID':\n case 'SESSION_ID':\n return ctx.sessionId || ''\n case 'CWD':\n case 'PWD':\n return ctx.cwd\n default:\n // Check custom context env first, then process.env\n return ctx.env[varName] ?? process.env[varName] ?? ''\n }\n })\n\n // 5. !`command` - Dynamic command execution\n // Only execute if explicitly allowed (security consideration)\n if (ctx.allowDynamicCommands) {\n const commandPattern = /!\\`([^`]+)\\`/g\n const matches = [...result.matchAll(commandPattern)]\n\n for (const match of matches) {\n const command = match[1]\n try {\n const { stdout } = await execAsync(command, {\n timeout: ctx.commandTimeout,\n cwd: ctx.cwd,\n })\n result = result.replace(match[0], stdout.trim())\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error)\n result = result.replace(match[0], `(error: ${errorMessage})`)\n }\n }\n } else {\n // Replace !`command` with placeholder when not allowed\n result = result.replace(/!\\`([^`]+)\\`/g, '(dynamic commands disabled)')\n }\n\n return result\n}\n\n/**\n * Check if a string contains substitution patterns\n * Useful for optimization - skip substitution if no patterns present\n */\nexport function hasSubstitutionPatterns(content: string): boolean {\n return /\\$(?:ARGUMENTS|\\d+|\\{[A-Z_][A-Z0-9_]*\\})|!\\`/.test(content)\n}\n\n/**\n * Extract all substitution patterns from a string\n * Useful for validation and documentation\n */\nexport function extractSubstitutionPatterns(content: string): {\n arguments: string[]\n indexedArgs: number[]\n variables: string[]\n commands: string[]\n} {\n const patterns = {\n arguments: [] as string[],\n indexedArgs: [] as number[],\n variables: [] as string[],\n commands: [] as string[],\n }\n\n // $ARGUMENTS\n if (/\\$ARGUMENTS(?!\\[)/.test(content)) {\n patterns.arguments.push('$ARGUMENTS')\n }\n\n // $ARGUMENTS[N]\n const argIndexMatches = content.matchAll(/\\$ARGUMENTS\\[(\\d+)\\]/g)\n for (const match of argIndexMatches) {\n patterns.indexedArgs.push(parseInt(match[1], 10))\n }\n\n // $N\n const shortIndexMatches = content.matchAll(/\\$(\\d+)(?!\\d)/g)\n for (const match of shortIndexMatches) {\n patterns.indexedArgs.push(parseInt(match[1], 10))\n }\n\n // ${VARIABLE}\n const varMatches = content.matchAll(/\\$\\{([A-Z_][A-Z0-9_]*)\\}/gi)\n for (const match of varMatches) {\n patterns.variables.push(match[1])\n }\n\n // !`command`\n const cmdMatches = content.matchAll(/!\\`([^`]+)\\`/g)\n for (const match of cmdMatches) {\n patterns.commands.push(match[1])\n }\n\n // Deduplicate indexed args\n patterns.indexedArgs = [...new Set(patterns.indexedArgs)].sort(\n (a, b) => a - b,\n )\n patterns.variables = [...new Set(patterns.variables)]\n\n return patterns\n}\n\n/**\n * Validate arguments against expected patterns\n * Returns error message if validation fails, null if valid\n */\nexport function validateArguments(\n args: string,\n patterns: ReturnType<typeof extractSubstitutionPatterns>,\n): string | null {\n const argValues = args.trim() ? args.trim().split(/\\s+/) : []\n const maxIndex = Math.max(...patterns.indexedArgs, -1)\n\n if (maxIndex >= 0 && argValues.length <= maxIndex) {\n return `Expected at least ${maxIndex + 1} argument(s), got ${argValues.length}`\n }\n\n return null\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|