@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 CHANGED
@@ -22,25 +22,29 @@ cat yap.txt | slave Summarize this text
22
22
  ```
23
23
 
24
24
  Options:
25
- - `-c`, `--cont`, `--continue` — add your previous invocation to the context of this one
26
- - `-a`, `--amend` — retry the previous invocation, using the context from before it
27
- - `-w`, `--web` — allow the model to search the web
28
- - `-n`, `--no-fs`, `--nofs` — disable filesystem access
29
- - `-s`, `--sandbox` — allow the model to use a docker container
30
- - `-p`, `--profile` `<name>` — use the specified profile
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 <name> <provider> <model> [--set-default]` — create a 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 <name> <provider> <model> [--set-default]` — update a profile
38
- - `--set-default <profile name>` — set default profile
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 <name>` — delete a profile
41
- - `--update-api-key <provider>` — update an API Key
42
- - `--logs` — get info about logs
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
@@ -4,7 +4,7 @@
4
4
  "type": "git",
5
5
  "url": "git+https://github.com/calamity-inc/slave.git"
6
6
  },
7
- "version": "0.4.1",
7
+ "version": "0.4.2",
8
8
  "type": "commonjs",
9
9
  "bin": {
10
10
  "slave": "src/main.js"
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 entries = Object.entries(skills);
66
- if (entries.length == 0) {
67
- return undefined;
66
+ const parts = [];
67
+ if (state.systemPrompt) {
68
+ parts.push(state.systemPrompt);
68
69
  }
69
- return "You have the following skills available:\n" + entries.map(([name, skill]) => `- ${name}: ${skill.description}`).join("\n");
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
- type: "function",
310
- name: "writeFile",
311
- parameters: {
312
- type: "object",
313
- properties: {
314
- path: { type: "string" },
315
- data: { type: "string" },
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
- required: [ "path", "data" ],
318
- additionalProperties: false,
332
+ strict: true,
319
333
  },
320
- strict: true,
321
- },
322
- {
323
- type: "function",
324
- name: "fileFindAndReplace",
325
- parameters: {
326
- type: "object",
327
- properties: {
328
- path: { type: "string" },
329
- substring: { type: "string" },
330
- replacement: { type: "string" },
331
- expectedMatches: { type: ["number", "null"] },
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
- required: [ "path", "substring", "replacement", "expectedMatches" ],
334
- additionalProperties: false,
348
+ strict: true,
335
349
  },
336
- strict: true,
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
- type: "function",
360
- name: "sandboxCopyIn",
361
- parameters: {
362
- type: "object",
363
- properties: {
364
- hostPath: { type: "string" },
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
- strict: true,
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("--logs", "get info about logs")
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 filesystemEnabled = options.fs !== false && !options.nofs;
171
- initTools(filesystemEnabled, options.sandbox, options.web);
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 (!filesystemEnabled) {
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
- overrides.push("stdin provided");
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
- //console.log({ model: profile.model, prompt, previous_provider_state });
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);