@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.
- package/README.md +14 -3
- package/index.js +83 -27
- 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>` —
|
|
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
|
-
- `--
|
|
36
|
-
- `--
|
|
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
|
-
|
|
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
|
-
|
|
413
|
+
printJunk("$ " + name + argsStr);
|
|
375
414
|
const output = String(functions[name](args));
|
|
376
|
-
|
|
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:
|
|
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 == "
|
|
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:
|
|
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("--
|
|
495
|
-
.option("--
|
|
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
|
|
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
|
-
|
|
578
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
664
|
+
logWriteBoth("");
|
|
610
665
|
}
|
|
611
666
|
console.log(markdownToConsoleText(response.output_text) + "\n");
|
|
667
|
+
logWrite(response.output_text);
|