@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.
Files changed (39) hide show
  1. package/README.md +44 -3
  2. package/dist/src/cli/args.js +65 -0
  3. package/dist/src/index.js +109 -26
  4. package/dist/src/session/session-store.js +82 -0
  5. package/dist/src/tools/find-references.js +1 -1
  6. package/dist/src/tools/get-dependencies.js +1 -1
  7. package/dist/src/tools/read-symbol.js +1 -2
  8. package/dist/src/tools/registry.js +26 -61
  9. package/dist/src/tools/search-code-map.js +1 -1
  10. package/dist/src/ui/cli-ink.js +91 -19
  11. package/dist/tests/agent.test.js +2 -3
  12. package/dist/tests/cli-args.test.js +73 -0
  13. package/dist/tests/cli-oneshot.integration.test.js +26 -0
  14. package/dist/tests/dependency-graph.test.js +12 -12
  15. package/dist/tests/file-tools.test.js +2 -3
  16. package/dist/tests/find-references.test.js +6 -6
  17. package/dist/tests/guardrails.test.js +1 -1
  18. package/dist/tests/indexer.test.js +9 -9
  19. package/dist/tests/model-client-openai.test.js +1 -1
  20. package/dist/tests/read-symbol.test.js +16 -17
  21. package/dist/tests/search-code-map.test.js +2 -2
  22. package/dist/tests/session-store.test.js +115 -0
  23. package/dist/tests/session.test.js +1 -1
  24. package/dist/tests/system-prompt.test.js +1 -1
  25. package/dist/tests/tool-registry.test.js +1 -1
  26. package/package.json +7 -2
  27. package/dist/src/agent/agent.js +0 -209
  28. package/dist/src/agent/types.js +0 -1
  29. package/dist/src/model/client.js +0 -374
  30. package/dist/src/prompt/system-prompt.js +0 -91
  31. package/dist/src/safety/guardrails.js +0 -55
  32. package/dist/src/session/session.js +0 -95
  33. package/dist/src/tools/edit-file.js +0 -73
  34. package/dist/src/tools/helpers.js +0 -42
  35. package/dist/src/tools/list-files.js +0 -63
  36. package/dist/src/tools/read-file.js +0 -79
  37. package/dist/src/tools/run-command.js +0 -92
  38. package/dist/src/tools/search.js +0 -153
  39. package/dist/src/tools/write-file.js +0 -44
@@ -1,10 +1,10 @@
1
1
  import process from "node:process";
2
- import { CodingAgent } from "../agent/agent.js";
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 { createModelClient } from "../model/client.js";
7
- import { ToolRegistry } from "../tools/registry.js";
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 = ToolRegistry.createDefault(config, projectIndex);
43
- const agent = new CodingAgent({
44
- config,
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", "/exit". Start with --verbose or -v for detailed logs.',
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);
@@ -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 "../src/agent/agent.js";
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 parseResponseRefs = index.dependencyEdges.filter((e) => e.from === "parseResponse");
10
- assert.ok(parseResponseRefs.some((e) => e.to === "ModelResponse"), "parseResponse should reference ModelResponse");
11
- assert.ok(parseResponseRefs.some((e) => e.to === "ToolCall"), "parseResponse should reference ToolCall");
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("createModelClient has expected dependencies", async () => {
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 === "createModelClient");
17
- assert.ok(edges.some((e) => e.to === "AgentConfig"), "createModelClient should reference AgentConfig");
18
- assert.ok(edges.length >= 1, "createModelClient should have at least one dependency");
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("AnthropicModelClient implements ModelClient", async () => {
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 implementsEdge = index.dependencyEdges.find((e) => e.from === "AnthropicModelClient" &&
24
- e.to === "ModelClient" &&
25
- e.kind === "implements");
26
- assert.ok(implementsEdge, "AnthropicModelClient should implement ModelClient");
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 ModelResponse", async () => {
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: "ModelResponse" });
11
- assert.ok(result.includes("# References to ModelResponse"));
12
- assert.ok(result.includes("parseResponse"));
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 { ToolRegistry } = await import("../src/tools/registry.js");
24
+ const { createToolRegistry } = await import("../src/tools/registry.js");
25
25
  const { createTestAgentConfig } = await import("./test-utils.js");
26
- const registry = ToolRegistry.createDefault(createTestAgentConfig(root), projectIndex);
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 "../src/safety/guardrails.js";
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 agentSymbols = index.getSymbolsInFile("src/agent/agent.ts");
100
- assert.ok(agentSymbols.length >= 1);
101
- assert.ok(agentSymbols.some((s) => s.name === "CodingAgent"));
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.text.includes("runTurn"));
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("CodingAgent.runTurn");
144
+ const byQualified = index.getSymbol("loadAgentConfig");
145
145
  assert.ok(byQualified);
146
- assert.equal(byQualified.qualifiedName, "CodingAgent.runTurn");
147
- const byName = index.getSymbol("runTurn");
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("parseResponse", 1);
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 === "parseResponse"), "should include parseResponse");
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 "../src/model/client.js";
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 { ToolRegistry } from "../src/tools/registry.js";
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: "CodingAgent.runTurn" });
14
- assert.ok(result.includes("# CodingAgent.runTurn"));
15
- assert.ok(result.includes("src/agent/agent.ts"));
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("runTurn") || result.includes("session"));
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: "parseResponse",
35
+ name: "formatConfigForDisplay",
36
36
  includeBody: false,
37
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("=>"));
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 = ToolRegistry.createDefault(config, projectIndex);
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 parseResponse", async () => {
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: "parseResponse" });
73
- assert.ok(result.includes("## Referenced Types"));
74
- assert.ok(result.includes("ModelResponse"));
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 = ToolRegistry.createDefault(config);
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 { ToolRegistry } = await import("../src/tools/registry.js");
24
+ const { createToolRegistry } = await import("../src/tools/registry.js");
25
25
  const { createTestAgentConfig } = await import("./test-utils.js");
26
- const registry = ToolRegistry.createDefault(createTestAgentConfig(root), projectIndex);
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 "../src/session/session.js";
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 "../src/prompt/system-prompt.js";
4
+ import { buildSystemPrompt } from "@minicode/agent-sdk";
5
5
  function createMinimalConfig(workspaceRoot) {
6
6
  return {
7
7
  modelProvider: "openai-compatible",