@termaxjs/web-ai 0.1.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/LICENSE +201 -0
- package/dist/config.d.ts +511 -0
- package/dist/config.js +708 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/lib/compact.d.ts +8 -0
- package/dist/lib/compact.js +179 -0
- package/dist/lib/index.d.ts +4 -0
- package/dist/lib/index.js +4 -0
- package/dist/lib/miniWindowGeometry.d.ts +17 -0
- package/dist/lib/miniWindowGeometry.js +59 -0
- package/dist/lib/redact.d.ts +1 -0
- package/dist/lib/redact.js +34 -0
- package/dist/lib/security.d.ts +72 -0
- package/dist/lib/security.js +376 -0
- package/dist/tools/agent.d.ts +4 -0
- package/dist/tools/agent.js +22 -0
- package/dist/tools/context.d.ts +30 -0
- package/dist/tools/context.js +8 -0
- package/dist/tools/edit.d.ts +27 -0
- package/dist/tools/edit.js +161 -0
- package/dist/tools/fs.d.ts +93 -0
- package/dist/tools/fs.js +215 -0
- package/dist/tools/search.d.ts +48 -0
- package/dist/tools/search.js +118 -0
- package/dist/tools/shell.d.ts +7 -0
- package/dist/tools/shell.js +37 -0
- package/dist/tools/subagent.d.ts +2 -0
- package/dist/tools/subagent.js +3 -0
- package/dist/tools/terminal.d.ts +44 -0
- package/dist/tools/terminal.js +100 -0
- package/dist/tools/todo.d.ts +2 -0
- package/dist/tools/todo.js +3 -0
- package/dist/tools/tools.d.ts +236 -0
- package/dist/tools/tools.js +42 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.js +1 -0
- package/package.json +51 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ModelMessage } from "ai";
|
|
2
|
+
export type CompactResult = {
|
|
3
|
+
messages: ModelMessage[];
|
|
4
|
+
compacted: boolean;
|
|
5
|
+
droppedCount: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function compactModelMessages(messages: ModelMessage[], contextLimit: number): ModelMessage[];
|
|
8
|
+
export declare function compactModelMessagesDetailed(messages: ModelMessage[], contextLimit: number): CompactResult;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const KEEP_TAIL = 24;
|
|
2
|
+
const ELISION_TEXT = "[elided to save context — see prior tool call in history]";
|
|
3
|
+
function approxBytes(messages) {
|
|
4
|
+
let n = 0;
|
|
5
|
+
for (const m of messages) {
|
|
6
|
+
if (typeof m.content === "string")
|
|
7
|
+
n += m.content.length;
|
|
8
|
+
else if (Array.isArray(m.content)) {
|
|
9
|
+
for (const part of m.content) {
|
|
10
|
+
if (part.type === "text" && typeof part.text === "string")
|
|
11
|
+
n += part.text.length;
|
|
12
|
+
else if (part.type === "tool-result")
|
|
13
|
+
n += JSON.stringify(part.output ?? "").length;
|
|
14
|
+
else if (part.type === "tool-call")
|
|
15
|
+
n += JSON.stringify(part.input ?? "").length;
|
|
16
|
+
else
|
|
17
|
+
n += 64;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return n;
|
|
22
|
+
}
|
|
23
|
+
function elideToolResult(part) {
|
|
24
|
+
if (part.type !== "tool-result")
|
|
25
|
+
return { changed: false, part };
|
|
26
|
+
if (part.output &&
|
|
27
|
+
typeof part.output === "object" &&
|
|
28
|
+
part.output.__elided) {
|
|
29
|
+
return { changed: false, part };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
changed: true,
|
|
33
|
+
part: {
|
|
34
|
+
...part,
|
|
35
|
+
output: { type: "text", value: ELISION_TEXT, __elided: true },
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function pathOfInput(input) {
|
|
40
|
+
if (!input || typeof input !== "object")
|
|
41
|
+
return null;
|
|
42
|
+
const p = input.path;
|
|
43
|
+
return typeof p === "string" && p.length > 0 ? p : null;
|
|
44
|
+
}
|
|
45
|
+
function collectMutationPaths(messages) {
|
|
46
|
+
const paths = new Set();
|
|
47
|
+
for (const m of messages) {
|
|
48
|
+
if (!Array.isArray(m.content))
|
|
49
|
+
continue;
|
|
50
|
+
for (const part of m.content) {
|
|
51
|
+
if (part.type !== "tool-call")
|
|
52
|
+
continue;
|
|
53
|
+
const name = part.toolName;
|
|
54
|
+
if (name === "edit" ||
|
|
55
|
+
name === "multi_edit" ||
|
|
56
|
+
name === "write_file" ||
|
|
57
|
+
name === "create_directory") {
|
|
58
|
+
const p = pathOfInput(part.input);
|
|
59
|
+
if (p)
|
|
60
|
+
paths.add(p);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return paths;
|
|
65
|
+
}
|
|
66
|
+
function collectLastReadIdxPerPath(messages) {
|
|
67
|
+
const lastIdx = new Map();
|
|
68
|
+
for (let i = 0; i < messages.length; i++) {
|
|
69
|
+
const m = messages[i];
|
|
70
|
+
if (!Array.isArray(m.content))
|
|
71
|
+
continue;
|
|
72
|
+
for (const part of m.content) {
|
|
73
|
+
if (part.type !== "tool-call")
|
|
74
|
+
continue;
|
|
75
|
+
if (part.toolName !== "read_file")
|
|
76
|
+
continue;
|
|
77
|
+
const p = pathOfInput(part.input);
|
|
78
|
+
if (p)
|
|
79
|
+
lastIdx.set(p, i);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return lastIdx;
|
|
83
|
+
}
|
|
84
|
+
function dropSupersededReads(messages) {
|
|
85
|
+
const mutated = collectMutationPaths(messages);
|
|
86
|
+
const lastReadIdx = collectLastReadIdxPerPath(messages);
|
|
87
|
+
const callIdxToPath = new Map();
|
|
88
|
+
for (let i = 0; i < messages.length; i++) {
|
|
89
|
+
const m = messages[i];
|
|
90
|
+
if (!Array.isArray(m.content))
|
|
91
|
+
continue;
|
|
92
|
+
for (const part of m.content) {
|
|
93
|
+
if (part.type !== "tool-call" || part.toolName !== "read_file")
|
|
94
|
+
continue;
|
|
95
|
+
const p = pathOfInput(part.input);
|
|
96
|
+
const id = part.toolCallId;
|
|
97
|
+
if (p && typeof id === "string")
|
|
98
|
+
callIdxToPath.set(id, p);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
let touched = false;
|
|
102
|
+
const out = messages.map((m, i) => {
|
|
103
|
+
if (!Array.isArray(m.content))
|
|
104
|
+
return m;
|
|
105
|
+
let local = false;
|
|
106
|
+
const nextContent = m.content.map((part) => {
|
|
107
|
+
if (part.type !== "tool-result")
|
|
108
|
+
return part;
|
|
109
|
+
const id = part.toolCallId;
|
|
110
|
+
if (typeof id !== "string")
|
|
111
|
+
return part;
|
|
112
|
+
const path = callIdxToPath.get(id);
|
|
113
|
+
if (!path)
|
|
114
|
+
return part;
|
|
115
|
+
const isStale = mutated.has(path) ||
|
|
116
|
+
(lastReadIdx.has(path) && lastReadIdx.get(path) > i);
|
|
117
|
+
if (!isStale)
|
|
118
|
+
return part;
|
|
119
|
+
const r = elideToolResult(part);
|
|
120
|
+
if (r.changed)
|
|
121
|
+
local = true;
|
|
122
|
+
return r.part;
|
|
123
|
+
});
|
|
124
|
+
if (!local)
|
|
125
|
+
return m;
|
|
126
|
+
touched = true;
|
|
127
|
+
return { ...m, content: nextContent };
|
|
128
|
+
});
|
|
129
|
+
return { out, touched };
|
|
130
|
+
}
|
|
131
|
+
export function compactModelMessages(messages, contextLimit) {
|
|
132
|
+
return compactModelMessagesDetailed(messages, contextLimit).messages;
|
|
133
|
+
}
|
|
134
|
+
export function compactModelMessagesDetailed(messages, contextLimit) {
|
|
135
|
+
let dropped = 0;
|
|
136
|
+
let working = messages;
|
|
137
|
+
let approxTokens = approxBytes(working) / 4;
|
|
138
|
+
if (approxTokens >= 0.55 * contextLimit) {
|
|
139
|
+
const r = dropSupersededReads(working);
|
|
140
|
+
if (r.touched) {
|
|
141
|
+
working = r.out;
|
|
142
|
+
dropped++;
|
|
143
|
+
approxTokens = approxBytes(working) / 4;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (approxTokens < 0.7 * contextLimit) {
|
|
147
|
+
return {
|
|
148
|
+
messages: working,
|
|
149
|
+
compacted: dropped > 0,
|
|
150
|
+
droppedCount: dropped,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const out = working.slice();
|
|
154
|
+
const stopIdx = Math.max(0, out.length - KEEP_TAIL);
|
|
155
|
+
for (let i = 0; i < stopIdx; i++) {
|
|
156
|
+
if (out[i].role === "system")
|
|
157
|
+
continue;
|
|
158
|
+
if (!Array.isArray(out[i].content))
|
|
159
|
+
continue;
|
|
160
|
+
let local = false;
|
|
161
|
+
const next = out[i].content.map((part) => {
|
|
162
|
+
const r = elideToolResult(part);
|
|
163
|
+
if (r.changed)
|
|
164
|
+
local = true;
|
|
165
|
+
return r.part;
|
|
166
|
+
});
|
|
167
|
+
if (local) {
|
|
168
|
+
out[i] = { ...out[i], content: next };
|
|
169
|
+
dropped++;
|
|
170
|
+
if (approxBytes(out) / 4 < 0.6 * contextLimit)
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
messages: out,
|
|
176
|
+
compacted: dropped > 0,
|
|
177
|
+
droppedCount: dropped,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type Geom = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
w: number;
|
|
5
|
+
h: number;
|
|
6
|
+
};
|
|
7
|
+
export type Viewport = {
|
|
8
|
+
vw: number;
|
|
9
|
+
vh: number;
|
|
10
|
+
};
|
|
11
|
+
export type ResizeDir = "n" | "s" | "e" | "w" | "ne" | "nw" | "se" | "sw";
|
|
12
|
+
export declare const MIN_W = 400;
|
|
13
|
+
export declare const MIN_H = 280;
|
|
14
|
+
export declare function defaultGeom(vp: Viewport): Geom;
|
|
15
|
+
export declare function clampGeom(g: Geom, vp: Viewport): Geom;
|
|
16
|
+
export declare function applyDrag(start: Geom, dx: number, dy: number, vp: Viewport): Geom;
|
|
17
|
+
export declare function applyResize(start: Geom, dir: ResizeDir, dx: number, dy: number, vp: Viewport): Geom;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const MIN_W = 400;
|
|
2
|
+
export const MIN_H = 280;
|
|
3
|
+
const MARGIN_X = 16;
|
|
4
|
+
const BOTTOM_GAP = 96;
|
|
5
|
+
const TOP_GAP = 16;
|
|
6
|
+
const clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
|
|
7
|
+
export function defaultGeom(vp) {
|
|
8
|
+
const w = Math.max(MIN_W, Math.min(500, vp.vw - MARGIN_X * 2));
|
|
9
|
+
const h = Math.max(MIN_H, Math.min(600, vp.vh - BOTTOM_GAP - TOP_GAP));
|
|
10
|
+
return clampGeom({ x: vp.vw - w - MARGIN_X, y: vp.vh - h - BOTTOM_GAP, w, h }, vp);
|
|
11
|
+
}
|
|
12
|
+
export function clampGeom(g, vp) {
|
|
13
|
+
const w = clamp(g.w, MIN_W, Math.max(MIN_W, vp.vw));
|
|
14
|
+
const h = clamp(g.h, MIN_H, Math.max(MIN_H, vp.vh));
|
|
15
|
+
return {
|
|
16
|
+
w,
|
|
17
|
+
h,
|
|
18
|
+
x: clamp(g.x, 0, Math.max(0, vp.vw - w)),
|
|
19
|
+
y: clamp(g.y, 0, Math.max(0, vp.vh - h)),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function applyDrag(start, dx, dy, vp) {
|
|
23
|
+
return clampGeom({ ...start, x: start.x + dx, y: start.y + dy }, vp);
|
|
24
|
+
}
|
|
25
|
+
export function applyResize(start, dir, dx, dy, vp) {
|
|
26
|
+
let left = start.x;
|
|
27
|
+
let top = start.y;
|
|
28
|
+
let right = start.x + start.w;
|
|
29
|
+
let bottom = start.y + start.h;
|
|
30
|
+
const movesW = dir.includes("w");
|
|
31
|
+
const movesE = dir.includes("e");
|
|
32
|
+
const movesN = dir.includes("n");
|
|
33
|
+
const movesS = dir.includes("s");
|
|
34
|
+
if (movesE)
|
|
35
|
+
right += dx;
|
|
36
|
+
if (movesW)
|
|
37
|
+
left += dx;
|
|
38
|
+
if (movesS)
|
|
39
|
+
bottom += dy;
|
|
40
|
+
if (movesN)
|
|
41
|
+
top += dy;
|
|
42
|
+
left = Math.max(0, left);
|
|
43
|
+
top = Math.max(0, top);
|
|
44
|
+
right = Math.min(vp.vw, right);
|
|
45
|
+
bottom = Math.min(vp.vh, bottom);
|
|
46
|
+
if (right - left < MIN_W) {
|
|
47
|
+
if (movesW)
|
|
48
|
+
left = right - MIN_W;
|
|
49
|
+
else
|
|
50
|
+
right = left + MIN_W;
|
|
51
|
+
}
|
|
52
|
+
if (bottom - top < MIN_H) {
|
|
53
|
+
if (movesN)
|
|
54
|
+
top = bottom - MIN_H;
|
|
55
|
+
else
|
|
56
|
+
bottom = top + MIN_H;
|
|
57
|
+
}
|
|
58
|
+
return clampGeom({ x: left, y: top, w: right - left, h: bottom - top }, vp);
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function redactSensitive(text: string): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const PATTERNS = [
|
|
2
|
+
{ kind: "openai-key", re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g },
|
|
3
|
+
{ kind: "anthropic-key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
|
|
4
|
+
{ kind: "aws-access-key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
|
|
5
|
+
{ kind: "github-token", re: /\bgh[opsur]_[A-Za-z0-9]{36,}\b/g },
|
|
6
|
+
{ kind: "github-pat", re: /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g },
|
|
7
|
+
{ kind: "google-api-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
8
|
+
{ kind: "slack-token", re: /\bxox[bpsare]-[A-Za-z0-9-]{10,}\b/g },
|
|
9
|
+
{
|
|
10
|
+
kind: "stripe-key",
|
|
11
|
+
re: /\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
kind: "jwt",
|
|
15
|
+
re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
16
|
+
},
|
|
17
|
+
{ kind: "bearer", re: /\bBearer\s+[A-Za-z0-9._-]{20,}/g },
|
|
18
|
+
{
|
|
19
|
+
kind: "env-assign",
|
|
20
|
+
re: /\b((?:[A-Z][A-Z0-9_]*)?(?:API[_-]?KEY|SECRET(?:[_-]?KEY)?|ACCESS[_-]?TOKEN|AUTH[_-]?TOKEN|PASSWORD|PASSWD|PRIVATE[_-]?KEY|CLIENT[_-]?SECRET)[A-Z0-9_]*)\s*[:=]\s*(["']?)([^\s"';|&]+)\2/gi,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
export function redactSensitive(text) {
|
|
24
|
+
let out = text;
|
|
25
|
+
for (const { kind, re } of PATTERNS) {
|
|
26
|
+
if (kind === "env-assign") {
|
|
27
|
+
out = out.replace(re, (_m, name, q, _val) => `${name}=${q}<REDACTED>${q}`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
out = out.replace(re, `<REDACTED:${kind}>`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path-safety guards for AI tool calls.
|
|
3
|
+
*
|
|
4
|
+
* Goals:
|
|
5
|
+
* - Block reads of files that almost always contain secrets (.env*, *.pem,
|
|
6
|
+
* id_rsa*, .aws/credentials, .ssh/, .git/, kube/azure config, etc.).
|
|
7
|
+
* - Block writes/exec into the same set, plus directories where automated
|
|
8
|
+
* mutation is dangerous (system dirs, Windows system dirs).
|
|
9
|
+
*
|
|
10
|
+
* This is a *defense layer*, not a sandbox. The model may still be coaxed
|
|
11
|
+
* into doing something silly within allowed paths — the user-confirmation
|
|
12
|
+
* UI for write/exec is the real safety net. These checks ensure that
|
|
13
|
+
* read tools (which auto-approve) can never silently exfiltrate obvious
|
|
14
|
+
* secrets, and that a single bad approval can't blow up the system.
|
|
15
|
+
*
|
|
16
|
+
* Defense-in-depth notes:
|
|
17
|
+
* - Comparison surface is lowercased *only for matching*. Original path is
|
|
18
|
+
* preserved for basename pattern checks and error messages.
|
|
19
|
+
* - Windows drive prefix (e.g. `C:`) is stripped from the comparison form so
|
|
20
|
+
* Unix-style root prefix checks behave consistently on both platforms.
|
|
21
|
+
* - Protected directories match exact-equal-or-descendant, not raw
|
|
22
|
+
* substring-with-trailing-slash. Bare names (`/Users/me/.ssh`) and
|
|
23
|
+
* case-variants (`/Users/me/.SSH/config` on macOS/Windows case-insensitive
|
|
24
|
+
* filesystems) are caught.
|
|
25
|
+
* - The caller is expected to additionally validate the *canonical* path
|
|
26
|
+
* (post symlink resolution) via `getAiAdapter().native.canonicalize` + a second
|
|
27
|
+
* `checkReadable` pass, since a symlink at an "innocent" path can point
|
|
28
|
+
* into a protected directory.
|
|
29
|
+
*/
|
|
30
|
+
export type SafetyResult = {
|
|
31
|
+
ok: true;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
reason: string;
|
|
35
|
+
};
|
|
36
|
+
export declare function checkReadable(path: string): SafetyResult;
|
|
37
|
+
export declare function checkWritable(path: string): SafetyResult;
|
|
38
|
+
/**
|
|
39
|
+
* Lightweight heuristic for blocking obviously destructive shell commands
|
|
40
|
+
* even after the user has approved them. The approval UI shows the command
|
|
41
|
+
* verbatim, so the user is the primary gate; this just catches a couple of
|
|
42
|
+
* patterns that almost certainly indicate the model went off the rails.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Two-phase safety check that also defends against symlink traversal: first
|
|
46
|
+
* checks the literal path, then (if it exists) canonicalizes it via the
|
|
47
|
+
* native FS and re-checks the resolved path. A symlink at `./innocent.txt`
|
|
48
|
+
* pointing into `~/.ssh/id_rsa` is caught on the second pass.
|
|
49
|
+
*
|
|
50
|
+
* Returns the canonical path on success so callers can use it for the actual
|
|
51
|
+
* read — avoids TOCTOU between the safety check and the read.
|
|
52
|
+
*/
|
|
53
|
+
export declare function checkReadableCanonical(path: string, canonicalize: (p: string) => Promise<string>): Promise<{
|
|
54
|
+
ok: true;
|
|
55
|
+
canonical: string;
|
|
56
|
+
} | {
|
|
57
|
+
ok: false;
|
|
58
|
+
reason: string;
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Same pattern as {@link checkReadableCanonical} but for writes. The canonical
|
|
62
|
+
* path is only available if the file already exists — for new-file creates
|
|
63
|
+
* we additionally canonicalize the parent directory.
|
|
64
|
+
*/
|
|
65
|
+
export declare function checkWritableCanonical(path: string, canonicalize: (p: string) => Promise<string>): Promise<{
|
|
66
|
+
ok: true;
|
|
67
|
+
canonical: string;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
reason: string;
|
|
71
|
+
}>;
|
|
72
|
+
export declare function checkShellCommand(cmd: string): SafetyResult;
|