@calamity-inc/slave 0.3.2 → 0.4.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 +6 -2
- package/package.json +3 -3
- package/{index.js → src/core.js} +480 -679
- package/src/log.js +38 -0
- package/src/main.js +207 -0
- package/src/markdown.js +13 -0
- package/src/paths.js +9 -0
- package/src/state.js +32 -0
package/{index.js → src/core.js}
RENAMED
|
@@ -1,679 +1,480 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
path
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
path
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (!
|
|
179
|
-
return "
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
},
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
tools,
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
state.push({ role: message.role, content: message.content });
|
|
482
|
-
|
|
483
|
-
while (message.stop_reason == "tool_use" || message.stop_reason == "pause_turn") {
|
|
484
|
-
const toolCalls = message.content.filter((item) => item.type == "tool_use");
|
|
485
|
-
if (toolCalls.length) {
|
|
486
|
-
const outputs = toolCalls.map((call) => {
|
|
487
|
-
return {
|
|
488
|
-
type: "tool_result",
|
|
489
|
-
tool_use_id: call.id,
|
|
490
|
-
content: useTool(call.name, call.input, JSON.stringify(call.input))
|
|
491
|
-
};
|
|
492
|
-
});
|
|
493
|
-
state.push({ role: "user", content: outputs });
|
|
494
|
-
used_tools = true;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/*if (message.stop_reason == "max_tokens" && message.content[message.content.length].type == "thinking") {
|
|
498
|
-
state.push({ role: "user", content: "Please continue from where you left off." });
|
|
499
|
-
}*/
|
|
500
|
-
|
|
501
|
-
message = await client.messages.create({
|
|
502
|
-
model,
|
|
503
|
-
max_tokens: 20000,
|
|
504
|
-
cache_control: { type: "ephemeral" },
|
|
505
|
-
messages: state,
|
|
506
|
-
tools: anthropicTools,
|
|
507
|
-
});
|
|
508
|
-
state.push({ role: message.role, content: message.content });
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (message.stop_reason != "end_turn") {
|
|
512
|
-
logWrite(`unexpected stop_reason: ${message.stop_reason}\n${JSON.stringify(message, null, 2)}`);
|
|
513
|
-
console.log(message);
|
|
514
|
-
throw new Error(`unexpected stop_reason: ${message.stop_reason}`);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
//console.log(message);
|
|
518
|
-
return {
|
|
519
|
-
output_text: message.content.find((item) => item.type == "text").text,
|
|
520
|
-
state: JSON.stringify(state),
|
|
521
|
-
used_tools,
|
|
522
|
-
};
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
marked.use(markedTerminal());
|
|
526
|
-
const markdownToConsoleText = (md) => {
|
|
527
|
-
const _console_error = console.error; // prevent unintended output about unknown languages, e.g. "```env\ndeez\n```\n"
|
|
528
|
-
console.error = undefined;
|
|
529
|
-
const text = marked.parse(md).trimEnd() + "\n";
|
|
530
|
-
console.error = _console_error;
|
|
531
|
-
return text;
|
|
532
|
-
};
|
|
533
|
-
|
|
534
|
-
const profileToString = (profile) => {
|
|
535
|
-
return profile.provider.startsWith("http") ? `${profile.model} via ${profile.provider}` : `${profile.provider}/${profile.model}`;
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
const program = new Command();
|
|
539
|
-
program
|
|
540
|
-
.name("slave")
|
|
541
|
-
.usage("[options] [--] [prompt...]")
|
|
542
|
-
.description("Vibe coding without the kaboom factor.")
|
|
543
|
-
.argument("[prompt...]")
|
|
544
|
-
.option("-c, --continue", "add your previous invocation to the context of this one").option("--cont")
|
|
545
|
-
.option("-a, --amend", "retry the previous invocation, using the context from before it")
|
|
546
|
-
.option("-w, --web", "allow the model to search the web")
|
|
547
|
-
.option("-n, --no-fs", "disable filesystem access").option("--nofs")
|
|
548
|
-
.option("-s, --sandbox", "allow the model to use a docker container")
|
|
549
|
-
.option("-p, --profile <name>", "profile name", "default")
|
|
550
|
-
.option("--create-profile <name> <provider> <model>", "create a profile")
|
|
551
|
-
.option("--list-profiles", "list all profiles")
|
|
552
|
-
.option("--delete-profile <name>", "delete a profile")
|
|
553
|
-
.option("--update-api-key <provider>", "update an API key")
|
|
554
|
-
.helpOption("-?, --help", "display help")
|
|
555
|
-
.parse();
|
|
556
|
-
const options = program.opts();
|
|
557
|
-
|
|
558
|
-
if (options.createProfile) {
|
|
559
|
-
if (!program.args[0] || !program.args[1]) {
|
|
560
|
-
console.log("Syntax: slave --create-profile <name> <provider> <model>");
|
|
561
|
-
process.exit(1);
|
|
562
|
-
}
|
|
563
|
-
const provider = program.args[0];
|
|
564
|
-
if (provider != "openai" && provider != "anthropic" && !provider.startsWith("http://") && !provider.startsWith("https://")) {
|
|
565
|
-
console.log(`Invalid provider: ${provider}`);
|
|
566
|
-
process.exit(1);
|
|
567
|
-
}
|
|
568
|
-
const isCreate = !(options.createProfile in state.profiles);
|
|
569
|
-
state.profiles[options.createProfile] = { provider, model: program.args[1] };
|
|
570
|
-
if (!state.apiKeys[provider]) {
|
|
571
|
-
await promptForApiKey(provider);
|
|
572
|
-
}
|
|
573
|
-
saveState();
|
|
574
|
-
console.log(`${isCreate ? "Created" : "Updated"} ${options.createProfile} profile.`);
|
|
575
|
-
process.exit(0);
|
|
576
|
-
}
|
|
577
|
-
if (options.listProfiles) {
|
|
578
|
-
const names = Object.keys(state.profiles);
|
|
579
|
-
if (names.length == 0) {
|
|
580
|
-
console.log("No profiles.");
|
|
581
|
-
}
|
|
582
|
-
for (const name of names) {
|
|
583
|
-
console.log(`${name}: ${profileToString(state.profiles[name])}`);
|
|
584
|
-
}
|
|
585
|
-
process.exit(0);
|
|
586
|
-
}
|
|
587
|
-
if (options.deleteProfile) {
|
|
588
|
-
delete state.profiles[options.deleteProfile];
|
|
589
|
-
saveState();
|
|
590
|
-
console.log(`Deleted ${options.deleteProfile} profile.`);
|
|
591
|
-
process.exit(0);
|
|
592
|
-
}
|
|
593
|
-
if (options.updateApiKey) {
|
|
594
|
-
await promptForApiKey(options.updateApiKey);
|
|
595
|
-
saveState();
|
|
596
|
-
console.log("Saved.");
|
|
597
|
-
process.exit(0);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
const profile = state.profiles[options.profile];
|
|
601
|
-
if (!profile) {
|
|
602
|
-
if (options.profile == "default") {
|
|
603
|
-
console.log(markdownToConsoleText("You did not specify a profile, but a default profile does not exist. Use e.g. `slave --create-profile default openai gpt-6-luna` to create one."));
|
|
604
|
-
}
|
|
605
|
-
else {
|
|
606
|
-
console.log(markdownToConsoleText(`No such profile. Use e.g. \`slave --create-profile ${options.profile} openai gpt-6-luna\` to create it.`));
|
|
607
|
-
}
|
|
608
|
-
process.exit(1);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
let prompt = program.args.join(" ").trim();
|
|
612
|
-
let previous_provider_state;
|
|
613
|
-
const overrides = [profileToString(profile)];
|
|
614
|
-
state.providerStates[profile.provider] ??= {};
|
|
615
|
-
if (options.continue || options.cont) {
|
|
616
|
-
previous_provider_state = state.providerStates[profile.provider].previous;
|
|
617
|
-
sandbox_name = state.providerStates[profile.provider].sandbox_name;
|
|
618
|
-
}
|
|
619
|
-
else if (options.amend) {
|
|
620
|
-
previous_provider_state = state.providerStates[profile.provider].previous_previous;
|
|
621
|
-
sandbox_name = state.providerStates[profile.provider].sandbox_name;
|
|
622
|
-
}
|
|
623
|
-
const filesystemEnabled = options.fs !== false && !options.nofs;
|
|
624
|
-
initTools(filesystemEnabled, options.sandbox, options.web);
|
|
625
|
-
if (options.web) {
|
|
626
|
-
overrides.push("web search enabled");
|
|
627
|
-
}
|
|
628
|
-
if (!filesystemEnabled) {
|
|
629
|
-
overrides.push("no filesystem access");
|
|
630
|
-
}
|
|
631
|
-
if (options.sandbox) {
|
|
632
|
-
overrides.push("sandbox enabled");
|
|
633
|
-
}
|
|
634
|
-
if (!prompt) {
|
|
635
|
-
console.log("No prompt given.");
|
|
636
|
-
process.exit(1);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
logWriteBoth((previous_provider_state ? "FOLLOW-UP" : "PROMPT") + ": " + prompt);
|
|
640
|
-
if (!process.stdin.isTTY) {
|
|
641
|
-
const chunks = [];
|
|
642
|
-
for await (const chunk of process.stdin) {
|
|
643
|
-
chunks.push(chunk);
|
|
644
|
-
}
|
|
645
|
-
prompt = `${Buffer.concat(chunks).toString("utf8")}\n\n${prompt}`;
|
|
646
|
-
overrides.push("stdin provided");
|
|
647
|
-
}
|
|
648
|
-
logWriteBoth(`(${overrides.join(", ")})\n`);
|
|
649
|
-
|
|
650
|
-
//console.log({ model: profile.model, prompt, previous_provider_state });
|
|
651
|
-
|
|
652
|
-
const clientOpts = {
|
|
653
|
-
apiKey: state.apiKeys[profile.provider],
|
|
654
|
-
};
|
|
655
|
-
let response;
|
|
656
|
-
if (profile.provider == "anthropic") {
|
|
657
|
-
const client = new Anthropic(clientOpts);
|
|
658
|
-
response = await anthropic_generateResponseOptionallyUsingTools(client, profile.model, prompt, previous_provider_state);
|
|
659
|
-
}
|
|
660
|
-
else {
|
|
661
|
-
if (profile.provider != "openai") {
|
|
662
|
-
clientOpts.baseURL = profile.provider;
|
|
663
|
-
clientOpts.apiKey ||= "dummy";
|
|
664
|
-
}
|
|
665
|
-
const client = new OpenAI(clientOpts);
|
|
666
|
-
response = await openai_generateResponseOptionallyUsingTools(client, profile.model, prompt, previous_provider_state);
|
|
667
|
-
}
|
|
668
|
-
state.providerStates[profile.provider].previous_previous = previous_provider_state;
|
|
669
|
-
state.providerStates[profile.provider].previous = response.state;
|
|
670
|
-
if (state.providerStates[profile.provider].sandbox_name && state.providerStates[profile.provider].sandbox_name != sandbox_name) {
|
|
671
|
-
execFileSync("docker", ["rm", "-f", state.providerStates[profile.provider].sandbox_name], { encoding: "utf8" });
|
|
672
|
-
}
|
|
673
|
-
state.providerStates[profile.provider].sandbox_name = sandbox_name;
|
|
674
|
-
saveState();
|
|
675
|
-
if (response.used_tools) {
|
|
676
|
-
logWriteBoth("");
|
|
677
|
-
}
|
|
678
|
-
console.log(markdownToConsoleText(response.output_text) + "\n");
|
|
679
|
-
logWrite(response.output_text);
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { execFileSync, spawnSync } = require("node:child_process");
|
|
4
|
+
const { randomInt } = require("node:crypto");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
|
|
8
|
+
const ignore = require("ignore");
|
|
9
|
+
|
|
10
|
+
const { logWrite } = require("./log.js");
|
|
11
|
+
|
|
12
|
+
const slaveignore = ignore();
|
|
13
|
+
if (fs.existsSync(".slaveignore")) {
|
|
14
|
+
slaveignore.add(fs.readFileSync(".slaveignore", "utf8").replaceAll("/\n", "\n"));
|
|
15
|
+
}
|
|
16
|
+
const gitignore = ignore();
|
|
17
|
+
if (fs.existsSync(".gitignore")) {
|
|
18
|
+
gitignore.add(fs.readFileSync(".gitignore", "utf8").replaceAll("/\n", "\n"));
|
|
19
|
+
}
|
|
20
|
+
const isOutOfReach = (p, accessType) => {
|
|
21
|
+
const rel = path.relative(".", p);
|
|
22
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
const segs = rel.split(path.sep);
|
|
26
|
+
if (segs.includes(".env") || segs.includes(".git")) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (rel && (slaveignore.ignores(rel) || (accessType == "write" && gitignore.ignores(rel)))) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const fixPath = (path, accessType) => {
|
|
36
|
+
if (path.startsWith("/")) {
|
|
37
|
+
path = path.substring(1);
|
|
38
|
+
path ||= ".";
|
|
39
|
+
}
|
|
40
|
+
return isOutOfReach(path, accessType) ? "" : path;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const readFileForModel = (filePath) => fs.readFileSync(filePath, "utf8").replaceAll("\r\n", "\n");
|
|
44
|
+
const writeFileFromModel = (filePath, data, usesCRLF) => {
|
|
45
|
+
if (fs.existsSync(filePath)) {
|
|
46
|
+
if (usesCRLF === undefined) {
|
|
47
|
+
usesCRLF = fs.readFileSync(filePath, "utf8").includes("\r\n");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
const normalized = data.replaceAll("\r\n", "\n");
|
|
54
|
+
fs.writeFileSync(filePath, usesCRLF ? normalized.replaceAll("\n", "\r\n") : normalized, "utf8");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
let sandbox_name;
|
|
58
|
+
const getSandboxName = () => {
|
|
59
|
+
return sandbox_name;
|
|
60
|
+
};
|
|
61
|
+
const setSandboxName = (sn) => {
|
|
62
|
+
sandbox_name = sn;
|
|
63
|
+
};
|
|
64
|
+
const ensureSandbox = () => {
|
|
65
|
+
if (!sandbox_name) {
|
|
66
|
+
sandbox_name = "slave-sandbox-" + Array.from({ length: 10 }, () => "abcdefghijklmnopqrstuvwxyz0123456789"[randomInt(36)]).join("");
|
|
67
|
+
// `docker pull ghcr.io/calamity-inc/slave-sandbox` is not needed to get the image, but would be needed to update it.
|
|
68
|
+
execFileSync("docker", ["run", "-d", "--name", sandbox_name, "ghcr.io/calamity-inc/slave-sandbox", "sleep", "infinity"], { encoding: "utf8" });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const functions = {
|
|
73
|
+
listFiles: ({ path }) => {
|
|
74
|
+
path ??= ".";
|
|
75
|
+
path = fixPath(path);
|
|
76
|
+
if (!path) {
|
|
77
|
+
return "Out of your reach";
|
|
78
|
+
}
|
|
79
|
+
let files;
|
|
80
|
+
try {
|
|
81
|
+
files = fs.readdirSync(path, { withFileTypes: true });
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
return e.code;
|
|
85
|
+
}
|
|
86
|
+
return JSON.stringify(files.map(dirent => dirent.isDirectory() ? `${dirent.name}/` : dirent.name));
|
|
87
|
+
},
|
|
88
|
+
findFiles: ({ path, substring, recursive }) => {
|
|
89
|
+
path ??= ".";
|
|
90
|
+
path = fixPath(path);
|
|
91
|
+
if (!path) {
|
|
92
|
+
return "Out of your reach";
|
|
93
|
+
}
|
|
94
|
+
let files;
|
|
95
|
+
try {
|
|
96
|
+
files = fs.readdirSync(path, { withFileTypes: true, recursive });
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
return e.code;
|
|
100
|
+
}
|
|
101
|
+
files = files.filter(dirent => dirent.isFile())
|
|
102
|
+
.map(dirent => {
|
|
103
|
+
if (dirent.parentPath != ".") {
|
|
104
|
+
return `${dirent.parentPath.replaceAll("\\", "/")}/${dirent.name}`
|
|
105
|
+
}
|
|
106
|
+
return dirent.name;
|
|
107
|
+
})
|
|
108
|
+
.filter(file => !isOutOfReach(file));
|
|
109
|
+
const res = [];
|
|
110
|
+
for (const file of files) {
|
|
111
|
+
try {
|
|
112
|
+
const matches = readFileForModel(file).split(substring).length - 1;
|
|
113
|
+
if (matches) {
|
|
114
|
+
res.push(`${file} has ${matches} match${matches == 1 ? "" : "es"}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
// file might be busy, skip it
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return res.join("\n");
|
|
122
|
+
},
|
|
123
|
+
readFile: ({ path }) => {
|
|
124
|
+
path = fixPath(path);
|
|
125
|
+
if (!path) {
|
|
126
|
+
return "You don't have read access to this file";
|
|
127
|
+
}
|
|
128
|
+
if (!fs.existsSync(path) || !fs.statSync(path).isFile()) {
|
|
129
|
+
return "No such file";
|
|
130
|
+
}
|
|
131
|
+
const cont = readFileForModel(path);
|
|
132
|
+
if (cont.length > 10485760) {
|
|
133
|
+
return "File is over 10485760 characters long";
|
|
134
|
+
}
|
|
135
|
+
return cont;
|
|
136
|
+
},
|
|
137
|
+
writeFile: ({ path, data }) => {
|
|
138
|
+
path = fixPath(path, "write");
|
|
139
|
+
if (!path) {
|
|
140
|
+
return "You don't have write access to this file";
|
|
141
|
+
}
|
|
142
|
+
writeFileFromModel(path, data);
|
|
143
|
+
return "OK";
|
|
144
|
+
},
|
|
145
|
+
fileFindAndReplace: ({ path, substring, replacement, expectedMatches }) => {
|
|
146
|
+
path = fixPath(path, "write");
|
|
147
|
+
if (!path) {
|
|
148
|
+
return "You don't have write access to this file";
|
|
149
|
+
}
|
|
150
|
+
const fileContent = fs.readFileSync(path, "utf8");
|
|
151
|
+
const usesCRLF = fileContent.includes("\r\n");
|
|
152
|
+
const arr = fileContent.replaceAll("\r\n", "\n").split(substring);
|
|
153
|
+
const matches = arr.length - 1;
|
|
154
|
+
if (matches == 0 || (expectedMatches && matches != expectedMatches)) {
|
|
155
|
+
return `There were ${matches} match${matches == 1 ? "" : "es"}`;
|
|
156
|
+
}
|
|
157
|
+
writeFileFromModel(path, arr.join(replacement), usesCRLF);
|
|
158
|
+
return "OK";
|
|
159
|
+
},
|
|
160
|
+
sandboxExec: ({ command, args }) => {
|
|
161
|
+
ensureSandbox();
|
|
162
|
+
args ??= [];
|
|
163
|
+
if (typeof args == "string") {
|
|
164
|
+
args = JSON.parse(args);
|
|
165
|
+
}
|
|
166
|
+
const result = spawnSync("docker", [ "exec", sandbox_name, command, ...args ], { encoding: "utf8" });
|
|
167
|
+
const output = (result.stdout ?? "") + (result.stderr ?? "");
|
|
168
|
+
if (result.error) {
|
|
169
|
+
return (output || result.error.message).trimEnd();
|
|
170
|
+
}
|
|
171
|
+
if (result.status !== 0 && !output) {
|
|
172
|
+
return `Command exited with status ${result.status}`;
|
|
173
|
+
}
|
|
174
|
+
return output;
|
|
175
|
+
},
|
|
176
|
+
sandboxCopyIn: ({ hostPath, sbxPath }) => {
|
|
177
|
+
hostPath = fixPath(hostPath);
|
|
178
|
+
if (!hostPath) {
|
|
179
|
+
return "You don't have read access to this file";
|
|
180
|
+
}
|
|
181
|
+
if (!fs.existsSync(hostPath) || !fs.statSync(hostPath).isFile()) {
|
|
182
|
+
return "Host path must point to a file";
|
|
183
|
+
}
|
|
184
|
+
ensureSandbox();
|
|
185
|
+
try {
|
|
186
|
+
execFileSync("docker", ["cp", "-L", hostPath, `${sandbox_name}:${sbxPath}`], { encoding: "utf8" });
|
|
187
|
+
return "OK";
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
return (e.stdout ?? e.message).trimEnd();
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
sandboxCopyOut: ({ sbxPath, hostPath }) => {
|
|
194
|
+
hostPath = fixPath(hostPath, "write");
|
|
195
|
+
if (!hostPath) {
|
|
196
|
+
return "You don't have write access to this file";
|
|
197
|
+
}
|
|
198
|
+
if (fs.existsSync(hostPath) && !fs.statSync(hostPath).isFile()) {
|
|
199
|
+
return "Host path must point to a file";
|
|
200
|
+
}
|
|
201
|
+
ensureSandbox();
|
|
202
|
+
execFileSync("docker", ["cp", "-L", `${sandbox_name}:${sbxPath}`, hostPath], { encoding: "utf8" });
|
|
203
|
+
return "OK";
|
|
204
|
+
},
|
|
205
|
+
/*system: ({ command }) => {
|
|
206
|
+
//process.exit(1);
|
|
207
|
+
return "It's interesting that you called this tool! Unfortunately for you, it does nothing.";
|
|
208
|
+
}*/
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const tools = [];
|
|
212
|
+
const initTools = (fs, sandbox, web_search) => {
|
|
213
|
+
if (fs) {
|
|
214
|
+
tools.push(
|
|
215
|
+
{
|
|
216
|
+
type: "function",
|
|
217
|
+
name: "listFiles",
|
|
218
|
+
parameters: {
|
|
219
|
+
type: "object",
|
|
220
|
+
properties: {
|
|
221
|
+
path: { type: ["string", "null"] },
|
|
222
|
+
},
|
|
223
|
+
required: [ "path" ],
|
|
224
|
+
additionalProperties: false,
|
|
225
|
+
},
|
|
226
|
+
strict: true,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
type: "function",
|
|
230
|
+
name: "findFiles",
|
|
231
|
+
parameters: {
|
|
232
|
+
type: "object",
|
|
233
|
+
properties: {
|
|
234
|
+
path: { type: ["string", "null"] },
|
|
235
|
+
substring: { type: "string" },
|
|
236
|
+
recursive: { type: ["boolean", "null"] },
|
|
237
|
+
},
|
|
238
|
+
required: [ "path", "substring", "recursive" ],
|
|
239
|
+
additionalProperties: false,
|
|
240
|
+
},
|
|
241
|
+
strict: true,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
type: "function",
|
|
245
|
+
name: "readFile",
|
|
246
|
+
parameters: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
path: { type: "string" },
|
|
250
|
+
},
|
|
251
|
+
required: [ "path" ],
|
|
252
|
+
additionalProperties: false,
|
|
253
|
+
},
|
|
254
|
+
strict: true,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
type: "function",
|
|
258
|
+
name: "writeFile",
|
|
259
|
+
parameters: {
|
|
260
|
+
type: "object",
|
|
261
|
+
properties: {
|
|
262
|
+
path: { type: "string" },
|
|
263
|
+
data: { type: "string" },
|
|
264
|
+
},
|
|
265
|
+
required: [ "path", "data" ],
|
|
266
|
+
additionalProperties: false,
|
|
267
|
+
},
|
|
268
|
+
strict: true,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
type: "function",
|
|
272
|
+
name: "fileFindAndReplace",
|
|
273
|
+
parameters: {
|
|
274
|
+
type: "object",
|
|
275
|
+
properties: {
|
|
276
|
+
path: { type: "string" },
|
|
277
|
+
substring: { type: "string" },
|
|
278
|
+
replacement: { type: "string" },
|
|
279
|
+
expectedMatches: { type: ["number", "null"] },
|
|
280
|
+
},
|
|
281
|
+
required: [ "path", "substring", "replacement", "expectedMatches" ],
|
|
282
|
+
additionalProperties: false,
|
|
283
|
+
},
|
|
284
|
+
strict: true,
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (sandbox) {
|
|
289
|
+
tools.push({
|
|
290
|
+
type: "function",
|
|
291
|
+
name: "sandboxExec",
|
|
292
|
+
description: "Runs a command without Bash in an isolated sandbox.",
|
|
293
|
+
parameters: {
|
|
294
|
+
type: "object",
|
|
295
|
+
properties: {
|
|
296
|
+
command: { type: "string" },
|
|
297
|
+
args: { type: "array", items: { type: "string" } },
|
|
298
|
+
},
|
|
299
|
+
required: [ "command", "args" ],
|
|
300
|
+
additionalProperties: false,
|
|
301
|
+
},
|
|
302
|
+
strict: true,
|
|
303
|
+
});
|
|
304
|
+
if (fs) {
|
|
305
|
+
tools.push(
|
|
306
|
+
{
|
|
307
|
+
type: "function",
|
|
308
|
+
name: "sandboxCopyIn",
|
|
309
|
+
parameters: {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {
|
|
312
|
+
hostPath: { type: "string" },
|
|
313
|
+
sbxPath: { type: "string" },
|
|
314
|
+
},
|
|
315
|
+
required: [ "hostPath", "sbxPath" ],
|
|
316
|
+
additionalProperties: false,
|
|
317
|
+
},
|
|
318
|
+
strict: true,
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
type: "function",
|
|
322
|
+
name: "sandboxCopyOut",
|
|
323
|
+
parameters: {
|
|
324
|
+
type: "object",
|
|
325
|
+
properties: {
|
|
326
|
+
sbxPath: { type: "string" },
|
|
327
|
+
hostPath: { type: "string" },
|
|
328
|
+
},
|
|
329
|
+
required: [ "sbxPath", "hostPath" ],
|
|
330
|
+
additionalProperties: false,
|
|
331
|
+
},
|
|
332
|
+
strict: true,
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (web_search) {
|
|
338
|
+
tools.push({ type: "web_search" });
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const toolToAnthropic = (t) => {
|
|
343
|
+
if (t.type == "function") {
|
|
344
|
+
return {
|
|
345
|
+
name: t.name,
|
|
346
|
+
description: t.description,
|
|
347
|
+
input_schema: {
|
|
348
|
+
type: t.parameters.type,
|
|
349
|
+
properties: t.parameters.properties,
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
else if (t.type == "web_search") {
|
|
354
|
+
return {
|
|
355
|
+
type: "web_search_20260318",
|
|
356
|
+
name: "web_search"
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const printJunk = (line) => {
|
|
362
|
+
logWrite(line);
|
|
363
|
+
line = line.replaceAll("\r", "<CR>").replaceAll("\n", "<NL>").replaceAll("\t", " ");
|
|
364
|
+
if (line.length > 80) {
|
|
365
|
+
line = line.substring(0, 77) + "...";
|
|
366
|
+
}
|
|
367
|
+
console.log(line);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const useTool = (name, args, argsStr) => {
|
|
371
|
+
printJunk("$ " + name + argsStr);
|
|
372
|
+
const output = String(functions[name](args));
|
|
373
|
+
printJunk(output);
|
|
374
|
+
return output;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const openai_generateResponseOptionallyUsingTools = async (client, model, input, state) => {
|
|
378
|
+
let response = await client.responses.create({
|
|
379
|
+
model,
|
|
380
|
+
previous_response_id: state,
|
|
381
|
+
input,
|
|
382
|
+
tools,
|
|
383
|
+
});
|
|
384
|
+
let used_tools = false;
|
|
385
|
+
|
|
386
|
+
while (true) {
|
|
387
|
+
const toolCalls = response.output.filter((item) => item.type == "function_call");
|
|
388
|
+
if (toolCalls.length == 0) {
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const outputs = toolCalls.map((call) => {
|
|
393
|
+
return {
|
|
394
|
+
type: "function_call_output",
|
|
395
|
+
call_id: call.call_id,
|
|
396
|
+
output: useTool(call.name, JSON.parse(call.arguments), call.arguments)
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
response = await client.responses.create({
|
|
401
|
+
model,
|
|
402
|
+
previous_response_id: response.id,
|
|
403
|
+
input: outputs,
|
|
404
|
+
tools,
|
|
405
|
+
});
|
|
406
|
+
used_tools = true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
output_text: response.output_text,
|
|
411
|
+
state: response.id,
|
|
412
|
+
used_tools,
|
|
413
|
+
};
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const anthropic_generateResponseOptionallyUsingTools = async (client, model, input, state) => {
|
|
417
|
+
state = JSON.parse(state || "[]");
|
|
418
|
+
state.push({ role: "user", content: input });
|
|
419
|
+
let used_tools = false;
|
|
420
|
+
|
|
421
|
+
const anthropicTools = tools.map(t => toolToAnthropic(t));
|
|
422
|
+
|
|
423
|
+
let message = await client.messages.create({
|
|
424
|
+
model,
|
|
425
|
+
max_tokens: 20000,
|
|
426
|
+
cache_control: { type: "ephemeral" },
|
|
427
|
+
messages: state,
|
|
428
|
+
tools: anthropicTools,
|
|
429
|
+
});
|
|
430
|
+
state.push({ role: message.role, content: message.content });
|
|
431
|
+
|
|
432
|
+
while (message.stop_reason == "tool_use" || message.stop_reason == "pause_turn") {
|
|
433
|
+
const toolCalls = message.content.filter((item) => item.type == "tool_use");
|
|
434
|
+
if (toolCalls.length) {
|
|
435
|
+
const outputs = toolCalls.map((call) => {
|
|
436
|
+
return {
|
|
437
|
+
type: "tool_result",
|
|
438
|
+
tool_use_id: call.id,
|
|
439
|
+
content: useTool(call.name, call.input, JSON.stringify(call.input))
|
|
440
|
+
};
|
|
441
|
+
});
|
|
442
|
+
state.push({ role: "user", content: outputs });
|
|
443
|
+
used_tools = true;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/*if (message.stop_reason == "max_tokens" && message.content[message.content.length].type == "thinking") {
|
|
447
|
+
state.push({ role: "user", content: "Please continue from where you left off." });
|
|
448
|
+
}*/
|
|
449
|
+
|
|
450
|
+
message = await client.messages.create({
|
|
451
|
+
model,
|
|
452
|
+
max_tokens: 20000,
|
|
453
|
+
cache_control: { type: "ephemeral" },
|
|
454
|
+
messages: state,
|
|
455
|
+
tools: anthropicTools,
|
|
456
|
+
});
|
|
457
|
+
state.push({ role: message.role, content: message.content });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (message.stop_reason != "end_turn") {
|
|
461
|
+
logWrite(`unexpected stop_reason: ${message.stop_reason}\n${JSON.stringify(message, null, 2)}`);
|
|
462
|
+
console.log(message);
|
|
463
|
+
throw new Error(`unexpected stop_reason: ${message.stop_reason}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
//console.log(message);
|
|
467
|
+
return {
|
|
468
|
+
output_text: message.content.find((item) => item.type == "text").text,
|
|
469
|
+
state: JSON.stringify(state),
|
|
470
|
+
used_tools,
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
module.exports = {
|
|
475
|
+
initTools,
|
|
476
|
+
getSandboxName,
|
|
477
|
+
setSandboxName,
|
|
478
|
+
openai_generateResponseOptionallyUsingTools,
|
|
479
|
+
anthropic_generateResponseOptionallyUsingTools,
|
|
480
|
+
};
|