@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/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight CLI coding agent optimized for **local models** by providing AST-based intelligent context for smaller models running on consumer hardware.
|
|
4
4
|
|
|
5
|
+
> minicode gives local models a dependency-aware map of your codebase, so agents read less, reason better, and ship changes faster.
|
|
6
|
+
|
|
5
7
|
Read operations dominate token usage in typical agent sessions; minicode addresses this by optimizing for **specific languages** — indexing your project at startup with language plugins (TypeScript/JavaScript built-in) and injecting a compact **code map** (signatures only) into the system prompt, plus symbol-level tools (`read_symbol`, `find_references`, `get_dependencies`) so the model reads only what it needs instead of entire files. This keeps prompts lean enough for smaller models in the 20B range, with faster inference and better attention over the relevant code.
|
|
6
8
|
|
|
7
9
|
## Quick Start (LM Studio)
|
|
@@ -21,7 +23,7 @@ OPENAI_BASE_URL=http://localhost:1234/v1
|
|
|
21
23
|
OPENAI_API_KEY=
|
|
22
24
|
MAX_STEPS=50
|
|
23
25
|
MAX_TOKENS=4096
|
|
24
|
-
MAX_CONTEXT_TOKENS=
|
|
26
|
+
MAX_CONTEXT_TOKENS=60000
|
|
25
27
|
WORKSPACE_ROOT=.
|
|
26
28
|
COMMAND_TIMEOUT_MS=30000
|
|
27
29
|
MAX_FILE_SIZE_BYTES=1000000
|
|
@@ -46,6 +48,20 @@ or you can also pass it an intial prompt from the start:
|
|
|
46
48
|
minicode "Add error handling to src/api.ts"
|
|
47
49
|
```
|
|
48
50
|
|
|
51
|
+
Run a single task and exit (useful for scripts/CI/orchestration):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
minicode --oneshot "Find TODOs and summarize action items"
|
|
55
|
+
# short flag
|
|
56
|
+
minicode -1 "Refactor parseArgs and run tests"
|
|
57
|
+
|
|
58
|
+
# JSON output (for pipeline parsing)
|
|
59
|
+
minicode --oneshot --json "Summarize recent changes"
|
|
60
|
+
|
|
61
|
+
# Write final output to a file (suppresses terminal response output)
|
|
62
|
+
minicode --oneshot --out result.txt "Generate release notes"
|
|
63
|
+
```
|
|
64
|
+
|
|
49
65
|
**Requirements:** Node.js 22+, LM Studio (or any OpenAI-compatible local server), `rg` in PATH (recommended). Set `MODEL` to match the model name in LM Studio.
|
|
50
66
|
|
|
51
67
|
### Install from source
|
|
@@ -81,6 +97,8 @@ For a deep technical walkthrough of AST parsing, dependency graph construction,
|
|
|
81
97
|
|
|
82
98
|
For agent-loop internals (session lifecycle, tool execution, streaming, loop detection, and model client behavior), see [docs/AGENT_RUNTIME.md](docs/AGENT_RUNTIME.md).
|
|
83
99
|
|
|
100
|
+
For the proposed reusable package architecture and public interfaces for a standalone runtime SDK, see [docs/SDK_SPEC.md](docs/SDK_SPEC.md).
|
|
101
|
+
|
|
84
102
|
minicode reduces token usage by indexing your project and providing targeted tools:
|
|
85
103
|
|
|
86
104
|
- **Code map** — A compact project skeleton (signatures only) is injected into the system prompt so the model can orient itself without reading full files.
|
|
@@ -110,6 +128,17 @@ The graph powers:
|
|
|
110
128
|
- **`find_references`** — Returns symbols that call or reference a given symbol.
|
|
111
129
|
- **`read_symbol`** — Shows "Used by", "Calls", and "Referenced Types" derived from the graph.
|
|
112
130
|
|
|
131
|
+
### Why this differs from a tree-sitter-first approach
|
|
132
|
+
|
|
133
|
+
Tree-sitter-focused agents are excellent for fast, generic syntax parsing across many languages. minicode takes a different path for TypeScript/JavaScript by using the TypeScript compiler AST to build a project symbol graph and drive graph-aware tools.
|
|
134
|
+
|
|
135
|
+
Advantages of this approach in minicode:
|
|
136
|
+
|
|
137
|
+
- **Dependency-aware navigation** — tools can follow call/type/inheritance edges (`calls`, `references`, `extends`, `implements`) instead of relying on text-only search.
|
|
138
|
+
- **Higher-signal context under tight budgets** — code-map ranking prioritizes exported and highly referenced symbols so key APIs survive truncation.
|
|
139
|
+
- **Targeted reads for local models** — symbol-level tools (`read_symbol`, `find_references`, `get_dependencies`) reduce unnecessary file reads and improve attention on relevant code.
|
|
140
|
+
- **Fast iterative indexing** — syntax-only AST parsing (without full type-checking) keeps startup and reindexing lightweight while preserving structural code intelligence.
|
|
141
|
+
|
|
113
142
|
## Plugin System
|
|
114
143
|
|
|
115
144
|
### Supported Languages
|
|
@@ -228,6 +257,20 @@ npm run dev -- --verbose "Fix the bug"
|
|
|
228
257
|
npm run dev -- -v
|
|
229
258
|
```
|
|
230
259
|
|
|
260
|
+
One-shot mode in development:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
npm run dev -- --oneshot "Fix lint errors and explain changes"
|
|
264
|
+
npm run dev -- --oneshot --json "Summarize TODOs"
|
|
265
|
+
npm run dev -- --oneshot --out result.txt "Draft changelog"
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Exit codes
|
|
269
|
+
|
|
270
|
+
- `0`: Success
|
|
271
|
+
- `1`: Runtime failure
|
|
272
|
+
- `2`: CLI usage/validation error (for example, `--oneshot` without a prompt)
|
|
273
|
+
|
|
231
274
|
## Scripts
|
|
232
275
|
|
|
233
276
|
- `npm run dev` - start the CLI in TypeScript mode
|
|
@@ -237,5 +280,3 @@ npm run dev -- -v
|
|
|
237
280
|
- `npm run lint` - run ESLint on TypeScript source and tests
|
|
238
281
|
- `npm test` - run Node test suite
|
|
239
282
|
|
|
240
|
-
|
|
241
|
-
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export class CliUsageError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "CliUsageError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function parseCliArgs(argv) {
|
|
8
|
+
const args = argv.slice(2);
|
|
9
|
+
let verbose = false;
|
|
10
|
+
let oneshot = false;
|
|
11
|
+
let json = false;
|
|
12
|
+
let outFile;
|
|
13
|
+
const taskParts = [];
|
|
14
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
15
|
+
const arg = args[i];
|
|
16
|
+
if (arg === undefined) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (arg === "--verbose" || arg === "-v") {
|
|
20
|
+
verbose = true;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (arg === "--oneshot" || arg === "-1") {
|
|
24
|
+
oneshot = true;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (arg === "--json") {
|
|
28
|
+
json = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (arg === "--out") {
|
|
32
|
+
const value = args[i + 1];
|
|
33
|
+
if (!value || value.startsWith("-")) {
|
|
34
|
+
throw new CliUsageError("--out requires a file path. Example: --out result.txt");
|
|
35
|
+
}
|
|
36
|
+
outFile = value;
|
|
37
|
+
i += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (arg.startsWith("--out=")) {
|
|
41
|
+
const value = arg.slice("--out=".length).trim();
|
|
42
|
+
if (value.length === 0) {
|
|
43
|
+
throw new CliUsageError("--out requires a non-empty file path. Example: --out=result.txt");
|
|
44
|
+
}
|
|
45
|
+
outFile = value;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
taskParts.push(arg);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
verbose,
|
|
52
|
+
oneshot,
|
|
53
|
+
json,
|
|
54
|
+
...(outFile ? { outFile } : {}),
|
|
55
|
+
task: taskParts.join(" ").trim(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function validateCliArgs(args) {
|
|
59
|
+
if (args.oneshot && args.task.length === 0) {
|
|
60
|
+
throw new CliUsageError("--oneshot requires a task prompt. Example: minicode --oneshot \"Fix lint errors\"");
|
|
61
|
+
}
|
|
62
|
+
if (!args.oneshot && (args.json || args.outFile)) {
|
|
63
|
+
throw new CliUsageError("--json and --out are only supported with --oneshot.");
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from "node:process";
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
3
4
|
import { createInterface } from "node:readline/promises";
|
|
4
|
-
import { CodingAgent } from "
|
|
5
|
+
import { CodingAgent, createModelClient } from "@minicode/agent-sdk";
|
|
5
6
|
import { formatConfigForDisplay, loadAgentConfig } from "./agent/config.js";
|
|
7
|
+
import { listSessions, loadSession, loadSessionByLabel, saveSession, } from "./session/session-store.js";
|
|
6
8
|
import { computeFileHashes, getWorkspaceCacheDir, loadIndex, saveIndex, } from "./indexer/cache.js";
|
|
7
9
|
import { buildProjectIndex } from "./indexer/project-index.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const filtered = args.filter((a) => a !== "--verbose" && a !== "-v");
|
|
14
|
-
const task = filtered.join(" ").trim();
|
|
15
|
-
return { verbose, task };
|
|
16
|
-
}
|
|
10
|
+
import { createToolRegistry } from "./tools/registry.js";
|
|
11
|
+
import { CliUsageError, parseCliArgs, validateCliArgs, } from "./cli/args.js";
|
|
12
|
+
const EXIT_CODE_SUCCESS = 0;
|
|
13
|
+
const EXIT_CODE_RUNTIME_ERROR = 1;
|
|
14
|
+
const EXIT_CODE_USAGE_ERROR = 2;
|
|
17
15
|
function printBanner() {
|
|
18
16
|
console.log("minicode");
|
|
19
17
|
console.log('Type your request, or "/exit" to quit.');
|
|
20
18
|
}
|
|
21
|
-
async function
|
|
19
|
+
async function createAgentRuntime(verbose, onProgress) {
|
|
22
20
|
const config = await loadAgentConfig();
|
|
23
21
|
const modelClient = createModelClient(config);
|
|
24
22
|
let projectIndex;
|
|
@@ -37,15 +35,26 @@ async function runInteractive(verbose, initialTask) {
|
|
|
37
35
|
catch {
|
|
38
36
|
projectIndex = undefined;
|
|
39
37
|
}
|
|
40
|
-
const toolRegistry =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
const toolRegistry = createToolRegistry(config, projectIndex);
|
|
39
|
+
function buildAgent(session) {
|
|
40
|
+
return new CodingAgent({
|
|
41
|
+
config,
|
|
42
|
+
modelClient,
|
|
43
|
+
toolRegistry,
|
|
44
|
+
verbose,
|
|
45
|
+
...(session ? { session } : {}),
|
|
46
|
+
...(projectIndex !== undefined
|
|
47
|
+
? { getCodeMap: () => projectIndex.getCodeMap() }
|
|
48
|
+
: {}),
|
|
49
|
+
...(onProgress ? { onProgress } : {}),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return { agent: buildAgent(), config, toolRegistry, projectIndex, buildAgent };
|
|
53
|
+
}
|
|
54
|
+
async function runInteractive(verbose, initialTask) {
|
|
55
|
+
const runtime = await createAgentRuntime(verbose, (msg) => console.error(` ${msg}`));
|
|
56
|
+
let { agent } = runtime;
|
|
57
|
+
const { config, buildAgent } = runtime;
|
|
49
58
|
printBanner();
|
|
50
59
|
console.log(`Workspace: ${config.workspaceRoot}`);
|
|
51
60
|
console.log(`Provider: ${config.modelProvider}`);
|
|
@@ -64,7 +73,7 @@ async function runInteractive(verbose, initialTask) {
|
|
|
64
73
|
turnAbortController.abort();
|
|
65
74
|
}
|
|
66
75
|
else if (shuttingDown) {
|
|
67
|
-
process.exit(
|
|
76
|
+
process.exit(EXIT_CODE_RUNTIME_ERROR);
|
|
68
77
|
}
|
|
69
78
|
else {
|
|
70
79
|
shuttingDown = true;
|
|
@@ -89,7 +98,7 @@ async function runInteractive(verbose, initialTask) {
|
|
|
89
98
|
break;
|
|
90
99
|
}
|
|
91
100
|
if (trimmed === "/help") {
|
|
92
|
-
console.log('Commands: "/help", "/config", "/exit"');
|
|
101
|
+
console.log('Commands: "/help", "/config", "/save [label]", "/load [label]", "/sessions", "/exit"');
|
|
93
102
|
console.log("Start with --verbose or -v to log prompts, responses, and tool calls.");
|
|
94
103
|
continue;
|
|
95
104
|
}
|
|
@@ -97,6 +106,57 @@ async function runInteractive(verbose, initialTask) {
|
|
|
97
106
|
console.log("\n" + formatConfigForDisplay(config) + "\n");
|
|
98
107
|
continue;
|
|
99
108
|
}
|
|
109
|
+
if (trimmed === "/save" || trimmed.startsWith("/save ")) {
|
|
110
|
+
const label = trimmed.slice("/save".length).trim() || undefined;
|
|
111
|
+
try {
|
|
112
|
+
const meta = await saveSession(agent.getSession(), label);
|
|
113
|
+
console.log(`Session saved as "${meta.label}" (${meta.messageCount} messages)`);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
117
|
+
console.error(`Failed to save session: ${msg}`);
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (trimmed === "/sessions") {
|
|
122
|
+
const sessions = await listSessions();
|
|
123
|
+
if (sessions.length === 0) {
|
|
124
|
+
console.log("No saved sessions found.");
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.log("Saved sessions:");
|
|
128
|
+
for (const s of sessions) {
|
|
129
|
+
console.log(` ${s.label} (${s.messageCount} msgs, saved ${s.savedAt})`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (trimmed === "/load" || trimmed.startsWith("/load ")) {
|
|
135
|
+
const arg = trimmed.slice("/load".length).trim();
|
|
136
|
+
if (arg.length === 0) {
|
|
137
|
+
const sessions = await listSessions();
|
|
138
|
+
if (sessions.length === 0) {
|
|
139
|
+
console.log("No saved sessions found.");
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.log("Saved sessions:");
|
|
143
|
+
for (const s of sessions) {
|
|
144
|
+
console.log(` ${s.label} (${s.messageCount} msgs, saved ${s.savedAt})`);
|
|
145
|
+
}
|
|
146
|
+
console.log('\nUse "/load <label>" to restore a session.');
|
|
147
|
+
}
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const result = (await loadSessionByLabel(arg)) ??
|
|
151
|
+
(await loadSession(arg));
|
|
152
|
+
if (!result) {
|
|
153
|
+
console.log(`No session found matching "${arg}".`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
agent = buildAgent(result.session);
|
|
157
|
+
console.log(`Session "${result.label}" restored (${result.session.getMessages().length} messages).`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
100
160
|
turnAbortController = new AbortController();
|
|
101
161
|
try {
|
|
102
162
|
const { text, usage } = await agent.runTurn(trimmed, {
|
|
@@ -121,18 +181,41 @@ async function runInteractive(verbose, initialTask) {
|
|
|
121
181
|
}
|
|
122
182
|
rl.close();
|
|
123
183
|
}
|
|
184
|
+
async function runOneshot(params) {
|
|
185
|
+
const { agent } = await createAgentRuntime(params.verbose);
|
|
186
|
+
const { text, usage } = await agent.runTurn(params.task);
|
|
187
|
+
const payload = params.json
|
|
188
|
+
? JSON.stringify({ text, usage: usage ?? null }, null, 2)
|
|
189
|
+
: text;
|
|
190
|
+
if (params.outFile) {
|
|
191
|
+
await writeFile(params.outFile, payload + "\n", "utf8");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
console.log(payload);
|
|
195
|
+
}
|
|
124
196
|
async function main() {
|
|
125
|
-
const
|
|
197
|
+
const cliArgs = parseCliArgs(process.argv);
|
|
198
|
+
validateCliArgs(cliArgs);
|
|
199
|
+
if (cliArgs.oneshot) {
|
|
200
|
+
await runOneshot(cliArgs);
|
|
201
|
+
process.exitCode = EXIT_CODE_SUCCESS;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
126
204
|
const uiMode = process.env.CLI_UI_MODE ?? "ink";
|
|
127
205
|
if (uiMode !== "legacy" && process.stdin.isTTY) {
|
|
128
206
|
const { runInkCli } = await import("./ui/cli-ink.js");
|
|
129
|
-
await runInkCli(verbose, task.length > 0 ? task : undefined);
|
|
207
|
+
await runInkCli(cliArgs.verbose, cliArgs.task.length > 0 ? cliArgs.task : undefined);
|
|
208
|
+
process.exitCode = EXIT_CODE_SUCCESS;
|
|
130
209
|
return;
|
|
131
210
|
}
|
|
132
|
-
await runInteractive(verbose, task.length > 0 ? task : undefined);
|
|
211
|
+
await runInteractive(cliArgs.verbose, cliArgs.task.length > 0 ? cliArgs.task : undefined);
|
|
212
|
+
process.exitCode = EXIT_CODE_SUCCESS;
|
|
133
213
|
}
|
|
134
214
|
main().catch((error) => {
|
|
135
215
|
const message = error instanceof Error ? error.message : String(error);
|
|
136
216
|
console.error(`Fatal error: ${message}`);
|
|
137
|
-
|
|
217
|
+
if (error instanceof CliUsageError) {
|
|
218
|
+
process.exit(EXIT_CODE_USAGE_ERROR);
|
|
219
|
+
}
|
|
220
|
+
process.exit(EXIT_CODE_RUNTIME_ERROR);
|
|
138
221
|
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Session } from "@minicode/agent-sdk";
|
|
5
|
+
let sessionsDir = path.join(os.homedir(), ".minicode", "sessions");
|
|
6
|
+
/** Override sessions directory (for testing). */
|
|
7
|
+
export function setSessionsDir(dir) {
|
|
8
|
+
sessionsDir = dir;
|
|
9
|
+
}
|
|
10
|
+
export async function saveSession(session, label) {
|
|
11
|
+
await mkdir(sessionsDir, { recursive: true });
|
|
12
|
+
const savedAt = new Date().toISOString();
|
|
13
|
+
const snapshot = session.toJSON();
|
|
14
|
+
const resolvedLabel = label && label.trim().length > 0
|
|
15
|
+
? label.trim()
|
|
16
|
+
: new Date().toLocaleString();
|
|
17
|
+
const data = {
|
|
18
|
+
label: resolvedLabel,
|
|
19
|
+
savedAt,
|
|
20
|
+
session: snapshot,
|
|
21
|
+
};
|
|
22
|
+
const filePath = path.join(sessionsDir, `${snapshot.id}.json`);
|
|
23
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
24
|
+
return {
|
|
25
|
+
id: snapshot.id,
|
|
26
|
+
label: resolvedLabel,
|
|
27
|
+
createdAt: snapshot.createdAt,
|
|
28
|
+
savedAt,
|
|
29
|
+
messageCount: snapshot.messages.length,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export async function listSessions() {
|
|
33
|
+
let files;
|
|
34
|
+
try {
|
|
35
|
+
files = await readdir(sessionsDir);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const results = [];
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
if (!file.endsWith(".json"))
|
|
43
|
+
continue;
|
|
44
|
+
try {
|
|
45
|
+
const raw = await readFile(path.join(sessionsDir, file), "utf8");
|
|
46
|
+
const data = JSON.parse(raw);
|
|
47
|
+
results.push({
|
|
48
|
+
id: data.session.id,
|
|
49
|
+
label: data.label,
|
|
50
|
+
createdAt: data.session.createdAt,
|
|
51
|
+
savedAt: data.savedAt,
|
|
52
|
+
messageCount: data.session.messages.length,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// skip corrupt files
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
results.sort((a, b) => b.savedAt.localeCompare(a.savedAt));
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
export async function loadSession(sessionId) {
|
|
63
|
+
const filePath = path.join(sessionsDir, `${sessionId}.json`);
|
|
64
|
+
try {
|
|
65
|
+
const raw = await readFile(filePath, "utf8");
|
|
66
|
+
const data = JSON.parse(raw);
|
|
67
|
+
return {
|
|
68
|
+
session: Session.fromJSON(data.session),
|
|
69
|
+
label: data.label,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export async function loadSessionByLabel(label) {
|
|
77
|
+
const sessions = await listSessions();
|
|
78
|
+
const match = sessions.find((s) => s.label.toLowerCase() === label.toLowerCase());
|
|
79
|
+
if (!match)
|
|
80
|
+
return undefined;
|
|
81
|
+
return loadSession(match.id);
|
|
82
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { resolveWorkspacePath, validateFileReadSize, } from "
|
|
3
|
-
import { expectNonEmptyString, expectOptionalBoolean } from "./helpers.js";
|
|
2
|
+
import { resolveWorkspacePath, validateFileReadSize, expectNonEmptyString, expectOptionalBoolean, } from "@minicode/agent-sdk";
|
|
4
3
|
const LEADING_CONTEXT_LINES = 3;
|
|
5
4
|
export function createReadSymbolTool(config, projectIndex) {
|
|
6
5
|
return {
|
|
@@ -1,68 +1,33 @@
|
|
|
1
|
-
import { createEditFileTool } from "
|
|
1
|
+
import { ToolRegistry, createReadFileTool, createWriteFileTool, createEditFileTool, createSearchTool, createListFilesTool, createRunCommandTool, } from "@minicode/agent-sdk";
|
|
2
2
|
import { createFindReferencesTool } from "./find-references.js";
|
|
3
3
|
import { createGetDependenciesTool } from "./get-dependencies.js";
|
|
4
|
-
import { createListFilesTool } from "./list-files.js";
|
|
5
|
-
import { createReadFileTool } from "./read-file.js";
|
|
6
4
|
import { createReadSymbolTool } from "./read-symbol.js";
|
|
7
|
-
import { createRunCommandTool } from "./run-command.js";
|
|
8
5
|
import { createSearchCodeMapTool } from "./search-code-map.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
name: tool.name,
|
|
20
|
-
description: tool.description,
|
|
21
|
-
input_schema: tool.inputSchema,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
export class ToolRegistry {
|
|
25
|
-
toolsByName = new Map();
|
|
26
|
-
constructor(tools) {
|
|
27
|
-
for (const tool of tools) {
|
|
28
|
-
if (this.toolsByName.has(tool.name)) {
|
|
29
|
-
throw new Error(`Duplicate tool registration for "${tool.name}".`);
|
|
30
|
-
}
|
|
31
|
-
this.toolsByName.set(tool.name, tool);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
static createDefault(config, projectIndex) {
|
|
35
|
-
const tools = [
|
|
36
|
-
createReadFileTool(config),
|
|
37
|
-
createWriteFileTool(config, projectIndex),
|
|
38
|
-
createEditFileTool(config, projectIndex),
|
|
39
|
-
createSearchTool(config),
|
|
40
|
-
createListFilesTool(config),
|
|
41
|
-
createRunCommandTool(config),
|
|
42
|
-
];
|
|
43
|
-
if (projectIndex) {
|
|
44
|
-
tools.splice(1, 0, createReadSymbolTool(config, projectIndex));
|
|
45
|
-
tools.splice(2, 0, createFindReferencesTool(projectIndex));
|
|
46
|
-
tools.splice(3, 0, createGetDependenciesTool(projectIndex));
|
|
47
|
-
tools.splice(4, 0, createSearchCodeMapTool(projectIndex));
|
|
48
|
-
}
|
|
49
|
-
return new ToolRegistry(tools);
|
|
50
|
-
}
|
|
51
|
-
getToolSchemas() {
|
|
52
|
-
return [...this.toolsByName.values()].map(toToolSchema);
|
|
53
|
-
}
|
|
54
|
-
async execute(name, input) {
|
|
55
|
-
const tool = this.toolsByName.get(name);
|
|
56
|
-
if (!tool) {
|
|
57
|
-
return `Tool error: Unknown tool "${name}".`;
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
const inputObject = ensureInputObject(input);
|
|
61
|
-
return await tool.execute(inputObject);
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
const message = error instanceof Error ? error.message : "Unknown tool failure";
|
|
65
|
-
return `Tool error (${name}): ${message}`;
|
|
6
|
+
export { ToolRegistry };
|
|
7
|
+
/**
|
|
8
|
+
* Create a ToolRegistry with the SDK's core tools plus indexer-specific tools
|
|
9
|
+
* when a ProjectIndex is available.
|
|
10
|
+
*/
|
|
11
|
+
export function createToolRegistry(config, projectIndex) {
|
|
12
|
+
const hooks = projectIndex
|
|
13
|
+
? {
|
|
14
|
+
afterWrite: (relPath, content) => projectIndex.reindexFile(relPath, content),
|
|
15
|
+
afterEdit: (relPath, content) => projectIndex.reindexFile(relPath, content),
|
|
66
16
|
}
|
|
17
|
+
: undefined;
|
|
18
|
+
const tools = [
|
|
19
|
+
createReadFileTool(config),
|
|
20
|
+
createWriteFileTool(config, hooks ? { afterWrite: hooks.afterWrite } : undefined),
|
|
21
|
+
createEditFileTool(config, hooks ? { afterEdit: hooks.afterEdit } : undefined),
|
|
22
|
+
createSearchTool(config),
|
|
23
|
+
createListFilesTool(config),
|
|
24
|
+
createRunCommandTool(config),
|
|
25
|
+
];
|
|
26
|
+
if (projectIndex) {
|
|
27
|
+
tools.splice(1, 0, createReadSymbolTool(config, projectIndex));
|
|
28
|
+
tools.splice(2, 0, createFindReferencesTool(projectIndex));
|
|
29
|
+
tools.splice(3, 0, createGetDependenciesTool(projectIndex));
|
|
30
|
+
tools.splice(4, 0, createSearchCodeMapTool(projectIndex));
|
|
67
31
|
}
|
|
32
|
+
return new ToolRegistry(tools);
|
|
68
33
|
}
|