@calamity-inc/slave 0.4.0 → 0.4.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/README.md +19 -24
- package/package.json +2 -1
- package/src/core.js +214 -74
- package/src/main.js +95 -8
- package/src/skills.js +63 -0
- package/src/state.js +4 -3
package/README.md
CHANGED
|
@@ -22,42 +22,37 @@ cat yap.txt | slave Summarize this text
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Options:
|
|
25
|
-
-
|
|
26
|
-
- `-
|
|
27
|
-
- `-
|
|
28
|
-
- `-
|
|
29
|
-
- `-
|
|
30
|
-
- `-
|
|
25
|
+
- `--long`/`--multiline` — write a multiline prompt (mutually exclusive with piping text in)
|
|
26
|
+
- `-c`/`--cont`/`--continue` — add your previous invocation to the context of this one
|
|
27
|
+
- `-a`/`--amend` — retry the previous invocation, using the context from before it
|
|
28
|
+
- `-w`/`--web` — allow the model to search the web
|
|
29
|
+
- `-n`/`--no-fs`/`--nofs` — disable filesystem access
|
|
30
|
+
- `-r`/`--readonly` — disable filesystem write access
|
|
31
|
+
- `-s`/`--sandbox` — allow the model to use a docker container
|
|
32
|
+
- `-p`/`--profile` `<name>` — use the specified profile
|
|
31
33
|
|
|
32
34
|
Single-character flags can be combined, so `slave -cw prompt` is equivalent to `slave -c -w prompt`.
|
|
33
35
|
|
|
34
36
|
Subcommands:
|
|
35
|
-
- `--create-profile
|
|
37
|
+
- `--create-profile` `<name>` `<provider>` `<model>` `[--set-default]` — create a profile
|
|
36
38
|
- `provider` can be `openai`, `anthropic`, or an OpenAI-compatible base URL like `http://127.0.0.1:1234/v1` for [LM Studio](https://lmstudio.ai/developers).
|
|
37
|
-
- `--update-profile
|
|
38
|
-
- `--set-default
|
|
39
|
+
- `--update-profile` `<name>` `<provider>` `<model>` `[--set-default]` — update a profile
|
|
40
|
+
- `--set-default` `<profile name>` — set default profile
|
|
39
41
|
- `--list-profiles` — list all profiles
|
|
40
|
-
- `--delete-profile
|
|
41
|
-
- `--update-api-key
|
|
42
|
+
- `--delete-profile` `<name>` — delete a profile
|
|
43
|
+
- `--update-api-key` `<provider>` — update an API Key
|
|
44
|
+
- `--set-system`/`--set-system-prompt` `[prompt]` `[--long|--multiline]` — set or clear the system prompt
|
|
45
|
+
- `--get-system`/`--get-system-prompt` — get the system prompt
|
|
46
|
+
- `--skills` — get info about skills
|
|
47
|
+
- `--logs` — get info about logs
|
|
42
48
|
|
|
43
49
|
## Security
|
|
44
50
|
|
|
45
51
|
The model can't run commands on your system; it only gets limited access to your filesystem:
|
|
46
|
-
-
|
|
47
|
-
- `.git` folder
|
|
52
|
+
- Restricted to the working directory and its subdirectories.
|
|
53
|
+
- Can't read the `.git` folder, `.env` files, or any files excluded by `.slaveignore`. Although the model will still be able to know of their existence.
|
|
48
54
|
- Files that are `.gitignore`d can't be written to.
|
|
49
|
-
- You can provide a `.slaveignore` for files that the model should not be allowed to read or write. It will still be able to know of their existence.
|
|
50
55
|
|
|
51
56
|
However, these restrictions are only as good as your practices; you should:
|
|
52
57
|
- Only run `slave` in Git-tracked folders and be sure that anything you don't want to lose is committed or at least staged so you can always revert any changes made by the model.
|
|
53
58
|
- Review changes to your code before running anything, including tests.
|
|
54
|
-
|
|
55
|
-
## Logs
|
|
56
|
-
|
|
57
|
-
Each invocation that prompts a model creates a log file:
|
|
58
|
-
|
|
59
|
-
- Linux: `~/.local/state/slave-nodejs`, or `$XDG_STATE_HOME/slave-nodejs` if `XDG_STATE_HOME` is set
|
|
60
|
-
- macOS: `~/Library/Logs/slave-nodejs`
|
|
61
|
-
- Windows: `%LOCALAPPDATA%\slave-nodejs\Log`
|
|
62
|
-
|
|
63
|
-
When a new log file is created, any log file older than 30 days is deleted.
|
package/package.json
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
"type": "git",
|
|
5
5
|
"url": "git+https://github.com/calamity-inc/slave.git"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.4.
|
|
7
|
+
"version": "0.4.2",
|
|
8
8
|
"type": "commonjs",
|
|
9
9
|
"bin": {
|
|
10
10
|
"slave": "src/main.js"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@anthropic-ai/sdk": "^1.3.0",
|
|
14
|
+
"chalk": "^5",
|
|
14
15
|
"commander": "^15.0.0",
|
|
15
16
|
"env-paths": "^4.0.0",
|
|
16
17
|
"ignore": "^7.0.13",
|
package/src/core.js
CHANGED
|
@@ -5,9 +5,12 @@ const { randomInt } = require("node:crypto");
|
|
|
5
5
|
const fs = require("node:fs");
|
|
6
6
|
const path = require("node:path");
|
|
7
7
|
|
|
8
|
+
const chalk = require("chalk").default;
|
|
8
9
|
const ignore = require("ignore");
|
|
9
10
|
|
|
10
11
|
const { logWrite } = require("./log.js");
|
|
12
|
+
const { skills } = require("./skills.js");
|
|
13
|
+
const { state } = require("./state.js");
|
|
11
14
|
|
|
12
15
|
const slaveignore = ignore();
|
|
13
16
|
if (fs.existsSync(".slaveignore")) {
|
|
@@ -23,7 +26,7 @@ const isOutOfReach = (p, accessType) => {
|
|
|
23
26
|
return true;
|
|
24
27
|
}
|
|
25
28
|
const segs = rel.split(path.sep);
|
|
26
|
-
if (segs.includes(".
|
|
29
|
+
if (segs.includes(".git") || segs.some(seg => seg.startsWith(".env"))) {
|
|
27
30
|
return true;
|
|
28
31
|
}
|
|
29
32
|
if (rel && (slaveignore.ignores(rel) || (accessType == "write" && gitignore.ignores(rel)))) {
|
|
@@ -32,6 +35,11 @@ const isOutOfReach = (p, accessType) => {
|
|
|
32
35
|
return false;
|
|
33
36
|
};
|
|
34
37
|
|
|
38
|
+
const isPathWithinFolder = (folder, p) => {
|
|
39
|
+
const rel = path.relative(folder, p);
|
|
40
|
+
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
41
|
+
};
|
|
42
|
+
|
|
35
43
|
const fixPath = (path, accessType) => {
|
|
36
44
|
if (path.startsWith("/")) {
|
|
37
45
|
path = path.substring(1);
|
|
@@ -54,7 +62,32 @@ const writeFileFromModel = (filePath, data, usesCRLF) => {
|
|
|
54
62
|
fs.writeFileSync(filePath, usesCRLF ? normalized.replaceAll("\n", "\r\n") : normalized, "utf8");
|
|
55
63
|
};
|
|
56
64
|
|
|
57
|
-
|
|
65
|
+
const getSystemPrompt = () => {
|
|
66
|
+
const parts = [];
|
|
67
|
+
if (state.systemPrompt) {
|
|
68
|
+
parts.push(state.systemPrompt);
|
|
69
|
+
}
|
|
70
|
+
const skillEntries = Object.entries(skills);
|
|
71
|
+
if (skillEntries.length) {
|
|
72
|
+
parts.push("You have the following skills available:\n" + skillEntries.map(([name, skill]) => `- ${name}: ${skill.description}`).join("\n"));
|
|
73
|
+
}
|
|
74
|
+
return parts.length ? parts.join("\n\n") : undefined;
|
|
75
|
+
};
|
|
76
|
+
const getSkillResourcePath = (name, resourcePath) => {
|
|
77
|
+
if (Object.hasOwn(skills, name)) {
|
|
78
|
+
const absResourcePath = path.join(skills[name].path, resourcePath);
|
|
79
|
+
if (isPathWithinFolder(skills[name].path, absResourcePath)) {
|
|
80
|
+
if (fs.existsSync(absResourcePath)) {
|
|
81
|
+
return [true, absResourcePath];
|
|
82
|
+
}
|
|
83
|
+
return [false, "No such resource"];
|
|
84
|
+
}
|
|
85
|
+
return [false, "I'm gonna call the alignment police on you!!!"];
|
|
86
|
+
}
|
|
87
|
+
return [false, "No such skill"];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
let sandbox_name, sandbox_running;
|
|
58
91
|
const getSandboxName = () => {
|
|
59
92
|
return sandbox_name;
|
|
60
93
|
};
|
|
@@ -67,9 +100,23 @@ const ensureSandbox = () => {
|
|
|
67
100
|
// `docker pull ghcr.io/calamity-inc/slave-sandbox` is not needed to get the image, but would be needed to update it.
|
|
68
101
|
execFileSync("docker", ["run", "-d", "--name", sandbox_name, "ghcr.io/calamity-inc/slave-sandbox", "sleep", "infinity"], { encoding: "utf8" });
|
|
69
102
|
}
|
|
103
|
+
else if (!sandbox_running) {
|
|
104
|
+
execFileSync("docker", ["start", sandbox_name], { encoding: "utf8" });
|
|
105
|
+
}
|
|
106
|
+
sandbox_running = true;
|
|
107
|
+
};
|
|
108
|
+
const copyFileToSandbox = (hostPath, sbxPath) => {
|
|
109
|
+
ensureSandbox();
|
|
110
|
+
try {
|
|
111
|
+
execFileSync("docker", ["cp", "-L", hostPath, `${sandbox_name}:${sbxPath}`], { encoding: "utf8" });
|
|
112
|
+
return "OK";
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
return (e.stdout ?? e.message).trimEnd();
|
|
116
|
+
}
|
|
70
117
|
};
|
|
71
118
|
|
|
72
|
-
const functions = {
|
|
119
|
+
const functions = Object.assign(Object.create(null), {
|
|
73
120
|
listFiles: ({ path }) => {
|
|
74
121
|
path ??= ".";
|
|
75
122
|
path = fixPath(path);
|
|
@@ -159,10 +206,6 @@ const functions = {
|
|
|
159
206
|
},
|
|
160
207
|
sandboxExec: ({ command, args }) => {
|
|
161
208
|
ensureSandbox();
|
|
162
|
-
args ??= [];
|
|
163
|
-
if (typeof args == "string") {
|
|
164
|
-
args = JSON.parse(args);
|
|
165
|
-
}
|
|
166
209
|
const result = spawnSync("docker", [ "exec", sandbox_name, command, ...args ], { encoding: "utf8" });
|
|
167
210
|
const output = (result.stdout ?? "") + (result.stderr ?? "");
|
|
168
211
|
if (result.error) {
|
|
@@ -181,32 +224,49 @@ const functions = {
|
|
|
181
224
|
if (!fs.existsSync(hostPath) || !fs.statSync(hostPath).isFile()) {
|
|
182
225
|
return "Host path must point to a file";
|
|
183
226
|
}
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
execFileSync("docker", ["cp", "-L", hostPath, `${sandbox_name}:${sbxPath}`], { encoding: "utf8" });
|
|
187
|
-
return "OK";
|
|
188
|
-
}
|
|
189
|
-
catch (e) {
|
|
190
|
-
return (e.stdout ?? e.message).trimEnd();
|
|
191
|
-
}
|
|
227
|
+
return copyFileToSandbox(hostPath, sbxPath);
|
|
192
228
|
},
|
|
193
229
|
sandboxCopyOut: ({ sbxPath, hostPath }) => {
|
|
194
230
|
hostPath = fixPath(hostPath, "write");
|
|
195
231
|
if (!hostPath) {
|
|
196
232
|
return "You don't have write access to this file";
|
|
197
233
|
}
|
|
198
|
-
|
|
234
|
+
const isCreate = !fs.existsSync(hostPath);
|
|
235
|
+
if (!isCreate && !fs.statSync(hostPath).isFile()) {
|
|
199
236
|
return "Host path must point to a file";
|
|
200
237
|
}
|
|
201
238
|
ensureSandbox();
|
|
239
|
+
if (isCreate) {
|
|
240
|
+
fs.mkdirSync(path.dirname(hostPath), { recursive: true });
|
|
241
|
+
}
|
|
202
242
|
execFileSync("docker", ["cp", "-L", `${sandbox_name}:${sbxPath}`, hostPath], { encoding: "utf8" });
|
|
203
243
|
return "OK";
|
|
204
244
|
},
|
|
245
|
+
readSkill: ({ name }) => {
|
|
246
|
+
if (Object.hasOwn(skills, name)) {
|
|
247
|
+
return readFileForModel(path.join(skills[name].path, "SKILL.md"));
|
|
248
|
+
}
|
|
249
|
+
return "No such skill";
|
|
250
|
+
},
|
|
251
|
+
readSkillResource: ({ name, path }) => {
|
|
252
|
+
const [ok, res] = getSkillResourcePath(name, path)
|
|
253
|
+
if (ok) {
|
|
254
|
+
return readFileForModel(res);
|
|
255
|
+
}
|
|
256
|
+
return res;
|
|
257
|
+
},
|
|
258
|
+
copySkillResourceToSandbox: ({ name, path, sbxPath }) => {
|
|
259
|
+
const [ok, res] = getSkillResourcePath(name, path)
|
|
260
|
+
if (ok) {
|
|
261
|
+
return copyFileToSandbox(res, sbxPath);
|
|
262
|
+
}
|
|
263
|
+
return res;
|
|
264
|
+
},
|
|
205
265
|
/*system: ({ command }) => {
|
|
206
266
|
//process.exit(1);
|
|
207
267
|
return "It's interesting that you called this tool! Unfortunately for you, it does nothing.";
|
|
208
268
|
}*/
|
|
209
|
-
};
|
|
269
|
+
});
|
|
210
270
|
|
|
211
271
|
const tools = [];
|
|
212
272
|
const initTools = (fs, sandbox, web_search) => {
|
|
@@ -228,6 +288,7 @@ const initTools = (fs, sandbox, web_search) => {
|
|
|
228
288
|
{
|
|
229
289
|
type: "function",
|
|
230
290
|
name: "findFiles",
|
|
291
|
+
description: "Searches the contents of files for a literal text string",
|
|
231
292
|
parameters: {
|
|
232
293
|
type: "object",
|
|
233
294
|
properties: {
|
|
@@ -253,37 +314,41 @@ const initTools = (fs, sandbox, web_search) => {
|
|
|
253
314
|
},
|
|
254
315
|
strict: true,
|
|
255
316
|
},
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
type: "
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
317
|
+
);
|
|
318
|
+
if (fs != "readonly") {
|
|
319
|
+
tools.push(
|
|
320
|
+
{
|
|
321
|
+
type: "function",
|
|
322
|
+
name: "writeFile",
|
|
323
|
+
parameters: {
|
|
324
|
+
type: "object",
|
|
325
|
+
properties: {
|
|
326
|
+
path: { type: "string" },
|
|
327
|
+
data: { type: "string" },
|
|
328
|
+
},
|
|
329
|
+
required: [ "path", "data" ],
|
|
330
|
+
additionalProperties: false,
|
|
264
331
|
},
|
|
265
|
-
|
|
266
|
-
additionalProperties: false,
|
|
332
|
+
strict: true,
|
|
267
333
|
},
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
334
|
+
{
|
|
335
|
+
type: "function",
|
|
336
|
+
name: "fileFindAndReplace",
|
|
337
|
+
parameters: {
|
|
338
|
+
type: "object",
|
|
339
|
+
properties: {
|
|
340
|
+
path: { type: "string" },
|
|
341
|
+
substring: { type: "string" },
|
|
342
|
+
replacement: { type: "string" },
|
|
343
|
+
expectedMatches: { type: ["number", "null"] },
|
|
344
|
+
},
|
|
345
|
+
required: [ "path", "substring", "replacement", "expectedMatches" ],
|
|
346
|
+
additionalProperties: false,
|
|
280
347
|
},
|
|
281
|
-
|
|
282
|
-
additionalProperties: false,
|
|
348
|
+
strict: true,
|
|
283
349
|
},
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
);
|
|
350
|
+
);
|
|
351
|
+
}
|
|
287
352
|
}
|
|
288
353
|
if (sandbox) {
|
|
289
354
|
tools.push({
|
|
@@ -302,22 +367,22 @@ const initTools = (fs, sandbox, web_search) => {
|
|
|
302
367
|
strict: true,
|
|
303
368
|
});
|
|
304
369
|
if (fs) {
|
|
305
|
-
tools.push(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
sbxPath: { type: "string" },
|
|
314
|
-
},
|
|
315
|
-
required: [ "hostPath", "sbxPath" ],
|
|
316
|
-
additionalProperties: false,
|
|
370
|
+
tools.push({
|
|
371
|
+
type: "function",
|
|
372
|
+
name: "sandboxCopyIn",
|
|
373
|
+
parameters: {
|
|
374
|
+
type: "object",
|
|
375
|
+
properties: {
|
|
376
|
+
hostPath: { type: "string" },
|
|
377
|
+
sbxPath: { type: "string" },
|
|
317
378
|
},
|
|
318
|
-
|
|
379
|
+
required: [ "hostPath", "sbxPath" ],
|
|
380
|
+
additionalProperties: false,
|
|
319
381
|
},
|
|
320
|
-
|
|
382
|
+
strict: true,
|
|
383
|
+
});
|
|
384
|
+
if (fs != "readonly") {
|
|
385
|
+
tools.push({
|
|
321
386
|
type: "function",
|
|
322
387
|
name: "sandboxCopyOut",
|
|
323
388
|
parameters: {
|
|
@@ -330,8 +395,56 @@ const initTools = (fs, sandbox, web_search) => {
|
|
|
330
395
|
additionalProperties: false,
|
|
331
396
|
},
|
|
332
397
|
strict: true,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (Object.keys(skills).length) {
|
|
403
|
+
tools.push(
|
|
404
|
+
{
|
|
405
|
+
type: "function",
|
|
406
|
+
name: "readSkill",
|
|
407
|
+
parameters: {
|
|
408
|
+
type: "object",
|
|
409
|
+
properties: {
|
|
410
|
+
name: { type: "string" },
|
|
411
|
+
},
|
|
412
|
+
required: [ "name" ],
|
|
413
|
+
additionalProperties: false,
|
|
333
414
|
},
|
|
334
|
-
|
|
415
|
+
strict: true,
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
type: "function",
|
|
419
|
+
name: "readSkillResource",
|
|
420
|
+
parameters: {
|
|
421
|
+
type: "object",
|
|
422
|
+
properties: {
|
|
423
|
+
name: { type: "string" },
|
|
424
|
+
path: { type: "string" },
|
|
425
|
+
},
|
|
426
|
+
required: [ "name", "path" ],
|
|
427
|
+
additionalProperties: false,
|
|
428
|
+
},
|
|
429
|
+
strict: true,
|
|
430
|
+
},
|
|
431
|
+
);
|
|
432
|
+
if (sandbox) {
|
|
433
|
+
tools.push({
|
|
434
|
+
type: "function",
|
|
435
|
+
name: "copySkillResourceToSandbox",
|
|
436
|
+
parameters: {
|
|
437
|
+
type: "object",
|
|
438
|
+
properties: {
|
|
439
|
+
name: { type: "string" },
|
|
440
|
+
path: { type: "string" },
|
|
441
|
+
sbxPath: { type: "string" },
|
|
442
|
+
},
|
|
443
|
+
required: [ "name", "path", "sbxPath" ],
|
|
444
|
+
additionalProperties: false,
|
|
445
|
+
},
|
|
446
|
+
strict: true,
|
|
447
|
+
});
|
|
335
448
|
}
|
|
336
449
|
}
|
|
337
450
|
if (web_search) {
|
|
@@ -344,10 +457,8 @@ const toolToAnthropic = (t) => {
|
|
|
344
457
|
return {
|
|
345
458
|
name: t.name,
|
|
346
459
|
description: t.description,
|
|
347
|
-
input_schema:
|
|
348
|
-
|
|
349
|
-
properties: t.parameters.properties,
|
|
350
|
-
}
|
|
460
|
+
input_schema: t.parameters,
|
|
461
|
+
strict: t.strict,
|
|
351
462
|
};
|
|
352
463
|
}
|
|
353
464
|
else if (t.type == "web_search") {
|
|
@@ -377,6 +488,7 @@ const useTool = (name, args, argsStr) => {
|
|
|
377
488
|
const openai_generateResponseOptionallyUsingTools = async (client, model, input, state) => {
|
|
378
489
|
let response = await client.responses.create({
|
|
379
490
|
model,
|
|
491
|
+
instructions: getSystemPrompt(),
|
|
380
492
|
previous_response_id: state,
|
|
381
493
|
input,
|
|
382
494
|
tools,
|
|
@@ -399,6 +511,7 @@ const openai_generateResponseOptionallyUsingTools = async (client, model, input,
|
|
|
399
511
|
|
|
400
512
|
response = await client.responses.create({
|
|
401
513
|
model,
|
|
514
|
+
instructions: getSystemPrompt(),
|
|
402
515
|
previous_response_id: response.id,
|
|
403
516
|
input: outputs,
|
|
404
517
|
tools,
|
|
@@ -413,17 +526,43 @@ const openai_generateResponseOptionallyUsingTools = async (client, model, input,
|
|
|
413
526
|
};
|
|
414
527
|
};
|
|
415
528
|
|
|
529
|
+
const anthropic_streamMessage = (client, res, params) => {
|
|
530
|
+
const stream = client.messages.stream(params);
|
|
531
|
+
let thought = "";
|
|
532
|
+
stream.on("thinking", (delta) => {
|
|
533
|
+
thought += delta;
|
|
534
|
+
const i = thought.indexOf("\n\n");
|
|
535
|
+
if (i != -1) {
|
|
536
|
+
console.log(chalk.gray(thought.substring(0, i)));
|
|
537
|
+
logWrite(`[Thought summary: ${thought.substring(0, i)}]`);
|
|
538
|
+
res.used_tools = true; // Need a newline before response just the same
|
|
539
|
+
thought = thought.substring(i + 2);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
stream.on("contentBlock", () => {
|
|
543
|
+
if (thought) {
|
|
544
|
+
console.log(chalk.gray(thought));
|
|
545
|
+
logWrite(`[Thought summary: ${thought}]`);
|
|
546
|
+
res.used_tools = true; // Need a newline before response just the same
|
|
547
|
+
thought = "";
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
return stream.finalMessage();
|
|
551
|
+
};
|
|
552
|
+
|
|
416
553
|
const anthropic_generateResponseOptionallyUsingTools = async (client, model, input, state) => {
|
|
417
554
|
state = JSON.parse(state || "[]");
|
|
418
555
|
state.push({ role: "user", content: input });
|
|
419
|
-
|
|
556
|
+
const res = { used_tools: false };
|
|
420
557
|
|
|
421
558
|
const anthropicTools = tools.map(t => toolToAnthropic(t));
|
|
422
559
|
|
|
423
|
-
let message = await client
|
|
560
|
+
let message = await anthropic_streamMessage(client, res, {
|
|
424
561
|
model,
|
|
425
|
-
max_tokens:
|
|
562
|
+
max_tokens: 64000,
|
|
563
|
+
thinking: { type: "adaptive", display: "summarized" },
|
|
426
564
|
cache_control: { type: "ephemeral" },
|
|
565
|
+
system: getSystemPrompt(),
|
|
427
566
|
messages: state,
|
|
428
567
|
tools: anthropicTools,
|
|
429
568
|
});
|
|
@@ -440,17 +579,19 @@ const anthropic_generateResponseOptionallyUsingTools = async (client, model, inp
|
|
|
440
579
|
};
|
|
441
580
|
});
|
|
442
581
|
state.push({ role: "user", content: outputs });
|
|
443
|
-
used_tools = true;
|
|
582
|
+
res.used_tools = true;
|
|
444
583
|
}
|
|
445
584
|
|
|
446
585
|
/*if (message.stop_reason == "max_tokens" && message.content[message.content.length].type == "thinking") {
|
|
447
586
|
state.push({ role: "user", content: "Please continue from where you left off." });
|
|
448
587
|
}*/
|
|
449
588
|
|
|
450
|
-
message = await client
|
|
589
|
+
message = await anthropic_streamMessage(client, res, {
|
|
451
590
|
model,
|
|
452
|
-
max_tokens:
|
|
591
|
+
max_tokens: 64000,
|
|
592
|
+
thinking: { type: "adaptive", display: "summarized" },
|
|
453
593
|
cache_control: { type: "ephemeral" },
|
|
594
|
+
system: getSystemPrompt(),
|
|
454
595
|
messages: state,
|
|
455
596
|
tools: anthropicTools,
|
|
456
597
|
});
|
|
@@ -464,14 +605,13 @@ const anthropic_generateResponseOptionallyUsingTools = async (client, model, inp
|
|
|
464
605
|
}
|
|
465
606
|
|
|
466
607
|
//console.log(message);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
used_tools,
|
|
471
|
-
};
|
|
608
|
+
res.output_text = message.content.find((item) => item.type == "text").text;
|
|
609
|
+
res.state = JSON.stringify(state);
|
|
610
|
+
return res;
|
|
472
611
|
};
|
|
473
612
|
|
|
474
613
|
module.exports = {
|
|
614
|
+
getSystemPrompt,
|
|
475
615
|
initTools,
|
|
476
616
|
getSandboxName,
|
|
477
617
|
setSandboxName,
|
package/src/main.js
CHANGED
|
@@ -10,10 +10,12 @@ async function main() {
|
|
|
10
10
|
.usage("[options] [--] [prompt...]")
|
|
11
11
|
.description("Vibe coding without the kaboom factor.")
|
|
12
12
|
.argument("[prompt...]")
|
|
13
|
+
.option("--long, --multiline", "write a multiline prompt")
|
|
13
14
|
.option("-c, --continue", "add your previous invocation to the context of this one").option("--cont")
|
|
14
15
|
.option("-a, --amend", "retry the previous invocation, using the context from before it")
|
|
15
16
|
.option("-w, --web", "allow the model to search the web")
|
|
16
17
|
.option("-n, --no-fs", "disable filesystem access").option("--nofs")
|
|
18
|
+
.option("-r, --readonly", "disable filesystem write access")
|
|
17
19
|
.option("-s, --sandbox", "allow the model to use a docker container")
|
|
18
20
|
.option("-p, --profile <name>", "profile name")
|
|
19
21
|
.option("--create-profile <name> <provider> <model>", "create a profile")
|
|
@@ -22,6 +24,10 @@ async function main() {
|
|
|
22
24
|
.option("--list-profiles", "list all profiles")
|
|
23
25
|
.option("--delete-profile <name>", "delete a profile")
|
|
24
26
|
.option("--update-api-key <provider>", "update an API key")
|
|
27
|
+
.option("--set-system, --set-system-prompt", "set or clear the system prompt")
|
|
28
|
+
.option("--get-system, --get-system-prompt", "get the system prompt")
|
|
29
|
+
.option("--skills", "get info about skills")
|
|
30
|
+
.option("--logs", "get info about logs")
|
|
25
31
|
.helpOption("-?, --help", "display help")
|
|
26
32
|
.parse();
|
|
27
33
|
const options = program.opts();
|
|
@@ -39,7 +45,7 @@ async function main() {
|
|
|
39
45
|
process.exit(1);
|
|
40
46
|
}
|
|
41
47
|
const name = options.createProfile || options.updateProfile;
|
|
42
|
-
if (
|
|
48
|
+
if (Object.hasOwn(state.profiles, name)) {
|
|
43
49
|
if (options.createProfile) {
|
|
44
50
|
console.error("Profile already exists. Did you mean --update-profile?");
|
|
45
51
|
process.exit(1);
|
|
@@ -88,7 +94,7 @@ async function main() {
|
|
|
88
94
|
process.exit(0);
|
|
89
95
|
}
|
|
90
96
|
if (options.deleteProfile) {
|
|
91
|
-
if (
|
|
97
|
+
if (Object.hasOwn(state.profiles, options.deleteProfile)) {
|
|
92
98
|
delete state.profiles[options.deleteProfile];
|
|
93
99
|
saveState();
|
|
94
100
|
console.log(`Deleted ${options.deleteProfile} profile.`);
|
|
@@ -104,6 +110,35 @@ async function main() {
|
|
|
104
110
|
console.log("Saved.");
|
|
105
111
|
process.exit(0);
|
|
106
112
|
}
|
|
113
|
+
if (options.skills) {
|
|
114
|
+
const { skillsDir, skills } = require("./skills.js");
|
|
115
|
+
console.log(`Skills are loaded from ${skillsDir}`);
|
|
116
|
+
if (Object.keys(skills).length) {
|
|
117
|
+
console.log();
|
|
118
|
+
console.log(`Currently there's ${Object.keys(skills).length} skill${Object.keys(skills).length == 1 ? "" : "s"}:`);
|
|
119
|
+
for (const [name, skill] of Object.entries(skills)) {
|
|
120
|
+
console.log(`- ${name}: ${skill.description}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
if (options.logs) {
|
|
126
|
+
const { logDir } = require("./paths.js");
|
|
127
|
+
console.log(`Each invocation that prompts a model creates a log file in ${logDir}`);
|
|
128
|
+
console.log(`When a new log file is created, any log file older than 30 days is deleted.`);
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
if (options.getSystemPrompt) {
|
|
132
|
+
const { getSystemPrompt } = require("./core.js");
|
|
133
|
+
const sp = getSystemPrompt();
|
|
134
|
+
if (sp !== undefined) {
|
|
135
|
+
console.log(sp);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.log("No system prompt.");
|
|
139
|
+
}
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
107
142
|
|
|
108
143
|
const { markdownToConsoleText } = require("./markdown.js");
|
|
109
144
|
|
|
@@ -122,6 +157,43 @@ async function main() {
|
|
|
122
157
|
}
|
|
123
158
|
|
|
124
159
|
let prompt = program.args.join(" ").trim();
|
|
160
|
+
if (options.multiline) {
|
|
161
|
+
const { createInterface } = require("node:readline");
|
|
162
|
+
console.log("Enter your prompt (Ctrl+D on an empty line to finish):");
|
|
163
|
+
const lines = [];
|
|
164
|
+
if (prompt) {
|
|
165
|
+
lines.push(prompt);
|
|
166
|
+
console.log("> " + prompt);
|
|
167
|
+
}
|
|
168
|
+
prompt = await new Promise((resolve) => {
|
|
169
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
170
|
+
rl.setPrompt("> ");
|
|
171
|
+
rl.prompt();
|
|
172
|
+
rl.on("line", (line) => {
|
|
173
|
+
lines.push(line);
|
|
174
|
+
rl.prompt();
|
|
175
|
+
});
|
|
176
|
+
rl.on("close", () => {
|
|
177
|
+
resolve(lines.join("\n"));
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
console.log("\n");
|
|
181
|
+
}
|
|
182
|
+
let printedPrompt = prompt.replaceAll("\r", "<CR>").replaceAll("\n", "<NL>").replaceAll("\t", " ");
|
|
183
|
+
|
|
184
|
+
if (options.setSystemPrompt) {
|
|
185
|
+
state.systemPrompt = prompt.trim();
|
|
186
|
+
if (state.systemPrompt) {
|
|
187
|
+
console.log("Set custom system prompt: " + printedPrompt);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
delete state.systemPrompt;
|
|
191
|
+
console.log("Cleared custom system prompt.");
|
|
192
|
+
}
|
|
193
|
+
saveState();
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
|
|
125
197
|
if (!prompt) {
|
|
126
198
|
console.error("No prompt given.");
|
|
127
199
|
process.exit(1);
|
|
@@ -134,6 +206,7 @@ async function main() {
|
|
|
134
206
|
|
|
135
207
|
const { initTools, getSandboxName, setSandboxName, anthropic_generateResponseOptionallyUsingTools, openai_generateResponseOptionallyUsingTools } = require("./core.js");
|
|
136
208
|
const { logWrite, logWriteBoth } = require("./log.js");
|
|
209
|
+
const { skills } = require("./skills.js");
|
|
137
210
|
|
|
138
211
|
let previous_provider_state;
|
|
139
212
|
const overrides = [profileToString(profile)];
|
|
@@ -146,30 +219,41 @@ async function main() {
|
|
|
146
219
|
previous_provider_state = state.providerStates[profile.provider].previous_previous;
|
|
147
220
|
setSandboxName(state.providerStates[profile.provider].sandbox_name);
|
|
148
221
|
}
|
|
149
|
-
const
|
|
150
|
-
initTools(
|
|
222
|
+
const fsMode = (options.fs !== false && !options.nofs) && (options.readonly ? "readonly" : true);
|
|
223
|
+
initTools(fsMode, options.sandbox, options.web);
|
|
224
|
+
if (state.systemPrompt) {
|
|
225
|
+
overrides.push("custom system prompt");
|
|
226
|
+
}
|
|
227
|
+
if (Object.keys(skills).length) {
|
|
228
|
+
overrides.push(`${Object.keys(skills).length} skill${Object.keys(skills).length == 1 ? "" : "s"}`);
|
|
229
|
+
}
|
|
151
230
|
if (options.web) {
|
|
152
231
|
overrides.push("web search enabled");
|
|
153
232
|
}
|
|
154
|
-
if (!
|
|
233
|
+
if (!fsMode) {
|
|
155
234
|
overrides.push("no filesystem access");
|
|
156
235
|
}
|
|
236
|
+
else if (fsMode == "readonly") {
|
|
237
|
+
overrides.push("read-only filesystem access");
|
|
238
|
+
}
|
|
157
239
|
if (options.sandbox) {
|
|
158
240
|
overrides.push("sandbox enabled");
|
|
159
241
|
}
|
|
160
242
|
|
|
161
|
-
logWriteBoth((previous_provider_state ? "FOLLOW-UP" : "PROMPT") + ": " + prompt);
|
|
162
243
|
if (!process.stdin.isTTY) {
|
|
163
244
|
const chunks = [];
|
|
164
245
|
for await (const chunk of process.stdin) {
|
|
165
246
|
chunks.push(chunk);
|
|
166
247
|
}
|
|
167
248
|
prompt = `${Buffer.concat(chunks).toString("utf8")}\n\n${prompt}`;
|
|
168
|
-
|
|
249
|
+
printedPrompt = `[stdin]<NL><NL>${printedPrompt}`;
|
|
169
250
|
}
|
|
251
|
+
console.log((previous_provider_state ? "FOLLOW-UP" : "PROMPT") + ": " + printedPrompt);
|
|
252
|
+
logWrite((previous_provider_state ? "FOLLOW-UP" : "PROMPT") + ": " + prompt);
|
|
170
253
|
logWriteBoth(`(${overrides.join(", ")})\n`);
|
|
171
254
|
|
|
172
|
-
|
|
255
|
+
/*console.log({ model: profile.model, prompt, previous_provider_state, fsMode });
|
|
256
|
+
return;*/
|
|
173
257
|
|
|
174
258
|
const clientOpts = {
|
|
175
259
|
apiKey: state.apiKeys[profile.provider],
|
|
@@ -196,6 +280,9 @@ async function main() {
|
|
|
196
280
|
saveState();
|
|
197
281
|
if (response.used_tools) {
|
|
198
282
|
logWriteBoth("");
|
|
283
|
+
if (getSandboxName()) {
|
|
284
|
+
execFileSync("docker", ["stop", getSandboxName(), "-t", "0"], { encoding: "utf8" });
|
|
285
|
+
}
|
|
199
286
|
}
|
|
200
287
|
console.log(markdownToConsoleText(response.output_text) + "\n");
|
|
201
288
|
logWrite(response.output_text);
|
package/src/skills.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { configDir } = require("./paths.js");
|
|
6
|
+
|
|
7
|
+
const skillsDir = path.join(configDir, "skills");
|
|
8
|
+
|
|
9
|
+
const unquote = (value) => {
|
|
10
|
+
const first = value[0];
|
|
11
|
+
if ((first === "\"" || first === "'") && value.at(-1) === first && value.length >= 2) {
|
|
12
|
+
return value.slice(1, -1);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const parseFrontmatter = (text) => {
|
|
18
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
19
|
+
if (!match) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const fields = {};
|
|
24
|
+
let currentKey = null;
|
|
25
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
26
|
+
const keyValue = /^([\w-]+):\s*(.*)$/.exec(line);
|
|
27
|
+
if (keyValue) {
|
|
28
|
+
currentKey = keyValue[1];
|
|
29
|
+
const raw = keyValue[2].trim();
|
|
30
|
+
// Block scalars (| or >) are folded into a single line below.
|
|
31
|
+
fields[currentKey] = /^[>|][+-]?$/.test(raw) ? "" : unquote(raw);
|
|
32
|
+
}
|
|
33
|
+
else if (currentKey && /^\s+\S/.test(line)) {
|
|
34
|
+
// Indented continuation line of a multi-line value.
|
|
35
|
+
fields[currentKey] = `${fields[currentKey]} ${line.trim()}`.trim();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return fields;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
42
|
+
const skills = Object.create(null);
|
|
43
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
const skillPath = path.join(skillsDir, entry.name);
|
|
46
|
+
const skillFile = path.join(skillPath, "SKILL.md");
|
|
47
|
+
if (!fs.existsSync(skillFile)) {
|
|
48
|
+
console.log(`Invalid skill (missing SKILL.md): ${skillPath}`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const frontmatter = parseFrontmatter(fs.readFileSync(skillFile, "utf8"));
|
|
52
|
+
if (!frontmatter.description) {
|
|
53
|
+
console.log(`Invalid skill (missing description): ${skillPath}`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
skills[frontmatter.name || entry.name] = {
|
|
57
|
+
path: skillPath,
|
|
58
|
+
description: frontmatter.description,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { skillsDir, skills };
|
package/src/state.js
CHANGED
|
@@ -6,9 +6,10 @@ const { createInterface } = require("node:readline/promises");
|
|
|
6
6
|
const { configDir } = require("./paths.js");
|
|
7
7
|
|
|
8
8
|
const state = fs.existsSync(path.join(configDir, "state.json")) ? JSON.parse(fs.readFileSync(path.join(configDir, "state.json"), "utf8")) : {};
|
|
9
|
-
|
|
10
|
-
state.
|
|
11
|
-
state.
|
|
9
|
+
const nullProto = (obj) => Object.assign(Object.create(null), obj);
|
|
10
|
+
state.profiles = nullProto(state.profiles);
|
|
11
|
+
state.apiKeys = nullProto(state.apiKeys);
|
|
12
|
+
state.providerStates = nullProto(state.providerStates);
|
|
12
13
|
|
|
13
14
|
const promptForApiKey = async (provider) => {
|
|
14
15
|
const readline = createInterface({ input: process.stdin, output: process.stdout });
|