netheriteai-code 0.4.0 → 1.0.2
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/package.json +13 -2
- package/src/agent.js +25 -19
- package/src/cli.js +2 -2
- package/src/ollama.js +3 -3
- package/src/tools.js +134 -21
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "netheriteai-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "NetheriteAI:Code by hurdacu. High-performance coding assistant.",
|
|
5
5
|
"author": "hurdacu",
|
|
6
|
+
"license": "MIT",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"bin": {
|
|
8
9
|
"netheriteai-code": "bin/netheriteai-code.js",
|
|
@@ -14,5 +15,15 @@
|
|
|
14
15
|
"models": "node ./bin/netheriteai-code.js models",
|
|
15
16
|
"chat": "node ./bin/netheriteai-code.js chat",
|
|
16
17
|
"tui": "node ./bin/netheriteai-code.js tui"
|
|
17
|
-
}
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18.0.0"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"ai",
|
|
24
|
+
"coding",
|
|
25
|
+
"assistant",
|
|
26
|
+
"tui",
|
|
27
|
+
"terminal"
|
|
28
|
+
]
|
|
18
29
|
}
|
package/src/agent.js
CHANGED
|
@@ -200,10 +200,10 @@ export async function runAgentTurn({ workspaceRoot, model, history, userPrompt,
|
|
|
200
200
|
name: toolCall.function.name,
|
|
201
201
|
result,
|
|
202
202
|
});
|
|
203
|
+
// Workaround: Use 'user' role instead of 'tool' because the remote server rejects 'tool' role.
|
|
203
204
|
messages.push({
|
|
204
|
-
role: "
|
|
205
|
-
|
|
206
|
-
content: JSON.stringify(result),
|
|
205
|
+
role: "user",
|
|
206
|
+
content: `Observation from write_file:\n${JSON.stringify(result, null, 2)}`,
|
|
207
207
|
});
|
|
208
208
|
onEvent?.({ type: "assistant", text: `Wrote ${path}.` });
|
|
209
209
|
return {
|
|
@@ -220,10 +220,10 @@ export async function runAgentTurn({ workspaceRoot, model, history, userPrompt,
|
|
|
220
220
|
name: toolCall.function.name,
|
|
221
221
|
result: toolError,
|
|
222
222
|
});
|
|
223
|
+
// Workaround: Use 'user' role instead of 'tool' because the remote server rejects 'tool' role.
|
|
223
224
|
messages.push({
|
|
224
|
-
role: "
|
|
225
|
-
|
|
226
|
-
content: JSON.stringify(toolError),
|
|
225
|
+
role: "user",
|
|
226
|
+
content: `Error from write_file:\n${toolError.error}`,
|
|
227
227
|
});
|
|
228
228
|
onEvent?.({ type: "assistant", text: `Error: ${toolError.error}` });
|
|
229
229
|
return {
|
|
@@ -245,6 +245,7 @@ export async function runAgentTurn({ workspaceRoot, model, history, userPrompt,
|
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
const observations = [];
|
|
248
249
|
for (const toolCall of toolCalls) {
|
|
249
250
|
onEvent?.({
|
|
250
251
|
type: "tool_start",
|
|
@@ -269,11 +270,7 @@ export async function runAgentTurn({ workspaceRoot, model, history, userPrompt,
|
|
|
269
270
|
result,
|
|
270
271
|
});
|
|
271
272
|
|
|
272
|
-
|
|
273
|
-
messages.push({
|
|
274
|
-
role: "user",
|
|
275
|
-
content: `Observation from ${toolCall.function.name}:\n${JSON.stringify(result, null, 2)}`,
|
|
276
|
-
});
|
|
273
|
+
observations.push(`Observation from ${toolCall.function.name}:\n${JSON.stringify(result, null, 2)}`);
|
|
277
274
|
} catch (error) {
|
|
278
275
|
onEvent?.({
|
|
279
276
|
type: "tool_result",
|
|
@@ -283,15 +280,24 @@ export async function runAgentTurn({ workspaceRoot, model, history, userPrompt,
|
|
|
283
280
|
error: error instanceof Error ? error.message : String(error),
|
|
284
281
|
},
|
|
285
282
|
});
|
|
286
|
-
|
|
287
|
-
role: "tool",
|
|
288
|
-
tool_call_id: toolCall.id,
|
|
289
|
-
content: JSON.stringify({
|
|
290
|
-
ok: false,
|
|
291
|
-
error: error instanceof Error ? error.message : String(error),
|
|
292
|
-
}),
|
|
293
|
-
});
|
|
283
|
+
observations.push(`Error from ${toolCall.function.name}:\n${error instanceof Error ? error.message : String(error)}`);
|
|
294
284
|
}
|
|
295
285
|
}
|
|
286
|
+
|
|
287
|
+
// Workaround: Use 'user' role instead of 'tool' because the remote server rejects 'tool' role.
|
|
288
|
+
if (observations.length > 0) {
|
|
289
|
+
messages.push({
|
|
290
|
+
role: "user",
|
|
291
|
+
content: observations.join("\n\n"),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Keep history lean to avoid server overload
|
|
296
|
+
if (messages.length > 20) {
|
|
297
|
+
const systemMsg = messages[0];
|
|
298
|
+
const recent = messages.slice(-12);
|
|
299
|
+
messages.length = 0;
|
|
300
|
+
messages.push(systemMsg, ...recent);
|
|
301
|
+
}
|
|
296
302
|
}
|
|
297
303
|
}
|
package/src/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import { printTable, resolveWorkspaceRoot } from "./utils.js";
|
|
|
11
11
|
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.
|
|
14
|
+
const VERSION = "1.0.2";
|
|
15
15
|
|
|
16
16
|
async function handleAutoUpdate() {
|
|
17
17
|
// If we were just restarted by an update, don't check again
|
|
@@ -343,7 +343,7 @@ async function runChatSession({ workspaceRoot, model, useTui, session }) {
|
|
|
343
343
|
getModel: () => activeModel,
|
|
344
344
|
agentName: "NetheriteAI:Code",
|
|
345
345
|
providerName: "NetheriteAI",
|
|
346
|
-
version:
|
|
346
|
+
version: VERSION,
|
|
347
347
|
workspaceLabel: workspaceRoot,
|
|
348
348
|
sessionId: session.id,
|
|
349
349
|
initialSessionTitle: session.title,
|
package/src/ollama.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
|
|
4
4
|
const execFileAsync = promisify(execFile);
|
|
5
|
-
const BASE_URL = process.env.NETHERITE_BASE_URL || "http://176.88.249.119:11434";
|
|
5
|
+
const BASE_URL = process.env.OLLAMA_BASE_URL || process.env.NETHERITE_BASE_URL || "http://176.88.249.119:11434";
|
|
6
6
|
const PREFERRED_DEFAULT_MODELS = ["glm-5:cloud"];
|
|
7
7
|
|
|
8
8
|
async function request(pathname, body, signal, retries = 3) {
|
|
@@ -62,7 +62,7 @@ export async function pickDefaultModel() {
|
|
|
62
62
|
|
|
63
63
|
export async function chat({ model, messages, tools, signal }) {
|
|
64
64
|
const body = { model, messages, stream: false };
|
|
65
|
-
if (tools?.length && model !== "glm-5:cloud") body.tools = tools;
|
|
65
|
+
if (tools?.length && (model !== "glm-5:cloud" || tools.length <= 5)) body.tools = tools;
|
|
66
66
|
const response = await request("/api/chat", body, signal);
|
|
67
67
|
const json = await response.json();
|
|
68
68
|
let content = json.message?.content || "";
|
|
@@ -81,7 +81,7 @@ export async function chat({ model, messages, tools, signal }) {
|
|
|
81
81
|
|
|
82
82
|
export async function chatStream({ model, messages, tools, onChunk, onReasoningChunk, signal }) {
|
|
83
83
|
const body = { model, messages, stream: true };
|
|
84
|
-
if (tools?.length && model !== "glm-5:cloud") body.tools = tools;
|
|
84
|
+
if (tools?.length && (model !== "glm-5:cloud" || tools.length <= 10)) body.tools = tools;
|
|
85
85
|
|
|
86
86
|
const response = await request("/api/chat", body, signal);
|
|
87
87
|
if (!response.body) throw new Error("Server down");
|
package/src/tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
+
import crypto from "node:crypto";
|
|
4
5
|
import {
|
|
5
6
|
clampText,
|
|
6
7
|
ensureDir,
|
|
@@ -11,6 +12,8 @@ import {
|
|
|
11
12
|
toAbsoluteInsideRoot,
|
|
12
13
|
} from "./utils.js";
|
|
13
14
|
|
|
15
|
+
const activeProcesses = new Map();
|
|
16
|
+
|
|
14
17
|
function todoFile(root) {
|
|
15
18
|
return path.join(root, ".netherite", "todos.json");
|
|
16
19
|
}
|
|
@@ -105,6 +108,63 @@ export function getToolDefinitions() {
|
|
|
105
108
|
},
|
|
106
109
|
},
|
|
107
110
|
},
|
|
111
|
+
{
|
|
112
|
+
type: "function",
|
|
113
|
+
function: {
|
|
114
|
+
name: "make_dir",
|
|
115
|
+
description: "Create a new directory.",
|
|
116
|
+
parameters: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: { path: { type: "string" } },
|
|
119
|
+
required: ["path"],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: "function",
|
|
125
|
+
function: {
|
|
126
|
+
name: "remove_path",
|
|
127
|
+
description: "Delete a file or directory.",
|
|
128
|
+
parameters: {
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: {
|
|
131
|
+
path: { type: "string" },
|
|
132
|
+
recursive: { type: "boolean" },
|
|
133
|
+
},
|
|
134
|
+
required: ["path"],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: "function",
|
|
140
|
+
function: {
|
|
141
|
+
name: "rename_path",
|
|
142
|
+
description: "Rename or move a file or directory.",
|
|
143
|
+
parameters: {
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: {
|
|
146
|
+
oldPath: { type: "string" },
|
|
147
|
+
newPath: { type: "string" },
|
|
148
|
+
},
|
|
149
|
+
required: ["oldPath", "newPath"],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
type: "function",
|
|
155
|
+
function: {
|
|
156
|
+
name: "send_input",
|
|
157
|
+
description: "Send input to a background process.",
|
|
158
|
+
parameters: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {
|
|
161
|
+
process_id: { type: "string" },
|
|
162
|
+
input: { type: "string" },
|
|
163
|
+
},
|
|
164
|
+
required: ["process_id", "input"],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
108
168
|
];
|
|
109
169
|
}
|
|
110
170
|
|
|
@@ -136,42 +196,61 @@ async function streamFilePreview(text, hooks = {}, maxLines = 12) {
|
|
|
136
196
|
}
|
|
137
197
|
|
|
138
198
|
async function runSpawnedCommand(root, command, args = [], hooks = {}) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
199
|
+
const id = crypto.randomUUID();
|
|
200
|
+
const child = spawn(command, args, {
|
|
201
|
+
cwd: root,
|
|
202
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
203
|
+
});
|
|
144
204
|
|
|
145
|
-
|
|
146
|
-
|
|
205
|
+
const p = { child, stdout: "", stderr: "" };
|
|
206
|
+
activeProcesses.set(id, p);
|
|
147
207
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
208
|
+
child.stdout?.on("data", (chunk) => {
|
|
209
|
+
const text = chunk.toString("utf8");
|
|
210
|
+
p.stdout += text;
|
|
211
|
+
hooks.onProgress?.({ stream: "stdout", text });
|
|
212
|
+
});
|
|
153
213
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
214
|
+
child.stderr?.on("data", (chunk) => {
|
|
215
|
+
const text = chunk.toString("utf8");
|
|
216
|
+
p.stderr += text;
|
|
217
|
+
hooks.onProgress?.({ stream: "stderr", text });
|
|
218
|
+
});
|
|
159
219
|
|
|
160
|
-
|
|
220
|
+
const exitPromise = new Promise((resolve, reject) => {
|
|
221
|
+
child.on("error", (err) => {
|
|
222
|
+
activeProcesses.delete(id);
|
|
223
|
+
reject(err);
|
|
224
|
+
});
|
|
161
225
|
child.on("close", (code) => {
|
|
226
|
+
activeProcesses.delete(id);
|
|
162
227
|
if (code === 0) {
|
|
163
228
|
resolve({
|
|
164
229
|
ok: true,
|
|
165
230
|
command,
|
|
166
231
|
args,
|
|
167
|
-
stdout: clampText(stdout),
|
|
168
|
-
stderr: clampText(stderr),
|
|
232
|
+
stdout: clampText(p.stdout),
|
|
233
|
+
stderr: clampText(p.stderr),
|
|
169
234
|
});
|
|
170
235
|
return;
|
|
171
236
|
}
|
|
172
|
-
reject(new Error((stderr || stdout || `${command} exited with code ${code}`).trim()));
|
|
237
|
+
reject(new Error((p.stderr || p.stdout || `${command} exited with code ${code}`).trim()));
|
|
173
238
|
});
|
|
174
239
|
});
|
|
240
|
+
|
|
241
|
+
const timeoutPromise = new Promise((resolve) => setTimeout(() => {
|
|
242
|
+
if (activeProcesses.has(id)) {
|
|
243
|
+
resolve({
|
|
244
|
+
status: "running",
|
|
245
|
+
process_id: id,
|
|
246
|
+
message: "Process is still running and may be waiting for input. Use 'send_input' to interact with it.",
|
|
247
|
+
stdout: clampText(p.stdout),
|
|
248
|
+
stderr: clampText(p.stderr),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}, 2000));
|
|
252
|
+
|
|
253
|
+
return await Promise.race([exitPromise, timeoutPromise]);
|
|
175
254
|
}
|
|
176
255
|
|
|
177
256
|
async function runOneCommand(root, command, args = [], hooks = {}) {
|
|
@@ -275,6 +354,40 @@ export async function executeToolCall(root, toolCall, hooks = {}) {
|
|
|
275
354
|
fs.rmSync(target, { recursive: Boolean(args.recursive), force: false });
|
|
276
355
|
return { ok: true, path: args.path };
|
|
277
356
|
}
|
|
357
|
+
case "rename_path": {
|
|
358
|
+
const oldTarget = toAbsoluteInsideRoot(root, args.oldPath);
|
|
359
|
+
const newTarget = toAbsoluteInsideRoot(root, args.newPath);
|
|
360
|
+
if (!fs.existsSync(oldTarget)) throw new Error(`Source path not found: ${args.oldPath}`);
|
|
361
|
+
ensureDir(path.dirname(newTarget));
|
|
362
|
+
fs.renameSync(oldTarget, newTarget);
|
|
363
|
+
return { ok: true, oldPath: args.oldPath, newPath: args.newPath };
|
|
364
|
+
}
|
|
365
|
+
case "send_input": {
|
|
366
|
+
const id = args.process_id;
|
|
367
|
+
const p = activeProcesses.get(id);
|
|
368
|
+
if (!p) throw new Error(`Process ${id} not found`);
|
|
369
|
+
p.stdout = "";
|
|
370
|
+
p.stderr = "";
|
|
371
|
+
if (args.input) {
|
|
372
|
+
p.child.stdin.write(args.input);
|
|
373
|
+
hooks.onProgress?.({ stream: "stdout", text: args.input });
|
|
374
|
+
}
|
|
375
|
+
const exitPromise = new Promise((resolve, reject) => {
|
|
376
|
+
const onExit = (code) => {
|
|
377
|
+
activeProcesses.delete(id);
|
|
378
|
+
if (code === 0) resolve({ ok: true, code, stdout: clampText(p.stdout), stderr: clampText(p.stderr) });
|
|
379
|
+
else reject(new Error((p.stderr || p.stdout || `exited with code ${code}`).trim()));
|
|
380
|
+
};
|
|
381
|
+
p.child.once("close", onExit);
|
|
382
|
+
p.child.once("error", reject);
|
|
383
|
+
});
|
|
384
|
+
const timeoutPromise = new Promise((resolve) => setTimeout(() => {
|
|
385
|
+
if (activeProcesses.has(id)) {
|
|
386
|
+
resolve({ status: "running", process_id: id, stdout: clampText(p.stdout), stderr: clampText(p.stderr) });
|
|
387
|
+
}
|
|
388
|
+
}, 2000));
|
|
389
|
+
return await Promise.race([exitPromise, timeoutPromise]);
|
|
390
|
+
}
|
|
278
391
|
case "run_command": {
|
|
279
392
|
if (args.commandLine) {
|
|
280
393
|
return await runShellLine(root, args.commandLine, hooks);
|