calc-mcp-server 0.1.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/.claude/commands/opsx/apply.md +152 -0
- package/.claude/commands/opsx/archive.md +157 -0
- package/.claude/commands/opsx/bulk-archive.md +242 -0
- package/.claude/commands/opsx/continue.md +114 -0
- package/.claude/commands/opsx/explore.md +174 -0
- package/.claude/commands/opsx/ff.md +94 -0
- package/.claude/commands/opsx/new.md +69 -0
- package/.claude/commands/opsx/onboard.md +534 -0
- package/.claude/commands/opsx/sync.md +134 -0
- package/.claude/commands/opsx/verify.md +164 -0
- package/.claude/settings.local.json +8 -0
- package/.claude/skills/npm-publish/SKILL.md +164 -0
- package/.claude/skills/openspec-apply-change/SKILL.md +156 -0
- package/.claude/skills/openspec-archive-change/SKILL.md +161 -0
- package/.claude/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.claude/skills/openspec-continue-change/SKILL.md +118 -0
- package/.claude/skills/openspec-explore/SKILL.md +289 -0
- package/.claude/skills/openspec-ff-change/SKILL.md +101 -0
- package/.claude/skills/openspec-new-change/SKILL.md +74 -0
- package/.claude/skills/openspec-onboard/SKILL.md +538 -0
- package/.claude/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.claude/skills/openspec-verify-change/SKILL.md +168 -0
- package/CLAUDE.md +92 -0
- package/README.md +319 -0
- package/build/engines/decimal.d.ts +10 -0
- package/build/engines/decimal.d.ts.map +1 -0
- package/build/engines/decimal.js +61 -0
- package/build/engines/decimal.js.map +1 -0
- package/build/engines/programmer.d.ts +18 -0
- package/build/engines/programmer.d.ts.map +1 -0
- package/build/engines/programmer.js +103 -0
- package/build/engines/programmer.js.map +1 -0
- package/build/errors/handler.d.ts +10 -0
- package/build/errors/handler.d.ts.map +1 -0
- package/build/errors/handler.js +37 -0
- package/build/errors/handler.js.map +1 -0
- package/build/errors/types.d.ts +25 -0
- package/build/errors/types.d.ts.map +1 -0
- package/build/errors/types.js +2 -0
- package/build/errors/types.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +16 -0
- package/build/index.js.map +1 -0
- package/build/mcp/server.d.ts +3 -0
- package/build/mcp/server.d.ts.map +1 -0
- package/build/mcp/server.js +270 -0
- package/build/mcp/server.js.map +1 -0
- package/build/mcp/tools/ascii.d.ts +11 -0
- package/build/mcp/tools/ascii.d.ts.map +1 -0
- package/build/mcp/tools/ascii.js +93 -0
- package/build/mcp/tools/ascii.js.map +1 -0
- package/build/mcp/tools/basic.d.ts +6 -0
- package/build/mcp/tools/basic.d.ts.map +1 -0
- package/build/mcp/tools/basic.js +34 -0
- package/build/mcp/tools/basic.js.map +1 -0
- package/build/mcp/tools/conversion.d.ts +8 -0
- package/build/mcp/tools/conversion.d.ts.map +1 -0
- package/build/mcp/tools/conversion.js +81 -0
- package/build/mcp/tools/conversion.js.map +1 -0
- package/build/mcp/tools/programmer.d.ts +6 -0
- package/build/mcp/tools/programmer.d.ts.map +1 -0
- package/build/mcp/tools/programmer.js +29 -0
- package/build/mcp/tools/programmer.js.map +1 -0
- package/build/parser/ast.d.ts +47 -0
- package/build/parser/ast.d.ts.map +1 -0
- package/build/parser/ast.js +2 -0
- package/build/parser/ast.js.map +1 -0
- package/build/parser/lexer.d.ts +24 -0
- package/build/parser/lexer.d.ts.map +1 -0
- package/build/parser/lexer.js +168 -0
- package/build/parser/lexer.js.map +1 -0
- package/build/parser/parser.d.ts +14 -0
- package/build/parser/parser.d.ts.map +1 -0
- package/build/parser/parser.js +115 -0
- package/build/parser/parser.js.map +1 -0
- package/docs/plans/2025-02-24-mcp-calculator-design.md +344 -0
- package/docs/plans/2025-02-24-mcp-calculator-implementation.md +2626 -0
- package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/design.md +46 -0
- package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/proposal.md +21 -0
- package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/specs/ascii-conversion/spec.md +22 -0
- package/openspec/changes/archive/2026-02-24-simplify-ascii-tools/tasks.md +24 -0
- package/openspec/config.yaml +20 -0
- package/openspec/specs/ascii-conversion/spec.md +43 -0
- package/package.json +40 -0
- package/src/engines/decimal.ts +69 -0
- package/src/engines/programmer.ts +112 -0
- package/src/errors/handler.ts +55 -0
- package/src/errors/types.ts +37 -0
- package/src/index.ts +20 -0
- package/src/mcp/server.ts +287 -0
- package/src/mcp/tools/ascii.ts +116 -0
- package/src/mcp/tools/basic.ts +44 -0
- package/src/mcp/tools/conversion.ts +95 -0
- package/src/mcp/tools/programmer.ts +36 -0
- package/src/parser/ast.ts +51 -0
- package/src/parser/lexer.ts +216 -0
- package/src/parser/parser.ts +154 -0
- package/test/integration/ascii.test.ts +450 -0
- package/test/integration/basic-calculate.test.ts +272 -0
- package/test/integration/conversion.test.ts +357 -0
- package/test/integration/programmer-calculate.test.ts +363 -0
- package/test/unit/decimal-engine.test.ts +134 -0
- package/test/unit/error-handler.test.ts +173 -0
- package/test/unit/lexer.test.ts +176 -0
- package/test/unit/parser.test.ts +197 -0
- package/test/unit/programmer-engine.test.ts +234 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Token, TokenType } from './ast.js';
|
|
2
|
+
|
|
3
|
+
export class Lexer {
|
|
4
|
+
private pos = 0;
|
|
5
|
+
private input = '';
|
|
6
|
+
private length = 0;
|
|
7
|
+
|
|
8
|
+
tokenize(input: string): Token[] {
|
|
9
|
+
this.input = input;
|
|
10
|
+
this.pos = 0;
|
|
11
|
+
this.length = input.length;
|
|
12
|
+
const tokens: Token[] = [];
|
|
13
|
+
|
|
14
|
+
while (this.pos < this.length) {
|
|
15
|
+
const char = this.input[this.pos];
|
|
16
|
+
|
|
17
|
+
if (this.isWhitespace(char)) {
|
|
18
|
+
this.skipWhitespace();
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const token = this.readToken();
|
|
23
|
+
if (token && token.type !== 'WHITESPACE') {
|
|
24
|
+
tokens.push(token);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
tokens.push({
|
|
29
|
+
type: 'EOF',
|
|
30
|
+
value: '',
|
|
31
|
+
position: { start: this.pos, end: this.pos },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return tokens;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private readToken(): Token | null {
|
|
38
|
+
const char = this.input[this.pos];
|
|
39
|
+
const start = this.pos;
|
|
40
|
+
|
|
41
|
+
// Hex literal: 0x or 0X
|
|
42
|
+
if (char === '0' && this.peek() === 'x') {
|
|
43
|
+
return this.readHexLiteral(start);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Binary literal: 0b or 0B
|
|
47
|
+
if (char === '0' && this.peek() === 'b') {
|
|
48
|
+
return this.readBinaryLiteral(start);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Octal literal: 0o or 0O
|
|
52
|
+
if (char === '0' && this.peek() === 'o') {
|
|
53
|
+
return this.readOctalLiteral(start);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Number (decimal or float)
|
|
57
|
+
if (this.isDigit(char) || (char === '.' && this.isDigit(this.peek()))) {
|
|
58
|
+
return this.readNumber(start);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Operators
|
|
62
|
+
if (this.isOperatorChar(char)) {
|
|
63
|
+
return this.readOperator(start);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Parentheses
|
|
67
|
+
if (char === '(') {
|
|
68
|
+
this.advance();
|
|
69
|
+
return { type: 'LPAREN', value: '(', position: { start, end: this.pos } };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (char === ')') {
|
|
73
|
+
this.advance();
|
|
74
|
+
return { type: 'RPAREN', value: ')', position: { start, end: this.pos } };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Char literal
|
|
78
|
+
if (char === "'") {
|
|
79
|
+
return this.readCharLiteral(start);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Unknown character
|
|
83
|
+
this.advance();
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private readNumber(start: number): Token {
|
|
88
|
+
let value = '';
|
|
89
|
+
|
|
90
|
+
while (this.pos < this.length && (this.isDigit(this.input[this.pos]) || this.input[this.pos] === '.')) {
|
|
91
|
+
value += this.input[this.pos];
|
|
92
|
+
this.advance();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { type: 'NUMBER', value, position: { start, end: this.pos } };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private readHexLiteral(start: number): Token {
|
|
99
|
+
this.advance(); // '0'
|
|
100
|
+
this.advance(); // 'x'
|
|
101
|
+
|
|
102
|
+
let value = '0x';
|
|
103
|
+
while (this.pos < this.length && this.isHexDigit(this.input[this.pos])) {
|
|
104
|
+
value += this.input[this.pos];
|
|
105
|
+
this.advance();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { type: 'HEX', value, position: { start, end: this.pos } };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private readBinaryLiteral(start: number): Token {
|
|
112
|
+
this.advance(); // '0'
|
|
113
|
+
this.advance(); // 'b'
|
|
114
|
+
|
|
115
|
+
let value = '0b';
|
|
116
|
+
while (this.pos < this.length && (this.input[this.pos] === '0' || this.input[this.pos] === '1')) {
|
|
117
|
+
value += this.input[this.pos];
|
|
118
|
+
this.advance();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { type: 'BINARY', value, position: { start, end: this.pos } };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private readOctalLiteral(start: number): Token {
|
|
125
|
+
this.advance(); // '0'
|
|
126
|
+
this.advance(); // 'o'
|
|
127
|
+
|
|
128
|
+
let value = '0o';
|
|
129
|
+
while (this.pos < this.length && this.isOctalDigit(this.input[this.pos])) {
|
|
130
|
+
value += this.input[this.pos];
|
|
131
|
+
this.advance();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { type: 'OCTAL', value, position: { start, end: this.pos } };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private readOperator(start: number): Token {
|
|
138
|
+
// Check for multi-char operators: <<, >>, >>>
|
|
139
|
+
const twoChar = this.input.substring(this.pos, this.pos + 2);
|
|
140
|
+
|
|
141
|
+
if (twoChar === '<<' || twoChar === '>>') {
|
|
142
|
+
// Check for >>>
|
|
143
|
+
if (twoChar === '>>' && this.peek(2) === '>') {
|
|
144
|
+
this.pos += 3;
|
|
145
|
+
return { type: 'OPERATOR', value: '>>>', position: { start, end: this.pos } };
|
|
146
|
+
}
|
|
147
|
+
this.pos += 2;
|
|
148
|
+
return { type: 'OPERATOR', value: twoChar, position: { start, end: this.pos } };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Single char operators
|
|
152
|
+
const op = this.input[this.pos];
|
|
153
|
+
this.advance();
|
|
154
|
+
return { type: 'OPERATOR', value: op, position: { start, end: this.pos } };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private readCharLiteral(start: number): Token {
|
|
158
|
+
this.advance(); // opening '
|
|
159
|
+
|
|
160
|
+
let value = "'";
|
|
161
|
+
|
|
162
|
+
// Handle escape sequences
|
|
163
|
+
if (this.input[this.pos] === '\\') {
|
|
164
|
+
value += this.input[this.pos];
|
|
165
|
+
this.advance();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this.pos < this.length) {
|
|
169
|
+
value += this.input[this.pos];
|
|
170
|
+
this.advance();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (this.pos < this.length && this.input[this.pos] === "'") {
|
|
174
|
+
value += this.input[this.pos];
|
|
175
|
+
this.advance();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { type: 'CHAR_LITERAL', value, position: { start, end: this.pos } };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private skipWhitespace(): void {
|
|
182
|
+
while (this.pos < this.length && this.isWhitespace(this.input[this.pos])) {
|
|
183
|
+
this.pos++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private advance(): void {
|
|
188
|
+
this.pos++;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private peek(offset = 1): string {
|
|
192
|
+
return this.pos + offset < this.length ? this.input[this.pos + offset] : '';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private isWhitespace(char: string): boolean {
|
|
196
|
+
return /\s/.test(char);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private isDigit(char: string): boolean {
|
|
200
|
+
return /[0-9]/.test(char);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private isHexDigit(char: string): boolean {
|
|
204
|
+
return /[0-9a-fA-F]/.test(char);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private isOctalDigit(char: string): boolean {
|
|
208
|
+
return /[0-7]/.test(char);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private isOperatorChar(char: string): boolean {
|
|
212
|
+
return ['+', '-', '*', '/', '%', '&', '|', '^', '~', '!', '<', '>'].includes(char);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type { Token, TokenType };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Token, ASTNode, BinaryOpNode, UnaryOpNode, LiteralNode, GroupNode, TokenType } from './ast.js';
|
|
2
|
+
import { ErrorHandler } from '../errors/handler.js';
|
|
3
|
+
|
|
4
|
+
// Operator precedence (higher number = higher precedence)
|
|
5
|
+
const PRECEDENCE: Record<string, number> = {
|
|
6
|
+
'(': 10,
|
|
7
|
+
')': 10,
|
|
8
|
+
'~': 9,
|
|
9
|
+
'*': 8,
|
|
10
|
+
'/': 8,
|
|
11
|
+
'%': 8,
|
|
12
|
+
'+': 7,
|
|
13
|
+
'-': 7,
|
|
14
|
+
'<<': 6,
|
|
15
|
+
'>>': 6,
|
|
16
|
+
'>>>': 6,
|
|
17
|
+
'&': 5,
|
|
18
|
+
'^': 4,
|
|
19
|
+
'|': 3,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class Parser {
|
|
23
|
+
private pos = 0;
|
|
24
|
+
private tokens: Token[] = [];
|
|
25
|
+
private errorHandler = new ErrorHandler();
|
|
26
|
+
|
|
27
|
+
parse(tokens: Token[]): ASTNode {
|
|
28
|
+
this.tokens = tokens;
|
|
29
|
+
this.pos = 0;
|
|
30
|
+
|
|
31
|
+
if (this.tokens.length === 0 || (this.tokens.length === 1 && this.tokens[0].type === 'EOF')) {
|
|
32
|
+
throw new Error('Expression cannot be empty');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ast = this.parseExpression();
|
|
36
|
+
|
|
37
|
+
if (this.current()?.type !== 'EOF') {
|
|
38
|
+
const token = this.current()!;
|
|
39
|
+
throw this.errorHandler.syntaxError(
|
|
40
|
+
`Unexpected token: ${token.value}`,
|
|
41
|
+
token.position.start,
|
|
42
|
+
token.position.end
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return ast;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private parseExpression(minPrecedence = 0): ASTNode {
|
|
50
|
+
let left = this.parseUnary();
|
|
51
|
+
|
|
52
|
+
while (true) {
|
|
53
|
+
const op = this.current();
|
|
54
|
+
if (!op || op.type !== 'OPERATOR') {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const precedence = PRECEDENCE[op.value] ?? 0;
|
|
59
|
+
if (precedence < minPrecedence) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.advance();
|
|
64
|
+
const right = this.parseExpression(precedence + 1);
|
|
65
|
+
|
|
66
|
+
left = {
|
|
67
|
+
type: 'BinaryOp',
|
|
68
|
+
operator: op.value,
|
|
69
|
+
left,
|
|
70
|
+
right,
|
|
71
|
+
position: { start: left.position?.start ?? 0, end: right.position?.end ?? 0 },
|
|
72
|
+
} as BinaryOpNode;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return left;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private parseUnary(): ASTNode {
|
|
79
|
+
const token = this.current();
|
|
80
|
+
|
|
81
|
+
if (token && token.type === 'OPERATOR' && (token.value === '~' || token.value === '-')) {
|
|
82
|
+
this.advance();
|
|
83
|
+
const operand = this.parseUnary();
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
type: 'UnaryOp',
|
|
87
|
+
operator: token.value,
|
|
88
|
+
operand,
|
|
89
|
+
position: { start: token.position.start, end: operand.position?.end ?? token.position.end },
|
|
90
|
+
} as UnaryOpNode;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return this.parsePrimary();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private parsePrimary(): ASTNode {
|
|
97
|
+
const token = this.current();
|
|
98
|
+
|
|
99
|
+
if (!token || token.type === 'EOF') {
|
|
100
|
+
throw new Error('Unexpected end of input');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Parenthesized expression
|
|
104
|
+
if (token.type === 'LPAREN') {
|
|
105
|
+
this.advance();
|
|
106
|
+
const expr = this.parseExpression();
|
|
107
|
+
|
|
108
|
+
const closing = this.current();
|
|
109
|
+
if (!closing || closing.type !== 'RPAREN') {
|
|
110
|
+
throw this.errorHandler.syntaxError(
|
|
111
|
+
'Unmatched parenthesis: expected )',
|
|
112
|
+
token.position.start,
|
|
113
|
+
token.position.end
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
this.advance();
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
type: 'Group',
|
|
120
|
+
expression: expr,
|
|
121
|
+
position: { start: token.position.start, end: closing.position.end },
|
|
122
|
+
} as GroupNode;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Literal
|
|
126
|
+
if (this.isLiteral(token)) {
|
|
127
|
+
this.advance();
|
|
128
|
+
return {
|
|
129
|
+
type: 'Literal',
|
|
130
|
+
value: token.value,
|
|
131
|
+
tokenType: token.type,
|
|
132
|
+
position: token.position,
|
|
133
|
+
} as LiteralNode;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw this.errorHandler.syntaxError(
|
|
137
|
+
`Unexpected token: ${token.value}`,
|
|
138
|
+
token.position.start,
|
|
139
|
+
token.position.end
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private isLiteral(token: Token): boolean {
|
|
144
|
+
return ['NUMBER', 'HEX', 'BINARY', 'OCTAL', 'CHAR_LITERAL'].includes(token.type);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private current(): Token | undefined {
|
|
148
|
+
return this.tokens[this.pos];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private advance(): void {
|
|
152
|
+
this.pos++;
|
|
153
|
+
}
|
|
154
|
+
}
|