@capsuleer/calc 1.0.1
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/README.md +95 -0
- package/capsuleer.manifest.json +106 -0
- package/index.ts +94 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @capsuleer/calc
|
|
2
|
+
|
|
3
|
+
Calculator module for Capsuleer agents. The world's most over-engineered calculator — but that's the point. Lets agents evaluate math expressions, round results, and crunch descriptive statistics, with every operation emitting a structured trace event so nothing gets lost in the noise.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
capsuleer install calc
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
No dependencies. No `eval()`. No regrets.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
### Expressions
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// Basic arithmetic
|
|
19
|
+
const result = await calc.evaluate("(42 * 1.5) + 8")
|
|
20
|
+
// → 71
|
|
21
|
+
|
|
22
|
+
// Math functions and constants
|
|
23
|
+
const hyp = await calc.evaluate("sqrt(pow(3, 2) + pow(4, 2))")
|
|
24
|
+
// → 5
|
|
25
|
+
|
|
26
|
+
const circle = await calc.evaluate("PI * pow(7, 2)")
|
|
27
|
+
// → 153.93804002589985
|
|
28
|
+
|
|
29
|
+
// Chaining with prior results
|
|
30
|
+
const raw = await calc.evaluate("1 / 3")
|
|
31
|
+
const clean = await calc.round(raw, 4)
|
|
32
|
+
// → 0.3333
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Supported functions:** `sqrt`, `cbrt`, `abs`, `ceil`, `floor`, `round`, `log`, `log2`, `log10`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `pow`, `min`, `max`
|
|
36
|
+
|
|
37
|
+
**Constants:** `PI`, `E`
|
|
38
|
+
|
|
39
|
+
### Rounding
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
await calc.round(3.14159, 2) // → 3.14
|
|
43
|
+
await calc.round(1234.5) // → 1235 (nearest integer)
|
|
44
|
+
await calc.round(0.1 + 0.2, 10) // → 0.3 (float noise begone)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Statistics
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
const scores = [88, 92, 75, 100, 61, 84, 90]
|
|
51
|
+
const s = await calc.stats(scores)
|
|
52
|
+
// → {
|
|
53
|
+
// count: 7,
|
|
54
|
+
// sum: 590,
|
|
55
|
+
// mean: 84.28...,
|
|
56
|
+
// median: 88,
|
|
57
|
+
// min: 61,
|
|
58
|
+
// max: 100,
|
|
59
|
+
// range: 39
|
|
60
|
+
// }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Observability
|
|
66
|
+
|
|
67
|
+
```jsonl
|
|
68
|
+
{ "ok": true, "op": "calc.evaluate", "data": { "expr": "sqrt(144) + 3 ** 2", "result": 21 } }
|
|
69
|
+
{ "ok": true, "op": "calc.round", "data": { "value": 3.14159, "decimals": 2, "result": 3.14 } }
|
|
70
|
+
{ "ok": true, "op": "calc.stats", "data": { "count": 7, "result": { "mean": 84.28, "median": 88, ... } } }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Safety
|
|
76
|
+
|
|
77
|
+
`evaluate()` does not use `eval()`. Expressions are validated against an allowlist of characters and identifiers, then executed via `new Function()` with only the math environment in scope. Property access (`.`) and assignment (`=`) are explicitly blocked. Anything that doesn't look like a math expression throws immediately.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Policy
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const capsule = await Capsule({
|
|
85
|
+
policy: {
|
|
86
|
+
calc: {
|
|
87
|
+
evaluate: true,
|
|
88
|
+
round: true,
|
|
89
|
+
stats: true,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
See the [policy docs](https://axon.arclabs.it/docs/capsuleer/policy) for the full rule syntax.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "calc",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Calculator module — evaluate math expressions, round numbers, and compute descriptive statistics",
|
|
5
|
+
"exports": [
|
|
6
|
+
{
|
|
7
|
+
"name": "evaluate",
|
|
8
|
+
"declaration": "declare function evaluate(expr: string): Promise<number>",
|
|
9
|
+
"schema": {
|
|
10
|
+
"parameters": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"properties": {
|
|
13
|
+
"expr": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"required": [
|
|
18
|
+
"expr"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"returns": {
|
|
22
|
+
"type": "number"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "round",
|
|
28
|
+
"declaration": "declare function round(value: number, decimals: number): Promise<number>",
|
|
29
|
+
"schema": {
|
|
30
|
+
"parameters": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"value": {
|
|
34
|
+
"type": "number"
|
|
35
|
+
},
|
|
36
|
+
"decimals": {
|
|
37
|
+
"type": "number"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"required": [
|
|
41
|
+
"value",
|
|
42
|
+
"decimals"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"returns": {
|
|
46
|
+
"type": "number"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "stats",
|
|
52
|
+
"declaration": "declare function stats(values: Array<number>): Promise<{ count: number; sum: number; mean: number; median: number; min: number; max: number; range: number }>",
|
|
53
|
+
"schema": {
|
|
54
|
+
"parameters": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {
|
|
57
|
+
"values": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"items": {
|
|
60
|
+
"type": "number"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"required": [
|
|
65
|
+
"values"
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"returns": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"properties": {
|
|
71
|
+
"count": {
|
|
72
|
+
"type": "number"
|
|
73
|
+
},
|
|
74
|
+
"sum": {
|
|
75
|
+
"type": "number"
|
|
76
|
+
},
|
|
77
|
+
"mean": {
|
|
78
|
+
"type": "number"
|
|
79
|
+
},
|
|
80
|
+
"median": {
|
|
81
|
+
"type": "number"
|
|
82
|
+
},
|
|
83
|
+
"min": {
|
|
84
|
+
"type": "number"
|
|
85
|
+
},
|
|
86
|
+
"max": {
|
|
87
|
+
"type": "number"
|
|
88
|
+
},
|
|
89
|
+
"range": {
|
|
90
|
+
"type": "number"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"required": [
|
|
94
|
+
"count",
|
|
95
|
+
"sum",
|
|
96
|
+
"mean",
|
|
97
|
+
"median",
|
|
98
|
+
"min",
|
|
99
|
+
"max",
|
|
100
|
+
"range"
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { defineModule } from "@capsuleer/core"
|
|
2
|
+
|
|
3
|
+
type StatsResult = { count: number; sum: number; mean: number; median: number; min: number; max: number; range: number }
|
|
4
|
+
|
|
5
|
+
// Safe expression evaluator — supports +, -, *, /, **, %, parens,
|
|
6
|
+
// and a small set of math functions. No eval(), no arbitrary code.
|
|
7
|
+
const ALLOWED = /^[\d\s\+\-\*\/\%\(\)\.\,]+$|^[\d\s\+\-\*\/\%\(\)\.\,sqrt|cbrt|abs|ceil|floor|round|log|log2|log10|sin|cos|tan|asin|acos|atan|atan2|pow|min|max|PI|E]+$/
|
|
8
|
+
|
|
9
|
+
const mathEnv: Record<string, unknown> = {
|
|
10
|
+
sqrt: Math.sqrt, cbrt: Math.cbrt,
|
|
11
|
+
abs: Math.abs, ceil: Math.ceil, floor: Math.floor, round: Math.round,
|
|
12
|
+
log: Math.log, log2: Math.log2, log10: Math.log10,
|
|
13
|
+
sin: Math.sin, cos: Math.cos, tan: Math.tan,
|
|
14
|
+
asin: Math.asin, acos: Math.acos, atan: Math.atan, atan2: Math.atan2,
|
|
15
|
+
pow: Math.pow, min: Math.min, max: Math.max,
|
|
16
|
+
PI: Math.PI, E: Math.E,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function safeEval(expr: string): number {
|
|
20
|
+
const sanitized = expr.trim()
|
|
21
|
+
// Only allow digits, operators, parens, dots, commas, spaces, and known identifiers
|
|
22
|
+
if (/[^0-9\s\+\-\*\/\%\(\)\.\,a-zA-Z_]/.test(sanitized)) {
|
|
23
|
+
throw new Error(`Expression contains disallowed characters: ${expr}`)
|
|
24
|
+
}
|
|
25
|
+
// Block anything that looks like property access or assignment
|
|
26
|
+
if (/\.\s*[a-zA-Z]/.test(sanitized) || /=/.test(sanitized)) {
|
|
27
|
+
throw new Error(`Expression not allowed: ${expr}`)
|
|
28
|
+
}
|
|
29
|
+
const fn = new Function(...Object.keys(mathEnv), `"use strict"; return (${sanitized})`)
|
|
30
|
+
const result = fn(...Object.values(mathEnv))
|
|
31
|
+
if (typeof result !== "number" || !isFinite(result)) {
|
|
32
|
+
throw new Error(`Expression did not produce a finite number: ${result}`)
|
|
33
|
+
}
|
|
34
|
+
return result
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const calc = {
|
|
38
|
+
async evaluate(expr: string): Promise<number> {
|
|
39
|
+
try {
|
|
40
|
+
const result = safeEval(expr)
|
|
41
|
+
console.log(JSON.stringify({ ok: true, op: "calc.evaluate", data: { expr, result } }))
|
|
42
|
+
return result
|
|
43
|
+
} catch (err: any) {
|
|
44
|
+
console.log(JSON.stringify({ ok: false, op: "calc.evaluate", data: { expr }, error: err.message }))
|
|
45
|
+
throw err
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async round(value: number, decimals = 0): Promise<number> {
|
|
50
|
+
try {
|
|
51
|
+
const factor = Math.pow(10, decimals)
|
|
52
|
+
const result = Math.round(value * factor) / factor
|
|
53
|
+
console.log(JSON.stringify({ ok: true, op: "calc.round", data: { value, decimals, result } }))
|
|
54
|
+
return result
|
|
55
|
+
} catch (err: any) {
|
|
56
|
+
console.log(JSON.stringify({ ok: false, op: "calc.round", data: { value, decimals }, error: err.message }))
|
|
57
|
+
throw err
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
async stats(values: number[]): Promise<StatsResult> {
|
|
62
|
+
try {
|
|
63
|
+
if (values.length === 0) throw new Error("Cannot compute stats on an empty array")
|
|
64
|
+
const sorted = [...values].sort((a, b) => a - b)
|
|
65
|
+
const sum = values.reduce((a, b) => a + b, 0)
|
|
66
|
+
const mean = sum / values.length
|
|
67
|
+
const mid = Math.floor(sorted.length / 2)
|
|
68
|
+
const median = sorted.length % 2 === 0
|
|
69
|
+
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
70
|
+
: sorted[mid]
|
|
71
|
+
const result: StatsResult = {
|
|
72
|
+
count: values.length,
|
|
73
|
+
sum,
|
|
74
|
+
mean,
|
|
75
|
+
median,
|
|
76
|
+
min: sorted[0],
|
|
77
|
+
max: sorted[sorted.length - 1],
|
|
78
|
+
range: sorted[sorted.length - 1] - sorted[0],
|
|
79
|
+
}
|
|
80
|
+
console.log(JSON.stringify({ ok: true, op: "calc.stats", data: { count: values.length, result } }))
|
|
81
|
+
return result
|
|
82
|
+
} catch (err: any) {
|
|
83
|
+
console.log(JSON.stringify({ ok: false, op: "calc.stats", data: { count: values.length }, error: err.message }))
|
|
84
|
+
throw err
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default defineModule({
|
|
90
|
+
name: "calc",
|
|
91
|
+
version: "1.0.0",
|
|
92
|
+
description: "Calculator module — evaluate math expressions, round numbers, and compute descriptive statistics",
|
|
93
|
+
api: calc,
|
|
94
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@capsuleer/calc",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Calculator module for capsuleer — evaluate math expressions, round numbers, and compute descriptive statistics",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"types": "./index.ts",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"capsuleer",
|
|
10
|
+
"calculator",
|
|
11
|
+
"math",
|
|
12
|
+
"statistics"
|
|
13
|
+
],
|
|
14
|
+
"author": "cody",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@capsuleer/core": "latest"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"bun": ">=1.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|