@calamity-inc/slave 0.2.0 → 0.3.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/README.md CHANGED
@@ -5,7 +5,7 @@ Vibe coding without the kaboom factor.
5
5
  ## Setup
6
6
 
7
7
  1. Install slave: `npm i -g @calamity-inc/slave`
8
- 2. Create a default profile, e.g. `slave --create-profile default openai gpt-6-luna`
8
+ 2. Create a default profile, e.g. `slave --create-profile default openai gpt-6-luna` or `slave --create-profile default anthropic claude-haiku-5-5`
9
9
 
10
10
  ## Usage
11
11
 
@@ -21,23 +21,24 @@ cat yap.txt | slave Summarize this text
21
21
 
22
22
  Options:
23
23
  - `-c`, `--cont`, `--continue` — add your previous invocation to the context of this one
24
- - `--amend` — retry the previous invocation, using the context from before it
24
+ - `-a`, `--amend` — retry the previous invocation, using the context from before it
25
25
  - `-w`, `--web` — allow the model to search the web
26
26
  - `-n`, `--no-fs`, `--nofs` — disable filesystem access
27
- - `-p, --profile <name>` — use the specified profile
27
+ - `-s`, `--sandbox` — allow the model to use a docker container
28
+ - `-p`, `--profile` `<name>` — use the specified profile
28
29
 
29
30
  Single-character flags can be combined, so `slave -cw prompt` is equivalent to `slave -c -w prompt`.
30
31
 
31
32
  Subcommands:
32
33
  - `--create-profile <name> <provider> <model>` — creates a profile
33
- - `provider` can be `openai` or an OpenAI-compatible base URL, e.g. `http://127.0.0.1:1234/v1` for [LM Studio](https://lmstudio.ai/developers).
34
+ - `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).
34
35
  - `--delete-profile <name>` — deletes a profile
35
36
  - `--update-api-key <provider>` — updates an API Key
36
37
 
37
- ## Sandboxing / Security
38
+ ## Security
38
39
 
39
- The model gets no command prompt access and limited access to your filesystem:
40
- - Only within the working directory and subdirectories.
40
+ The model can't run commands on your system; it only gets limited access to your filesystem:
41
+ - Within the working directory and subdirectories.
41
42
  - `.git` folder and `.env` file can't be accessed by the model, tho it knows they exist, if they do.
42
43
  - Files that are `.gitignore`d can't be written to.
43
44
  - 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.
package/index.js CHANGED
@@ -1,25 +1,24 @@
1
1
  #!/usr/bin/env node
2
+
3
+ import { execFileSync, spawnSync } from "node:child_process";
2
4
  import fs from "node:fs";
3
5
  import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { parseEnv } from "node:util";
6
6
  import { createInterface } from "node:readline/promises";
7
- import ignore from "ignore";
7
+
8
+ import Anthropic from "@anthropic-ai/sdk";
8
9
  import { Command } from "commander";
10
+ import envPaths from "env-paths";
11
+ import ignore from "ignore";
9
12
  import { marked } from "marked";
10
13
  import { markedTerminal } from "marked-terminal";
11
14
  import OpenAI from "openai";
12
15
 
13
- const configDir = path.dirname(fileURLToPath(import.meta.url));
14
- const state = fs.existsSync(path.join(configDir, ".state.json")) ? JSON.parse(fs.readFileSync(path.join(configDir, ".state.json"), "utf8")) : {};
16
+
17
+ const configDir = envPaths("slave").data;
18
+ const state = fs.existsSync(path.join(configDir, "state.json")) ? JSON.parse(fs.readFileSync(path.join(configDir, "state.json"), "utf8")) : {};
15
19
  state.profiles ??= {};
16
20
  state.apiKeys ??= {};
17
- if (!state.apiKeys.openai && fs.existsSync(path.join(configDir, ".env"))) {
18
- const env = parseEnv(fs.readFileSync(path.join(configDir, ".env"), "utf8"));
19
- if (env.OPENAI_API_KEY) {
20
- state.apiKeys.openai = env.OPENAI_API_KEY;
21
- }
22
- }
21
+ state.providerStates ??= {};
23
22
  const promptForApiKey = async (provider) => {
24
23
  const readline = createInterface({ input: process.stdin, output: process.stdout });
25
24
  try {
@@ -30,8 +29,9 @@ const promptForApiKey = async (provider) => {
30
29
  }
31
30
  };
32
31
  const saveState = () => {
33
- fs.writeFileSync(path.join(configDir, ".state.json"), JSON.stringify(state, null, 2), { encoding: "utf8", mode: 0o600 });
34
- fs.chmodSync(path.join(configDir, ".state.json"), 0o600);
32
+ fs.mkdirSync(configDir, { recursive: true });
33
+ fs.writeFileSync(path.join(configDir, "state.json"), JSON.stringify(state, null, 2), { encoding: "utf8", mode: 0o600 });
34
+ fs.chmodSync(path.join(configDir, "state.json"), 0o600);
35
35
  };
36
36
 
37
37
  const slaveignore = ignore();
@@ -42,7 +42,7 @@ const gitignore = ignore();
42
42
  if (fs.existsSync(".gitignore")) {
43
43
  gitignore.add(fs.readFileSync(".gitignore", "utf8").replaceAll("/\n", "\n"));
44
44
  }
45
- const isOutOfReach = (p, type) => {
45
+ const isOutOfReach = (p, accessType) => {
46
46
  const rel = path.relative(".", p);
47
47
  if (rel.startsWith("..") || path.isAbsolute(rel)) {
48
48
  return true;
@@ -51,12 +51,20 @@ const isOutOfReach = (p, type) => {
51
51
  if (segs.includes(".env") || segs.includes(".git")) {
52
52
  return true;
53
53
  }
54
- if (rel && (slaveignore.ignores(rel) || (type == "write" && gitignore.ignores(rel)))) {
54
+ if (rel && (slaveignore.ignores(rel) || (accessType == "write" && gitignore.ignores(rel)))) {
55
55
  return true;
56
56
  }
57
57
  return false;
58
58
  };
59
59
 
60
+ const fixPath = (path, accessType) => {
61
+ if (path.startsWith("/")) {
62
+ path = path.substring(1);
63
+ path ||= ".";
64
+ }
65
+ return isOutOfReach(path, accessType) ? "" : path;
66
+ };
67
+
60
68
  const readFileForModel = (filePath) => fs.readFileSync(filePath, "utf8").replaceAll("\r\n", "\n");
61
69
  const writeFileFromModel = (filePath, data, usesCRLF) => {
62
70
  if (fs.existsSync(filePath)) {
@@ -71,10 +79,25 @@ const writeFileFromModel = (filePath, data, usesCRLF) => {
71
79
  fs.writeFileSync(filePath, usesCRLF ? normalized.replaceAll("\n", "\r\n") : normalized, "utf8");
72
80
  };
73
81
 
82
+ let used_docker;
83
+ const ensureSandbox = () => {
84
+ if (!used_docker) {
85
+ try {
86
+ // `docker pull ghcr.io/calamity-inc/slave-sandbox` is not needed to get the image, but would be needed to update it.
87
+ execFileSync("docker", ["run", "-d", "--name", "slave-sandbox", "ghcr.io/calamity-inc/slave-sandbox", "sleep", "infinity"], { encoding: "utf8" });
88
+ }
89
+ catch (e) {
90
+ // container may already exist if it was not cleaned up previously. this is okay-ish.
91
+ }
92
+ used_docker = true;
93
+ }
94
+ };
95
+
74
96
  const functions = {
75
97
  ls: ({ path }) => {
76
98
  path ??= ".";
77
- if (isOutOfReach(path)) {
99
+ path = fixPath(path);
100
+ if (!path) {
78
101
  return "Out of your reach";
79
102
  }
80
103
  let files;
@@ -88,7 +111,8 @@ const functions = {
88
111
  },
89
112
  findFiles: ({ path, substring, recursive }) => {
90
113
  path ??= ".";
91
- if (isOutOfReach(path)) {
114
+ path = fixPath(path);
115
+ if (!path) {
92
116
  return "Out of your reach";
93
117
  }
94
118
  let files;
@@ -116,21 +140,27 @@ const functions = {
116
140
  return res.join("\n");
117
141
  },
118
142
  readFile: ({ path }) => {
119
- if (isOutOfReach(path)) {
120
- return "Out of your reach";
143
+ path = fixPath(path);
144
+ if (!path) {
145
+ return "You don't have read access to this file";
146
+ }
147
+ if (!fs.existsSync(path) || !fs.statSync(path).isFile()) {
148
+ return "No such file";
121
149
  }
122
150
  return readFileForModel(path);
123
151
  },
124
152
  writeFile: ({ path, data }) => {
125
- if (isOutOfReach(path, "write")) {
126
- return "Out of your reach";
153
+ path = fixPath(path, "write");
154
+ if (!path) {
155
+ return "You don't have write access to this file";
127
156
  }
128
157
  writeFileFromModel(path, data);
129
158
  return "OK";
130
159
  },
131
160
  fileFindAndReplace: ({ path, substring, replacement, expectedMatches }) => {
132
- if (isOutOfReach(path, "write")) {
133
- return "Out of your reach";
161
+ path = fixPath(path, "write");
162
+ if (!path) {
163
+ return "You don't have write access to this file";
134
164
  }
135
165
  const fileContent = fs.readFileSync(path, "utf8");
136
166
  const usesCRLF = fileContent.includes("\r\n");
@@ -142,128 +172,235 @@ const functions = {
142
172
  writeFileFromModel(path, arr.join(replacement), usesCRLF);
143
173
  return "OK";
144
174
  },
145
- system: ({ command }) => {
175
+ sandboxExec: ({ command, args }) => {
176
+ ensureSandbox();
177
+ const result = spawnSync("docker", [ "exec", "slave-sandbox", command, ...args ], { encoding: "utf8" });
178
+ const output = (result.stdout ?? "") + (result.stderr ?? "");
179
+ if (result.error) {
180
+ return (output || result.error.message).trimEnd();
181
+ }
182
+ if (result.status !== 0 && !output) {
183
+ return `Command exited with status ${result.status}`;
184
+ }
185
+ return output;
186
+ },
187
+ sandboxCopyIn: ({ hostPath, sbxPath }) => {
188
+ hostPath = fixPath(hostPath);
189
+ if (!hostPath) {
190
+ return "You don't have read access to this file";
191
+ }
192
+ if (!fs.existsSync(hostPath) || !fs.statSync(hostPath).isFile()) {
193
+ return "Host path must point to a file";
194
+ }
195
+ ensureSandbox();
196
+ try {
197
+ execFileSync("docker", ["cp", "-L", hostPath, `slave-sandbox:${sbxPath}`], { encoding: "utf8" });
198
+ return "OK";
199
+ }
200
+ catch (e) {
201
+ return (e.stdout ?? e.message).trimEnd();
202
+ }
203
+ },
204
+ sandboxCopyOut: ({ sbxPath, hostPath }) => {
205
+ hostPath = fixPath(hostPath, "write");
206
+ if (!hostPath) {
207
+ return "You don't have write access to this file";
208
+ }
209
+ if (fs.existsSync(hostPath) && !fs.statSync(hostPath).isFile()) {
210
+ return "Host path must point to a file";
211
+ }
212
+ ensureSandbox();
213
+ execFileSync("docker", ["cp", "-L", `slave-sandbox:${sbxPath}`, hostPath], { encoding: "utf8" });
214
+ return "OK";
215
+ },
216
+ /*system: ({ command }) => {
146
217
  //process.exit(1);
147
218
  return "It's interesting that you called this tool! Unfortunately for you, it does nothing.";
148
- },
219
+ }*/
149
220
  };
150
221
 
151
- let tools = [
152
- {
153
- type: "function",
154
- name: "ls",
155
- description: "Lists files",
156
- parameters: {
157
- type: "object",
158
- properties: {
159
- path: { type: ["string", "null"] },
222
+ const tools = [];
223
+ const initTools = (fs, sandbox, web_search) => {
224
+ if (fs) {
225
+ tools.push(
226
+ {
227
+ type: "function",
228
+ name: "ls",
229
+ description: "Lists files",
230
+ parameters: {
231
+ type: "object",
232
+ properties: {
233
+ path: { type: ["string", "null"] },
234
+ },
235
+ required: [ "path" ],
236
+ additionalProperties: false,
237
+ },
238
+ strict: true,
160
239
  },
161
- required: [ "path" ],
162
- additionalProperties: false,
163
- },
164
- strict: true,
165
- },
166
- {
167
- type: "function",
168
- name: "findFiles",
169
- parameters: {
170
- type: "object",
171
- properties: {
172
- path: { type: ["string", "null"] },
173
- substring: { type: "string" },
174
- recursive: { type: ["boolean", "null"] },
240
+ {
241
+ type: "function",
242
+ name: "findFiles",
243
+ parameters: {
244
+ type: "object",
245
+ properties: {
246
+ path: { type: ["string", "null"] },
247
+ substring: { type: "string" },
248
+ recursive: { type: ["boolean", "null"] },
249
+ },
250
+ required: [ "path", "substring", "recursive" ],
251
+ additionalProperties: false,
252
+ },
253
+ strict: true,
175
254
  },
176
- required: [ "path", "substring", "recursive" ],
177
- additionalProperties: false,
178
- },
179
- strict: true,
180
- },
181
- {
182
- type: "function",
183
- name: "readFile",
184
- parameters: {
185
- type: "object",
186
- properties: {
187
- path: { type: "string" },
255
+ {
256
+ type: "function",
257
+ name: "readFile",
258
+ parameters: {
259
+ type: "object",
260
+ properties: {
261
+ path: { type: "string" },
262
+ },
263
+ required: [ "path" ],
264
+ additionalProperties: false,
265
+ },
266
+ strict: true,
188
267
  },
189
- required: [ "path" ],
190
- additionalProperties: false,
191
- },
192
- strict: true,
193
- },
194
- {
195
- type: "function",
196
- name: "writeFile",
197
- parameters: {
198
- type: "object",
199
- properties: {
200
- path: { type: "string" },
201
- data: { type: "string" },
268
+ {
269
+ type: "function",
270
+ name: "writeFile",
271
+ parameters: {
272
+ type: "object",
273
+ properties: {
274
+ path: { type: "string" },
275
+ data: { type: "string" },
276
+ },
277
+ required: [ "path", "data" ],
278
+ additionalProperties: false,
279
+ },
280
+ strict: true,
202
281
  },
203
- required: [ "path", "data" ],
204
- additionalProperties: false,
205
- },
206
- strict: true,
207
- },
208
- {
209
- type: "function",
210
- name: "fileFindAndReplace",
211
- parameters: {
212
- type: "object",
213
- properties: {
214
- path: { type: "string" },
215
- substring: { type: "string" },
216
- replacement: { type: "string" },
217
- expectedMatches: { type: ["number", "null"] },
282
+ {
283
+ type: "function",
284
+ name: "fileFindAndReplace",
285
+ parameters: {
286
+ type: "object",
287
+ properties: {
288
+ path: { type: "string" },
289
+ substring: { type: "string" },
290
+ replacement: { type: "string" },
291
+ expectedMatches: { type: ["number", "null"] },
292
+ },
293
+ required: [ "path", "substring", "replacement", "expectedMatches" ],
294
+ additionalProperties: false,
295
+ },
296
+ strict: true,
218
297
  },
219
- required: [ "path", "substring", "replacement", "expectedMatches" ],
220
- additionalProperties: false,
221
- },
222
- strict: true,
223
- },
224
- // This is useful to see where the model maybe expects a better tool:
225
- /*{
226
- type: "function",
227
- name: "system",
228
- parameters: {
229
- type: "object",
230
- properties: {
231
- command: { type: "string" },
298
+ );
299
+ }
300
+ if (sandbox) {
301
+ tools.push({
302
+ type: "function",
303
+ name: "sandboxExec",
304
+ description: "Runs a command without Bash in an isolated sandbox.",
305
+ parameters: {
306
+ type: "object",
307
+ properties: {
308
+ command: { type: "string" },
309
+ args: { type: "array", items: { type: "string" } },
310
+ },
311
+ required: [ "command", "args" ],
312
+ additionalProperties: false,
232
313
  },
233
- required: [ "command" ],
234
- additionalProperties: false,
235
- },
236
- strict: true,
237
- },*/
238
- ];
239
-
240
- const generateResponseOptionallyUsingTools = async (client, model, input, previous_response_id) => {
314
+ strict: true,
315
+ });
316
+ if (fs) {
317
+ tools.push(
318
+ {
319
+ type: "function",
320
+ name: "sandboxCopyIn",
321
+ parameters: {
322
+ type: "object",
323
+ properties: {
324
+ hostPath: { type: "string" },
325
+ sbxPath: { type: "string" },
326
+ },
327
+ required: [ "hostPath", "sbxPath" ],
328
+ additionalProperties: false,
329
+ },
330
+ strict: true,
331
+ },
332
+ {
333
+ type: "function",
334
+ name: "sandboxCopyOut",
335
+ parameters: {
336
+ type: "object",
337
+ properties: {
338
+ sbxPath: { type: "string" },
339
+ hostPath: { type: "string" },
340
+ },
341
+ required: [ "sbxPath", "hostPath" ],
342
+ additionalProperties: false,
343
+ },
344
+ strict: true,
345
+ },
346
+ );
347
+ }
348
+ }
349
+ if (web_search) {
350
+ tools.push({ type: "web_search" });
351
+ }
352
+ };
353
+
354
+ const toolToAnthropic = (t) => {
355
+ if (t.type == "function") {
356
+ return {
357
+ name: t.name,
358
+ description: t.description,
359
+ input_schema: {
360
+ type: t.parameters.type,
361
+ properties: t.parameters.properties,
362
+ }
363
+ };
364
+ }
365
+ else if (t.type == "web_search") {
366
+ return {
367
+ type: "web_search_20260318",
368
+ name: "web_search"
369
+ };
370
+ }
371
+ };
372
+
373
+ const useTool = (name, args, argsStr) => {
374
+ console.log("$ " + name + argsStr);
375
+ const output = String(functions[name](args));
376
+ let consoleOutput = output.replaceAll("\r", "<CR>").replaceAll("\n", "<NL>");
377
+ if (name == "readFile" && consoleOutput.length > 80) {
378
+ consoleOutput = consoleOutput.substring(0, 77) + "...";
379
+ }
380
+ console.log(consoleOutput);
381
+ return output;
382
+ };
383
+
384
+ const openai_generateResponseOptionallyUsingTools = async (client, model, input, state) => {
241
385
  let response = await client.responses.create({
242
386
  model,
243
- previous_response_id,
387
+ previous_response_id: state,
244
388
  input,
245
389
  tools,
246
390
  });
247
391
  let used_tools = false;
248
392
 
249
393
  while (true) {
250
- const toolCalls = response.output.filter((item) => item.type === "function_call");
394
+ const toolCalls = response.output.filter((item) => item.type == "function_call");
251
395
  if (toolCalls.length == 0) {
252
396
  break;
253
397
  }
254
398
 
255
399
  const outputs = toolCalls.map((call) => {
256
- console.log("$ " + call.name + call.arguments);
257
- const output = String(functions[call.name](JSON.parse(call.arguments)));
258
- let consoleOutput = output.replaceAll("\r", "<CR>").replaceAll("\n", "<NL>");
259
- if (call.name == "readFile" && consoleOutput.length > 80) {
260
- consoleOutput = consoleOutput.substring(0, 77) + "...";
261
- }
262
- console.log(consoleOutput);
263
400
  return {
264
401
  type: "function_call_output",
265
402
  call_id: call.call_id,
266
- output,
403
+ output: useTool(call.name, JSON.parse(call.arguments), call.arguments)
267
404
  };
268
405
  });
269
406
 
@@ -276,8 +413,59 @@ const generateResponseOptionallyUsingTools = async (client, model, input, previo
276
413
  used_tools = true;
277
414
  }
278
415
 
279
- response.used_tools = used_tools;
280
- return response;
416
+ return {
417
+ output_text: response.output_text,
418
+ state: response.id,
419
+ used_tools,
420
+ };
421
+ };
422
+
423
+ const anthropic_generateResponseOptionallyUsingTools = async (client, model, input, state) => {
424
+ state = JSON.parse(state || "[]");
425
+ state.push({ role: "user", content: input });
426
+ let used_tools = false;
427
+
428
+ const anthropicTools = tools.map(t => toolToAnthropic(t));
429
+
430
+ let message = await client.messages.create({
431
+ model,
432
+ max_tokens: 4096,
433
+ cache_control: { type: "ephemeral" },
434
+ messages: state,
435
+ tools: anthropicTools,
436
+ });
437
+ state.push({ role: message.role, content: message.content });
438
+
439
+ while (message.stop_reason == "tool_use" || message.stop_reason == "max_tokens" || message.stop_reason == "pause_turn") {
440
+ const toolCalls = message.content.filter((item) => item.type == "tool_use");
441
+ if (toolCalls.length) {
442
+ const outputs = toolCalls.map((call) => {
443
+ return {
444
+ type: "tool_result",
445
+ tool_use_id: call.id,
446
+ content: useTool(call.name, call.input, JSON.stringify(call.input))
447
+ };
448
+ });
449
+ state.push({ role: "user", content: outputs });
450
+ used_tools = true;
451
+ }
452
+
453
+ message = await client.messages.create({
454
+ model,
455
+ max_tokens: 4096,
456
+ cache_control: { type: "ephemeral" },
457
+ messages: state,
458
+ tools: anthropicTools,
459
+ });
460
+ state.push({ role: message.role, content: message.content });
461
+ }
462
+
463
+ //console.log(message);
464
+ return {
465
+ output_text: message.content.find((item) => item.type == "text").text,
466
+ state: JSON.stringify(state),
467
+ used_tools,
468
+ };
281
469
  };
282
470
 
283
471
  marked.use(markedTerminal());
@@ -297,9 +485,10 @@ program
297
485
  .description("Vibe coding without the kaboom factor.")
298
486
  .argument("[prompt...]")
299
487
  .option("-c, --continue", "add your previous invocation to the context of this one").option("--cont")
300
- .option("--amend", "retry the previous invocation, using the context from before it")
488
+ .option("-a, --amend", "retry the previous invocation, using the context from before it")
301
489
  .option("-w, --web", "allow the model to search the web")
302
490
  .option("-n, --no-fs", "disable filesystem access").option("--nofs")
491
+ .option("-s, --sandbox", "allow the model to use a docker container")
303
492
  .option("-p, --profile <name>", "profile name", "default")
304
493
  .option("--create-profile <name> <provider> <model>", "create a profile")
305
494
  .option("--delete-profile <name>", "delete a profile") // TODO
@@ -314,7 +503,7 @@ if (options.createProfile) {
314
503
  process.exit(1);
315
504
  }
316
505
  const provider = program.args[0];
317
- if (provider != "openai" && !provider.startsWith("http://") && !provider.startsWith("https://")) {
506
+ if (provider != "openai" && provider != "anthropic" && !provider.startsWith("http://") && !provider.startsWith("https://")) {
318
507
  console.log(`Invalid provider: ${provider}`);
319
508
  process.exit(1);
320
509
  }
@@ -352,22 +541,26 @@ if (!profile) {
352
541
  }
353
542
 
354
543
  let prompt = program.args.join(" ").trim();
355
- let previous_response_id;
544
+ let previous_provider_state;
356
545
  const overrides = [profile.provider.startsWith("http") ? `${profile.model} via ${profile.provider}` : `${profile.provider}/${profile.model}`];
546
+ state.providerStates[profile.provider] ??= {};
357
547
  if (options.continue || options.cont) {
358
- previous_response_id = state.previous_response_id;
548
+ previous_provider_state = state.providerStates[profile.provider].previous;
359
549
  }
360
550
  else if (options.amend) {
361
- previous_response_id = state.previous_previous_response_id;
551
+ previous_provider_state = state.providerStates[profile.provider].previous_previous;
362
552
  }
553
+ const filesystemEnabled = options.fs !== false && !options.nofs;
554
+ initTools(filesystemEnabled, options.sandbox, options.web);
363
555
  if (options.web) {
364
- tools.push({ type: "web_search" });
365
556
  overrides.push("web search enabled");
366
557
  }
367
- if (options.fs == false || options.nofs) {
368
- tools = tools.filter(tool => tool.type != "function");
558
+ if (!filesystemEnabled) {
369
559
  overrides.push("no filesystem access");
370
560
  }
561
+ if (options.sandbox) {
562
+ overrides.push("sandbox enabled");
563
+ }
371
564
  let stdinText = "";
372
565
  if (!process.stdin.isTTY) {
373
566
  const chunks = [];
@@ -381,28 +574,38 @@ if (!prompt) {
381
574
  process.exit(1);
382
575
  }
383
576
 
384
- console.log((previous_response_id ? "FOLLOW-UP" : "PROMPT") + ": " + prompt);
577
+ console.log((previous_provider_state ? "FOLLOW-UP" : "PROMPT") + ": " + prompt);
385
578
  if (stdinText) {
386
579
  overrides.push("stdin provided");
387
580
  prompt = `${stdinText}\n\n${prompt}`;
388
581
  }
389
582
  console.log(`(${overrides.join(", ")})\n`);
390
583
 
391
- //console.log({ model: profile.model, prompt, previous_response_id });
584
+ //console.log({ model: profile.model, prompt, previous_provider_state });
392
585
 
393
586
  const clientOpts = {
394
587
  apiKey: state.apiKeys[profile.provider],
395
588
  };
396
- if (profile.provider != "openai") {
397
- clientOpts.baseURL = profile.provider;
398
- clientOpts.apiKey ||= "dummy";
589
+ let response;
590
+ if (profile.provider == "anthropic") {
591
+ const client = new Anthropic(clientOpts);
592
+ response = await anthropic_generateResponseOptionallyUsingTools(client, profile.model, prompt, previous_provider_state);
593
+ }
594
+ else {
595
+ if (profile.provider != "openai") {
596
+ clientOpts.baseURL = profile.provider;
597
+ clientOpts.apiKey ||= "dummy";
598
+ }
599
+ const client = new OpenAI(clientOpts);
600
+ response = await openai_generateResponseOptionallyUsingTools(client, profile.model, prompt, previous_provider_state);
399
601
  }
400
- const client = new OpenAI(clientOpts);
401
- const response = await generateResponseOptionallyUsingTools(client, profile.model, prompt, previous_response_id);
402
- state.previous_previous_response_id = previous_response_id;
403
- state.previous_response_id = response.id;
602
+ state.providerStates[profile.provider].previous_previous = previous_provider_state;
603
+ state.providerStates[profile.provider].previous = response.state;
404
604
  saveState();
405
605
  if (response.used_tools) {
606
+ if (used_docker) {
607
+ execFileSync("docker", ["rm", "-f", "slave-sandbox"], { encoding: "utf8" });
608
+ }
406
609
  console.log();
407
610
  }
408
611
  console.log(markdownToConsoleText(response.output_text) + "\n");
package/package.json CHANGED
@@ -4,13 +4,15 @@
4
4
  "type": "git",
5
5
  "url": "git+https://github.com/calamity-inc/slave.git"
6
6
  },
7
- "version": "0.2.0",
7
+ "version": "0.3.0",
8
8
  "type": "module",
9
9
  "bin": {
10
10
  "slave": "index.js"
11
11
  },
12
12
  "dependencies": {
13
+ "@anthropic-ai/sdk": "^1.3.0",
13
14
  "commander": "^15.0.0",
15
+ "env-paths": "^4.0.0",
14
16
  "ignore": "^7.0.13",
15
17
  "marked": "^12",
16
18
  "marked-terminal": "7.0.0",
package/.slaveignore DELETED
@@ -1 +0,0 @@
1
- .state.json
Binary file