@link-assistant/hive-mind 0.39.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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +24 -0
- package/README.md +769 -0
- package/package.json +58 -0
- package/src/agent.lib.mjs +705 -0
- package/src/agent.prompts.lib.mjs +196 -0
- package/src/buildUserMention.lib.mjs +71 -0
- package/src/claude-limits.lib.mjs +389 -0
- package/src/claude.lib.mjs +1445 -0
- package/src/claude.prompts.lib.mjs +203 -0
- package/src/codex.lib.mjs +552 -0
- package/src/codex.prompts.lib.mjs +194 -0
- package/src/config.lib.mjs +207 -0
- package/src/contributing-guidelines.lib.mjs +268 -0
- package/src/exit-handler.lib.mjs +205 -0
- package/src/git.lib.mjs +145 -0
- package/src/github-issue-creator.lib.mjs +246 -0
- package/src/github-linking.lib.mjs +152 -0
- package/src/github.batch.lib.mjs +272 -0
- package/src/github.graphql.lib.mjs +258 -0
- package/src/github.lib.mjs +1479 -0
- package/src/hive.config.lib.mjs +254 -0
- package/src/hive.mjs +1500 -0
- package/src/instrument.mjs +191 -0
- package/src/interactive-mode.lib.mjs +1000 -0
- package/src/lenv-reader.lib.mjs +206 -0
- package/src/lib.mjs +490 -0
- package/src/lino.lib.mjs +176 -0
- package/src/local-ci-checks.lib.mjs +324 -0
- package/src/memory-check.mjs +419 -0
- package/src/model-mapping.lib.mjs +145 -0
- package/src/model-validation.lib.mjs +278 -0
- package/src/opencode.lib.mjs +479 -0
- package/src/opencode.prompts.lib.mjs +194 -0
- package/src/protect-branch.mjs +159 -0
- package/src/review.mjs +433 -0
- package/src/reviewers-hive.mjs +643 -0
- package/src/sentry.lib.mjs +284 -0
- package/src/solve.auto-continue.lib.mjs +568 -0
- package/src/solve.auto-pr.lib.mjs +1374 -0
- package/src/solve.branch-errors.lib.mjs +341 -0
- package/src/solve.branch.lib.mjs +230 -0
- package/src/solve.config.lib.mjs +342 -0
- package/src/solve.error-handlers.lib.mjs +256 -0
- package/src/solve.execution.lib.mjs +291 -0
- package/src/solve.feedback.lib.mjs +436 -0
- package/src/solve.mjs +1128 -0
- package/src/solve.preparation.lib.mjs +210 -0
- package/src/solve.repo-setup.lib.mjs +114 -0
- package/src/solve.repository.lib.mjs +961 -0
- package/src/solve.results.lib.mjs +558 -0
- package/src/solve.session.lib.mjs +135 -0
- package/src/solve.validation.lib.mjs +325 -0
- package/src/solve.watch.lib.mjs +572 -0
- package/src/start-screen.mjs +324 -0
- package/src/task.mjs +308 -0
- package/src/telegram-bot.mjs +1481 -0
- package/src/telegram-markdown.lib.mjs +64 -0
- package/src/usage-limit.lib.mjs +218 -0
- package/src/version.lib.mjs +41 -0
- package/src/youtrack/solve.youtrack.lib.mjs +116 -0
- package/src/youtrack/youtrack-sync.mjs +219 -0
- package/src/youtrack/youtrack.lib.mjs +425 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent prompts module
|
|
3
|
+
* Handles building prompts for Agent commands
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build the user prompt for Agent
|
|
8
|
+
* @param {Object} params - Parameters for building the user prompt
|
|
9
|
+
* @returns {string} The formatted user prompt
|
|
10
|
+
*/
|
|
11
|
+
export const buildUserPrompt = (params) => {
|
|
12
|
+
const {
|
|
13
|
+
issueUrl,
|
|
14
|
+
issueNumber,
|
|
15
|
+
prNumber,
|
|
16
|
+
prUrl,
|
|
17
|
+
branchName,
|
|
18
|
+
tempDir,
|
|
19
|
+
isContinueMode,
|
|
20
|
+
forkedRepo,
|
|
21
|
+
feedbackLines,
|
|
22
|
+
forkActionsUrl,
|
|
23
|
+
owner,
|
|
24
|
+
repo,
|
|
25
|
+
argv
|
|
26
|
+
} = params;
|
|
27
|
+
|
|
28
|
+
const promptLines = [];
|
|
29
|
+
|
|
30
|
+
// Issue or PR reference
|
|
31
|
+
if (isContinueMode) {
|
|
32
|
+
promptLines.push(`Issue to solve: ${issueNumber ? `https://github.com/${owner}/${repo}/issues/${issueNumber}` : `Issue linked to PR #${prNumber}`}`);
|
|
33
|
+
} else {
|
|
34
|
+
promptLines.push(`Issue to solve: ${issueUrl}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Basic info
|
|
38
|
+
promptLines.push(`Your prepared branch: ${branchName}`);
|
|
39
|
+
promptLines.push(`Your prepared working directory: ${tempDir}`);
|
|
40
|
+
|
|
41
|
+
// PR info if available
|
|
42
|
+
if (prUrl) {
|
|
43
|
+
promptLines.push(`Your prepared Pull Request: ${prUrl}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Fork info if applicable
|
|
47
|
+
if (argv && argv.fork && forkedRepo) {
|
|
48
|
+
promptLines.push(`Your forked repository: ${forkedRepo}`);
|
|
49
|
+
promptLines.push(`Original repository (upstream): ${owner}/${repo}`);
|
|
50
|
+
|
|
51
|
+
// Check for GitHub Actions on fork
|
|
52
|
+
if (branchName && forkActionsUrl) {
|
|
53
|
+
promptLines.push(`GitHub Actions on your fork: ${forkActionsUrl}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Add blank line
|
|
58
|
+
promptLines.push('');
|
|
59
|
+
|
|
60
|
+
// Add feedback info if in continue mode and there are feedback items
|
|
61
|
+
if (isContinueMode && feedbackLines && feedbackLines.length > 0) {
|
|
62
|
+
// Add each feedback line directly
|
|
63
|
+
feedbackLines.forEach(line => promptLines.push(line));
|
|
64
|
+
promptLines.push('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Add thinking instruction based on --think level
|
|
68
|
+
if (argv && argv.think) {
|
|
69
|
+
const thinkMessages = {
|
|
70
|
+
low: 'Think.',
|
|
71
|
+
medium: 'Think hard.',
|
|
72
|
+
high: 'Think harder.',
|
|
73
|
+
max: 'Ultrathink.'
|
|
74
|
+
};
|
|
75
|
+
promptLines.push(thinkMessages[argv.think]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Final instruction
|
|
79
|
+
promptLines.push(isContinueMode ? 'Continue.' : 'Proceed.');
|
|
80
|
+
|
|
81
|
+
// Build the final prompt
|
|
82
|
+
return promptLines.join('\n');
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build the system prompt for Agent - adapted for Agent's capabilities
|
|
87
|
+
* @param {Object} params - Parameters for building the prompt
|
|
88
|
+
* @returns {string} The formatted system prompt
|
|
89
|
+
*/
|
|
90
|
+
export const buildSystemPrompt = (params) => {
|
|
91
|
+
const { owner, repo, issueNumber, prNumber, branchName, argv } = params;
|
|
92
|
+
|
|
93
|
+
// Build thinking instruction based on --think level
|
|
94
|
+
let thinkLine = '';
|
|
95
|
+
if (argv && argv.think) {
|
|
96
|
+
const thinkMessages = {
|
|
97
|
+
low: 'You always think on every step.',
|
|
98
|
+
medium: 'You always think hard on every step.',
|
|
99
|
+
high: 'You always think harder on every step.',
|
|
100
|
+
max: 'You always ultrathink on every step.'
|
|
101
|
+
};
|
|
102
|
+
thinkLine = `\n${thinkMessages[argv.think]}\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return `You are AI issue solver using @link-assistant/agent.${thinkLine}
|
|
106
|
+
|
|
107
|
+
General guidelines.
|
|
108
|
+
- When you execute commands, always save their logs to files for easier reading if the output becomes large.
|
|
109
|
+
- When running commands, do not set a timeout yourself — let them run as long as needed.
|
|
110
|
+
- When running sudo commands (especially package installations), always run them in the background to avoid timeout issues.
|
|
111
|
+
- When CI is failing, make sure you download the logs locally and carefully investigate them.
|
|
112
|
+
- When a code or log file has more than 1500 lines, read it in chunks of 1500 lines.
|
|
113
|
+
- When facing a complex problem, do as much tracing as possible and turn on all verbose modes.
|
|
114
|
+
- When you create debug, test, or example/experiment scripts for fixing, always keep them in an examples and/or experiments folders so you can reuse them later.
|
|
115
|
+
- When testing your assumptions, use the experiment scripts, and add it to experiments folder.
|
|
116
|
+
- When your experiments can show real world use case of the software, add it to examples folder.
|
|
117
|
+
- When you face something extremely hard, use divide and conquer — it always helps.
|
|
118
|
+
|
|
119
|
+
Initial research.
|
|
120
|
+
- When you start, make sure you create detailed plan for yourself and follow your todo list step by step, make sure that as many points from these guidelines are added to your todo list to keep track of everything that can help you solve the issue with highest possible quality.
|
|
121
|
+
- When you read issue, read all details and comments thoroughly.
|
|
122
|
+
- When you see screenshots or images in issue descriptions, pull request descriptions, comments, or discussions, use WebFetch tool to download the image first, then use Read tool to view and analyze it.
|
|
123
|
+
- When you need issue details, use gh issue view https://github.com/${owner}/${repo}/issues/${issueNumber}.
|
|
124
|
+
- When you need related code, use gh search code --owner ${owner} [keywords].
|
|
125
|
+
- When you need repo context, read files in your working directory.
|
|
126
|
+
- When you study related work, study the most recent related pull requests.
|
|
127
|
+
- When issue is not defined enough, write a comment to ask clarifying questions.
|
|
128
|
+
- When accessing GitHub Gists, use gh gist view command instead of direct URL fetching.
|
|
129
|
+
- When you are fixing a bug, please make sure you first find the actual root cause, do as many experiments as needed.
|
|
130
|
+
- When you are fixing a bug and code does not have enough tracing/logs, add them and make sure they stay in the code, but are switched off by default.
|
|
131
|
+
- When you need latest comments on pull request, use gh api repos/${owner}/${repo}/pulls/${prNumber}/comments.
|
|
132
|
+
- When you need latest comments on issue, use gh api repos/${owner}/${repo}/issues/${issueNumber}/comments.
|
|
133
|
+
|
|
134
|
+
Solution development and testing.
|
|
135
|
+
- When issue is solvable, implement code with tests.
|
|
136
|
+
- When coding, each atomic step that can be useful by itself should be commited to the pull request's branch, meaning if work will be interrupted by any reason parts of solution will still be kept intact and safe in pull request.
|
|
137
|
+
- When you test:
|
|
138
|
+
start from testing of small functions using separate scripts;
|
|
139
|
+
write unit tests with mocks for easy and quick start.
|
|
140
|
+
- When you test integrations, use existing framework.
|
|
141
|
+
- When you test solution draft, include automated checks in pr.
|
|
142
|
+
- When issue is unclear, write comment on issue asking questions.
|
|
143
|
+
- When you encounter any problems that you unable to solve yourself, write a comment to the pull request asking for help.
|
|
144
|
+
- When you need human help, use gh pr comment ${prNumber} --body "your message" to comment on existing PR.
|
|
145
|
+
|
|
146
|
+
Preparing pull request.
|
|
147
|
+
- When you code, follow contributing guidelines.
|
|
148
|
+
- When you commit, write clear message.
|
|
149
|
+
- When you need examples of style, use gh pr list --repo ${owner}/${repo} --state merged --search [keywords].
|
|
150
|
+
- When you open pr, describe solution draft and include tests.
|
|
151
|
+
- When there is a package with version and GitHub Actions workflows for automatic release, update the version in your pull request to prepare for next release.
|
|
152
|
+
- When you update existing pr ${prNumber}, use gh pr edit to modify title and description.
|
|
153
|
+
- When you finalize the pull request:
|
|
154
|
+
follow style from merged prs for code, title, and description,
|
|
155
|
+
make sure no uncommitted changes corresponding to the original requirements are left behind,
|
|
156
|
+
make sure the default branch is merged to the pull request's branch,
|
|
157
|
+
make sure all CI checks passing if they exist before you finish,
|
|
158
|
+
check for latest comments on the issue and pull request to ensure no recent feedback was missed,
|
|
159
|
+
double-check that all changes in the pull request answer to original requirements of the issue,
|
|
160
|
+
make sure no new bugs are introduced in pull request by carefully reading gh pr diff,
|
|
161
|
+
make sure no previously existing features were removed without an explicit request from users via the issue description, issue comments, and/or pull request comments.
|
|
162
|
+
- When you finish implementation, use gh pr ready ${prNumber}.
|
|
163
|
+
|
|
164
|
+
Workflow and collaboration.
|
|
165
|
+
- When you check branch, verify with git branch --show-current.
|
|
166
|
+
- When you push, push only to branch ${branchName}.
|
|
167
|
+
- When you finish, create a pull request from branch ${branchName}.
|
|
168
|
+
- When pr ${prNumber} already exists for this branch, update it instead of creating new one.
|
|
169
|
+
- When you organize workflow, use pull requests instead of direct merges to default branch (main or master).
|
|
170
|
+
- When you manage commits, preserve commit history for later analysis.
|
|
171
|
+
- When you contribute, keep repository history forward-moving with regular commits, pushes, and reverts if needed.
|
|
172
|
+
- When you face conflict that you cannot resolve yourself, ask for help.
|
|
173
|
+
- When you collaborate, respect branch protections by working only on ${branchName}.
|
|
174
|
+
- When you mention result, include pull request url or comment url.
|
|
175
|
+
- When you need to create pr, remember pr ${prNumber} already exists for this branch.
|
|
176
|
+
|
|
177
|
+
Self review.
|
|
178
|
+
- When you check your solution draft, run all tests locally.
|
|
179
|
+
- When you check your solution draft, verify git status shows a clean working tree with no uncommitted changes.
|
|
180
|
+
- When you compare with repo style, use gh pr diff [number].
|
|
181
|
+
- When you finalize, confirm code, tests, and description are consistent.
|
|
182
|
+
|
|
183
|
+
GitHub CLI command patterns.
|
|
184
|
+
- When listing PR comments, use gh api repos/OWNER/REPO/pulls/NUMBER/comments.
|
|
185
|
+
- When listing issue comments, use gh api repos/OWNER/REPO/issues/NUMBER/comments.
|
|
186
|
+
- When adding PR comment, use gh pr comment NUMBER --body "text" --repo OWNER/REPO.
|
|
187
|
+
- When adding issue comment, use gh issue comment NUMBER --body "text" --repo OWNER/REPO.
|
|
188
|
+
- When viewing PR details, use gh pr view NUMBER --repo OWNER/REPO.
|
|
189
|
+
- When filtering with jq, use gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --jq 'reverse | .[0:5]'.`;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Export all functions as default object too
|
|
193
|
+
export default {
|
|
194
|
+
buildUserPrompt,
|
|
195
|
+
buildSystemPrompt
|
|
196
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a Telegram user mention link in various parse modes.
|
|
3
|
+
*
|
|
4
|
+
* This is a simplified version that doesn't require external dependencies.
|
|
5
|
+
* It handles the most common cases for Telegram user mentions.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} options - Options for building the mention link.
|
|
8
|
+
* @param {Object} [options.user] - Telegram user object with id, username, first_name, last_name.
|
|
9
|
+
* @param {number|string} [options.id] - Telegram user ID (overrides user.id).
|
|
10
|
+
* @param {string} [options.username] - Telegram username (without '@', overrides user.username).
|
|
11
|
+
* @param {string} [options.first_name] - User's first name (overrides user.first_name).
|
|
12
|
+
* @param {string} [options.last_name] - User's last name (overrides user.last_name).
|
|
13
|
+
* @param {'HTML'|'Markdown'|'MarkdownV2'} [options.parseMode='HTML'] - The parse mode to use.
|
|
14
|
+
* @returns {string} A formatted mention link for the user.
|
|
15
|
+
*/
|
|
16
|
+
export function buildUserMention({
|
|
17
|
+
user,
|
|
18
|
+
id: idParam,
|
|
19
|
+
username: usernameParam,
|
|
20
|
+
first_name: firstNameParam,
|
|
21
|
+
last_name: lastNameParam,
|
|
22
|
+
parseMode = 'HTML',
|
|
23
|
+
}) {
|
|
24
|
+
// Derive core fields from `user` with inline overrides
|
|
25
|
+
const id = idParam ?? user?.id;
|
|
26
|
+
const username = usernameParam ?? user?.username;
|
|
27
|
+
const firstName = firstNameParam ?? user?.first_name;
|
|
28
|
+
const lastName = lastNameParam ?? user?.last_name;
|
|
29
|
+
|
|
30
|
+
let displayName;
|
|
31
|
+
if (username) {
|
|
32
|
+
displayName = `@${username}`;
|
|
33
|
+
} else {
|
|
34
|
+
// Trim all string names, then filter out empty values
|
|
35
|
+
const raw = [firstName, lastName];
|
|
36
|
+
// Trim whitespace and Hangul filler (ㅤ) characters from names
|
|
37
|
+
const trimmedAll = raw.map((rawName) => (
|
|
38
|
+
typeof rawName === 'string' ? rawName.trim().replace(/^[\s\t\n\rㅤ]+|[\s\t\n\rㅤ]+$/g, '') : rawName
|
|
39
|
+
));
|
|
40
|
+
const cleaned = trimmedAll.filter((name) => typeof name === 'string' && name.length > 0);
|
|
41
|
+
// Use cleaned names or fallback to id
|
|
42
|
+
if (cleaned.length > 0) {
|
|
43
|
+
displayName = cleaned.join(' ');
|
|
44
|
+
} else {
|
|
45
|
+
displayName = String(id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const link = username ? `https://t.me/${username}` : `tg://user?id=${id}`;
|
|
50
|
+
|
|
51
|
+
switch (parseMode) {
|
|
52
|
+
case 'Markdown':
|
|
53
|
+
// Legacy Markdown: [text](url)
|
|
54
|
+
return `[${displayName}](${link})`;
|
|
55
|
+
case 'MarkdownV2': {
|
|
56
|
+
// MarkdownV2 requires escaping special characters
|
|
57
|
+
const escapedName = displayName.replace(/([_*[\]()~`>#+\-=|{}.!])/g, '\\$1');
|
|
58
|
+
return `[${escapedName}](${link})`;
|
|
59
|
+
}
|
|
60
|
+
case 'HTML':
|
|
61
|
+
default: {
|
|
62
|
+
// HTML mode: <a href="url">text</a>
|
|
63
|
+
const escapedHtml = displayName
|
|
64
|
+
.replace(/&/g, '&')
|
|
65
|
+
.replace(/</g, '<')
|
|
66
|
+
.replace(/>/g, '>')
|
|
67
|
+
.replace(/"/g, '"');
|
|
68
|
+
return `<a href="${link}">${escapedHtml}</a>`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude usage limits library
|
|
4
|
+
* Provides functions to fetch and parse Claude usage limits via OAuth API
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default path to Claude credentials file
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_CREDENTIALS_PATH = join(homedir(), '.claude', '.credentials.json');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Anthropic OAuth usage API endpoint
|
|
18
|
+
*/
|
|
19
|
+
const USAGE_API_ENDPOINT = 'https://api.anthropic.com/api/oauth/usage';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Read Claude credentials from the credentials file
|
|
23
|
+
*
|
|
24
|
+
* @param {string} credentialsPath - Path to credentials file (optional)
|
|
25
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
26
|
+
* @returns {Object|null} Credentials object or null if not found
|
|
27
|
+
*/
|
|
28
|
+
async function readCredentials(credentialsPath = DEFAULT_CREDENTIALS_PATH, verbose = false) {
|
|
29
|
+
try {
|
|
30
|
+
const content = await readFile(credentialsPath, 'utf-8');
|
|
31
|
+
const credentials = JSON.parse(content);
|
|
32
|
+
|
|
33
|
+
if (verbose) {
|
|
34
|
+
console.log('[VERBOSE] /limits credentials loaded from:', credentialsPath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return credentials;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (verbose) {
|
|
40
|
+
console.error('[VERBOSE] /limits failed to read credentials:', error.message);
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format an ISO date string to a human-readable reset time
|
|
48
|
+
*
|
|
49
|
+
* @param {string} isoDate - ISO date string (e.g., "2025-12-03T17:59:59.626485+00:00")
|
|
50
|
+
* @param {boolean} includeTimezone - Whether to include timezone suffix (default: true)
|
|
51
|
+
* @returns {string} Human-readable reset time (e.g., "Dec 3, 6:59pm UTC")
|
|
52
|
+
*/
|
|
53
|
+
function formatResetTime(isoDate, includeTimezone = true) {
|
|
54
|
+
if (!isoDate) return null;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const date = new Date(isoDate);
|
|
58
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
59
|
+
const month = months[date.getUTCMonth()];
|
|
60
|
+
const day = date.getUTCDate();
|
|
61
|
+
const hours = date.getUTCHours();
|
|
62
|
+
const minutes = date.getUTCMinutes();
|
|
63
|
+
|
|
64
|
+
// Convert 24h to 12h format
|
|
65
|
+
const hour12 = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
|
|
66
|
+
const ampm = hours >= 12 ? 'pm' : 'am';
|
|
67
|
+
|
|
68
|
+
const timeStr = `${month} ${day}, ${hour12}:${minutes.toString().padStart(2, '0')}${ampm}`;
|
|
69
|
+
return includeTimezone ? `${timeStr} UTC` : timeStr;
|
|
70
|
+
} catch {
|
|
71
|
+
return isoDate;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Format relative time from now to a future date
|
|
77
|
+
*
|
|
78
|
+
* @param {string} isoDate - ISO date string
|
|
79
|
+
* @returns {string|null} Relative time string (e.g., "1h 34m" or "6d 20h 13m") or null if date is in the past
|
|
80
|
+
*/
|
|
81
|
+
function formatRelativeTime(isoDate) {
|
|
82
|
+
if (!isoDate) return null;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const now = new Date();
|
|
86
|
+
const target = new Date(isoDate);
|
|
87
|
+
const diffMs = target - now;
|
|
88
|
+
|
|
89
|
+
// Check for invalid date (NaN)
|
|
90
|
+
if (isNaN(diffMs)) return null;
|
|
91
|
+
|
|
92
|
+
if (diffMs < 0) return null; // Past date
|
|
93
|
+
|
|
94
|
+
const totalHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
95
|
+
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
96
|
+
|
|
97
|
+
// If hours >= 24, show days
|
|
98
|
+
if (totalHours >= 24) {
|
|
99
|
+
const days = Math.floor(totalHours / 24);
|
|
100
|
+
const hours = totalHours % 24;
|
|
101
|
+
return `${days}d ${hours}h ${minutes}m`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return `${totalHours}h ${minutes}m`;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Format current time in UTC
|
|
112
|
+
*
|
|
113
|
+
* @returns {string} Current time in UTC (e.g., "Dec 3, 6:45pm UTC")
|
|
114
|
+
*/
|
|
115
|
+
function formatCurrentTime() {
|
|
116
|
+
const now = new Date();
|
|
117
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
118
|
+
const month = months[now.getUTCMonth()];
|
|
119
|
+
const day = now.getUTCDate();
|
|
120
|
+
const hours = now.getUTCHours();
|
|
121
|
+
const minutes = now.getUTCMinutes();
|
|
122
|
+
|
|
123
|
+
// Convert 24h to 12h format
|
|
124
|
+
const hour12 = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
|
|
125
|
+
const ampm = hours >= 12 ? 'pm' : 'am';
|
|
126
|
+
|
|
127
|
+
return `${month} ${day}, ${hour12}:${minutes.toString().padStart(2, '0')}${ampm} UTC`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get Claude usage limits by calling the Anthropic OAuth usage API
|
|
132
|
+
* This approach is more reliable than trying to parse CLI output
|
|
133
|
+
* and doesn't require the 'expect' command.
|
|
134
|
+
*
|
|
135
|
+
* Returns usage data for:
|
|
136
|
+
* - Current session (five_hour) usage percentage and reset time
|
|
137
|
+
* - Current week (all models / seven_day) usage percentage and reset date
|
|
138
|
+
* - Current week (Sonnet only / seven_day_sonnet) usage percentage and reset date
|
|
139
|
+
*
|
|
140
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
141
|
+
* @param {string} credentialsPath - Optional path to credentials file
|
|
142
|
+
* @returns {Object} Object with success boolean, and either usage data or error message
|
|
143
|
+
*/
|
|
144
|
+
export async function getClaudeUsageLimits(verbose = false, credentialsPath = DEFAULT_CREDENTIALS_PATH) {
|
|
145
|
+
try {
|
|
146
|
+
// Read credentials
|
|
147
|
+
const credentials = await readCredentials(credentialsPath, verbose);
|
|
148
|
+
|
|
149
|
+
if (!credentials) {
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
error: 'Could not read Claude credentials. Make sure Claude is properly installed and authenticated.'
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const accessToken = credentials?.claudeAiOauth?.accessToken;
|
|
157
|
+
|
|
158
|
+
if (!accessToken) {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
error: 'No access token found in Claude credentials. Please use `/solve` or `/hive` commands to trigger re-authentication of Claude.'
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (verbose) {
|
|
166
|
+
console.log('[VERBOSE] /limits fetching usage from API...');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Call the Anthropic OAuth usage API
|
|
170
|
+
const response = await fetch(USAGE_API_ENDPOINT, {
|
|
171
|
+
method: 'GET',
|
|
172
|
+
headers: {
|
|
173
|
+
'Accept': 'application/json',
|
|
174
|
+
'Content-Type': 'application/json',
|
|
175
|
+
'User-Agent': 'claude-code/2.0.55',
|
|
176
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
177
|
+
'anthropic-beta': 'oauth-2025-04-20'
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
const errorText = await response.text();
|
|
183
|
+
if (verbose) {
|
|
184
|
+
console.error('[VERBOSE] /limits API error:', response.status, errorText);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check for specific error conditions
|
|
188
|
+
if (response.status === 401) {
|
|
189
|
+
return {
|
|
190
|
+
success: false,
|
|
191
|
+
error: 'Claude authentication expired. Please use `/solve` or `/hive` commands to trigger re-authentication of Claude.'
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
success: false,
|
|
197
|
+
error: `Failed to fetch usage from API: ${response.status} ${response.statusText}`
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const data = await response.json();
|
|
202
|
+
|
|
203
|
+
if (verbose) {
|
|
204
|
+
console.log('[VERBOSE] /limits API response:', JSON.stringify(data, null, 2));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Parse the API response
|
|
208
|
+
// API returns:
|
|
209
|
+
// - five_hour: { utilization: number, resets_at: string }
|
|
210
|
+
// - seven_day: { utilization: number, resets_at: string }
|
|
211
|
+
// - seven_day_sonnet: { utilization: number, resets_at: string } (optional)
|
|
212
|
+
|
|
213
|
+
const usage = {
|
|
214
|
+
currentSession: {
|
|
215
|
+
percentage: data.five_hour?.utilization ?? null,
|
|
216
|
+
resetTime: formatResetTime(data.five_hour?.resets_at),
|
|
217
|
+
resetsAt: data.five_hour?.resets_at ?? null
|
|
218
|
+
},
|
|
219
|
+
allModels: {
|
|
220
|
+
percentage: data.seven_day?.utilization ?? null,
|
|
221
|
+
resetTime: formatResetTime(data.seven_day?.resets_at),
|
|
222
|
+
resetsAt: data.seven_day?.resets_at ?? null
|
|
223
|
+
},
|
|
224
|
+
sonnetOnly: {
|
|
225
|
+
percentage: data.seven_day_sonnet?.utilization ?? null,
|
|
226
|
+
resetTime: formatResetTime(data.seven_day_sonnet?.resets_at),
|
|
227
|
+
resetsAt: data.seven_day_sonnet?.resets_at ?? null
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
success: true,
|
|
233
|
+
usage
|
|
234
|
+
};
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (verbose) {
|
|
237
|
+
console.error('[VERBOSE] /limits error:', error);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
error: `Failed to get usage limits: ${error.message}`
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Generate a text-based progress bar for usage percentage
|
|
248
|
+
* @param {number} percentage - Usage percentage (0-100)
|
|
249
|
+
* @returns {string} Text-based progress bar
|
|
250
|
+
*/
|
|
251
|
+
export function getProgressBar(percentage) {
|
|
252
|
+
const totalBlocks = 30;
|
|
253
|
+
const filledBlocks = Math.round((percentage / 100) * totalBlocks);
|
|
254
|
+
const emptyBlocks = totalBlocks - filledBlocks;
|
|
255
|
+
return '\u2593'.repeat(filledBlocks) + '\u2591'.repeat(emptyBlocks);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Calculate the percentage of time that has passed in a period
|
|
260
|
+
* @param {string} resetsAt - ISO date string when the period resets
|
|
261
|
+
* @param {number} periodHours - Total duration of the period in hours (5 for session, 168 for week)
|
|
262
|
+
* @returns {number|null} Percentage of time passed (0-100) or null if unable to calculate
|
|
263
|
+
*/
|
|
264
|
+
export function calculateTimePassedPercentage(resetsAt, periodHours) {
|
|
265
|
+
if (!resetsAt) return null;
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const now = new Date();
|
|
269
|
+
const resetTime = new Date(resetsAt);
|
|
270
|
+
const periodMs = periodHours * 60 * 60 * 1000; // Convert hours to milliseconds
|
|
271
|
+
|
|
272
|
+
// Calculate when the period started
|
|
273
|
+
const startTime = new Date(resetTime.getTime() - periodMs);
|
|
274
|
+
|
|
275
|
+
// Calculate time passed and total duration
|
|
276
|
+
const timePassed = now.getTime() - startTime.getTime();
|
|
277
|
+
const percentage = Math.max(0, Math.min(100, (timePassed / periodMs) * 100));
|
|
278
|
+
|
|
279
|
+
return Math.round(percentage);
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Format Claude usage data into a Telegram-friendly message
|
|
287
|
+
* @param {Object} usage - The usage object from getClaudeUsageLimits
|
|
288
|
+
* @returns {string} Formatted message
|
|
289
|
+
*/
|
|
290
|
+
export function formatUsageMessage(usage) {
|
|
291
|
+
// Use code block for monospace font to align progress bars properly
|
|
292
|
+
let message = '```\n';
|
|
293
|
+
|
|
294
|
+
// Show current time
|
|
295
|
+
message += `Current time: ${formatCurrentTime()}\n\n`;
|
|
296
|
+
|
|
297
|
+
// Current session (five_hour)
|
|
298
|
+
message += 'Current session\n';
|
|
299
|
+
if (usage.currentSession.percentage !== null) {
|
|
300
|
+
// Add time passed progress bar first
|
|
301
|
+
const timePassed = calculateTimePassedPercentage(usage.currentSession.resetsAt, 5);
|
|
302
|
+
if (timePassed !== null) {
|
|
303
|
+
const timeBar = getProgressBar(timePassed);
|
|
304
|
+
message += `${timeBar} ${timePassed}% passed\n`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Add usage progress bar second
|
|
308
|
+
const pct = usage.currentSession.percentage;
|
|
309
|
+
const bar = getProgressBar(pct);
|
|
310
|
+
message += `${bar} ${pct}% used\n`;
|
|
311
|
+
|
|
312
|
+
if (usage.currentSession.resetTime) {
|
|
313
|
+
const relativeTime = formatRelativeTime(usage.currentSession.resetsAt);
|
|
314
|
+
if (relativeTime) {
|
|
315
|
+
message += `Resets in ${relativeTime} (${usage.currentSession.resetTime})\n`;
|
|
316
|
+
} else {
|
|
317
|
+
message += `Resets ${usage.currentSession.resetTime}\n`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
message += 'N/A\n';
|
|
322
|
+
}
|
|
323
|
+
message += '\n';
|
|
324
|
+
|
|
325
|
+
// Current week (all models / seven_day)
|
|
326
|
+
message += 'Current week (all models)\n';
|
|
327
|
+
if (usage.allModels.percentage !== null) {
|
|
328
|
+
// Add time passed progress bar first (168 hours = 7 days)
|
|
329
|
+
const timePassed = calculateTimePassedPercentage(usage.allModels.resetsAt, 168);
|
|
330
|
+
if (timePassed !== null) {
|
|
331
|
+
const timeBar = getProgressBar(timePassed);
|
|
332
|
+
message += `${timeBar} ${timePassed}% passed\n`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Add usage progress bar second
|
|
336
|
+
const pct = usage.allModels.percentage;
|
|
337
|
+
const bar = getProgressBar(pct);
|
|
338
|
+
message += `${bar} ${pct}% used\n`;
|
|
339
|
+
|
|
340
|
+
if (usage.allModels.resetTime) {
|
|
341
|
+
const relativeTime = formatRelativeTime(usage.allModels.resetsAt);
|
|
342
|
+
if (relativeTime) {
|
|
343
|
+
message += `Resets in ${relativeTime} (${usage.allModels.resetTime})\n`;
|
|
344
|
+
} else {
|
|
345
|
+
message += `Resets ${usage.allModels.resetTime}\n`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
message += 'N/A\n';
|
|
350
|
+
}
|
|
351
|
+
message += '\n';
|
|
352
|
+
|
|
353
|
+
// Current week (Sonnet only / seven_day_sonnet)
|
|
354
|
+
message += 'Current week (Sonnet only)\n';
|
|
355
|
+
if (usage.sonnetOnly.percentage !== null) {
|
|
356
|
+
// Add time passed progress bar first (168 hours = 7 days)
|
|
357
|
+
const timePassed = calculateTimePassedPercentage(usage.sonnetOnly.resetsAt, 168);
|
|
358
|
+
if (timePassed !== null) {
|
|
359
|
+
const timeBar = getProgressBar(timePassed);
|
|
360
|
+
message += `${timeBar} ${timePassed}% passed\n`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Add usage progress bar second
|
|
364
|
+
const pct = usage.sonnetOnly.percentage;
|
|
365
|
+
const bar = getProgressBar(pct);
|
|
366
|
+
message += `${bar} ${pct}% used\n`;
|
|
367
|
+
|
|
368
|
+
if (usage.sonnetOnly.resetTime) {
|
|
369
|
+
const relativeTime = formatRelativeTime(usage.sonnetOnly.resetsAt);
|
|
370
|
+
if (relativeTime) {
|
|
371
|
+
message += `Resets in ${relativeTime} (${usage.sonnetOnly.resetTime})\n`;
|
|
372
|
+
} else {
|
|
373
|
+
message += `Resets ${usage.sonnetOnly.resetTime}\n`;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
message += 'N/A\n';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
message += '```';
|
|
381
|
+
return message;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export default {
|
|
385
|
+
getClaudeUsageLimits,
|
|
386
|
+
getProgressBar,
|
|
387
|
+
calculateTimePassedPercentage,
|
|
388
|
+
formatUsageMessage
|
|
389
|
+
};
|