assistme 0.3.4 → 0.3.6
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/dist/{chunk-KX7ITO55.js → chunk-4YWS463E.js} +174 -22
- package/dist/{chunk-TTEGHE2E.js → chunk-JVA6DHXD.js} +6 -4
- package/dist/{config-PUIS2TQL.js → config-T4357GAE.js} +1 -1
- package/dist/index.js +513 -164
- package/dist/job-runner-JT3JWZBV.js +7 -0
- package/package.json +2 -1
- package/src/agent/event-hooks.ts +16 -8
- package/src/agent/job-runner.ts +52 -40
- package/src/agent/processor.ts +24 -27
- package/src/agent/scheduler.ts +22 -59
- package/src/agent/skill-evaluator.ts +45 -52
- package/src/agent/skills.ts +57 -36
- package/src/browser/controller.ts +434 -13
- package/src/mcp/browser-server.ts +1 -1
- package/src/tools/filesystem.ts +32 -35
- package/src/tools/shell.ts +18 -22
- package/src/utils/config.test.ts +1 -1
- package/src/utils/config.ts +15 -9
- package/src/utils/constants.ts +77 -0
- package/src/utils/errors.ts +37 -0
- package/src/utils/schemas.ts +115 -0
- package/dist/job-runner-P2L6MOOX.js +0 -7
package/src/tools/filesystem.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { readFile, writeFile, readdir, stat, mkdir } from "fs/promises";
|
|
2
|
-
import { resolve, relative, join } from "path";
|
|
2
|
+
import { resolve, relative, join, sep } from "path";
|
|
3
3
|
import { glob } from "glob";
|
|
4
4
|
import { getConfig } from "../utils/config.js";
|
|
5
|
+
import { AppError } from "../utils/errors.js";
|
|
6
|
+
import {
|
|
7
|
+
MAX_FILE_SEARCH_RESULTS,
|
|
8
|
+
MAX_CONTENT_SEARCH_FILES,
|
|
9
|
+
MAX_CONTENT_SEARCH_RESULTS,
|
|
10
|
+
} from "../utils/constants.js";
|
|
5
11
|
|
|
6
12
|
function assertWithinWorkspace(filePath: string): string {
|
|
7
13
|
const config = getConfig();
|
|
8
14
|
const resolved = resolve(config.workspacePath, filePath);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
// Use path.relative() to prevent path traversal attacks like "/workspace-evil/workspace/file"
|
|
16
|
+
const rel = relative(config.workspacePath, resolved);
|
|
17
|
+
if (rel.startsWith("..") || rel.startsWith(sep + sep)) {
|
|
18
|
+
throw new AppError(
|
|
19
|
+
`Access denied: path "${filePath}" is outside workspace "${config.workspacePath}"`,
|
|
20
|
+
"PATH_TRAVERSAL"
|
|
12
21
|
);
|
|
13
22
|
}
|
|
14
23
|
return resolved;
|
|
@@ -27,15 +36,10 @@ export async function readFileContent(
|
|
|
27
36
|
const end = limit ? start + limit : lines.length;
|
|
28
37
|
const selected = lines.slice(start, end);
|
|
29
38
|
|
|
30
|
-
return selected
|
|
31
|
-
.map((line, i) => `${String(start + i + 1).padStart(5)} | ${line}`)
|
|
32
|
-
.join("\n");
|
|
39
|
+
return selected.map((line, i) => `${String(start + i + 1).padStart(5)} | ${line}`).join("\n");
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
export async function writeFileContent(
|
|
36
|
-
path: string,
|
|
37
|
-
content: string
|
|
38
|
-
): Promise<string> {
|
|
42
|
+
export async function writeFileContent(path: string, content: string): Promise<string> {
|
|
39
43
|
const resolved = assertWithinWorkspace(path);
|
|
40
44
|
// Ensure parent directory exists
|
|
41
45
|
const dir = resolve(resolved, "..");
|
|
@@ -44,14 +48,9 @@ export async function writeFileContent(
|
|
|
44
48
|
return `File written: ${path} (${content.length} bytes)`;
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
export async function searchFiles(
|
|
48
|
-
pattern: string,
|
|
49
|
-
directory?: string
|
|
50
|
-
): Promise<string> {
|
|
51
|
+
export async function searchFiles(pattern: string, directory?: string): Promise<string> {
|
|
51
52
|
const config = getConfig();
|
|
52
|
-
const cwd = directory
|
|
53
|
-
? assertWithinWorkspace(directory)
|
|
54
|
-
: config.workspacePath;
|
|
53
|
+
const cwd = directory ? assertWithinWorkspace(directory) : config.workspacePath;
|
|
55
54
|
|
|
56
55
|
const matches = await glob(pattern, {
|
|
57
56
|
cwd,
|
|
@@ -62,16 +61,14 @@ export async function searchFiles(
|
|
|
62
61
|
if (matches.length === 0) return "No files found matching the pattern.";
|
|
63
62
|
|
|
64
63
|
return matches
|
|
65
|
-
.slice(0,
|
|
64
|
+
.slice(0, MAX_FILE_SEARCH_RESULTS)
|
|
66
65
|
.map((m) => relative(config.workspacePath, join(cwd, m)))
|
|
67
66
|
.join("\n");
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
export async function listDirectory(path?: string): Promise<string> {
|
|
71
70
|
const config = getConfig();
|
|
72
|
-
const resolved = path
|
|
73
|
-
? assertWithinWorkspace(path)
|
|
74
|
-
: config.workspacePath;
|
|
71
|
+
const resolved = path ? assertWithinWorkspace(path) : config.workspacePath;
|
|
75
72
|
|
|
76
73
|
const entries = await readdir(resolved, { withFileTypes: true });
|
|
77
74
|
const results: string[] = [];
|
|
@@ -80,9 +77,7 @@ export async function listDirectory(path?: string): Promise<string> {
|
|
|
80
77
|
if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
|
|
81
78
|
const icon = entry.isDirectory() ? "📁" : "📄";
|
|
82
79
|
const info = entry.isFile()
|
|
83
|
-
? await stat(join(resolved, entry.name)).then(
|
|
84
|
-
(s) => ` (${formatSize(s.size)})`
|
|
85
|
-
)
|
|
80
|
+
? await stat(join(resolved, entry.name)).then((s) => ` (${formatSize(s.size)})`)
|
|
86
81
|
: "";
|
|
87
82
|
results.push(`${icon} ${entry.name}${info}`);
|
|
88
83
|
}
|
|
@@ -102,9 +97,7 @@ export async function searchContent(
|
|
|
102
97
|
directory?: string
|
|
103
98
|
): Promise<string> {
|
|
104
99
|
const config = getConfig();
|
|
105
|
-
const cwd = directory
|
|
106
|
-
? assertWithinWorkspace(directory)
|
|
107
|
-
: config.workspacePath;
|
|
100
|
+
const cwd = directory ? assertWithinWorkspace(directory) : config.workspacePath;
|
|
108
101
|
|
|
109
102
|
const files = await glob(fileGlob || "**/*", {
|
|
110
103
|
cwd,
|
|
@@ -112,10 +105,16 @@ export async function searchContent(
|
|
|
112
105
|
ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"],
|
|
113
106
|
});
|
|
114
107
|
|
|
115
|
-
|
|
108
|
+
let regex: RegExp;
|
|
109
|
+
try {
|
|
110
|
+
regex = new RegExp(pattern, "gi");
|
|
111
|
+
} catch {
|
|
112
|
+
// Fall back to literal string match if the pattern is invalid regex
|
|
113
|
+
regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
114
|
+
}
|
|
116
115
|
const results: string[] = [];
|
|
117
116
|
|
|
118
|
-
for (const file of files.slice(0,
|
|
117
|
+
for (const file of files.slice(0, MAX_CONTENT_SEARCH_FILES)) {
|
|
119
118
|
try {
|
|
120
119
|
const content = await readFile(join(cwd, file), "utf-8");
|
|
121
120
|
const lines = content.split("\n");
|
|
@@ -126,17 +125,15 @@ export async function searchContent(
|
|
|
126
125
|
results.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
|
|
127
126
|
regex.lastIndex = 0;
|
|
128
127
|
|
|
129
|
-
if (results.length >=
|
|
128
|
+
if (results.length >= MAX_CONTENT_SEARCH_RESULTS) break;
|
|
130
129
|
}
|
|
131
130
|
}
|
|
132
131
|
|
|
133
|
-
if (results.length >=
|
|
132
|
+
if (results.length >= MAX_CONTENT_SEARCH_RESULTS) break;
|
|
134
133
|
} catch {
|
|
135
134
|
// Skip binary/unreadable files
|
|
136
135
|
}
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
return results.length > 0
|
|
140
|
-
? results.join("\n")
|
|
141
|
-
: "No matches found.";
|
|
138
|
+
return results.length > 0 ? results.join("\n") : "No matches found.";
|
|
142
139
|
}
|
package/src/tools/shell.ts
CHANGED
|
@@ -1,33 +1,29 @@
|
|
|
1
1
|
import { exec } from "child_process";
|
|
2
2
|
import { getConfig } from "../utils/config.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const MAX_OUTPUT = 50_000; // 50KB output cap
|
|
3
|
+
import { AppError } from "../utils/errors.js";
|
|
4
|
+
import { SHELL_TIMEOUT_MS, SHELL_MAX_OUTPUT } from "../utils/constants.js";
|
|
6
5
|
|
|
7
6
|
// Commands that are never allowed — matched as case-insensitive regex patterns
|
|
8
7
|
const BLOCKED_PATTERNS: RegExp[] = [
|
|
9
|
-
/rm\s+(-\w*\s+)*-\w*r\w*\s+\/($|\s)/i,
|
|
10
|
-
/rm\s+(-\w*\s+)*-\w*r\w*\s+~($|\s|\/)/i,
|
|
11
|
-
/\bmkfs\b/i,
|
|
12
|
-
/\bdd\s+.*\bif=/i,
|
|
13
|
-
/:\s*\(\s*\)\s*\{/,
|
|
14
|
-
/\bchmod\s+(-\w+\s+)*-R\s+777\s+\//i,
|
|
15
|
-
/>\s*\/dev\/sd[a-z]/i,
|
|
16
|
-
/\bshutdown\b/i,
|
|
17
|
-
/\breboot\b/i,
|
|
18
|
-
/\bsystemctl\s+(start|stop|disable|mask)\b/i,
|
|
8
|
+
/rm\s+(-\w*\s+)*-\w*r\w*\s+\/($|\s)/i, // rm -rf /, rm -fr /, etc.
|
|
9
|
+
/rm\s+(-\w*\s+)*-\w*r\w*\s+~($|\s|\/)/i, // rm -rf ~, rm -fr ~/, etc.
|
|
10
|
+
/\bmkfs\b/i, // mkfs (any form)
|
|
11
|
+
/\bdd\s+.*\bif=/i, // dd if=
|
|
12
|
+
/:\s*\(\s*\)\s*\{/, // fork bomb :(){}
|
|
13
|
+
/\bchmod\s+(-\w+\s+)*-R\s+777\s+\//i, // chmod -R 777 /
|
|
14
|
+
/>\s*\/dev\/sd[a-z]/i, // write to raw disk
|
|
15
|
+
/\bshutdown\b/i, // shutdown
|
|
16
|
+
/\breboot\b/i, // reboot
|
|
17
|
+
/\bsystemctl\s+(start|stop|disable|mask)\b/i, // dangerous systemctl ops
|
|
19
18
|
];
|
|
20
19
|
|
|
21
20
|
export function isBlocked(command: string): boolean {
|
|
22
21
|
return BLOCKED_PATTERNS.some((pattern) => pattern.test(command));
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
export async function executeShell(
|
|
26
|
-
command: string,
|
|
27
|
-
cwd?: string
|
|
28
|
-
): Promise<string> {
|
|
24
|
+
export async function executeShell(command: string, cwd?: string): Promise<string> {
|
|
29
25
|
if (isBlocked(command)) {
|
|
30
|
-
throw new
|
|
26
|
+
throw new AppError(`Command blocked for safety: "${command}"`, "COMMAND_BLOCKED");
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
const config = getConfig();
|
|
@@ -38,7 +34,7 @@ export async function executeShell(
|
|
|
38
34
|
command,
|
|
39
35
|
{
|
|
40
36
|
cwd: workDir,
|
|
41
|
-
timeout:
|
|
37
|
+
timeout: SHELL_TIMEOUT_MS,
|
|
42
38
|
maxBuffer: 1024 * 1024, // 1MB buffer
|
|
43
39
|
env: { ...process.env, TERM: "dumb" },
|
|
44
40
|
},
|
|
@@ -56,10 +52,10 @@ export async function executeShell(
|
|
|
56
52
|
}
|
|
57
53
|
|
|
58
54
|
// Truncate if too large
|
|
59
|
-
if (output.length >
|
|
55
|
+
if (output.length > SHELL_MAX_OUTPUT) {
|
|
60
56
|
output =
|
|
61
|
-
output.slice(0,
|
|
62
|
-
`\n\n[Output truncated at ${
|
|
57
|
+
output.slice(0, SHELL_MAX_OUTPUT) +
|
|
58
|
+
`\n\n[Output truncated at ${SHELL_MAX_OUTPUT} bytes]`;
|
|
63
59
|
}
|
|
64
60
|
|
|
65
61
|
resolve(output || "(no output)");
|
package/src/utils/config.test.ts
CHANGED
|
@@ -49,7 +49,7 @@ describe("config module", () => {
|
|
|
49
49
|
const config = getConfig();
|
|
50
50
|
expect(config.sessionName).toBe("Default");
|
|
51
51
|
expect(config.model).toBe("claude-sonnet-4-20250514");
|
|
52
|
-
expect(config.maxTurns).toBe(
|
|
52
|
+
expect(config.maxTurns).toBe(200);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
it("resolves workspacePath to absolute path", () => {
|
package/src/utils/config.ts
CHANGED
|
@@ -9,11 +9,18 @@ export interface AssistMeConfig {
|
|
|
9
9
|
sessionName: string;
|
|
10
10
|
model: string;
|
|
11
11
|
maxTurns: number;
|
|
12
|
+
taskTimeoutMinutes: number;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Default Supabase values — read from env vars first, then fall back to
|
|
17
|
+
* compile-time defaults. This keeps secrets out of plain source when deploying
|
|
18
|
+
* custom instances, while still allowing the hosted service to "just work".
|
|
19
|
+
*/
|
|
14
20
|
const SUPABASE_URL_DEFAULT =
|
|
15
|
-
"https://msgplwbgohpokajtibew.supabase.co";
|
|
21
|
+
process.env.ASSISTME_SUPABASE_URL || "https://msgplwbgohpokajtibew.supabase.co";
|
|
16
22
|
const SUPABASE_ANON_KEY_DEFAULT =
|
|
23
|
+
process.env.ASSISTME_SUPABASE_ANON_KEY ||
|
|
17
24
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1zZ3Bsd2Jnb2hwb2thanRpYmV3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjE3MTMzNTEsImV4cCI6MjA3NzI4OTM1MX0.YqiluL_mIBWKv5dteIcPAPQ_jRp9rzZlvAXvkQttDjs";
|
|
18
25
|
|
|
19
26
|
const CONFIG_DEFAULTS: Partial<AssistMeConfig> = {
|
|
@@ -21,7 +28,8 @@ const CONFIG_DEFAULTS: Partial<AssistMeConfig> = {
|
|
|
21
28
|
supabaseAnonKey: SUPABASE_ANON_KEY_DEFAULT,
|
|
22
29
|
sessionName: "Default",
|
|
23
30
|
model: "claude-sonnet-4-20250514",
|
|
24
|
-
maxTurns:
|
|
31
|
+
maxTurns: 200,
|
|
32
|
+
taskTimeoutMinutes: 10,
|
|
25
33
|
};
|
|
26
34
|
|
|
27
35
|
const config = new Conf<Partial<AssistMeConfig>>({
|
|
@@ -30,14 +38,11 @@ const config = new Conf<Partial<AssistMeConfig>>({
|
|
|
30
38
|
});
|
|
31
39
|
|
|
32
40
|
export function getConfig(): AssistMeConfig {
|
|
33
|
-
const supabaseUrl =
|
|
34
|
-
process.env.SUPABASE_URL || config.get("supabaseUrl") || SUPABASE_URL_DEFAULT;
|
|
41
|
+
const supabaseUrl = process.env.SUPABASE_URL || config.get("supabaseUrl") || SUPABASE_URL_DEFAULT;
|
|
35
42
|
const supabaseAnonKey =
|
|
36
43
|
process.env.SUPABASE_ANON_KEY || config.get("supabaseAnonKey") || SUPABASE_ANON_KEY_DEFAULT;
|
|
37
|
-
const anthropicApiKey =
|
|
38
|
-
|
|
39
|
-
const workspacePath =
|
|
40
|
-
config.get("workspacePath") || process.cwd();
|
|
44
|
+
const anthropicApiKey = process.env.ANTHROPIC_API_KEY || config.get("anthropicApiKey") || "";
|
|
45
|
+
const workspacePath = config.get("workspacePath") || process.cwd();
|
|
41
46
|
|
|
42
47
|
return {
|
|
43
48
|
supabaseUrl,
|
|
@@ -46,7 +51,8 @@ export function getConfig(): AssistMeConfig {
|
|
|
46
51
|
workspacePath: resolve(workspacePath),
|
|
47
52
|
sessionName: config.get("sessionName") || "Default",
|
|
48
53
|
model: config.get("model") || "claude-sonnet-4-20250514",
|
|
49
|
-
maxTurns: config.get("maxTurns") ||
|
|
54
|
+
maxTurns: config.get("maxTurns") || 200,
|
|
55
|
+
taskTimeoutMinutes: config.get("taskTimeoutMinutes") || 10,
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized constants for the entire application.
|
|
3
|
+
* All magic numbers and limits should be defined here.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ── Truncation Limits ──────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/** Max characters for final task response sent to DB */
|
|
9
|
+
export const MAX_RESPONSE_CONTENT_LENGTH = 50_000;
|
|
10
|
+
|
|
11
|
+
/** Max characters for tool result stored in events */
|
|
12
|
+
export const MAX_TOOL_RESULT_LENGTH = 10_000;
|
|
13
|
+
|
|
14
|
+
/** Max characters for tool result stored in skill extraction records */
|
|
15
|
+
export const MAX_SKILL_RECORD_RESULT_LENGTH = 300;
|
|
16
|
+
|
|
17
|
+
/** Max characters for tool input logged to console */
|
|
18
|
+
export const MAX_TOOL_INPUT_LOG_LENGTH = 200;
|
|
19
|
+
|
|
20
|
+
/** Max characters for job run summary */
|
|
21
|
+
export const MAX_JOB_SUMMARY_LENGTH = 10_000;
|
|
22
|
+
|
|
23
|
+
/** Max characters for conversation history per response */
|
|
24
|
+
export const MAX_HISTORY_RESPONSE_LENGTH = 1_500;
|
|
25
|
+
|
|
26
|
+
/** Max conversation history entries to include in prompt */
|
|
27
|
+
export const MAX_HISTORY_ENTRIES = 10;
|
|
28
|
+
|
|
29
|
+
/** Budget for skill descriptions in system prompt */
|
|
30
|
+
export const SKILL_DESCRIPTION_BUDGET_CHARS = 16_000;
|
|
31
|
+
|
|
32
|
+
// ── Timeouts ───────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/** Default task timeout in minutes */
|
|
35
|
+
export const DEFAULT_TASK_TIMEOUT_MINUTES = 10;
|
|
36
|
+
|
|
37
|
+
/** Shell command execution timeout in ms */
|
|
38
|
+
export const SHELL_TIMEOUT_MS = 30_000;
|
|
39
|
+
|
|
40
|
+
/** Shell output cap in characters */
|
|
41
|
+
export const SHELL_MAX_OUTPUT = 50_000;
|
|
42
|
+
|
|
43
|
+
/** CDP command timeout in ms */
|
|
44
|
+
export const CDP_COMMAND_TIMEOUT_MS = 15_000;
|
|
45
|
+
|
|
46
|
+
/** WebSocket connect timeout in ms */
|
|
47
|
+
export const WS_CONNECT_TIMEOUT_MS = 5_000;
|
|
48
|
+
|
|
49
|
+
/** Scheduler check interval in ms */
|
|
50
|
+
export const SCHEDULER_INTERVAL_MS = 30_000;
|
|
51
|
+
|
|
52
|
+
// ── Browser Cache Limits ───────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/** Max entries in refCache before pruning */
|
|
55
|
+
export const REF_CACHE_MAX_SIZE = 5_000;
|
|
56
|
+
|
|
57
|
+
/** Max entries in frameContexts before pruning */
|
|
58
|
+
export const FRAME_CONTEXTS_MAX_SIZE = 500;
|
|
59
|
+
|
|
60
|
+
// ── Search & Matching ──────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/** Max file search results */
|
|
63
|
+
export const MAX_FILE_SEARCH_RESULTS = 50;
|
|
64
|
+
|
|
65
|
+
/** Max files to scan for content search */
|
|
66
|
+
export const MAX_CONTENT_SEARCH_FILES = 200;
|
|
67
|
+
|
|
68
|
+
/** Max content search result lines */
|
|
69
|
+
export const MAX_CONTENT_SEARCH_RESULTS = 30;
|
|
70
|
+
|
|
71
|
+
// ── Retry & Polling ────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/** Max retries for event emission */
|
|
74
|
+
export const MAX_EMIT_RETRIES = 2;
|
|
75
|
+
|
|
76
|
+
/** Max retries for task completion */
|
|
77
|
+
export const MAX_COMPLETE_TASK_RETRIES = 2;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured error types for consistent error handling.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class AppError extends Error {
|
|
6
|
+
constructor(
|
|
7
|
+
message: string,
|
|
8
|
+
public readonly code: ErrorCode,
|
|
9
|
+
public readonly cause?: unknown
|
|
10
|
+
) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "AppError";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static fromUnknown(err: unknown, code: ErrorCode = "INTERNAL"): AppError {
|
|
16
|
+
if (err instanceof AppError) return err;
|
|
17
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
18
|
+
return new AppError(message, code, err);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ErrorCode =
|
|
23
|
+
| "INTERNAL"
|
|
24
|
+
| "NETWORK"
|
|
25
|
+
| "AUTH"
|
|
26
|
+
| "VALIDATION"
|
|
27
|
+
| "TIMEOUT"
|
|
28
|
+
| "PATH_TRAVERSAL"
|
|
29
|
+
| "COMMAND_BLOCKED"
|
|
30
|
+
| "BROWSER"
|
|
31
|
+
| "DB";
|
|
32
|
+
|
|
33
|
+
/** Extract error message from unknown error value */
|
|
34
|
+
export function errorMessage(err: unknown): string {
|
|
35
|
+
if (err instanceof Error) return err.message;
|
|
36
|
+
return String(err);
|
|
37
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zod schemas for runtime validation of MCP handler responses.
|
|
5
|
+
* Replaces unsafe `as` type casts throughout the codebase.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/** Safely parse data with a Zod schema, returning null on failure */
|
|
11
|
+
export function safeParse<T>(schema: z.ZodType<T>, data: unknown): T | null {
|
|
12
|
+
const result = schema.safeParse(data);
|
|
13
|
+
return result.success ? result.data : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Parse data with a Zod schema, throwing on failure */
|
|
17
|
+
export function parse<T>(schema: z.ZodType<T>, data: unknown): T {
|
|
18
|
+
return schema.parse(data);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ── DB Row Schemas ─────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export const ConversationMessageSchema = z.object({
|
|
24
|
+
id: z.string(),
|
|
25
|
+
conversation_id: z.string(),
|
|
26
|
+
content: z.string().optional().default(""),
|
|
27
|
+
status: z.string().optional().nullable(),
|
|
28
|
+
metadata: z.record(z.string(), z.unknown()).optional().default({}),
|
|
29
|
+
created_at: z.string().optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const SkillRowSchema = z.object({
|
|
33
|
+
id: z.string(),
|
|
34
|
+
name: z.string(),
|
|
35
|
+
description: z.string().optional().default(""),
|
|
36
|
+
version: z.string().optional().default("1.0.0"),
|
|
37
|
+
user_invocable: z.boolean().optional().default(true),
|
|
38
|
+
disable_model_invocation: z.boolean().optional().default(false),
|
|
39
|
+
keywords: z.array(z.string()).optional().default([]),
|
|
40
|
+
allowed_tools: z.array(z.string()).optional().default([]),
|
|
41
|
+
argument_hint: z.string().optional().default(""),
|
|
42
|
+
metadata: z.unknown().optional().default({}),
|
|
43
|
+
homepage: z.string().optional().default(""),
|
|
44
|
+
content: z.string(),
|
|
45
|
+
source: z.string().optional().default("manual"),
|
|
46
|
+
source_skill_id: z.string().optional().nullable(),
|
|
47
|
+
invocation_count: z.number().optional().default(0),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const JobRowSchema = z.object({
|
|
51
|
+
job_id: z.string(),
|
|
52
|
+
job_name: z.string(),
|
|
53
|
+
job_description: z.string().optional().default(""),
|
|
54
|
+
skill_id: z.string().optional().nullable(),
|
|
55
|
+
skill_name: z.string().optional().nullable(),
|
|
56
|
+
skill_description: z.string().optional().default(""),
|
|
57
|
+
skill_emoji: z.string().optional().default(""),
|
|
58
|
+
skill_content: z.string().optional().default(""),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const JobListRowSchema = z.object({
|
|
62
|
+
id: z.string(),
|
|
63
|
+
name: z.string(),
|
|
64
|
+
description: z.string().optional().default(""),
|
|
65
|
+
skill_count: z.number().optional().default(0),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const JobRunRowSchema = z.object({
|
|
69
|
+
run_id: z.string(),
|
|
70
|
+
job_name: z.string(),
|
|
71
|
+
status: z.string(),
|
|
72
|
+
trigger_type: z.string().optional().default("manual"),
|
|
73
|
+
started_at: z.string(),
|
|
74
|
+
completed_at: z.string().optional().nullable(),
|
|
75
|
+
summary: z.string().optional().nullable(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const SkillCreateResultSchema = z
|
|
79
|
+
.object({
|
|
80
|
+
out_id: z.string().optional(),
|
|
81
|
+
id: z.string().optional(),
|
|
82
|
+
out_name: z.string().optional(),
|
|
83
|
+
name: z.string().optional(),
|
|
84
|
+
})
|
|
85
|
+
.refine((data) => data.out_id || data.id, {
|
|
86
|
+
message: "Skill create result must have out_id or id",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export const SkillDecisionSchema = z.object({
|
|
90
|
+
action: z.enum(["create", "update", "skip"]),
|
|
91
|
+
name: z.string().optional(),
|
|
92
|
+
description: z.string().optional(),
|
|
93
|
+
instructions: z.string().optional(),
|
|
94
|
+
emoji: z.string().optional(),
|
|
95
|
+
keywords: z.array(z.string()).optional(),
|
|
96
|
+
existing_skill_name: z.string().optional(),
|
|
97
|
+
improved_instructions: z.string().optional(),
|
|
98
|
+
improved_description: z.string().optional(),
|
|
99
|
+
reason: z.string(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export type SkillDecision = z.infer<typeof SkillDecisionSchema>;
|
|
103
|
+
|
|
104
|
+
export const BrowseSkillRowSchema = z.object({
|
|
105
|
+
id: z.string(),
|
|
106
|
+
name: z.string(),
|
|
107
|
+
description: z.string().optional().default(""),
|
|
108
|
+
emoji: z.string().optional().default(""),
|
|
109
|
+
version: z.string().optional().default("1.0.0"),
|
|
110
|
+
author_name: z.string().optional().default(""),
|
|
111
|
+
category: z.string().optional().default(""),
|
|
112
|
+
install_count: z.number().optional().default(0),
|
|
113
|
+
avg_rating: z.number().optional().nullable(),
|
|
114
|
+
rating_count: z.number().optional().default(0),
|
|
115
|
+
});
|