netheriteai-code 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/state.js ADDED
@@ -0,0 +1,100 @@
1
+ import path from "node:path";
2
+ import { getStateFile, loadJson, saveJson } from "./utils.js";
3
+
4
+ const DEFAULT_STATE = {
5
+ selectedModel: "",
6
+ recentModels: [],
7
+ sessions: {},
8
+ workspaceSessions: {},
9
+ };
10
+
11
+ function nowIso() {
12
+ return new Date().toISOString();
13
+ }
14
+
15
+ function makeSessionId() {
16
+ return `s_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
17
+ }
18
+
19
+ export function loadState() {
20
+ const state = loadJson(getStateFile(), DEFAULT_STATE) || {};
21
+ return {
22
+ ...DEFAULT_STATE,
23
+ ...state,
24
+ recentModels: Array.isArray(state.recentModels) ? state.recentModels : [],
25
+ sessions: state.sessions && typeof state.sessions === "object" ? state.sessions : {},
26
+ workspaceSessions: state.workspaceSessions && typeof state.workspaceSessions === "object" ? state.workspaceSessions : {},
27
+ };
28
+ }
29
+
30
+ export function saveState(state) {
31
+ saveJson(getStateFile(), state);
32
+ }
33
+
34
+ export function getSessionKey(workspaceRoot) {
35
+ return path.resolve(workspaceRoot);
36
+ }
37
+
38
+ function normalizeSession(session, workspaceRoot, id) {
39
+ return {
40
+ id: session.id || id || makeSessionId(),
41
+ workspaceRoot: path.resolve(session.workspaceRoot || workspaceRoot || process.cwd()),
42
+ title: session.title || "",
43
+ createdAt: session.createdAt || nowIso(),
44
+ updatedAt: nowIso(),
45
+ messages: Array.isArray(session.messages) ? session.messages : [],
46
+ transcript: Array.isArray(session.transcript) ? session.transcript : [],
47
+ };
48
+ }
49
+
50
+ export function createSession(workspaceRoot) {
51
+ const session = normalizeSession({}, workspaceRoot);
52
+ saveSession(session);
53
+ return session;
54
+ }
55
+
56
+ export function loadSession(workspaceRoot) {
57
+ const state = loadState();
58
+ const key = getSessionKey(workspaceRoot);
59
+ const sessionId = state.workspaceSessions[key];
60
+ if (sessionId && state.sessions[sessionId]) {
61
+ return normalizeSession(state.sessions[sessionId], workspaceRoot, sessionId);
62
+ }
63
+ const legacy = state.sessions[key];
64
+ if (legacy && !legacy.id) {
65
+ const migrated = normalizeSession(legacy, workspaceRoot);
66
+ saveSession(migrated);
67
+ delete state.sessions[key];
68
+ saveState(state);
69
+ return migrated;
70
+ }
71
+ return createSession(workspaceRoot);
72
+ }
73
+
74
+ export function loadSessionById(sessionId) {
75
+ const state = loadState();
76
+ const session = state.sessions[sessionId];
77
+ if (!session) return null;
78
+ return normalizeSession(session, session.workspaceRoot, sessionId);
79
+ }
80
+
81
+ export function saveSession(session) {
82
+ const state = loadState();
83
+ const normalized = normalizeSession(session, session.workspaceRoot, session.id);
84
+ state.sessions[normalized.id] = normalized;
85
+ state.workspaceSessions[getSessionKey(normalized.workspaceRoot)] = normalized.id;
86
+ saveState(state);
87
+ return normalized;
88
+ }
89
+
90
+ export function getSelectedModel() {
91
+ const state = loadState();
92
+ return state.selectedModel || "";
93
+ }
94
+
95
+ export function setSelectedModel(model) {
96
+ const state = loadState();
97
+ state.selectedModel = model;
98
+ state.recentModels = [model, ...state.recentModels.filter((item) => item !== model)].slice(0, 10);
99
+ saveState(state);
100
+ }
package/src/tools.js ADDED
@@ -0,0 +1,455 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import {
5
+ clampText,
6
+ ensureDir,
7
+ formatBytes,
8
+ loadJson,
9
+ relativeToRoot,
10
+ saveJson,
11
+ toAbsoluteInsideRoot,
12
+ } from "./utils.js";
13
+
14
+ function todoFile(root) {
15
+ return path.join(root, ".netherite", "todos.json");
16
+ }
17
+
18
+ function loadTodos(root) {
19
+ return loadJson(todoFile(root), []);
20
+ }
21
+
22
+ function saveTodos(root, todos) {
23
+ saveJson(todoFile(root), todos);
24
+ }
25
+
26
+ function statEntry(entryPath) {
27
+ const stats = fs.statSync(entryPath);
28
+ return {
29
+ type: stats.isDirectory() ? "dir" : "file",
30
+ size: stats.isDirectory() ? "-" : formatBytes(stats.size),
31
+ modifiedAt: stats.mtime.toISOString(),
32
+ };
33
+ }
34
+
35
+ export function getToolDefinitions() {
36
+ return [
37
+ {
38
+ type: "function",
39
+ function: {
40
+ name: "list_files",
41
+ description: "List files or folders inside the workspace.",
42
+ parameters: {
43
+ type: "object",
44
+ properties: {
45
+ path: { type: "string", description: "Relative path inside the workspace." },
46
+ recursive: { type: "boolean", description: "Whether to recurse into child directories." },
47
+ },
48
+ },
49
+ },
50
+ },
51
+ {
52
+ type: "function",
53
+ function: {
54
+ name: "read_file",
55
+ description: "Read a text file inside the workspace.",
56
+ parameters: {
57
+ type: "object",
58
+ properties: {
59
+ path: { type: "string", description: "Relative file path." },
60
+ },
61
+ required: ["path"],
62
+ },
63
+ },
64
+ },
65
+ {
66
+ type: "function",
67
+ function: {
68
+ name: "create_file",
69
+ description: "Create a new text file inside the workspace. Fails if the file already exists.",
70
+ parameters: {
71
+ type: "object",
72
+ properties: {
73
+ path: { type: "string", description: "Relative file path." },
74
+ content: { type: "string", description: "Initial file content." },
75
+ },
76
+ required: ["path", "content"],
77
+ },
78
+ },
79
+ },
80
+ {
81
+ type: "function",
82
+ function: {
83
+ name: "write_file",
84
+ description: "Create or overwrite a text file inside the workspace.",
85
+ parameters: {
86
+ type: "object",
87
+ properties: {
88
+ path: { type: "string", description: "Relative file path." },
89
+ content: { type: "string", description: "New file content." },
90
+ },
91
+ required: ["path", "content"],
92
+ },
93
+ },
94
+ },
95
+ {
96
+ type: "function",
97
+ function: {
98
+ name: "append_file",
99
+ description: "Append text to the end of a file inside the workspace, creating it if needed.",
100
+ parameters: {
101
+ type: "object",
102
+ properties: {
103
+ path: { type: "string", description: "Relative file path." },
104
+ content: { type: "string", description: "Text to append." },
105
+ },
106
+ required: ["path", "content"],
107
+ },
108
+ },
109
+ },
110
+ {
111
+ type: "function",
112
+ function: {
113
+ name: "edit_file",
114
+ description: "Edit a text file by replacing an exact string match. Safer than overwriting the whole file.",
115
+ parameters: {
116
+ type: "object",
117
+ properties: {
118
+ path: { type: "string", description: "Relative file path." },
119
+ oldText: { type: "string", description: "Exact text to replace." },
120
+ newText: { type: "string", description: "Replacement text." },
121
+ replaceAll: { type: "boolean", description: "Replace all occurrences instead of just the first." },
122
+ },
123
+ required: ["path", "oldText", "newText"],
124
+ },
125
+ },
126
+ },
127
+ {
128
+ type: "function",
129
+ function: {
130
+ name: "make_dir",
131
+ description: "Create a directory inside the workspace.",
132
+ parameters: {
133
+ type: "object",
134
+ properties: {
135
+ path: { type: "string", description: "Relative directory path." },
136
+ },
137
+ required: ["path"],
138
+ },
139
+ },
140
+ },
141
+ {
142
+ type: "function",
143
+ function: {
144
+ name: "remove_path",
145
+ description: "Remove a file or directory inside the workspace.",
146
+ parameters: {
147
+ type: "object",
148
+ properties: {
149
+ path: { type: "string", description: "Relative path to delete." },
150
+ recursive: { type: "boolean", description: "Allow recursive directory deletion." },
151
+ },
152
+ required: ["path"],
153
+ },
154
+ },
155
+ },
156
+ {
157
+ type: "function",
158
+ function: {
159
+ name: "run_command",
160
+ description: "Run a shell command inside the workspace and capture stdout/stderr.",
161
+ parameters: {
162
+ type: "object",
163
+ properties: {
164
+ command: { type: "string", description: "Executable name, for example `npm` or `git`." },
165
+ args: { type: "array", items: { type: "string" }, description: "Argument array." },
166
+ commandLine: {
167
+ type: "string",
168
+ description: "Optional raw shell command line. Use it for pipes, redirects, globs, and shell syntax.",
169
+ },
170
+ },
171
+ },
172
+ },
173
+ },
174
+ {
175
+ type: "function",
176
+ function: {
177
+ name: "batch_command",
178
+ description: "Run multiple shell commands sequentially in the workspace.",
179
+ parameters: {
180
+ type: "object",
181
+ properties: {
182
+ commands: {
183
+ type: "array",
184
+ items: {
185
+ type: "object",
186
+ properties: {
187
+ command: { type: "string" },
188
+ args: { type: "array", items: { type: "string" } },
189
+ commandLine: { type: "string" },
190
+ },
191
+ },
192
+ },
193
+ },
194
+ required: ["commands"],
195
+ },
196
+ },
197
+ },
198
+ {
199
+ type: "function",
200
+ function: {
201
+ name: "todo_create",
202
+ description: "Create a todo item for the current workspace.",
203
+ parameters: {
204
+ type: "object",
205
+ properties: {
206
+ text: { type: "string" },
207
+ },
208
+ required: ["text"],
209
+ },
210
+ },
211
+ },
212
+ {
213
+ type: "function",
214
+ function: {
215
+ name: "todo_list",
216
+ description: "List todo items for the current workspace.",
217
+ parameters: {
218
+ type: "object",
219
+ properties: {},
220
+ },
221
+ },
222
+ },
223
+ {
224
+ type: "function",
225
+ function: {
226
+ name: "todo_complete",
227
+ description: "Mark a todo item complete by numeric id.",
228
+ parameters: {
229
+ type: "object",
230
+ properties: {
231
+ id: { type: "number" },
232
+ },
233
+ required: ["id"],
234
+ },
235
+ },
236
+ },
237
+ ];
238
+ }
239
+
240
+ function listRecursive(root, basePath) {
241
+ const entries = [];
242
+ for (const entry of fs.readdirSync(basePath)) {
243
+ const absolute = path.join(basePath, entry);
244
+ const rel = relativeToRoot(root, absolute);
245
+ entries.push({
246
+ path: rel,
247
+ ...statEntry(absolute),
248
+ });
249
+ if (fs.statSync(absolute).isDirectory()) {
250
+ entries.push(...listRecursive(root, absolute));
251
+ }
252
+ }
253
+ return entries;
254
+ }
255
+
256
+ async function streamFilePreview(text, hooks = {}, maxLines = 12) {
257
+ const lines = String(text || "").replace(/\r/g, "").split("\n").slice(0, maxLines);
258
+ for (let index = 0; index < lines.length; index += 1) {
259
+ hooks.onProgress?.({ stream: "preview", text: `${lines[index]}\n` });
260
+ await new Promise((resolve) => setTimeout(resolve, 0));
261
+ }
262
+ if (String(text || "").replace(/\r/g, "").split("\n").length > maxLines) {
263
+ hooks.onProgress?.({ stream: "preview", text: `... preview truncated\n` });
264
+ }
265
+ }
266
+
267
+ async function runSpawnedCommand(root, command, args = [], hooks = {}) {
268
+ return await new Promise((resolve, reject) => {
269
+ const child = spawn(command, args, {
270
+ cwd: root,
271
+ stdio: ["ignore", "pipe", "pipe"],
272
+ });
273
+
274
+ let stdout = "";
275
+ let stderr = "";
276
+
277
+ child.stdout?.on("data", (chunk) => {
278
+ const text = chunk.toString("utf8");
279
+ stdout += text;
280
+ hooks.onProgress?.({ stream: "stdout", text });
281
+ });
282
+
283
+ child.stderr?.on("data", (chunk) => {
284
+ const text = chunk.toString("utf8");
285
+ stderr += text;
286
+ hooks.onProgress?.({ stream: "stderr", text });
287
+ });
288
+
289
+ child.on("error", reject);
290
+ child.on("close", (code) => {
291
+ if (code === 0) {
292
+ resolve({
293
+ ok: true,
294
+ command,
295
+ args,
296
+ stdout: clampText(stdout),
297
+ stderr: clampText(stderr),
298
+ });
299
+ return;
300
+ }
301
+ reject(new Error((stderr || stdout || `${command} exited with code ${code}`).trim()));
302
+ });
303
+ });
304
+ }
305
+
306
+ async function runOneCommand(root, command, args = [], hooks = {}) {
307
+ return await runSpawnedCommand(root, command, args, hooks);
308
+ }
309
+
310
+ async function runShellLine(root, commandLine, hooks = {}) {
311
+ const result = await runSpawnedCommand(root, "bash", ["-lc", commandLine], hooks);
312
+ return {
313
+ ok: true,
314
+ commandLine,
315
+ stdout: result.stdout,
316
+ stderr: result.stderr,
317
+ };
318
+ }
319
+
320
+ export async function executeToolCall(root, toolCall, hooks = {}) {
321
+ const name = toolCall.function?.name || toolCall.name;
322
+ const rawArgs = toolCall.function?.arguments ?? toolCall.arguments ?? "{}";
323
+ const args = typeof rawArgs === "string" ? JSON.parse(rawArgs || "{}") : rawArgs;
324
+
325
+ switch (name) {
326
+ case "list_files": {
327
+ const target = toAbsoluteInsideRoot(root, args.path || ".");
328
+ const entries = args.recursive
329
+ ? listRecursive(root, target)
330
+ : fs.readdirSync(target).map((entry) => {
331
+ const absolute = path.join(target, entry);
332
+ return {
333
+ path: relativeToRoot(root, absolute),
334
+ ...statEntry(absolute),
335
+ };
336
+ });
337
+ return { entries };
338
+ }
339
+ case "read_file": {
340
+ const target = toAbsoluteInsideRoot(root, args.path);
341
+ const content = fs.readFileSync(target, "utf8");
342
+ return { path: args.path, content: clampText(content, 24000) };
343
+ }
344
+ case "create_file": {
345
+ const target = toAbsoluteInsideRoot(root, args.path);
346
+ if (fs.existsSync(target)) {
347
+ throw new Error(`File already exists: ${args.path}`);
348
+ }
349
+ ensureDir(path.dirname(target));
350
+ hooks.onProgress?.({ stream: "preview", text: `Creating ${args.path}\n` });
351
+ await streamFilePreview(args.content, hooks);
352
+ fs.writeFileSync(target, args.content, "utf8");
353
+ return { ok: true, path: args.path, bytesWritten: Buffer.byteLength(args.content, "utf8"), created: true };
354
+ }
355
+ case "write_file": {
356
+ const target = toAbsoluteInsideRoot(root, args.path);
357
+ ensureDir(path.dirname(target));
358
+ hooks.onProgress?.({ stream: "preview", text: `Writing ${args.path}\n` });
359
+ await streamFilePreview(args.content, hooks);
360
+ fs.writeFileSync(target, args.content, "utf8");
361
+ return { ok: true, path: args.path, bytesWritten: Buffer.byteLength(args.content, "utf8") };
362
+ }
363
+ case "append_file": {
364
+ const target = toAbsoluteInsideRoot(root, args.path);
365
+ ensureDir(path.dirname(target));
366
+ hooks.onProgress?.({ stream: "preview", text: `Appending ${args.path}\n` });
367
+ await streamFilePreview(args.content, hooks);
368
+ fs.appendFileSync(target, args.content, "utf8");
369
+ return { ok: true, path: args.path, bytesWritten: Buffer.byteLength(args.content, "utf8"), appended: true };
370
+ }
371
+ case "edit_file": {
372
+ const target = toAbsoluteInsideRoot(root, args.path);
373
+ const original = fs.readFileSync(target, "utf8");
374
+ if (!args.oldText.length) {
375
+ throw new Error("oldText must not be empty");
376
+ }
377
+ if (!original.includes(args.oldText)) {
378
+ throw new Error(`Target text not found in ${args.path}`);
379
+ }
380
+ const updated = args.replaceAll
381
+ ? original.split(args.oldText).join(args.newText)
382
+ : original.replace(args.oldText, args.newText);
383
+ hooks.onProgress?.({ stream: "preview", text: `Editing ${args.path}\n` });
384
+ await streamFilePreview(args.newText, hooks);
385
+ fs.writeFileSync(target, updated, "utf8");
386
+ return {
387
+ ok: true,
388
+ path: args.path,
389
+ replacements: args.replaceAll ? original.split(args.oldText).length - 1 : 1,
390
+ bytesWritten: Buffer.byteLength(updated, "utf8"),
391
+ };
392
+ }
393
+ case "make_dir": {
394
+ const target = toAbsoluteInsideRoot(root, args.path);
395
+ ensureDir(target);
396
+ return { ok: true, path: args.path, created: true };
397
+ }
398
+ case "remove_path": {
399
+ const target = toAbsoluteInsideRoot(root, args.path);
400
+ const stats = fs.statSync(target);
401
+ if (stats.isDirectory() && !args.recursive) {
402
+ throw new Error("Directory deletion requires recursive=true");
403
+ }
404
+ fs.rmSync(target, { recursive: Boolean(args.recursive), force: false });
405
+ return { ok: true, path: args.path };
406
+ }
407
+ case "run_command": {
408
+ if (args.commandLine) {
409
+ return await runShellLine(root, args.commandLine, hooks);
410
+ }
411
+ return await runOneCommand(root, args.command, args.args || [], hooks);
412
+ }
413
+ case "batch_command": {
414
+ const results = [];
415
+ for (const command of args.commands || []) {
416
+ if (command.commandLine) {
417
+ hooks.onProgress?.({ stream: "stdout", text: `$ ${command.commandLine}\n` });
418
+ results.push(await runShellLine(root, command.commandLine, hooks));
419
+ } else {
420
+ hooks.onProgress?.({ stream: "stdout", text: `$ ${[command.command, ...(command.args || [])].filter(Boolean).join(" ")}\n` });
421
+ results.push(await runOneCommand(root, command.command, command.args || [], hooks));
422
+ }
423
+ }
424
+ return { results };
425
+ }
426
+ case "todo_create": {
427
+ const todos = loadTodos(root);
428
+ const next = {
429
+ id: todos.length ? Math.max(...todos.map((item) => item.id)) + 1 : 1,
430
+ text: args.text,
431
+ done: false,
432
+ createdAt: new Date().toISOString(),
433
+ };
434
+ todos.push(next);
435
+ saveTodos(root, todos);
436
+ return next;
437
+ }
438
+ case "todo_list": {
439
+ return { todos: loadTodos(root) };
440
+ }
441
+ case "todo_complete": {
442
+ const todos = loadTodos(root);
443
+ const todo = todos.find((item) => item.id === args.id);
444
+ if (!todo) {
445
+ throw new Error(`Todo ${args.id} not found`);
446
+ }
447
+ todo.done = true;
448
+ todo.completedAt = new Date().toISOString();
449
+ saveTodos(root, todos);
450
+ return todo;
451
+ }
452
+ default:
453
+ throw new Error(`Unknown tool: ${name}`);
454
+ }
455
+ }