@calamity-inc/slave 0.4.1 → 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 +16 -12
- package/package.json +1 -1
- package/src/core.js +62 -48
- package/src/main.js +76 -13
package/README.md
CHANGED
|
@@ -22,25 +22,29 @@ 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
|
-
- `--
|
|
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
|
|
43
46
|
- `--skills` — get info about skills
|
|
47
|
+
- `--logs` — get info about logs
|
|
44
48
|
|
|
45
49
|
## Security
|
|
46
50
|
|
package/package.json
CHANGED
package/src/core.js
CHANGED
|
@@ -10,6 +10,7 @@ const ignore = require("ignore");
|
|
|
10
10
|
|
|
11
11
|
const { logWrite } = require("./log.js");
|
|
12
12
|
const { skills } = require("./skills.js");
|
|
13
|
+
const { state } = require("./state.js");
|
|
13
14
|
|
|
14
15
|
const slaveignore = ignore();
|
|
15
16
|
if (fs.existsSync(".slaveignore")) {
|
|
@@ -62,11 +63,15 @@ const writeFileFromModel = (filePath, data, usesCRLF) => {
|
|
|
62
63
|
};
|
|
63
64
|
|
|
64
65
|
const getSystemPrompt = () => {
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
67
|
-
|
|
66
|
+
const parts = [];
|
|
67
|
+
if (state.systemPrompt) {
|
|
68
|
+
parts.push(state.systemPrompt);
|
|
68
69
|
}
|
|
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;
|
|
70
75
|
};
|
|
71
76
|
const getSkillResourcePath = (name, resourcePath) => {
|
|
72
77
|
if (Object.hasOwn(skills, name)) {
|
|
@@ -82,7 +87,7 @@ const getSkillResourcePath = (name, resourcePath) => {
|
|
|
82
87
|
return [false, "No such skill"];
|
|
83
88
|
};
|
|
84
89
|
|
|
85
|
-
let sandbox_name;
|
|
90
|
+
let sandbox_name, sandbox_running;
|
|
86
91
|
const getSandboxName = () => {
|
|
87
92
|
return sandbox_name;
|
|
88
93
|
};
|
|
@@ -95,6 +100,10 @@ const ensureSandbox = () => {
|
|
|
95
100
|
// `docker pull ghcr.io/calamity-inc/slave-sandbox` is not needed to get the image, but would be needed to update it.
|
|
96
101
|
execFileSync("docker", ["run", "-d", "--name", sandbox_name, "ghcr.io/calamity-inc/slave-sandbox", "sleep", "infinity"], { encoding: "utf8" });
|
|
97
102
|
}
|
|
103
|
+
else if (!sandbox_running) {
|
|
104
|
+
execFileSync("docker", ["start", sandbox_name], { encoding: "utf8" });
|
|
105
|
+
}
|
|
106
|
+
sandbox_running = true;
|
|
98
107
|
};
|
|
99
108
|
const copyFileToSandbox = (hostPath, sbxPath) => {
|
|
100
109
|
ensureSandbox();
|
|
@@ -305,37 +314,41 @@ const initTools = (fs, sandbox, web_search) => {
|
|
|
305
314
|
},
|
|
306
315
|
strict: true,
|
|
307
316
|
},
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
type: "
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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,
|
|
316
331
|
},
|
|
317
|
-
|
|
318
|
-
additionalProperties: false,
|
|
332
|
+
strict: true,
|
|
319
333
|
},
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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,
|
|
332
347
|
},
|
|
333
|
-
|
|
334
|
-
additionalProperties: false,
|
|
348
|
+
strict: true,
|
|
335
349
|
},
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
);
|
|
350
|
+
);
|
|
351
|
+
}
|
|
339
352
|
}
|
|
340
353
|
if (sandbox) {
|
|
341
354
|
tools.push({
|
|
@@ -354,22 +367,22 @@ const initTools = (fs, sandbox, web_search) => {
|
|
|
354
367
|
strict: true,
|
|
355
368
|
});
|
|
356
369
|
if (fs) {
|
|
357
|
-
tools.push(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
sbxPath: { type: "string" },
|
|
366
|
-
},
|
|
367
|
-
required: [ "hostPath", "sbxPath" ],
|
|
368
|
-
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" },
|
|
369
378
|
},
|
|
370
|
-
|
|
379
|
+
required: [ "hostPath", "sbxPath" ],
|
|
380
|
+
additionalProperties: false,
|
|
371
381
|
},
|
|
372
|
-
|
|
382
|
+
strict: true,
|
|
383
|
+
});
|
|
384
|
+
if (fs != "readonly") {
|
|
385
|
+
tools.push({
|
|
373
386
|
type: "function",
|
|
374
387
|
name: "sandboxCopyOut",
|
|
375
388
|
parameters: {
|
|
@@ -382,8 +395,8 @@ const initTools = (fs, sandbox, web_search) => {
|
|
|
382
395
|
additionalProperties: false,
|
|
383
396
|
},
|
|
384
397
|
strict: true,
|
|
385
|
-
}
|
|
386
|
-
|
|
398
|
+
});
|
|
399
|
+
}
|
|
387
400
|
}
|
|
388
401
|
}
|
|
389
402
|
if (Object.keys(skills).length) {
|
|
@@ -598,6 +611,7 @@ const anthropic_generateResponseOptionallyUsingTools = async (client, model, inp
|
|
|
598
611
|
};
|
|
599
612
|
|
|
600
613
|
module.exports = {
|
|
614
|
+
getSystemPrompt,
|
|
601
615
|
initTools,
|
|
602
616
|
getSandboxName,
|
|
603
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,8 +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")
|
|
25
|
-
.option("--
|
|
27
|
+
.option("--set-system, --set-system-prompt", "set or clear the system prompt")
|
|
28
|
+
.option("--get-system, --get-system-prompt", "get the system prompt")
|
|
26
29
|
.option("--skills", "get info about skills")
|
|
30
|
+
.option("--logs", "get info about logs")
|
|
27
31
|
.helpOption("-?, --help", "display help")
|
|
28
32
|
.parse();
|
|
29
33
|
const options = program.opts();
|
|
@@ -106,12 +110,6 @@ async function main() {
|
|
|
106
110
|
console.log("Saved.");
|
|
107
111
|
process.exit(0);
|
|
108
112
|
}
|
|
109
|
-
if (options.logs) {
|
|
110
|
-
const { logDir } = require("./paths.js");
|
|
111
|
-
console.log(`Each invocation that prompts a model creates a log file in ${logDir}`);
|
|
112
|
-
console.log(`When a new log file is created, any log file older than 30 days is deleted.`);
|
|
113
|
-
process.exit(0);
|
|
114
|
-
}
|
|
115
113
|
if (options.skills) {
|
|
116
114
|
const { skillsDir, skills } = require("./skills.js");
|
|
117
115
|
console.log(`Skills are loaded from ${skillsDir}`);
|
|
@@ -124,6 +122,23 @@ async function main() {
|
|
|
124
122
|
}
|
|
125
123
|
process.exit(0);
|
|
126
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
|
+
}
|
|
127
142
|
|
|
128
143
|
const { markdownToConsoleText } = require("./markdown.js");
|
|
129
144
|
|
|
@@ -142,6 +157,43 @@ async function main() {
|
|
|
142
157
|
}
|
|
143
158
|
|
|
144
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
|
+
|
|
145
197
|
if (!prompt) {
|
|
146
198
|
console.error("No prompt given.");
|
|
147
199
|
process.exit(1);
|
|
@@ -167,33 +219,41 @@ async function main() {
|
|
|
167
219
|
previous_provider_state = state.providerStates[profile.provider].previous_previous;
|
|
168
220
|
setSandboxName(state.providerStates[profile.provider].sandbox_name);
|
|
169
221
|
}
|
|
170
|
-
const
|
|
171
|
-
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
|
+
}
|
|
172
227
|
if (Object.keys(skills).length) {
|
|
173
228
|
overrides.push(`${Object.keys(skills).length} skill${Object.keys(skills).length == 1 ? "" : "s"}`);
|
|
174
229
|
}
|
|
175
230
|
if (options.web) {
|
|
176
231
|
overrides.push("web search enabled");
|
|
177
232
|
}
|
|
178
|
-
if (!
|
|
233
|
+
if (!fsMode) {
|
|
179
234
|
overrides.push("no filesystem access");
|
|
180
235
|
}
|
|
236
|
+
else if (fsMode == "readonly") {
|
|
237
|
+
overrides.push("read-only filesystem access");
|
|
238
|
+
}
|
|
181
239
|
if (options.sandbox) {
|
|
182
240
|
overrides.push("sandbox enabled");
|
|
183
241
|
}
|
|
184
242
|
|
|
185
|
-
logWriteBoth((previous_provider_state ? "FOLLOW-UP" : "PROMPT") + ": " + prompt);
|
|
186
243
|
if (!process.stdin.isTTY) {
|
|
187
244
|
const chunks = [];
|
|
188
245
|
for await (const chunk of process.stdin) {
|
|
189
246
|
chunks.push(chunk);
|
|
190
247
|
}
|
|
191
248
|
prompt = `${Buffer.concat(chunks).toString("utf8")}\n\n${prompt}`;
|
|
192
|
-
|
|
249
|
+
printedPrompt = `[stdin]<NL><NL>${printedPrompt}`;
|
|
193
250
|
}
|
|
251
|
+
console.log((previous_provider_state ? "FOLLOW-UP" : "PROMPT") + ": " + printedPrompt);
|
|
252
|
+
logWrite((previous_provider_state ? "FOLLOW-UP" : "PROMPT") + ": " + prompt);
|
|
194
253
|
logWriteBoth(`(${overrides.join(", ")})\n`);
|
|
195
254
|
|
|
196
|
-
|
|
255
|
+
/*console.log({ model: profile.model, prompt, previous_provider_state, fsMode });
|
|
256
|
+
return;*/
|
|
197
257
|
|
|
198
258
|
const clientOpts = {
|
|
199
259
|
apiKey: state.apiKeys[profile.provider],
|
|
@@ -220,6 +280,9 @@ async function main() {
|
|
|
220
280
|
saveState();
|
|
221
281
|
if (response.used_tools) {
|
|
222
282
|
logWriteBoth("");
|
|
283
|
+
if (getSandboxName()) {
|
|
284
|
+
execFileSync("docker", ["stop", getSandboxName(), "-t", "0"], { encoding: "utf8" });
|
|
285
|
+
}
|
|
223
286
|
}
|
|
224
287
|
console.log(markdownToConsoleText(response.output_text) + "\n");
|
|
225
288
|
logWrite(response.output_text);
|