@sean.holung/minicode 0.2.0 → 0.2.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 +44 -3
- package/dist/src/cli/args.js +65 -0
- package/dist/src/index.js +109 -26
- package/dist/src/session/session-store.js +82 -0
- package/dist/src/tools/find-references.js +1 -1
- package/dist/src/tools/get-dependencies.js +1 -1
- package/dist/src/tools/read-symbol.js +1 -2
- package/dist/src/tools/registry.js +26 -61
- package/dist/src/tools/search-code-map.js +1 -1
- package/dist/src/ui/cli-ink.js +91 -19
- package/dist/tests/agent.test.js +2 -3
- package/dist/tests/cli-args.test.js +73 -0
- package/dist/tests/cli-oneshot.integration.test.js +26 -0
- package/dist/tests/dependency-graph.test.js +12 -12
- package/dist/tests/file-tools.test.js +2 -3
- package/dist/tests/find-references.test.js +6 -6
- package/dist/tests/guardrails.test.js +1 -1
- package/dist/tests/indexer.test.js +9 -9
- package/dist/tests/model-client-openai.test.js +1 -1
- package/dist/tests/read-symbol.test.js +16 -17
- package/dist/tests/search-code-map.test.js +2 -2
- package/dist/tests/session-store.test.js +115 -0
- package/dist/tests/session.test.js +1 -1
- package/dist/tests/system-prompt.test.js +1 -1
- package/dist/tests/tool-registry.test.js +1 -1
- package/package.json +7 -2
- package/dist/src/agent/agent.js +0 -209
- package/dist/src/agent/types.js +0 -1
- package/dist/src/model/client.js +0 -374
- package/dist/src/prompt/system-prompt.js +0 -91
- package/dist/src/safety/guardrails.js +0 -55
- package/dist/src/session/session.js +0 -95
- package/dist/src/tools/edit-file.js +0 -73
- package/dist/src/tools/helpers.js +0 -42
- package/dist/src/tools/list-files.js +0 -63
- package/dist/src/tools/read-file.js +0 -79
- package/dist/src/tools/run-command.js +0 -92
- package/dist/src/tools/search.js +0 -153
- package/dist/src/tools/write-file.js +0 -44
package/dist/src/ui/cli-ink.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
-
import { CodingAgent } from "
|
|
2
|
+
import { CodingAgent, createModelClient } from "@minicode/agent-sdk";
|
|
3
3
|
import { formatConfigForDisplay, loadAgentConfig } from "../agent/config.js";
|
|
4
4
|
import { computeFileHashes, getWorkspaceCacheDir, loadIndex, saveIndex, } from "../indexer/cache.js";
|
|
5
5
|
import { buildProjectIndex } from "../indexer/project-index.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { listSessions, loadSession, loadSessionByLabel, saveSession, } from "../session/session-store.js";
|
|
7
|
+
import { createToolRegistry } from "../tools/registry.js";
|
|
8
8
|
import { UiStore } from "./state/ui-store.js";
|
|
9
9
|
import { runInkApp } from "./app.js";
|
|
10
10
|
export async function runInkCli(verbose, initialTask) {
|
|
@@ -39,19 +39,9 @@ export async function runInkCli(verbose, initialTask) {
|
|
|
39
39
|
indexStatus,
|
|
40
40
|
});
|
|
41
41
|
store.setPhase("idle");
|
|
42
|
-
const toolRegistry =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
modelClient,
|
|
46
|
-
toolRegistry,
|
|
47
|
-
verbose,
|
|
48
|
-
...(projectIndex !== undefined ? { projectIndex } : {}),
|
|
49
|
-
...(verbose
|
|
50
|
-
? {
|
|
51
|
-
onProgress: (msg) => store.addItem({ type: "system", content: msg }),
|
|
52
|
-
}
|
|
53
|
-
: {}),
|
|
54
|
-
onUiUpdate: (event) => {
|
|
42
|
+
const toolRegistry = createToolRegistry(config, projectIndex);
|
|
43
|
+
function createUiUpdateHandler() {
|
|
44
|
+
return (event) => {
|
|
55
45
|
switch (event.type) {
|
|
56
46
|
case "streaming_chunk":
|
|
57
47
|
store.appendToStreamingContent(event.content);
|
|
@@ -85,8 +75,27 @@ export async function runInkCli(verbose, initialTask) {
|
|
|
85
75
|
store.setPhase("model_wait");
|
|
86
76
|
break;
|
|
87
77
|
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function buildAgent(session) {
|
|
81
|
+
return new CodingAgent({
|
|
82
|
+
config,
|
|
83
|
+
modelClient,
|
|
84
|
+
toolRegistry,
|
|
85
|
+
verbose,
|
|
86
|
+
...(session ? { session } : {}),
|
|
87
|
+
...(projectIndex !== undefined
|
|
88
|
+
? { getCodeMap: () => projectIndex.getCodeMap() }
|
|
89
|
+
: {}),
|
|
90
|
+
...(verbose
|
|
91
|
+
? {
|
|
92
|
+
onProgress: (msg) => store.addItem({ type: "system", content: msg }),
|
|
93
|
+
}
|
|
94
|
+
: {}),
|
|
95
|
+
onUiUpdate: createUiUpdateHandler(),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
let agent = buildAgent();
|
|
90
99
|
let turnAbortController = null;
|
|
91
100
|
const handleCtrlC = (inkExit) => {
|
|
92
101
|
if (turnAbortController) {
|
|
@@ -112,7 +121,7 @@ export async function runInkCli(verbose, initialTask) {
|
|
|
112
121
|
if (trimmed === "/help") {
|
|
113
122
|
store.addItem({
|
|
114
123
|
type: "system",
|
|
115
|
-
content: 'Commands: "/help", "/config", "/
|
|
124
|
+
content: 'Commands: "/help", "/config", "/save [label]", "/load [label]", "/sessions", "/exit".',
|
|
116
125
|
});
|
|
117
126
|
return;
|
|
118
127
|
}
|
|
@@ -123,6 +132,69 @@ export async function runInkCli(verbose, initialTask) {
|
|
|
123
132
|
});
|
|
124
133
|
return;
|
|
125
134
|
}
|
|
135
|
+
if (trimmed === "/save" || trimmed.startsWith("/save ")) {
|
|
136
|
+
const label = trimmed.slice("/save".length).trim() || undefined;
|
|
137
|
+
try {
|
|
138
|
+
const meta = await saveSession(agent.getSession(), label);
|
|
139
|
+
store.addItem({
|
|
140
|
+
type: "system",
|
|
141
|
+
content: `Session saved as "${meta.label}" (${meta.messageCount} messages)`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
146
|
+
store.addItem({ type: "system", content: `Failed to save session: ${msg}` });
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (trimmed === "/sessions") {
|
|
151
|
+
const sessions = await listSessions();
|
|
152
|
+
if (sessions.length === 0) {
|
|
153
|
+
store.addItem({ type: "system", content: "No saved sessions found." });
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const lines = sessions.map((s) => ` ${s.label} (${s.messageCount} msgs, saved ${s.savedAt})`);
|
|
157
|
+
store.addItem({
|
|
158
|
+
type: "system",
|
|
159
|
+
content: "Saved sessions:\n" + lines.join("\n"),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (trimmed === "/load" || trimmed.startsWith("/load ")) {
|
|
165
|
+
const arg = trimmed.slice("/load".length).trim();
|
|
166
|
+
if (arg.length === 0) {
|
|
167
|
+
const sessions = await listSessions();
|
|
168
|
+
if (sessions.length === 0) {
|
|
169
|
+
store.addItem({ type: "system", content: "No saved sessions found." });
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const lines = sessions.map((s) => ` ${s.label} (${s.messageCount} msgs, saved ${s.savedAt})`);
|
|
173
|
+
store.addItem({
|
|
174
|
+
type: "system",
|
|
175
|
+
content: "Saved sessions:\n" +
|
|
176
|
+
lines.join("\n") +
|
|
177
|
+
'\n\nUse "/load <label>" to restore a session.',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const result = (await loadSessionByLabel(arg)) ??
|
|
183
|
+
(await loadSession(arg));
|
|
184
|
+
if (!result) {
|
|
185
|
+
store.addItem({
|
|
186
|
+
type: "system",
|
|
187
|
+
content: `No session found matching "${arg}".`,
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
agent = buildAgent(result.session);
|
|
192
|
+
store.addItem({
|
|
193
|
+
type: "system",
|
|
194
|
+
content: `Session "${result.label}" restored (${result.session.getMessages().length} messages).`,
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
126
198
|
store.addItem({ type: "user", content: trimmed });
|
|
127
199
|
store.setPhase("sending");
|
|
128
200
|
store.setStep(0);
|
package/dist/tests/agent.test.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { test } from "node:test";
|
|
4
|
-
import { CodingAgent } from "
|
|
4
|
+
import { CodingAgent, ToolRegistry, } from "@minicode/agent-sdk";
|
|
5
5
|
import { buildProjectIndex } from "../src/indexer/project-index.js";
|
|
6
|
-
import { ToolRegistry } from "../src/tools/registry.js";
|
|
7
6
|
import { createTestAgentConfig } from "./test-utils.js";
|
|
8
7
|
class SequenceModelClient {
|
|
9
8
|
responses;
|
|
@@ -122,7 +121,7 @@ test("agent includes code map in system prompt when projectIndex is provided", a
|
|
|
122
121
|
config: createTestAgentConfig(root),
|
|
123
122
|
modelClient: spyClient,
|
|
124
123
|
toolRegistry: new ToolRegistry([createEchoTool()]),
|
|
125
|
-
projectIndex,
|
|
124
|
+
getCodeMap: () => projectIndex.getCodeMap(),
|
|
126
125
|
});
|
|
127
126
|
await agent.runTurn("List the project structure");
|
|
128
127
|
assert.ok(capturedSystem.includes("[Project Code Map]"));
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { CliUsageError, parseCliArgs, validateCliArgs, } from "../src/cli/args.js";
|
|
4
|
+
test("parseCliArgs parses oneshot and verbose flags", () => {
|
|
5
|
+
const parsed = parseCliArgs([
|
|
6
|
+
"node",
|
|
7
|
+
"src/index.ts",
|
|
8
|
+
"--oneshot",
|
|
9
|
+
"-v",
|
|
10
|
+
"Fix",
|
|
11
|
+
"lint",
|
|
12
|
+
]);
|
|
13
|
+
assert.equal(parsed.oneshot, true);
|
|
14
|
+
assert.equal(parsed.verbose, true);
|
|
15
|
+
assert.equal(parsed.task, "Fix lint");
|
|
16
|
+
});
|
|
17
|
+
test("parseCliArgs supports -1 short flag", () => {
|
|
18
|
+
const parsed = parseCliArgs([
|
|
19
|
+
"node",
|
|
20
|
+
"src/index.ts",
|
|
21
|
+
"-1",
|
|
22
|
+
"refactor",
|
|
23
|
+
"parser",
|
|
24
|
+
]);
|
|
25
|
+
assert.equal(parsed.oneshot, true);
|
|
26
|
+
assert.equal(parsed.verbose, false);
|
|
27
|
+
assert.equal(parsed.task, "refactor parser");
|
|
28
|
+
});
|
|
29
|
+
test("parseCliArgs supports --json and --out path", () => {
|
|
30
|
+
const parsed = parseCliArgs([
|
|
31
|
+
"node",
|
|
32
|
+
"src/index.ts",
|
|
33
|
+
"--oneshot",
|
|
34
|
+
"--json",
|
|
35
|
+
"--out",
|
|
36
|
+
"result.json",
|
|
37
|
+
"summarize",
|
|
38
|
+
"todos",
|
|
39
|
+
]);
|
|
40
|
+
assert.equal(parsed.oneshot, true);
|
|
41
|
+
assert.equal(parsed.json, true);
|
|
42
|
+
assert.equal(parsed.outFile, "result.json");
|
|
43
|
+
assert.equal(parsed.task, "summarize todos");
|
|
44
|
+
});
|
|
45
|
+
test("parseCliArgs supports --out=<file>", () => {
|
|
46
|
+
const parsed = parseCliArgs([
|
|
47
|
+
"node",
|
|
48
|
+
"src/index.ts",
|
|
49
|
+
"--oneshot",
|
|
50
|
+
"--out=result.txt",
|
|
51
|
+
"do",
|
|
52
|
+
"work",
|
|
53
|
+
]);
|
|
54
|
+
assert.equal(parsed.outFile, "result.txt");
|
|
55
|
+
assert.equal(parsed.task, "do work");
|
|
56
|
+
});
|
|
57
|
+
test("parseCliArgs rejects --out without value", () => {
|
|
58
|
+
assert.throws(() => parseCliArgs(["node", "src/index.ts", "--oneshot", "--out"]), CliUsageError);
|
|
59
|
+
});
|
|
60
|
+
test("validateCliArgs rejects oneshot without task", () => {
|
|
61
|
+
assert.throws(() => validateCliArgs({ verbose: false, oneshot: true, json: false, task: "" }), /--oneshot requires a task prompt/);
|
|
62
|
+
});
|
|
63
|
+
test("validateCliArgs rejects json without oneshot", () => {
|
|
64
|
+
assert.throws(() => validateCliArgs({
|
|
65
|
+
verbose: false,
|
|
66
|
+
oneshot: false,
|
|
67
|
+
json: true,
|
|
68
|
+
task: "hello",
|
|
69
|
+
}), /only supported with --oneshot/);
|
|
70
|
+
});
|
|
71
|
+
test("validateCliArgs allows non-oneshot empty task", () => {
|
|
72
|
+
assert.doesNotThrow(() => validateCliArgs({ verbose: false, oneshot: false, json: false, task: "" }));
|
|
73
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
const repoRoot = path.resolve(import.meta.dirname, "..");
|
|
6
|
+
const cliEntry = path.join(repoRoot, "src", "index.ts");
|
|
7
|
+
function runCli(args) {
|
|
8
|
+
return spawnSync("node", ["--import", "tsx", cliEntry, ...args], {
|
|
9
|
+
cwd: repoRoot,
|
|
10
|
+
env: {
|
|
11
|
+
...process.env,
|
|
12
|
+
CLI_UI_MODE: "legacy",
|
|
13
|
+
},
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
test("oneshot mode exits with usage code when prompt is missing", () => {
|
|
18
|
+
const result = runCli(["--oneshot"]);
|
|
19
|
+
assert.equal(result.status, 2);
|
|
20
|
+
assert.match(result.stderr, /--oneshot requires a task prompt/);
|
|
21
|
+
});
|
|
22
|
+
test("oneshot mode exits with usage code when --out has no value", () => {
|
|
23
|
+
const result = runCli(["--oneshot", "--out"]);
|
|
24
|
+
assert.equal(result.status, 2);
|
|
25
|
+
assert.match(result.stderr, /--out requires a file path/);
|
|
26
|
+
});
|
|
@@ -6,22 +6,22 @@ test("buildProjectIndex produces dependency edges", async () => {
|
|
|
6
6
|
const root = path.resolve(import.meta.dirname, "..");
|
|
7
7
|
const index = await buildProjectIndex(root);
|
|
8
8
|
assert.ok(index.dependencyEdges.length > 0, "should have dependency edges");
|
|
9
|
-
const
|
|
10
|
-
assert.ok(
|
|
11
|
-
assert.ok(
|
|
9
|
+
const createToolRegistryRefs = index.dependencyEdges.filter((e) => e.from === "createToolRegistry");
|
|
10
|
+
assert.ok(createToolRegistryRefs.some((e) => e.to === "ProjectIndex"), "createToolRegistry should reference ProjectIndex");
|
|
11
|
+
assert.ok(createToolRegistryRefs.some((e) => e.to === "AgentConfig"), "createToolRegistry should reference AgentConfig");
|
|
12
12
|
});
|
|
13
|
-
test("
|
|
13
|
+
test("loadAgentConfig has expected dependencies", async () => {
|
|
14
14
|
const root = path.resolve(import.meta.dirname, "..");
|
|
15
15
|
const index = await buildProjectIndex(root);
|
|
16
|
-
const edges = index.dependencyEdges.filter((e) => e.from === "
|
|
17
|
-
assert.ok(edges.some((e) => e.to === "AgentConfig"), "
|
|
18
|
-
assert.ok(edges.length >= 1, "
|
|
16
|
+
const edges = index.dependencyEdges.filter((e) => e.from === "loadAgentConfig");
|
|
17
|
+
assert.ok(edges.some((e) => e.to === "AgentConfig"), "loadAgentConfig should reference AgentConfig");
|
|
18
|
+
assert.ok(edges.length >= 1, "loadAgentConfig should have at least one dependency");
|
|
19
19
|
});
|
|
20
|
-
test("
|
|
20
|
+
test("buildProjectIndex indexes config and tool files", async () => {
|
|
21
21
|
const root = path.resolve(import.meta.dirname, "..");
|
|
22
22
|
const index = await buildProjectIndex(root);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
assert.ok(
|
|
23
|
+
const configSymbols = index.getSymbolsInFile("src/agent/config.ts");
|
|
24
|
+
assert.ok(configSymbols.some((s) => s.name === "loadAgentConfig"), "should index loadAgentConfig from config.ts");
|
|
25
|
+
const registrySymbols = index.getSymbolsInFile("src/tools/registry.ts");
|
|
26
|
+
assert.ok(registrySymbols.some((s) => s.name === "createToolRegistry"), "should index createToolRegistry from registry.ts");
|
|
27
27
|
});
|
|
@@ -3,9 +3,8 @@ import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { test } from "node:test";
|
|
6
|
+
import { createEditFileTool, createReadFileTool } from "@minicode/agent-sdk";
|
|
6
7
|
import { buildProjectIndex } from "../src/indexer/project-index.js";
|
|
7
|
-
import { createEditFileTool } from "../src/tools/edit-file.js";
|
|
8
|
-
import { createReadFileTool } from "../src/tools/read-file.js";
|
|
9
8
|
import { createTestAgentConfig } from "./test-utils.js";
|
|
10
9
|
async function createTempWorkspace() {
|
|
11
10
|
return mkdtemp(path.join(tmpdir(), "minicode-tests-"));
|
|
@@ -50,7 +49,7 @@ test("edit_file triggers reindex when projectIndex provided", async () => {
|
|
|
50
49
|
const index = await buildProjectIndex(workspaceRoot);
|
|
51
50
|
const before = index.getSymbol("add");
|
|
52
51
|
assert.ok(before?.signature.includes("a: number, b: number"));
|
|
53
|
-
const editTool = createEditFileTool(createTestAgentConfig(workspaceRoot), index);
|
|
52
|
+
const editTool = createEditFileTool(createTestAgentConfig(workspaceRoot), { afterEdit: (relPath, content) => index.reindexFile(relPath, content) });
|
|
54
53
|
await editTool.execute({
|
|
55
54
|
path: "src/util.ts",
|
|
56
55
|
old_string: "a: number, b: number",
|
|
@@ -3,13 +3,13 @@ import path from "node:path";
|
|
|
3
3
|
import { test } from "node:test";
|
|
4
4
|
import { buildProjectIndex } from "../src/indexer/project-index.js";
|
|
5
5
|
import { createFindReferencesTool } from "../src/tools/find-references.js";
|
|
6
|
-
test("find_references returns symbols that reference
|
|
6
|
+
test("find_references returns symbols that reference ProjectIndex", async () => {
|
|
7
7
|
const root = path.resolve(import.meta.dirname, "..");
|
|
8
8
|
const projectIndex = await buildProjectIndex(root);
|
|
9
9
|
const tool = createFindReferencesTool(projectIndex);
|
|
10
|
-
const result = await tool.execute({ name: "
|
|
11
|
-
assert.ok(result.includes("# References to
|
|
12
|
-
assert.ok(result.includes("
|
|
10
|
+
const result = await tool.execute({ name: "ProjectIndex" });
|
|
11
|
+
assert.ok(result.includes("# References to ProjectIndex"));
|
|
12
|
+
assert.ok(result.includes("createToolRegistry") || result.includes("createReadSymbolTool"));
|
|
13
13
|
});
|
|
14
14
|
test("find_references returns error for unknown symbol", async () => {
|
|
15
15
|
const root = path.resolve(import.meta.dirname, "..");
|
|
@@ -21,9 +21,9 @@ test("find_references returns error for unknown symbol", async () => {
|
|
|
21
21
|
test("find_references appears in tool registry when projectIndex provided", async () => {
|
|
22
22
|
const root = path.resolve(import.meta.dirname, "..");
|
|
23
23
|
const projectIndex = await buildProjectIndex(root);
|
|
24
|
-
const {
|
|
24
|
+
const { createToolRegistry } = await import("../src/tools/registry.js");
|
|
25
25
|
const { createTestAgentConfig } = await import("./test-utils.js");
|
|
26
|
-
const registry =
|
|
26
|
+
const registry = createToolRegistry(createTestAgentConfig(root), projectIndex);
|
|
27
27
|
const schemas = registry.getToolSchemas();
|
|
28
28
|
const findRefs = schemas.find((s) => s.name === "find_references");
|
|
29
29
|
assert.ok(findRefs);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { resolveWorkspacePath, validateCommand, validatePath, } from "
|
|
3
|
+
import { resolveWorkspacePath, validateCommand, validatePath, } from "@minicode/agent-sdk";
|
|
4
4
|
test("validatePath allows files within workspace", () => {
|
|
5
5
|
const workspaceRoot = "/tmp/workspace";
|
|
6
6
|
assert.equal(validatePath("src/index.ts", workspaceRoot), true);
|
|
@@ -96,12 +96,12 @@ test("buildProjectIndex works on minicode src/", async () => {
|
|
|
96
96
|
const index = await buildProjectIndex(root);
|
|
97
97
|
assert.ok(index.symbols.size > 0);
|
|
98
98
|
assert.ok(index.files.size > 0);
|
|
99
|
-
const
|
|
100
|
-
assert.ok(
|
|
101
|
-
assert.ok(
|
|
99
|
+
const configSymbols = index.getSymbolsInFile("src/agent/config.ts");
|
|
100
|
+
assert.ok(configSymbols.length >= 1);
|
|
101
|
+
assert.ok(configSymbols.some((s) => s.name === "loadAgentConfig"));
|
|
102
102
|
const codeMap = index.getCodeMap();
|
|
103
103
|
assert.ok(codeMap.text.includes("CodingAgent"));
|
|
104
|
-
assert.ok(codeMap.
|
|
104
|
+
assert.ok(codeMap.totalCount > 0);
|
|
105
105
|
});
|
|
106
106
|
test("getPluginForFile routes .tsx, .js, .jsx to TypeScript plugin", async () => {
|
|
107
107
|
const plugins = await loadPlugins("/tmp");
|
|
@@ -141,10 +141,10 @@ test("TypeScript plugin handles malformed syntax", () => {
|
|
|
141
141
|
test("ProjectIndex getSymbol finds by qualifiedName and by name", async () => {
|
|
142
142
|
const root = path.resolve(import.meta.dirname, "..");
|
|
143
143
|
const index = await buildProjectIndex(root);
|
|
144
|
-
const byQualified = index.getSymbol("
|
|
144
|
+
const byQualified = index.getSymbol("loadAgentConfig");
|
|
145
145
|
assert.ok(byQualified);
|
|
146
|
-
assert.equal(byQualified.qualifiedName, "
|
|
147
|
-
const byName = index.getSymbol("
|
|
146
|
+
assert.equal(byQualified.qualifiedName, "loadAgentConfig");
|
|
147
|
+
const byName = index.getSymbol("formatConfigForDisplay");
|
|
148
148
|
assert.ok(byName, "getSymbol should find by name when unique");
|
|
149
149
|
});
|
|
150
150
|
test("ProjectIndex getSymbolsInFile returns empty for non-existent file", async () => {
|
|
@@ -157,10 +157,10 @@ test("ProjectIndex getSymbolsInFile returns empty for non-existent file", async
|
|
|
157
157
|
test("ProjectIndex getDependencyCone returns target and dependencies", async () => {
|
|
158
158
|
const root = path.resolve(import.meta.dirname, "..");
|
|
159
159
|
const index = await buildProjectIndex(root);
|
|
160
|
-
const cone = index.getDependencyCone("
|
|
160
|
+
const cone = index.getDependencyCone("loadAgentConfig", 1);
|
|
161
161
|
assert.ok(Array.isArray(cone));
|
|
162
162
|
assert.ok(cone.length >= 1, "should include target symbol");
|
|
163
|
-
assert.ok(cone.some((s) => s.qualifiedName === "
|
|
163
|
+
assert.ok(cone.some((s) => s.qualifiedName === "loadAgentConfig"), "should include loadAgentConfig");
|
|
164
164
|
});
|
|
165
165
|
test("Code map handles empty symbols map", () => {
|
|
166
166
|
const result = generateCodeMap(new Map());
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
|
-
import { OpenAICompatibleModelClient, createModelClient, } from "
|
|
3
|
+
import { OpenAICompatibleModelClient, createModelClient, } from "@minicode/agent-sdk";
|
|
4
4
|
import { createTestAgentConfig } from "./test-utils.js";
|
|
5
5
|
test("openai-compatible client sends tool schemas and parses tool calls", async () => {
|
|
6
6
|
let capturedUrl = "";
|
|
@@ -3,19 +3,19 @@ import path from "node:path";
|
|
|
3
3
|
import { test } from "node:test";
|
|
4
4
|
import { buildProjectIndex } from "../src/indexer/project-index.js";
|
|
5
5
|
import { createReadSymbolTool } from "../src/tools/read-symbol.js";
|
|
6
|
-
import {
|
|
6
|
+
import { createToolRegistry } from "../src/tools/registry.js";
|
|
7
7
|
import { createTestAgentConfig } from "./test-utils.js";
|
|
8
8
|
test("read_symbol returns correct function body", async () => {
|
|
9
9
|
const root = path.resolve(import.meta.dirname, "..");
|
|
10
10
|
const config = createTestAgentConfig(root);
|
|
11
11
|
const projectIndex = await buildProjectIndex(root);
|
|
12
12
|
const tool = createReadSymbolTool(config, projectIndex);
|
|
13
|
-
const result = await tool.execute({ name: "
|
|
14
|
-
assert.ok(result.includes("#
|
|
15
|
-
assert.ok(result.includes("src/agent/
|
|
13
|
+
const result = await tool.execute({ name: "loadAgentConfig" });
|
|
14
|
+
assert.ok(result.includes("# loadAgentConfig"));
|
|
15
|
+
assert.ok(result.includes("src/agent/config.ts"));
|
|
16
16
|
assert.ok(result.includes("Lines:"));
|
|
17
17
|
assert.ok(/\d+\|/.test(result), "should have line numbers");
|
|
18
|
-
assert.ok(result.includes("
|
|
18
|
+
assert.ok(result.includes("loadAgentConfig") || result.includes("config"));
|
|
19
19
|
});
|
|
20
20
|
test("read_symbol returns error for unknown symbol name", async () => {
|
|
21
21
|
const root = path.resolve(import.meta.dirname, "..");
|
|
@@ -32,13 +32,13 @@ test("read_symbol with includeBody: false returns signature only", async () => {
|
|
|
32
32
|
const projectIndex = await buildProjectIndex(root);
|
|
33
33
|
const tool = createReadSymbolTool(config, projectIndex);
|
|
34
34
|
const result = await tool.execute({
|
|
35
|
-
name: "
|
|
35
|
+
name: "formatConfigForDisplay",
|
|
36
36
|
includeBody: false,
|
|
37
37
|
});
|
|
38
|
-
assert.ok(result.includes("#
|
|
39
|
-
assert.ok(result.includes("src/
|
|
40
|
-
assert.ok(!result.includes("return
|
|
41
|
-
assert.ok(result.includes("
|
|
38
|
+
assert.ok(result.includes("# formatConfigForDisplay"));
|
|
39
|
+
assert.ok(result.includes("src/agent/config.ts"));
|
|
40
|
+
assert.ok(!result.includes("return lines.join"));
|
|
41
|
+
assert.ok(result.includes("config") || result.includes("=>"));
|
|
42
42
|
});
|
|
43
43
|
test("read_symbol includes leading context and line numbers", async () => {
|
|
44
44
|
const root = path.resolve(import.meta.dirname, "..");
|
|
@@ -56,7 +56,7 @@ test("read_symbol appears in tool registry schemas when projectIndex provided",
|
|
|
56
56
|
const root = path.resolve(import.meta.dirname, "..");
|
|
57
57
|
const config = createTestAgentConfig(root);
|
|
58
58
|
const projectIndex = await buildProjectIndex(root);
|
|
59
|
-
const registry =
|
|
59
|
+
const registry = createToolRegistry(config, projectIndex);
|
|
60
60
|
const schemas = registry.getToolSchemas();
|
|
61
61
|
const readSymbol = schemas.find((s) => s.name === "read_symbol");
|
|
62
62
|
assert.ok(readSymbol, "read_symbol should be in schemas");
|
|
@@ -64,19 +64,18 @@ test("read_symbol appears in tool registry schemas when projectIndex provided",
|
|
|
64
64
|
const props = readSymbol.input_schema.properties;
|
|
65
65
|
assert.ok(props && "name" in props);
|
|
66
66
|
});
|
|
67
|
-
test("read_symbol includes Referenced Types section for
|
|
67
|
+
test("read_symbol includes Referenced Types section for createToolRegistry", async () => {
|
|
68
68
|
const root = path.resolve(import.meta.dirname, "..");
|
|
69
69
|
const config = createTestAgentConfig(root);
|
|
70
70
|
const projectIndex = await buildProjectIndex(root);
|
|
71
71
|
const tool = createReadSymbolTool(config, projectIndex);
|
|
72
|
-
const result = await tool.execute({ name: "
|
|
73
|
-
assert.ok(result.includes("
|
|
74
|
-
assert.ok(result.includes("
|
|
75
|
-
assert.ok(result.includes("ToolCall"));
|
|
72
|
+
const result = await tool.execute({ name: "createToolRegistry" });
|
|
73
|
+
assert.ok(result.includes("# createToolRegistry"));
|
|
74
|
+
assert.ok(result.includes("src/tools/registry.ts"));
|
|
76
75
|
});
|
|
77
76
|
test("read_symbol is not in tool registry when projectIndex is undefined", () => {
|
|
78
77
|
const config = createTestAgentConfig("/tmp");
|
|
79
|
-
const registry =
|
|
78
|
+
const registry = createToolRegistry(config);
|
|
80
79
|
const schemas = registry.getToolSchemas();
|
|
81
80
|
const readSymbol = schemas.find((s) => s.name === "read_symbol");
|
|
82
81
|
assert.equal(readSymbol, undefined);
|
|
@@ -21,9 +21,9 @@ test("search_code_map returns empty when no match", async () => {
|
|
|
21
21
|
test("search_code_map appears in tool registry when projectIndex provided", async () => {
|
|
22
22
|
const root = path.resolve(import.meta.dirname, "..");
|
|
23
23
|
const projectIndex = await buildProjectIndex(root);
|
|
24
|
-
const {
|
|
24
|
+
const { createToolRegistry } = await import("../src/tools/registry.js");
|
|
25
25
|
const { createTestAgentConfig } = await import("./test-utils.js");
|
|
26
|
-
const registry =
|
|
26
|
+
const registry = createToolRegistry(createTestAgentConfig(root), projectIndex);
|
|
27
27
|
const schemas = registry.getToolSchemas();
|
|
28
28
|
const searchCodeMap = schemas.find((s) => s.name === "search_code_map");
|
|
29
29
|
assert.ok(searchCodeMap);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtemp, readdir, rm } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { Session } from "@minicode/agent-sdk";
|
|
7
|
+
import { listSessions, loadSession, loadSessionByLabel, saveSession, setSessionsDir, } from "../src/session/session-store.js";
|
|
8
|
+
async function withTmpDir(fn) {
|
|
9
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "minicode-test-"));
|
|
10
|
+
setSessionsDir(dir);
|
|
11
|
+
try {
|
|
12
|
+
await fn(dir);
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
await rm(dir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
test("saveSession creates a JSON file in sessions dir", async () => {
|
|
19
|
+
await withTmpDir(async (dir) => {
|
|
20
|
+
const session = new Session("test-id");
|
|
21
|
+
session.addMessage({ role: "user", content: "hello" });
|
|
22
|
+
session.addMessage({ role: "assistant", content: "hi there" });
|
|
23
|
+
const meta = await saveSession(session, "my label");
|
|
24
|
+
assert.equal(meta.id, "test-id");
|
|
25
|
+
assert.equal(meta.label, "my label");
|
|
26
|
+
assert.equal(meta.messageCount, 2);
|
|
27
|
+
const files = await readdir(dir);
|
|
28
|
+
assert.equal(files.length, 1);
|
|
29
|
+
assert.equal(files[0], "test-id.json");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
test("saveSession uses timestamp label when none provided", async () => {
|
|
33
|
+
await withTmpDir(async () => {
|
|
34
|
+
const session = new Session("test-id");
|
|
35
|
+
const meta = await saveSession(session);
|
|
36
|
+
assert.ok(meta.label.length > 0);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
test("listSessions returns empty array when no sessions dir", async () => {
|
|
40
|
+
const nonexistent = path.join(os.tmpdir(), "minicode-nonexistent-" + Date.now());
|
|
41
|
+
setSessionsDir(nonexistent);
|
|
42
|
+
const sessions = await listSessions();
|
|
43
|
+
assert.equal(sessions.length, 0);
|
|
44
|
+
});
|
|
45
|
+
test("listSessions returns saved sessions sorted by savedAt desc", async () => {
|
|
46
|
+
await withTmpDir(async () => {
|
|
47
|
+
const s1 = new Session("s1");
|
|
48
|
+
s1.addMessage({ role: "user", content: "first" });
|
|
49
|
+
await saveSession(s1, "first session");
|
|
50
|
+
// Small delay to ensure distinct timestamps for ordering
|
|
51
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
52
|
+
const s2 = new Session("s2");
|
|
53
|
+
s2.addMessage({ role: "user", content: "second" });
|
|
54
|
+
s2.addMessage({ role: "assistant", content: "reply" });
|
|
55
|
+
await saveSession(s2, "second session");
|
|
56
|
+
const sessions = await listSessions();
|
|
57
|
+
assert.equal(sessions.length, 2);
|
|
58
|
+
assert.equal(sessions[0]?.label, "second session");
|
|
59
|
+
assert.equal(sessions[1]?.label, "first session");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
test("loadSession restores a session by id", async () => {
|
|
63
|
+
await withTmpDir(async () => {
|
|
64
|
+
const session = new Session("test-id");
|
|
65
|
+
session.addMessage({ role: "user", content: "hello" });
|
|
66
|
+
session.addMessage({ role: "assistant", content: "hi" });
|
|
67
|
+
await saveSession(session, "test label");
|
|
68
|
+
const result = await loadSession("test-id");
|
|
69
|
+
assert.ok(result);
|
|
70
|
+
assert.equal(result.label, "test label");
|
|
71
|
+
assert.equal(result.session.id, "test-id");
|
|
72
|
+
const msgs = result.session.getMessages();
|
|
73
|
+
assert.equal(msgs.length, 2);
|
|
74
|
+
assert.equal(msgs[0]?.content, "hello");
|
|
75
|
+
assert.equal(msgs[1]?.content, "hi");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
test("loadSession returns undefined for missing id", async () => {
|
|
79
|
+
await withTmpDir(async () => {
|
|
80
|
+
const result = await loadSession("nonexistent");
|
|
81
|
+
assert.equal(result, undefined);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
test("loadSessionByLabel finds session by label (case-insensitive)", async () => {
|
|
85
|
+
await withTmpDir(async () => {
|
|
86
|
+
const session = new Session("test-id");
|
|
87
|
+
session.addMessage({ role: "user", content: "hello" });
|
|
88
|
+
await saveSession(session, "My Label");
|
|
89
|
+
const result = await loadSessionByLabel("my label");
|
|
90
|
+
assert.ok(result);
|
|
91
|
+
assert.equal(result.label, "My Label");
|
|
92
|
+
assert.equal(result.session.id, "test-id");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
test("loadSessionByLabel returns undefined for no match", async () => {
|
|
96
|
+
await withTmpDir(async () => {
|
|
97
|
+
const result = await loadSessionByLabel("nope");
|
|
98
|
+
assert.equal(result, undefined);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
test("saving same session twice overwrites the file", async () => {
|
|
102
|
+
await withTmpDir(async (dir) => {
|
|
103
|
+
const session = new Session("test-id");
|
|
104
|
+
session.addMessage({ role: "user", content: "hello" });
|
|
105
|
+
await saveSession(session, "v1");
|
|
106
|
+
session.addMessage({ role: "assistant", content: "reply" });
|
|
107
|
+
await saveSession(session, "v2");
|
|
108
|
+
const files = await readdir(dir);
|
|
109
|
+
assert.equal(files.length, 1);
|
|
110
|
+
const result = await loadSession("test-id");
|
|
111
|
+
assert.ok(result);
|
|
112
|
+
assert.equal(result.label, "v2");
|
|
113
|
+
assert.equal(result.session.getMessages().length, 2);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { Session } from "
|
|
3
|
+
import { Session } from "@minicode/agent-sdk";
|
|
4
4
|
test("session stores and returns messages", () => {
|
|
5
5
|
const session = new Session("test");
|
|
6
6
|
session.addMessage({ role: "user", content: "hello" });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { test } from "node:test";
|
|
4
|
-
import { buildSystemPrompt } from "
|
|
4
|
+
import { buildSystemPrompt } from "@minicode/agent-sdk";
|
|
5
5
|
function createMinimalConfig(workspaceRoot) {
|
|
6
6
|
return {
|
|
7
7
|
modelProvider: "openai-compatible",
|