@sean.holung/minicode 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/LICENSE +201 -0
- package/README.md +241 -0
- package/dist/src/agent/agent.js +209 -0
- package/dist/src/agent/config.js +151 -0
- package/dist/src/agent/types.js +1 -0
- package/dist/src/index.js +138 -0
- package/dist/src/indexer/cache.js +121 -0
- package/dist/src/indexer/code-map.js +92 -0
- package/dist/src/indexer/plugin-loader.js +78 -0
- package/dist/src/indexer/plugins/typescript.js +327 -0
- package/dist/src/indexer/project-index.js +145 -0
- package/dist/src/indexer/types.js +1 -0
- package/dist/src/model/client.js +374 -0
- package/dist/src/prompt/system-prompt.js +91 -0
- package/dist/src/safety/guardrails.js +55 -0
- package/dist/src/session/session.js +95 -0
- package/dist/src/tools/edit-file.js +73 -0
- package/dist/src/tools/find-references.js +52 -0
- package/dist/src/tools/get-dependencies.js +56 -0
- package/dist/src/tools/helpers.js +42 -0
- package/dist/src/tools/list-files.js +63 -0
- package/dist/src/tools/read-file.js +79 -0
- package/dist/src/tools/read-symbol.js +96 -0
- package/dist/src/tools/registry.js +68 -0
- package/dist/src/tools/run-command.js +92 -0
- package/dist/src/tools/search-code-map.js +72 -0
- package/dist/src/tools/search.js +153 -0
- package/dist/src/tools/write-file.js +44 -0
- package/dist/src/ui/app.js +31 -0
- package/dist/src/ui/cli-ink.js +168 -0
- package/dist/src/ui/components/activity-pane.js +35 -0
- package/dist/src/ui/components/header-bar.js +6 -0
- package/dist/src/ui/components/input-composer.js +46 -0
- package/dist/src/ui/components/tool-timeline-item.js +37 -0
- package/dist/src/ui/events.js +1 -0
- package/dist/src/ui/state/ui-store.js +89 -0
- package/dist/src/ui/theme.js +23 -0
- package/dist/tests/agent.test.js +130 -0
- package/dist/tests/cache.test.js +37 -0
- package/dist/tests/config.test.js +37 -0
- package/dist/tests/dependency-graph.test.js +27 -0
- package/dist/tests/file-tools.test.js +73 -0
- package/dist/tests/find-references.test.js +30 -0
- package/dist/tests/get-dependencies.test.js +35 -0
- package/dist/tests/guardrails.test.js +18 -0
- package/dist/tests/indexer.test.js +201 -0
- package/dist/tests/model-client-openai.test.js +84 -0
- package/dist/tests/read-symbol.test.js +83 -0
- package/dist/tests/search-code-map.test.js +30 -0
- package/dist/tests/session.test.js +37 -0
- package/dist/tests/system-prompt.test.js +82 -0
- package/dist/tests/test-utils.js +18 -0
- package/dist/tests/tool-registry.test.js +41 -0
- package/package.json +43 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { OpenAICompatibleModelClient, createModelClient, } from "../src/model/client.js";
|
|
4
|
+
import { createTestAgentConfig } from "./test-utils.js";
|
|
5
|
+
test("openai-compatible client sends tool schemas and parses tool calls", async () => {
|
|
6
|
+
let capturedUrl = "";
|
|
7
|
+
let capturedBody = "";
|
|
8
|
+
const fetchImpl = async (input, init) => {
|
|
9
|
+
capturedUrl = String(input);
|
|
10
|
+
capturedBody = String(init?.body ?? "");
|
|
11
|
+
return new Response(JSON.stringify({
|
|
12
|
+
choices: [
|
|
13
|
+
{
|
|
14
|
+
finish_reason: "tool_calls",
|
|
15
|
+
message: {
|
|
16
|
+
content: "",
|
|
17
|
+
tool_calls: [
|
|
18
|
+
{
|
|
19
|
+
id: "call_1",
|
|
20
|
+
type: "function",
|
|
21
|
+
function: {
|
|
22
|
+
name: "read_file",
|
|
23
|
+
arguments: "{\"path\":\"src/index.ts\"}",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
usage: {
|
|
31
|
+
prompt_tokens: 10,
|
|
32
|
+
completion_tokens: 5,
|
|
33
|
+
},
|
|
34
|
+
}), {
|
|
35
|
+
status: 200,
|
|
36
|
+
headers: { "content-type": "application/json" },
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const client = new OpenAICompatibleModelClient({
|
|
40
|
+
baseUrl: "http://localhost:1234/v1",
|
|
41
|
+
fetchImpl,
|
|
42
|
+
});
|
|
43
|
+
const response = await client.chat({
|
|
44
|
+
model: "qwen2.5-coder-7b-instruct",
|
|
45
|
+
system: "System instructions",
|
|
46
|
+
messages: [{ role: "user", content: "Read src/index.ts" }],
|
|
47
|
+
tools: [
|
|
48
|
+
{
|
|
49
|
+
name: "read_file",
|
|
50
|
+
description: "Read a file",
|
|
51
|
+
input_schema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
path: { type: "string" },
|
|
55
|
+
},
|
|
56
|
+
required: ["path"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
maxTokens: 256,
|
|
61
|
+
});
|
|
62
|
+
assert.equal(capturedUrl, "http://localhost:1234/v1/chat/completions");
|
|
63
|
+
assert.equal(response.stopReason, "tool_use");
|
|
64
|
+
assert.equal(response.toolCalls.length, 1);
|
|
65
|
+
assert.equal(response.toolCalls[0]?.name, "read_file");
|
|
66
|
+
assert.deepEqual(response.toolCalls[0]?.input, { path: "src/index.ts" });
|
|
67
|
+
assert.equal(response.usage.inputTokens, 10);
|
|
68
|
+
assert.equal(response.usage.outputTokens, 5);
|
|
69
|
+
const parsedBody = JSON.parse(capturedBody);
|
|
70
|
+
const messages = parsedBody.messages;
|
|
71
|
+
assert.equal(messages[0]?.role, "system");
|
|
72
|
+
assert.equal(messages[1]?.role, "user");
|
|
73
|
+
const tools = parsedBody.tools;
|
|
74
|
+
assert.equal(tools[0]?.type, "function");
|
|
75
|
+
});
|
|
76
|
+
test("createModelClient returns openai-compatible client", () => {
|
|
77
|
+
const config = {
|
|
78
|
+
...createTestAgentConfig("/tmp"),
|
|
79
|
+
modelProvider: "openai-compatible",
|
|
80
|
+
openAiBaseUrl: "http://localhost:1234/v1",
|
|
81
|
+
};
|
|
82
|
+
const client = createModelClient(config);
|
|
83
|
+
assert.ok(client instanceof OpenAICompatibleModelClient);
|
|
84
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import { buildProjectIndex } from "../src/indexer/project-index.js";
|
|
5
|
+
import { createReadSymbolTool } from "../src/tools/read-symbol.js";
|
|
6
|
+
import { ToolRegistry } from "../src/tools/registry.js";
|
|
7
|
+
import { createTestAgentConfig } from "./test-utils.js";
|
|
8
|
+
test("read_symbol returns correct function body", async () => {
|
|
9
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
10
|
+
const config = createTestAgentConfig(root);
|
|
11
|
+
const projectIndex = await buildProjectIndex(root);
|
|
12
|
+
const tool = createReadSymbolTool(config, projectIndex);
|
|
13
|
+
const result = await tool.execute({ name: "CodingAgent.runTurn" });
|
|
14
|
+
assert.ok(result.includes("# CodingAgent.runTurn"));
|
|
15
|
+
assert.ok(result.includes("src/agent/agent.ts"));
|
|
16
|
+
assert.ok(result.includes("Lines:"));
|
|
17
|
+
assert.ok(/\d+\|/.test(result), "should have line numbers");
|
|
18
|
+
assert.ok(result.includes("runTurn") || result.includes("session"));
|
|
19
|
+
});
|
|
20
|
+
test("read_symbol returns error for unknown symbol name", async () => {
|
|
21
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
22
|
+
const config = createTestAgentConfig(root);
|
|
23
|
+
const projectIndex = await buildProjectIndex(root);
|
|
24
|
+
const tool = createReadSymbolTool(config, projectIndex);
|
|
25
|
+
const result = await tool.execute({ name: "NonExistentSymbol123" });
|
|
26
|
+
assert.ok(result.includes("not found"));
|
|
27
|
+
assert.ok(result.includes("search") || result.includes("read_file"));
|
|
28
|
+
});
|
|
29
|
+
test("read_symbol with includeBody: false returns signature only", async () => {
|
|
30
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
31
|
+
const config = createTestAgentConfig(root);
|
|
32
|
+
const projectIndex = await buildProjectIndex(root);
|
|
33
|
+
const tool = createReadSymbolTool(config, projectIndex);
|
|
34
|
+
const result = await tool.execute({
|
|
35
|
+
name: "parseResponse",
|
|
36
|
+
includeBody: false,
|
|
37
|
+
});
|
|
38
|
+
assert.ok(result.includes("# parseResponse"));
|
|
39
|
+
assert.ok(result.includes("src/model/client.ts"));
|
|
40
|
+
assert.ok(!result.includes("return {"));
|
|
41
|
+
assert.ok(result.includes("ModelResponse") || result.includes("=>"));
|
|
42
|
+
});
|
|
43
|
+
test("read_symbol includes leading context and line numbers", async () => {
|
|
44
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
45
|
+
const config = createTestAgentConfig(root);
|
|
46
|
+
const projectIndex = await buildProjectIndex(root);
|
|
47
|
+
const tool = createReadSymbolTool(config, projectIndex);
|
|
48
|
+
const symbol = projectIndex.getSymbol("loadAgentConfig");
|
|
49
|
+
assert.ok(symbol, "loadAgentConfig should exist");
|
|
50
|
+
const result = await tool.execute({ name: "loadAgentConfig" });
|
|
51
|
+
assert.ok(result.includes("# loadAgentConfig"));
|
|
52
|
+
assert.ok(result.includes("src/agent/config.ts"));
|
|
53
|
+
assert.ok(/\d+\|/.test(result), "should have line numbers in output");
|
|
54
|
+
});
|
|
55
|
+
test("read_symbol appears in tool registry schemas when projectIndex provided", async () => {
|
|
56
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
57
|
+
const config = createTestAgentConfig(root);
|
|
58
|
+
const projectIndex = await buildProjectIndex(root);
|
|
59
|
+
const registry = ToolRegistry.createDefault(config, projectIndex);
|
|
60
|
+
const schemas = registry.getToolSchemas();
|
|
61
|
+
const readSymbol = schemas.find((s) => s.name === "read_symbol");
|
|
62
|
+
assert.ok(readSymbol, "read_symbol should be in schemas");
|
|
63
|
+
assert.ok(readSymbol.description.includes("function") || readSymbol.description.includes("class"));
|
|
64
|
+
const props = readSymbol.input_schema.properties;
|
|
65
|
+
assert.ok(props && "name" in props);
|
|
66
|
+
});
|
|
67
|
+
test("read_symbol includes Referenced Types section for parseResponse", async () => {
|
|
68
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
69
|
+
const config = createTestAgentConfig(root);
|
|
70
|
+
const projectIndex = await buildProjectIndex(root);
|
|
71
|
+
const tool = createReadSymbolTool(config, projectIndex);
|
|
72
|
+
const result = await tool.execute({ name: "parseResponse" });
|
|
73
|
+
assert.ok(result.includes("## Referenced Types"));
|
|
74
|
+
assert.ok(result.includes("ModelResponse"));
|
|
75
|
+
assert.ok(result.includes("ToolCall"));
|
|
76
|
+
});
|
|
77
|
+
test("read_symbol is not in tool registry when projectIndex is undefined", () => {
|
|
78
|
+
const config = createTestAgentConfig("/tmp");
|
|
79
|
+
const registry = ToolRegistry.createDefault(config);
|
|
80
|
+
const schemas = registry.getToolSchemas();
|
|
81
|
+
const readSymbol = schemas.find((s) => s.name === "read_symbol");
|
|
82
|
+
assert.equal(readSymbol, undefined);
|
|
83
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import { buildProjectIndex } from "../src/indexer/project-index.js";
|
|
5
|
+
import { createSearchCodeMapTool } from "../src/tools/search-code-map.js";
|
|
6
|
+
test("search_code_map finds symbols by substring", async () => {
|
|
7
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
8
|
+
const projectIndex = await buildProjectIndex(root);
|
|
9
|
+
const tool = createSearchCodeMapTool(projectIndex);
|
|
10
|
+
const result = await tool.execute({ pattern: "ModelResponse" });
|
|
11
|
+
assert.ok(result.includes("# Symbols matching"));
|
|
12
|
+
assert.ok(result.includes("ModelResponse"));
|
|
13
|
+
});
|
|
14
|
+
test("search_code_map returns empty when no match", async () => {
|
|
15
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
16
|
+
const projectIndex = await buildProjectIndex(root);
|
|
17
|
+
const tool = createSearchCodeMapTool(projectIndex);
|
|
18
|
+
const result = await tool.execute({ pattern: "XyZNoSymbol123" });
|
|
19
|
+
assert.ok(result.includes("No symbols matching"));
|
|
20
|
+
});
|
|
21
|
+
test("search_code_map appears in tool registry when projectIndex provided", async () => {
|
|
22
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
23
|
+
const projectIndex = await buildProjectIndex(root);
|
|
24
|
+
const { ToolRegistry } = await import("../src/tools/registry.js");
|
|
25
|
+
const { createTestAgentConfig } = await import("./test-utils.js");
|
|
26
|
+
const registry = ToolRegistry.createDefault(createTestAgentConfig(root), projectIndex);
|
|
27
|
+
const schemas = registry.getToolSchemas();
|
|
28
|
+
const searchCodeMap = schemas.find((s) => s.name === "search_code_map");
|
|
29
|
+
assert.ok(searchCodeMap);
|
|
30
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { Session } from "../src/session/session.js";
|
|
4
|
+
test("session stores and returns messages", () => {
|
|
5
|
+
const session = new Session("test");
|
|
6
|
+
session.addMessage({ role: "user", content: "hello" });
|
|
7
|
+
session.addMessage({ role: "assistant", content: "world" });
|
|
8
|
+
const messages = session.getMessages();
|
|
9
|
+
assert.equal(messages.length, 2);
|
|
10
|
+
assert.equal(messages[0]?.role, "user");
|
|
11
|
+
assert.equal(messages[1]?.role, "assistant");
|
|
12
|
+
});
|
|
13
|
+
test("trim keeps recent messages while reducing token estimate", () => {
|
|
14
|
+
const session = new Session("test");
|
|
15
|
+
session.addMessage({ role: "user", content: "a".repeat(200) });
|
|
16
|
+
session.addMessage({
|
|
17
|
+
role: "assistant",
|
|
18
|
+
content: "thinking",
|
|
19
|
+
toolCalls: [{ id: "1", name: "read_file", input: { path: "x" } }],
|
|
20
|
+
});
|
|
21
|
+
session.addMessage({
|
|
22
|
+
role: "tool",
|
|
23
|
+
toolCallId: "1",
|
|
24
|
+
toolName: "read_file",
|
|
25
|
+
content: "tool-output",
|
|
26
|
+
});
|
|
27
|
+
session.addMessage({ role: "assistant", content: "done" });
|
|
28
|
+
const before = session.getTokenEstimate();
|
|
29
|
+
session.trim(30, 2);
|
|
30
|
+
const after = session.getTokenEstimate();
|
|
31
|
+
const messages = session.getMessages();
|
|
32
|
+
assert.ok(after <= before);
|
|
33
|
+
assert.equal(messages.length, 3);
|
|
34
|
+
assert.equal(messages[0]?.role, "assistant");
|
|
35
|
+
assert.equal(messages[1]?.role, "tool");
|
|
36
|
+
assert.equal(messages[2]?.role, "assistant");
|
|
37
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import { buildSystemPrompt } from "../src/prompt/system-prompt.js";
|
|
5
|
+
function createMinimalConfig(workspaceRoot) {
|
|
6
|
+
return {
|
|
7
|
+
modelProvider: "openai-compatible",
|
|
8
|
+
model: "test",
|
|
9
|
+
maxSteps: 10,
|
|
10
|
+
maxTokens: 1024,
|
|
11
|
+
maxContextTokens: 16_000,
|
|
12
|
+
workspaceRoot,
|
|
13
|
+
commandTimeoutMs: 5000,
|
|
14
|
+
maxFileSizeBytes: 1_000_000,
|
|
15
|
+
commandDenylist: [],
|
|
16
|
+
confirmDestructive: false,
|
|
17
|
+
keepRecentMessages: 10,
|
|
18
|
+
loopDetectionWindow: 6,
|
|
19
|
+
maxToolOutputChars: 15_000,
|
|
20
|
+
openAiBaseUrl: "http://localhost:1234/v1",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const MINIMAL_TOOLS = [
|
|
24
|
+
{
|
|
25
|
+
name: "read_file",
|
|
26
|
+
description: "Read a file",
|
|
27
|
+
input_schema: { type: "object", properties: { path: { type: "string" } } },
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
const TOOLS_WITH_SEARCH_CODE_MAP = [
|
|
31
|
+
...MINIMAL_TOOLS,
|
|
32
|
+
{
|
|
33
|
+
name: "search_code_map",
|
|
34
|
+
description: "Search the code map",
|
|
35
|
+
input_schema: { type: "object", properties: { pattern: { type: "string" } } },
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
test("buildSystemPrompt omits code map when undefined", () => {
|
|
39
|
+
const prompt = buildSystemPrompt(createMinimalConfig("/tmp"), MINIMAL_TOOLS);
|
|
40
|
+
assert.ok(!prompt.includes("[Project Code Map]"));
|
|
41
|
+
assert.ok(prompt.includes("[Identity]"));
|
|
42
|
+
assert.ok(prompt.includes("[Tool Descriptions]"));
|
|
43
|
+
});
|
|
44
|
+
test("buildSystemPrompt omits code map when empty", () => {
|
|
45
|
+
const prompt = buildSystemPrompt(createMinimalConfig("/tmp"), MINIMAL_TOOLS, { text: "", shownCount: 0, totalCount: 0 });
|
|
46
|
+
assert.ok(!prompt.includes("[Project Code Map]"));
|
|
47
|
+
});
|
|
48
|
+
test("buildSystemPrompt includes code map when provided", () => {
|
|
49
|
+
const codeMapResult = {
|
|
50
|
+
text: "# Project Code Map\n\n src/foo.ts\n function bar()",
|
|
51
|
+
shownCount: 1,
|
|
52
|
+
totalCount: 1,
|
|
53
|
+
};
|
|
54
|
+
const prompt = buildSystemPrompt(createMinimalConfig("/tmp"), MINIMAL_TOOLS, codeMapResult);
|
|
55
|
+
assert.ok(prompt.includes("[Project Code Map]"));
|
|
56
|
+
assert.ok(prompt.includes("src/foo.ts"));
|
|
57
|
+
assert.ok(prompt.includes("function bar()"));
|
|
58
|
+
});
|
|
59
|
+
test("buildSystemPrompt includes workspace context", () => {
|
|
60
|
+
const prompt = buildSystemPrompt(createMinimalConfig("/home/user/project"), MINIMAL_TOOLS);
|
|
61
|
+
assert.ok(prompt.includes("/home/user/project"));
|
|
62
|
+
});
|
|
63
|
+
test("buildSystemPrompt includes tool list", () => {
|
|
64
|
+
const prompt = buildSystemPrompt(createMinimalConfig("/tmp"), MINIMAL_TOOLS);
|
|
65
|
+
assert.ok(prompt.includes("read_file"));
|
|
66
|
+
assert.ok(prompt.includes("Read a file"));
|
|
67
|
+
});
|
|
68
|
+
test("buildSystemPrompt shows truncated stats and search_code_map hint when truncated", () => {
|
|
69
|
+
const codeMapResult = {
|
|
70
|
+
text: "# Project Code Map\n\n src/foo.ts\n function bar()",
|
|
71
|
+
shownCount: 5,
|
|
72
|
+
totalCount: 100,
|
|
73
|
+
};
|
|
74
|
+
const prompt = buildSystemPrompt(createMinimalConfig("/tmp"), TOOLS_WITH_SEARCH_CODE_MAP, codeMapResult);
|
|
75
|
+
assert.ok(prompt.includes("Showing 5 of 100 symbols"));
|
|
76
|
+
assert.ok(prompt.includes("search_code_map to find symbols not listed above"));
|
|
77
|
+
});
|
|
78
|
+
test("buildSystemPrompt detects project type from workspace", () => {
|
|
79
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
80
|
+
const prompt = buildSystemPrompt(createMinimalConfig(root), MINIMAL_TOOLS);
|
|
81
|
+
assert.ok(prompt.includes("Node.js") || prompt.includes("TypeScript"), "minicode has package.json");
|
|
82
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function createTestAgentConfig(workspaceRoot) {
|
|
2
|
+
return {
|
|
3
|
+
modelProvider: "anthropic",
|
|
4
|
+
model: "test-model",
|
|
5
|
+
maxSteps: 10,
|
|
6
|
+
maxTokens: 1024,
|
|
7
|
+
maxContextTokens: 16_000,
|
|
8
|
+
workspaceRoot,
|
|
9
|
+
commandTimeoutMs: 2_000,
|
|
10
|
+
maxFileSizeBytes: 1_000_000,
|
|
11
|
+
commandDenylist: [],
|
|
12
|
+
confirmDestructive: false,
|
|
13
|
+
keepRecentMessages: 10,
|
|
14
|
+
loopDetectionWindow: 6,
|
|
15
|
+
maxToolOutputChars: 15_000,
|
|
16
|
+
openAiBaseUrl: "http://localhost:1234/v1",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { ToolRegistry } from "../src/tools/registry.js";
|
|
4
|
+
const echoTool = {
|
|
5
|
+
name: "echo",
|
|
6
|
+
description: "Echo input value",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
value: { type: "string" },
|
|
11
|
+
},
|
|
12
|
+
required: ["value"],
|
|
13
|
+
},
|
|
14
|
+
execute: async (input) => `echo:${String(input.value)}`,
|
|
15
|
+
};
|
|
16
|
+
test("tool registry rejects duplicate tool names", () => {
|
|
17
|
+
assert.throws(() => new ToolRegistry([echoTool, echoTool]), /Duplicate tool registration/);
|
|
18
|
+
});
|
|
19
|
+
test("tool registry returns error for unknown tool", async () => {
|
|
20
|
+
const registry = new ToolRegistry([echoTool]);
|
|
21
|
+
const result = await registry.execute("missing", {});
|
|
22
|
+
assert.equal(result, 'Tool error: Unknown tool "missing".');
|
|
23
|
+
});
|
|
24
|
+
test("tool registry validates input shape", async () => {
|
|
25
|
+
const registry = new ToolRegistry([echoTool]);
|
|
26
|
+
const result = await registry.execute("echo", "bad-input");
|
|
27
|
+
assert.match(result, /Tool input must be a JSON object/);
|
|
28
|
+
});
|
|
29
|
+
test("tool registry returns wrapped tool execution errors", async () => {
|
|
30
|
+
const explodingTool = {
|
|
31
|
+
name: "explode",
|
|
32
|
+
description: "Always fails",
|
|
33
|
+
inputSchema: { type: "object", properties: {} },
|
|
34
|
+
execute: async () => {
|
|
35
|
+
throw new Error("boom");
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const registry = new ToolRegistry([explodingTool]);
|
|
39
|
+
const result = await registry.execute("explode", {});
|
|
40
|
+
assert.equal(result, "Tool error (explode): boom");
|
|
41
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sean.holung/minicode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A coding agent for mini models",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.0.0"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"minicode": "dist/src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev": "node --env-file=.env --import tsx src/index.ts",
|
|
19
|
+
"dev:ink": "CLI_UI_MODE=ink node --env-file=.env --import tsx src/index.ts",
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"start": "node dist/src/index.js",
|
|
22
|
+
"install:global": "npm run build && npm link",
|
|
23
|
+
"lint": "eslint \"src/**/*.ts\" \"src/**/*.tsx\" \"tests/**/*.ts\" --max-warnings=0",
|
|
24
|
+
"test": "node --test --import tsx \"tests/**/*.test.ts\"",
|
|
25
|
+
"verify-index": "node --import tsx test-programs/verify-index/verify.ts"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@anthropic-ai/sdk": "^0.76.0",
|
|
29
|
+
"@inkjs/ui": "^2.0.0",
|
|
30
|
+
"dotenv": "^17.3.1",
|
|
31
|
+
"ink": "^6.8.0",
|
|
32
|
+
"picocolors": "^1.1.1",
|
|
33
|
+
"react": "^19.2.4"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^25.2.3",
|
|
37
|
+
"@types/react": "^19.2.14",
|
|
38
|
+
"eslint": "^10.0.0",
|
|
39
|
+
"tsx": "^4.21.0",
|
|
40
|
+
"typescript": "^5.9.3",
|
|
41
|
+
"typescript-eslint": "^8.56.0"
|
|
42
|
+
}
|
|
43
|
+
}
|