@calamity-inc/slave 0.3.0 → 0.3.1

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.
Files changed (3) hide show
  1. package/README.md +14 -3
  2. package/index.js +83 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,10 +30,11 @@ Options:
30
30
  Single-character flags can be combined, so `slave -cw prompt` is equivalent to `slave -c -w prompt`.
31
31
 
32
32
  Subcommands:
33
- - `--create-profile <name> <provider> <model>` — creates a profile
33
+ - `--create-profile <name> <provider> <model>` — create a profile
34
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).
35
- - `--delete-profile <name>` — deletes a profile
36
- - `--update-api-key <provider>` — updates an API Key
35
+ - `--list-profiles` — list all profiles
36
+ - `--delete-profile <name>` — delete a profile
37
+ - `--update-api-key <provider>` — update an API Key
37
38
 
38
39
  ## Security
39
40
 
@@ -46,3 +47,13 @@ The model can't run commands on your system; it only gets limited access to your
46
47
  However, these restrictions are only as good as your practices; you should:
47
48
  - 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.
48
49
  - Review changes to your code before running anything, including tests.
50
+
51
+ ## Logs
52
+
53
+ Each invocation that prompts a model creates a log file:
54
+
55
+ - Linux: `~/.local/state/slave-nodejs`, or `$XDG_STATE_HOME/slave-nodejs` if `XDG_STATE_HOME` is set
56
+ - macOS: `~/Library/Logs/slave-nodejs`
57
+ - Windows: `%LOCALAPPDATA%\slave-nodejs\Log`
58
+
59
+ When a new log file is created, any log file older than 30 days is deleted.
package/index.js CHANGED
@@ -13,8 +13,8 @@ import { marked } from "marked";
13
13
  import { markedTerminal } from "marked-terminal";
14
14
  import OpenAI from "openai";
15
15
 
16
+ const { data: configDir, log: logDir } = envPaths("slave");
16
17
 
17
- const configDir = envPaths("slave").data;
18
18
  const state = fs.existsSync(path.join(configDir, "state.json")) ? JSON.parse(fs.readFileSync(path.join(configDir, "state.json"), "utf8")) : {};
19
19
  state.profiles ??= {};
20
20
  state.apiKeys ??= {};
@@ -31,7 +31,37 @@ const promptForApiKey = async (provider) => {
31
31
  const saveState = () => {
32
32
  fs.mkdirSync(configDir, { recursive: true });
33
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);
34
+ };
35
+
36
+ const rotateLogs = () => {
37
+ const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000; // 30 days
38
+ for (const name of fs.readdirSync(logDir)) {
39
+ try {
40
+ if (fs.statSync(path.join(logDir, name)).mtimeMs < cutoff) {
41
+ fs.unlinkSync(path.join(logDir, name));
42
+ }
43
+ }
44
+ catch (e) {
45
+ // another process may have removed it already; ignore.
46
+ }
47
+ }
48
+ };
49
+
50
+ let logFh;
51
+ const logWrite = (line) => {
52
+ if (!logFh) {
53
+ const now = new Date();
54
+ const pad = (n) => String(n).padStart(2, '0');
55
+ const logfileName = `${now.getUTCFullYear()}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}T${pad(now.getUTCHours())}-${pad(now.getUTCMinutes())}-${pad(now.getUTCSeconds())}Z.txt`;
56
+ fs.mkdirSync(logDir, { recursive: true });
57
+ logFh = fs.openSync(path.join(logDir, logfileName), "a", 0o600);
58
+ rotateLogs();
59
+ }
60
+ fs.writeSync(logFh, line + "\n");
61
+ };
62
+ const logWriteBoth = (line) => {
63
+ console.log(line);
64
+ logWrite(line);
35
65
  };
36
66
 
37
67
  const slaveignore = ignore();
@@ -370,14 +400,19 @@ const toolToAnthropic = (t) => {
370
400
  }
371
401
  };
372
402
 
403
+ const printJunk = (line) => {
404
+ logWrite(line);
405
+ line = line.replaceAll("\r", "<CR>").replaceAll("\n", "<NL>").replaceAll("\t", " ");
406
+ if (line.length > 80) {
407
+ line = line.substring(0, 77) + "...";
408
+ }
409
+ console.log(line);
410
+ }
411
+
373
412
  const useTool = (name, args, argsStr) => {
374
- console.log("$ " + name + argsStr);
413
+ printJunk("$ " + name + argsStr);
375
414
  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);
415
+ printJunk(output);
381
416
  return output;
382
417
  };
383
418
 
@@ -429,14 +464,14 @@ const anthropic_generateResponseOptionallyUsingTools = async (client, model, inp
429
464
 
430
465
  let message = await client.messages.create({
431
466
  model,
432
- max_tokens: 4096,
467
+ max_tokens: 20000,
433
468
  cache_control: { type: "ephemeral" },
434
469
  messages: state,
435
470
  tools: anthropicTools,
436
471
  });
437
472
  state.push({ role: message.role, content: message.content });
438
473
 
439
- while (message.stop_reason == "tool_use" || message.stop_reason == "max_tokens" || message.stop_reason == "pause_turn") {
474
+ while (message.stop_reason == "tool_use" || message.stop_reason == "pause_turn") {
440
475
  const toolCalls = message.content.filter((item) => item.type == "tool_use");
441
476
  if (toolCalls.length) {
442
477
  const outputs = toolCalls.map((call) => {
@@ -450,9 +485,13 @@ const anthropic_generateResponseOptionallyUsingTools = async (client, model, inp
450
485
  used_tools = true;
451
486
  }
452
487
 
488
+ /*if (message.stop_reason == "max_tokens" && message.content[message.content.length].type == "thinking") {
489
+ state.push({ role: "user", content: "Please continue from where you left off." });
490
+ }*/
491
+
453
492
  message = await client.messages.create({
454
493
  model,
455
- max_tokens: 4096,
494
+ max_tokens: 20000,
456
495
  cache_control: { type: "ephemeral" },
457
496
  messages: state,
458
497
  tools: anthropicTools,
@@ -460,6 +499,12 @@ const anthropic_generateResponseOptionallyUsingTools = async (client, model, inp
460
499
  state.push({ role: message.role, content: message.content });
461
500
  }
462
501
 
502
+ if (message.stop_reason != "end_turn") {
503
+ logWrite(`unexpected stop_reason: ${message.stop_reason}\n${JSON.stringify(message, null, 2)}`);
504
+ console.log(message);
505
+ throw new Error(`unexpected stop_reason: ${message.stop_reason}`);
506
+ }
507
+
463
508
  //console.log(message);
464
509
  return {
465
510
  output_text: message.content.find((item) => item.type == "text").text,
@@ -477,6 +522,9 @@ const markdownToConsoleText = (md) => {
477
522
  return text;
478
523
  };
479
524
 
525
+ const profileToString = (profile) => {
526
+ return profile.provider.startsWith("http") ? `${profile.model} via ${profile.provider}` : `${profile.provider}/${profile.model}`;
527
+ };
480
528
 
481
529
  const program = new Command();
482
530
  program
@@ -491,8 +539,9 @@ program
491
539
  .option("-s, --sandbox", "allow the model to use a docker container")
492
540
  .option("-p, --profile <name>", "profile name", "default")
493
541
  .option("--create-profile <name> <provider> <model>", "create a profile")
494
- .option("--delete-profile <name>", "delete a profile") // TODO
495
- .option("--update-api-key <provider>", "update an API key") // TODO
542
+ .option("--list-profiles", "list all profiles")
543
+ .option("--delete-profile <name>", "delete a profile")
544
+ .option("--update-api-key <provider>", "update an API key")
496
545
  .helpOption("-?, --help", "display help")
497
546
  .parse();
498
547
  const options = program.opts();
@@ -516,6 +565,16 @@ if (options.createProfile) {
516
565
  console.log(`${isCreate ? "Created" : "Updated"} ${options.createProfile} profile.`);
517
566
  process.exit(0);
518
567
  }
568
+ if (options.listProfiles) {
569
+ const names = Object.keys(state.profiles);
570
+ if (names.length == 0) {
571
+ console.log("No profiles.");
572
+ }
573
+ for (const name of names) {
574
+ console.log(`${name}: ${profileToString(state.profiles[name])}`);
575
+ }
576
+ process.exit(0);
577
+ }
519
578
  if (options.deleteProfile) {
520
579
  delete state.profiles[options.deleteProfile];
521
580
  saveState();
@@ -542,7 +601,7 @@ if (!profile) {
542
601
 
543
602
  let prompt = program.args.join(" ").trim();
544
603
  let previous_provider_state;
545
- const overrides = [profile.provider.startsWith("http") ? `${profile.model} via ${profile.provider}` : `${profile.provider}/${profile.model}`];
604
+ const overrides = [profileToString(profile)];
546
605
  state.providerStates[profile.provider] ??= {};
547
606
  if (options.continue || options.cont) {
548
607
  previous_provider_state = state.providerStates[profile.provider].previous;
@@ -561,25 +620,21 @@ if (!filesystemEnabled) {
561
620
  if (options.sandbox) {
562
621
  overrides.push("sandbox enabled");
563
622
  }
564
- let stdinText = "";
565
- if (!process.stdin.isTTY) {
566
- const chunks = [];
567
- for await (const chunk of process.stdin) {
568
- chunks.push(chunk);
569
- }
570
- stdinText = Buffer.concat(chunks).toString("utf8");
571
- }
572
623
  if (!prompt) {
573
624
  console.log("No prompt given.");
574
625
  process.exit(1);
575
626
  }
576
627
 
577
- console.log((previous_provider_state ? "FOLLOW-UP" : "PROMPT") + ": " + prompt);
578
- if (stdinText) {
628
+ logWriteBoth((previous_provider_state ? "FOLLOW-UP" : "PROMPT") + ": " + prompt);
629
+ if (!process.stdin.isTTY) {
630
+ const chunks = [];
631
+ for await (const chunk of process.stdin) {
632
+ chunks.push(chunk);
633
+ }
634
+ prompt = `${Buffer.concat(chunks).toString("utf8")}\n\n${prompt}`;
579
635
  overrides.push("stdin provided");
580
- prompt = `${stdinText}\n\n${prompt}`;
581
636
  }
582
- console.log(`(${overrides.join(", ")})\n`);
637
+ logWriteBoth(`(${overrides.join(", ")})\n`);
583
638
 
584
639
  //console.log({ model: profile.model, prompt, previous_provider_state });
585
640
 
@@ -606,6 +661,7 @@ if (response.used_tools) {
606
661
  if (used_docker) {
607
662
  execFileSync("docker", ["rm", "-f", "slave-sandbox"], { encoding: "utf8" });
608
663
  }
609
- console.log();
664
+ logWriteBoth("");
610
665
  }
611
666
  console.log(markdownToConsoleText(response.output_text) + "\n");
667
+ logWrite(response.output_text);
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.3.0",
7
+ "version": "0.3.1",
8
8
  "type": "module",
9
9
  "bin": {
10
10
  "slave": "index.js"