@gitterm/bot 0.1.0 → 0.2.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/dist/bot.d.ts +1 -1
- package/dist/index.js +354 -301
- package/dist/saved.d.ts +4 -1
- package/dist/types.d.ts +5 -0
- package/package.json +2 -2
package/dist/bot.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ import type { Bot, BotOptions } from "./types.js";
|
|
|
3
3
|
* A chat bot backed by GitTerm sandboxes: one sandbox per repository that pauses when idle and
|
|
4
4
|
* wakes on the next message, and one OpenCode session per chat thread.
|
|
5
5
|
*/
|
|
6
|
-
export declare function createBot(
|
|
6
|
+
export declare function createBot(initial: BotOptions): Bot;
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { resolve as resolvePath } from "node:path";
|
|
4
4
|
import {
|
|
5
5
|
AgentRunError,
|
|
6
|
-
createGittermClient,
|
|
6
|
+
createGittermClient as createGittermClient2,
|
|
7
7
|
GittermError as GittermError2,
|
|
8
8
|
WorkspaceLifecycleError
|
|
9
9
|
} from "@gitterm/sdk";
|
|
@@ -140,7 +140,7 @@ function modelsFor(model, saved) {
|
|
|
140
140
|
async function githubFor(gitterm, repo) {
|
|
141
141
|
return gitterm.integrations.connections.resolve(["github"], { repo: repo.url }).then(() => ["github"], () => []);
|
|
142
142
|
}
|
|
143
|
-
var RESUME_TIMEOUT_MS =
|
|
143
|
+
var RESUME_TIMEOUT_MS = 4 * 60000;
|
|
144
144
|
var SHARED_SPACE = "shared";
|
|
145
145
|
var spaceOf = (message, shareChannels = false) => `${message.tenant ?? "installation"}:${message.direct ? `dm:${message.thread.channel}` : shareChannels ? SHARED_SPACE : `channel:${message.thread.channel}`}`;
|
|
146
146
|
function createWorkspaceManager(input) {
|
|
@@ -306,6 +306,260 @@ ${SETUP_HINT}`);
|
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
// src/saved.ts
|
|
310
|
+
import {
|
|
311
|
+
createGittermClient
|
|
312
|
+
} from "@gitterm/sdk";
|
|
313
|
+
|
|
314
|
+
// src/env.ts
|
|
315
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
316
|
+
import { parseArgs, parseEnv } from "node:util";
|
|
317
|
+
var read = (env, name) => env[name]?.trim() || undefined;
|
|
318
|
+
function positive(env, name) {
|
|
319
|
+
const value = read(env, name);
|
|
320
|
+
if (!value)
|
|
321
|
+
return;
|
|
322
|
+
const parsed = Number(value);
|
|
323
|
+
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
324
|
+
throw new Error(`${name} must be a positive integer`);
|
|
325
|
+
return parsed;
|
|
326
|
+
}
|
|
327
|
+
function parseChannels(value) {
|
|
328
|
+
const channels = {};
|
|
329
|
+
for (const entry of value.split(",")) {
|
|
330
|
+
const [channel, repo] = entry.split("=", 2).map((part) => part.trim());
|
|
331
|
+
if (!channel || !repo) {
|
|
332
|
+
throw new Error(`GITTERM_BOT_CHANNELS entries look like <channel id>=<repository url>`);
|
|
333
|
+
}
|
|
334
|
+
channels[channel] = repo;
|
|
335
|
+
}
|
|
336
|
+
return channels;
|
|
337
|
+
}
|
|
338
|
+
var list = (value) => value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
339
|
+
function readSandboxEnv(path) {
|
|
340
|
+
if (!path)
|
|
341
|
+
return;
|
|
342
|
+
try {
|
|
343
|
+
const parsed = parseEnv(readFileSync(path, "utf8"));
|
|
344
|
+
return Object.fromEntries(Object.entries(parsed).filter((entry) => entry[1] !== undefined));
|
|
345
|
+
} catch (error) {
|
|
346
|
+
throw new Error(`Could not read the sandbox environment file ${path}`, { cause: error });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function readInstructionsFile(path) {
|
|
350
|
+
if (!path)
|
|
351
|
+
return;
|
|
352
|
+
try {
|
|
353
|
+
return readFileSync(path, "utf8").trim() || undefined;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
throw new Error(`Could not read the instructions file ${path}`, { cause: error });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function botOptionsFromEnv(env = process.env) {
|
|
359
|
+
const connections = read(env, "GITTERM_BOT_CONNECTIONS");
|
|
360
|
+
const provider = read(env, "GITTERM_BOT_PROVIDER");
|
|
361
|
+
const channels = read(env, "GITTERM_BOT_CHANNELS");
|
|
362
|
+
const options = {};
|
|
363
|
+
const botId = read(env, "GITTERM_BOT_ID");
|
|
364
|
+
if (botId)
|
|
365
|
+
options.botId = botId;
|
|
366
|
+
const allowedChannels = read(env, "GITTERM_BOT_ALLOWED_CHANNELS");
|
|
367
|
+
if (allowedChannels)
|
|
368
|
+
options.allowedChannels = list(allowedChannels);
|
|
369
|
+
const approvers = read(env, "GITTERM_BOT_APPROVERS");
|
|
370
|
+
if (approvers)
|
|
371
|
+
options.approvers = list(approvers);
|
|
372
|
+
if (read(env, "GITTERM_BOT_ALLOW_ALWAYS") === "true")
|
|
373
|
+
options.allowAlways = true;
|
|
374
|
+
if (read(env, "GITTERM_BOT_SHARE_CHANNELS") === "true")
|
|
375
|
+
options.shareChannels = true;
|
|
376
|
+
const maxPendingRequests = positive(env, "GITTERM_BOT_MAX_PENDING");
|
|
377
|
+
if (maxPendingRequests)
|
|
378
|
+
options.maxPendingRequests = maxPendingRequests;
|
|
379
|
+
const maxPendingPerUser = positive(env, "GITTERM_BOT_MAX_PENDING_PER_USER");
|
|
380
|
+
if (maxPendingPerUser)
|
|
381
|
+
options.maxPendingPerUser = maxPendingPerUser;
|
|
382
|
+
const repo = read(env, "GITTERM_BOT_REPO");
|
|
383
|
+
if (repo)
|
|
384
|
+
options.repo = repo;
|
|
385
|
+
if (channels)
|
|
386
|
+
options.channels = parseChannels(channels);
|
|
387
|
+
if (connections)
|
|
388
|
+
options.connections = list(connections);
|
|
389
|
+
const repos = read(env, "GITTERM_BOT_REPOS");
|
|
390
|
+
if (repos)
|
|
391
|
+
options.repos = list(repos);
|
|
392
|
+
const allowedUsers = read(env, "GITTERM_BOT_ALLOWED_USERS");
|
|
393
|
+
if (allowedUsers)
|
|
394
|
+
options.allowedUsers = list(allowedUsers);
|
|
395
|
+
if (read(env, "GITTERM_BOT_ALLOW_GUESTS") === "true")
|
|
396
|
+
options.allowGuests = true;
|
|
397
|
+
const setup = read(env, "GITTERM_BOT_SETUP");
|
|
398
|
+
if (setup)
|
|
399
|
+
options.setup = [setup];
|
|
400
|
+
const sandboxEnv = readSandboxEnv(read(env, "GITTERM_BOT_SANDBOX_ENV_FILE"));
|
|
401
|
+
if (sandboxEnv)
|
|
402
|
+
options.env = sandboxEnv;
|
|
403
|
+
const model = read(env, "GITTERM_BOT_MODEL");
|
|
404
|
+
const credential = read(env, "GITTERM_BOT_MODEL_CREDENTIAL");
|
|
405
|
+
const apiKey = read(env, "GITTERM_BOT_MODEL_API_KEY");
|
|
406
|
+
if ((credential || apiKey) && !model) {
|
|
407
|
+
throw new Error("GITTERM_BOT_MODEL_CREDENTIAL and GITTERM_BOT_MODEL_API_KEY need GITTERM_BOT_MODEL");
|
|
408
|
+
}
|
|
409
|
+
if (model) {
|
|
410
|
+
options.model = {
|
|
411
|
+
id: model,
|
|
412
|
+
...credential ? { credential } : {},
|
|
413
|
+
...apiKey ? { apiKey } : {}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const instructions = [
|
|
417
|
+
read(env, "GITTERM_BOT_INSTRUCTIONS"),
|
|
418
|
+
readInstructionsFile(read(env, "GITTERM_BOT_INSTRUCTIONS_FILE"))
|
|
419
|
+
].filter(Boolean).join(`
|
|
420
|
+
|
|
421
|
+
`);
|
|
422
|
+
if (instructions)
|
|
423
|
+
options.instructions = instructions;
|
|
424
|
+
const githubToken = read(env, "GITTERM_BOT_GITHUB_TOKEN");
|
|
425
|
+
if (provider || githubToken) {
|
|
426
|
+
options.workspace = {
|
|
427
|
+
...provider ? { provider: { type: provider } } : {},
|
|
428
|
+
...githubToken ? { repositoryCredentials: { token: githubToken } } : {}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const stateFile = read(env, "GITTERM_BOT_STATE_FILE");
|
|
432
|
+
if (stateFile)
|
|
433
|
+
options.stateFile = stateFile;
|
|
434
|
+
const runTimeoutMinutes = positive(env, "GITTERM_BOT_RUN_TIMEOUT_MINUTES");
|
|
435
|
+
if (runTimeoutMinutes)
|
|
436
|
+
options.runTimeoutMs = runTimeoutMinutes * 60000;
|
|
437
|
+
return options;
|
|
438
|
+
}
|
|
439
|
+
function cliOptions(argv = process.argv.slice(2)) {
|
|
440
|
+
if (existsSync(".env") && typeof process.loadEnvFile === "function")
|
|
441
|
+
process.loadEnvFile(".env");
|
|
442
|
+
const { values, positionals } = parseArgs({
|
|
443
|
+
args: argv,
|
|
444
|
+
allowPositionals: true,
|
|
445
|
+
options: {
|
|
446
|
+
repo: { type: "string" },
|
|
447
|
+
model: { type: "string" },
|
|
448
|
+
"instructions-file": { type: "string" },
|
|
449
|
+
help: { type: "boolean", short: "h" }
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
const flags = {
|
|
453
|
+
GITTERM_BOT_REPO: values.repo,
|
|
454
|
+
GITTERM_BOT_MODEL: values.model,
|
|
455
|
+
GITTERM_BOT_INSTRUCTIONS_FILE: values["instructions-file"]
|
|
456
|
+
};
|
|
457
|
+
const env = { ...process.env };
|
|
458
|
+
for (const [name, value] of Object.entries(flags))
|
|
459
|
+
if (value)
|
|
460
|
+
env[name] = value;
|
|
461
|
+
return { command: values.help ? "help" : positionals[0], options: botOptionsFromEnv(env) };
|
|
462
|
+
}
|
|
463
|
+
var BOT_ENV_HELP = ` Saved policy loads at startup; local settings cannot broaden its users, guests, or channels.
|
|
464
|
+
|
|
465
|
+
GITTERM_API_TOKEN The bot's GitTerm token (dashboard → Bots creates one)
|
|
466
|
+
GITTERM_SERVER_URL Self-hosted GitTerm API URL (default: hosted)
|
|
467
|
+
GITTERM_BOT_REPO Repository for every channel: https://github.com/acme/app[#branch]
|
|
468
|
+
GITTERM_BOT_REPOS More repositories people can name in a thread: url,url
|
|
469
|
+
GITTERM_BOT_CHANNELS Per-channel repositories: <channel id>=<url>,…
|
|
470
|
+
GITTERM_BOT_MODEL provider/model, e.g. anthropic/claude-sonnet-5-5
|
|
471
|
+
GITTERM_BOT_MODEL_CREDENTIAL Saved credential label (default: the provider's default)
|
|
472
|
+
GITTERM_BOT_MODEL_API_KEY Or a model API key for this bot only
|
|
473
|
+
GITTERM_BOT_GITHUB_TOKEN Your own GitHub token instead of a GitTerm GitHub connection
|
|
474
|
+
GITTERM_BOT_CONNECTIONS Tools besides GitHub, by name: Linear,Sentry
|
|
475
|
+
GITTERM_BOT_ALLOWED_USERS Only these user ids may use the bot: U012,U034
|
|
476
|
+
GITTERM_BOT_ID Names an unsaved bot's sandboxes (saved bots use their own id)
|
|
477
|
+
GITTERM_BOT_ALLOWED_CHANNELS Only these channel ids, whatever the repository routing
|
|
478
|
+
GITTERM_BOT_APPROVERS Permission approver ids (default: requester only)
|
|
479
|
+
GITTERM_BOT_ALLOW_ALWAYS true enables persistent approvals (default: disabled)
|
|
480
|
+
GITTERM_BOT_SHARE_CHANNELS true opts equally trusted channels into a shared sandbox
|
|
481
|
+
GITTERM_BOT_MAX_PENDING[_PER_USER] Queue bounds (defaults: 20 total, 3 per user)
|
|
482
|
+
GITTERM_BOT_ALLOW_GUESTS true lets guests and external people use the bot
|
|
483
|
+
GITTERM_BOT_INSTRUCTIONS[_FILE] What the agent should know and how to behave
|
|
484
|
+
GITTERM_BOT_SETUP Command run before the agent starts, e.g. "pnpm install"
|
|
485
|
+
GITTERM_BOT_SANDBOX_ENV_FILE dotenv file of variables for the sandbox (test credentials)
|
|
486
|
+
GITTERM_BOT_PROVIDER Compute provider for new sandboxes (railway, e2b, …)
|
|
487
|
+
GITTERM_BOT_STATE_FILE, GITTERM_BOT_RUN_TIMEOUT_MINUTES`;
|
|
488
|
+
var hasRepository = (options) => Boolean(options.repo || options.repos?.length || Object.keys(options.channels ?? {}).length);
|
|
489
|
+
|
|
490
|
+
// src/saved.ts
|
|
491
|
+
var isClient = (value) => typeof value === "object" && value !== null && ("runs" in value);
|
|
492
|
+
async function withSavedConfig(options, platform, log = console) {
|
|
493
|
+
const gitterm = isClient(options.gitterm) ? options.gitterm : createGittermClient(options.gitterm);
|
|
494
|
+
const base = { ...options, gitterm };
|
|
495
|
+
const saved = await gitterm.bots.self();
|
|
496
|
+
if (!saved)
|
|
497
|
+
return base;
|
|
498
|
+
if (saved.platform !== platform) {
|
|
499
|
+
throw new Error(`This GitTerm token belongs to the ${saved.platform} bot "${saved.name}"; run that bot with it`);
|
|
500
|
+
}
|
|
501
|
+
log.info(`Using the settings saved for "${saved.name}" in GitTerm.`);
|
|
502
|
+
return { ...applySavedConfig(base, saved, log), localOptions: base };
|
|
503
|
+
}
|
|
504
|
+
function applySavedConfig(options, saved, log = console) {
|
|
505
|
+
const merged = { ...options };
|
|
506
|
+
const localRepos = [
|
|
507
|
+
options.repo,
|
|
508
|
+
...options.repos ?? [],
|
|
509
|
+
...Object.values(options.channels ?? {})
|
|
510
|
+
].filter((repo) => repo !== undefined);
|
|
511
|
+
if (localRepos.some((repo) => repoKey(parseRepo(repo)) !== repoKey(parseRepo(saved.repo)))) {
|
|
512
|
+
throw new Error("Local repository routing exceeds the saved bot policy. Change its repository in GitTerm first.");
|
|
513
|
+
}
|
|
514
|
+
const localModel = typeof options.model === "string" ? options.model : options.model?.id;
|
|
515
|
+
if (localModel && localModel !== saved.model)
|
|
516
|
+
throw new Error("Local model exceeds the saved bot policy. Change its model in GitTerm first.");
|
|
517
|
+
if (options.connections?.some((reference) => !saved.connections.includes(reference))) {
|
|
518
|
+
throw new Error("Local connections exceed the saved bot policy. Select them in GitTerm first.");
|
|
519
|
+
}
|
|
520
|
+
merged.botId = saved.id;
|
|
521
|
+
if (saved.channels.length) {
|
|
522
|
+
merged.allowedChannels = options.allowedChannels ? options.allowedChannels.filter((id) => saved.channels.includes(id)) : saved.channels;
|
|
523
|
+
}
|
|
524
|
+
if (!hasRepository(options)) {
|
|
525
|
+
if (saved.channels.length) {
|
|
526
|
+
merged.channels = Object.fromEntries(saved.channels.map((id) => [id, saved.repo]));
|
|
527
|
+
} else {
|
|
528
|
+
merged.repo = saved.repo;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
merged.model ??= {
|
|
532
|
+
id: saved.model,
|
|
533
|
+
...saved.credential ? { credential: saved.credential } : {}
|
|
534
|
+
};
|
|
535
|
+
merged.connections ??= saved.connections;
|
|
536
|
+
if (saved.provider && !options.workspace?.provider) {
|
|
537
|
+
merged.workspace = {
|
|
538
|
+
...options.workspace,
|
|
539
|
+
provider: { type: saved.provider }
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
if (saved.allowedUsers.length) {
|
|
543
|
+
merged.allowedUsers = options.allowedUsers ? options.allowedUsers.filter((id) => saved.allowedUsers.includes(id)) : saved.allowedUsers;
|
|
544
|
+
}
|
|
545
|
+
merged.allowGuests = saved.allowGuests && (options.allowGuests ?? true);
|
|
546
|
+
if (saved.instructions)
|
|
547
|
+
merged.instructions ??= saved.instructions;
|
|
548
|
+
if (saved.setup)
|
|
549
|
+
merged.setup ??= [saved.setup];
|
|
550
|
+
if (saved.githubAccess === "token" && !merged.workspace?.repositoryCredentials) {
|
|
551
|
+
log.warn(`"${saved.name}" uses your own GitHub token, but GITTERM_BOT_GITHUB_TOKEN is not set. Public repositories still work.`);
|
|
552
|
+
}
|
|
553
|
+
log.info("Effective bot authorization", {
|
|
554
|
+
botId: saved.id,
|
|
555
|
+
source: "saved policy, intersected with local restrictions",
|
|
556
|
+
channels: merged.allowedChannels ?? "all invited channels",
|
|
557
|
+
users: merged.allowedUsers ?? "channel members",
|
|
558
|
+
allowGuests: merged.allowGuests
|
|
559
|
+
});
|
|
560
|
+
return merged;
|
|
561
|
+
}
|
|
562
|
+
|
|
309
563
|
// src/state.ts
|
|
310
564
|
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
311
565
|
import { dirname } from "node:path";
|
|
@@ -393,12 +647,14 @@ async function openStatusLine(adapter, thread, messageId) {
|
|
|
393
647
|
const ms = Date.now() - startedAt;
|
|
394
648
|
return ms >= TICK_MS ? ` (${minutes(ms)} min)` : "";
|
|
395
649
|
};
|
|
396
|
-
|
|
650
|
+
let queue = Promise.resolve();
|
|
651
|
+
const send = (update) => queue = queue.then(update).catch(noop);
|
|
652
|
+
const render = () => send(() => {
|
|
397
653
|
if (id)
|
|
398
|
-
return adapter.edit(thread, id, `${current.message}${elapsed()}`)
|
|
654
|
+
return adapter.edit(thread, id, `${current.message}${elapsed()}`);
|
|
399
655
|
const text = current.indicator && `${current.indicator}${elapsed()}`;
|
|
400
|
-
return adapter.indicate?.(thread, text)
|
|
401
|
-
};
|
|
656
|
+
return adapter.indicate?.(thread, text);
|
|
657
|
+
});
|
|
402
658
|
const timer = setInterval(() => {
|
|
403
659
|
if (id || current.indicator)
|
|
404
660
|
render();
|
|
@@ -418,18 +674,15 @@ async function openStatusLine(adapter, thread, messageId) {
|
|
|
418
674
|
async finish(text) {
|
|
419
675
|
end();
|
|
420
676
|
if (id) {
|
|
421
|
-
await adapter.edit(thread, id, text)
|
|
677
|
+
await send(() => adapter.edit(thread, id, text));
|
|
422
678
|
return;
|
|
423
679
|
}
|
|
424
|
-
await adapter.indicate?.(thread, "")
|
|
680
|
+
await send(() => adapter.indicate?.(thread, ""));
|
|
425
681
|
await adapter.post(thread, text).catch(noop);
|
|
426
682
|
},
|
|
427
683
|
async remove() {
|
|
428
684
|
end();
|
|
429
|
-
|
|
430
|
-
await adapter.remove(thread, id).catch(noop);
|
|
431
|
-
else
|
|
432
|
-
await adapter.indicate?.(thread, "").catch(noop);
|
|
685
|
+
await send(() => id ? adapter.remove(thread, id) : adapter.indicate?.(thread, ""));
|
|
433
686
|
},
|
|
434
687
|
stop: end
|
|
435
688
|
};
|
|
@@ -466,7 +719,7 @@ var threadKeyOf = (thread) => `${thread.channel}:${thread.thread}`;
|
|
|
466
719
|
var noop2 = () => {
|
|
467
720
|
return;
|
|
468
721
|
};
|
|
469
|
-
var
|
|
722
|
+
var isClient2 = (value) => typeof value === "object" && value !== null && ("runs" in value);
|
|
470
723
|
function describeAnswer(answer) {
|
|
471
724
|
if (answer.kind === "question")
|
|
472
725
|
return answer.labels.join(", ");
|
|
@@ -511,25 +764,9 @@ function recentIds(limit) {
|
|
|
511
764
|
return true;
|
|
512
765
|
};
|
|
513
766
|
}
|
|
514
|
-
function
|
|
515
|
-
const { adapter } = options;
|
|
516
|
-
const log = options.logger ?? console;
|
|
517
|
-
const gitterm = isClient(options.gitterm) ? options.gitterm : createGittermClient(options.gitterm);
|
|
518
|
-
const runTimeoutMs = options.runTimeoutMs ?? 60 * 60000;
|
|
519
|
-
const inputTimeoutMs = options.inputTimeoutMs ?? 30 * 60000;
|
|
520
|
-
for (const [name, value, maximum] of [
|
|
521
|
-
["runTimeoutMs", runTimeoutMs, 24 * 60 * 60000],
|
|
522
|
-
["inputTimeoutMs", inputTimeoutMs, 24 * 60 * 60000],
|
|
523
|
-
["maxPendingRequests", options.maxPendingRequests ?? 20, 1000],
|
|
524
|
-
["maxPendingPerUser", options.maxPendingPerUser ?? 3, 1000]
|
|
525
|
-
]) {
|
|
526
|
-
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum)
|
|
527
|
-
throw new Error(`${name} must be a positive integer no greater than ${maximum}`);
|
|
528
|
-
}
|
|
767
|
+
function settingsFor(options) {
|
|
529
768
|
const defaultRepo = options.repo ? parseRepo(options.repo) : undefined;
|
|
530
769
|
const extraRepos = (options.repos ?? []).map(parseRepo);
|
|
531
|
-
const allowedUsers = options.allowedUsers ? new Set(options.allowedUsers) : undefined;
|
|
532
|
-
const model = typeof options.model === "string" ? { id: options.model } : options.model;
|
|
533
770
|
const channelRepos = new Map(Object.entries(options.channels ?? {}).map(([channel, repo]) => [channel, parseRepo(repo)]));
|
|
534
771
|
const knownRepos = [
|
|
535
772
|
...new Map([defaultRepo, ...channelRepos.values(), ...extraRepos].filter((repo) => Boolean(repo)).map((repo) => [repoKey(repo), repo])).values()
|
|
@@ -537,6 +774,32 @@ function createBot(options) {
|
|
|
537
774
|
if (knownRepos.length === 0) {
|
|
538
775
|
throw new Error("Give the bot a `repo`, `repos` to choose from, or a repository per channel.");
|
|
539
776
|
}
|
|
777
|
+
return {
|
|
778
|
+
options,
|
|
779
|
+
defaultRepo,
|
|
780
|
+
extraRepos,
|
|
781
|
+
channelRepos,
|
|
782
|
+
knownRepos,
|
|
783
|
+
allowedUsers: options.allowedUsers ? new Set(options.allowedUsers) : undefined,
|
|
784
|
+
model: typeof options.model === "string" ? { id: options.model } : options.model
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function createBot(initial) {
|
|
788
|
+
const { adapter } = initial;
|
|
789
|
+
const log = initial.logger ?? console;
|
|
790
|
+
const gitterm = isClient2(initial.gitterm) ? initial.gitterm : createGittermClient2(initial.gitterm);
|
|
791
|
+
const runTimeoutMs = initial.runTimeoutMs ?? 60 * 60000;
|
|
792
|
+
const inputTimeoutMs = initial.inputTimeoutMs ?? 30 * 60000;
|
|
793
|
+
for (const [name, value, maximum] of [
|
|
794
|
+
["runTimeoutMs", runTimeoutMs, 24 * 60 * 60000],
|
|
795
|
+
["inputTimeoutMs", inputTimeoutMs, 24 * 60 * 60000],
|
|
796
|
+
["maxPendingRequests", initial.maxPendingRequests ?? 20, 1000],
|
|
797
|
+
["maxPendingPerUser", initial.maxPendingPerUser ?? 3, 1000]
|
|
798
|
+
]) {
|
|
799
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum)
|
|
800
|
+
throw new Error(`${name} must be a positive integer no greater than ${maximum}`);
|
|
801
|
+
}
|
|
802
|
+
let { options, defaultRepo, extraRepos, channelRepos, knownRepos, allowedUsers, model } = settingsFor(initial);
|
|
540
803
|
const lock = createLocks();
|
|
541
804
|
const fresh = recentIds(RECENT_MESSAGES);
|
|
542
805
|
const active = new Map;
|
|
@@ -550,6 +813,7 @@ function createBot(options) {
|
|
|
550
813
|
let running = false;
|
|
551
814
|
let leaseTimer;
|
|
552
815
|
let leaseId;
|
|
816
|
+
let savedAt;
|
|
553
817
|
const tasks = new Set;
|
|
554
818
|
const trackTask = (task) => {
|
|
555
819
|
tasks.add(task);
|
|
@@ -1093,6 +1357,59 @@ function createBot(options) {
|
|
|
1093
1357
|
}
|
|
1094
1358
|
}
|
|
1095
1359
|
}
|
|
1360
|
+
const workspaceManager = () => createWorkspaceManager({
|
|
1361
|
+
gitterm,
|
|
1362
|
+
platform: adapter.platform,
|
|
1363
|
+
scope,
|
|
1364
|
+
botId: options.botId,
|
|
1365
|
+
directRetentionMs: options.directRetentionMs,
|
|
1366
|
+
identityPolicy: {
|
|
1367
|
+
allowedUsers: options.allowedUsers,
|
|
1368
|
+
allowGuests: options.allowGuests,
|
|
1369
|
+
allowedChannels: options.allowedChannels,
|
|
1370
|
+
shareChannels: options.shareChannels
|
|
1371
|
+
},
|
|
1372
|
+
connections: options.connections ?? [],
|
|
1373
|
+
env: options.env ?? {},
|
|
1374
|
+
setup: options.setup ?? [],
|
|
1375
|
+
model,
|
|
1376
|
+
instructions: agentInstructions(adapter.displayName, options.instructions),
|
|
1377
|
+
overrides: options.workspace,
|
|
1378
|
+
log
|
|
1379
|
+
});
|
|
1380
|
+
async function reload() {
|
|
1381
|
+
const local = options.localOptions;
|
|
1382
|
+
if (!local)
|
|
1383
|
+
return;
|
|
1384
|
+
try {
|
|
1385
|
+
const saved = await gitterm.bots.self();
|
|
1386
|
+
if (!saved)
|
|
1387
|
+
return;
|
|
1388
|
+
savedAt = saved.updatedAt;
|
|
1389
|
+
if (saved.platform !== adapter.platform) {
|
|
1390
|
+
log.warn(`"${saved.name}" is now a ${saved.platform} bot; run it on that platform.`);
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
const next = settingsFor({
|
|
1394
|
+
...applySavedConfig(local, saved, log),
|
|
1395
|
+
adapter,
|
|
1396
|
+
localOptions: local
|
|
1397
|
+
});
|
|
1398
|
+
await checkSetup({
|
|
1399
|
+
gitterm,
|
|
1400
|
+
repos: next.knownRepos,
|
|
1401
|
+
connections: next.options.connections ?? [],
|
|
1402
|
+
model: next.model,
|
|
1403
|
+
overrides: next.options.workspace,
|
|
1404
|
+
log
|
|
1405
|
+
});
|
|
1406
|
+
({ options, defaultRepo, extraRepos, channelRepos, knownRepos, allowedUsers, model } = next);
|
|
1407
|
+
workspaces = workspaceManager();
|
|
1408
|
+
log.info(`Applied the settings saved for "${saved.name}". New sandboxes use them; reset replaces an existing one.`);
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
log.warn("Could not apply the newly saved settings; keeping the current ones. Save again in GitTerm to retry.", error);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1096
1413
|
const bot = {
|
|
1097
1414
|
async start() {
|
|
1098
1415
|
if (running)
|
|
@@ -1106,6 +1423,7 @@ function createBot(options) {
|
|
|
1106
1423
|
throw new Error("Load the saved configuration with withSavedConfig() before starting a saved bot");
|
|
1107
1424
|
}
|
|
1108
1425
|
if (savedIdentity) {
|
|
1426
|
+
savedAt = savedIdentity.updatedAt;
|
|
1109
1427
|
const id = randomUUID();
|
|
1110
1428
|
await claimLease(id);
|
|
1111
1429
|
leaseId = id;
|
|
@@ -1115,7 +1433,11 @@ function createBot(options) {
|
|
|
1115
1433
|
if (renewing || shutdown.signal.aborted)
|
|
1116
1434
|
return;
|
|
1117
1435
|
renewing = true;
|
|
1118
|
-
gitterm.bots.acquireLease(id).then(
|
|
1436
|
+
gitterm.bots.acquireLease(id).then(async (lease) => {
|
|
1437
|
+
failures = 0;
|
|
1438
|
+
if (lease.updatedAt && lease.updatedAt !== savedAt)
|
|
1439
|
+
await reload();
|
|
1440
|
+
}).catch((error) => {
|
|
1119
1441
|
const lost = error instanceof GittermError2 && error.code === "CONFLICT";
|
|
1120
1442
|
if (!lost && ++failures < 2)
|
|
1121
1443
|
return;
|
|
@@ -1154,26 +1476,7 @@ function createBot(options) {
|
|
|
1154
1476
|
accepts: servesChannel
|
|
1155
1477
|
});
|
|
1156
1478
|
scope = started.scope;
|
|
1157
|
-
workspaces =
|
|
1158
|
-
gitterm,
|
|
1159
|
-
platform: adapter.platform,
|
|
1160
|
-
scope,
|
|
1161
|
-
botId: options.botId,
|
|
1162
|
-
directRetentionMs: options.directRetentionMs,
|
|
1163
|
-
identityPolicy: {
|
|
1164
|
-
allowedUsers: options.allowedUsers,
|
|
1165
|
-
allowGuests: options.allowGuests,
|
|
1166
|
-
allowedChannels: options.allowedChannels,
|
|
1167
|
-
shareChannels: options.shareChannels
|
|
1168
|
-
},
|
|
1169
|
-
connections: options.connections ?? [],
|
|
1170
|
-
env: options.env ?? {},
|
|
1171
|
-
setup: options.setup ?? [],
|
|
1172
|
-
model,
|
|
1173
|
-
instructions: agentInstructions(adapter.displayName, options.instructions),
|
|
1174
|
-
overrides: options.workspace,
|
|
1175
|
-
log
|
|
1176
|
-
});
|
|
1479
|
+
workspaces = workspaceManager();
|
|
1177
1480
|
await recover();
|
|
1178
1481
|
markReady();
|
|
1179
1482
|
log.info(`GitTerm ${adapter.displayName} bot is running for ${knownRepos.map(repoLabel).join(", ")}.`);
|
|
@@ -1236,256 +1539,6 @@ async function downloadImage(url, options) {
|
|
|
1236
1539
|
reader.releaseLock();
|
|
1237
1540
|
}
|
|
1238
1541
|
}
|
|
1239
|
-
// src/saved.ts
|
|
1240
|
-
import {
|
|
1241
|
-
createGittermClient as createGittermClient2
|
|
1242
|
-
} from "@gitterm/sdk";
|
|
1243
|
-
|
|
1244
|
-
// src/env.ts
|
|
1245
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1246
|
-
import { parseArgs, parseEnv } from "node:util";
|
|
1247
|
-
var read = (env, name) => env[name]?.trim() || undefined;
|
|
1248
|
-
function positive(env, name) {
|
|
1249
|
-
const value = read(env, name);
|
|
1250
|
-
if (!value)
|
|
1251
|
-
return;
|
|
1252
|
-
const parsed = Number(value);
|
|
1253
|
-
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
1254
|
-
throw new Error(`${name} must be a positive integer`);
|
|
1255
|
-
return parsed;
|
|
1256
|
-
}
|
|
1257
|
-
function parseChannels(value) {
|
|
1258
|
-
const channels = {};
|
|
1259
|
-
for (const entry of value.split(",")) {
|
|
1260
|
-
const [channel, repo] = entry.split("=", 2).map((part) => part.trim());
|
|
1261
|
-
if (!channel || !repo) {
|
|
1262
|
-
throw new Error(`GITTERM_BOT_CHANNELS entries look like <channel id>=<repository url>`);
|
|
1263
|
-
}
|
|
1264
|
-
channels[channel] = repo;
|
|
1265
|
-
}
|
|
1266
|
-
return channels;
|
|
1267
|
-
}
|
|
1268
|
-
var list = (value) => value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
1269
|
-
function readSandboxEnv(path) {
|
|
1270
|
-
if (!path)
|
|
1271
|
-
return;
|
|
1272
|
-
try {
|
|
1273
|
-
const parsed = parseEnv(readFileSync(path, "utf8"));
|
|
1274
|
-
return Object.fromEntries(Object.entries(parsed).filter((entry) => entry[1] !== undefined));
|
|
1275
|
-
} catch (error) {
|
|
1276
|
-
throw new Error(`Could not read the sandbox environment file ${path}`, { cause: error });
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
function readInstructionsFile(path) {
|
|
1280
|
-
if (!path)
|
|
1281
|
-
return;
|
|
1282
|
-
try {
|
|
1283
|
-
return readFileSync(path, "utf8").trim() || undefined;
|
|
1284
|
-
} catch (error) {
|
|
1285
|
-
throw new Error(`Could not read the instructions file ${path}`, { cause: error });
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
function botOptionsFromEnv(env = process.env) {
|
|
1289
|
-
const connections = read(env, "GITTERM_BOT_CONNECTIONS");
|
|
1290
|
-
const provider = read(env, "GITTERM_BOT_PROVIDER");
|
|
1291
|
-
const channels = read(env, "GITTERM_BOT_CHANNELS");
|
|
1292
|
-
const options = {};
|
|
1293
|
-
const botId = read(env, "GITTERM_BOT_ID");
|
|
1294
|
-
if (botId)
|
|
1295
|
-
options.botId = botId;
|
|
1296
|
-
const allowedChannels = read(env, "GITTERM_BOT_ALLOWED_CHANNELS");
|
|
1297
|
-
if (allowedChannels)
|
|
1298
|
-
options.allowedChannels = list(allowedChannels);
|
|
1299
|
-
const approvers = read(env, "GITTERM_BOT_APPROVERS");
|
|
1300
|
-
if (approvers)
|
|
1301
|
-
options.approvers = list(approvers);
|
|
1302
|
-
if (read(env, "GITTERM_BOT_ALLOW_ALWAYS") === "true")
|
|
1303
|
-
options.allowAlways = true;
|
|
1304
|
-
if (read(env, "GITTERM_BOT_SHARE_CHANNELS") === "true")
|
|
1305
|
-
options.shareChannels = true;
|
|
1306
|
-
const maxPendingRequests = positive(env, "GITTERM_BOT_MAX_PENDING");
|
|
1307
|
-
if (maxPendingRequests)
|
|
1308
|
-
options.maxPendingRequests = maxPendingRequests;
|
|
1309
|
-
const maxPendingPerUser = positive(env, "GITTERM_BOT_MAX_PENDING_PER_USER");
|
|
1310
|
-
if (maxPendingPerUser)
|
|
1311
|
-
options.maxPendingPerUser = maxPendingPerUser;
|
|
1312
|
-
const repo = read(env, "GITTERM_BOT_REPO");
|
|
1313
|
-
if (repo)
|
|
1314
|
-
options.repo = repo;
|
|
1315
|
-
if (channels)
|
|
1316
|
-
options.channels = parseChannels(channels);
|
|
1317
|
-
if (connections)
|
|
1318
|
-
options.connections = list(connections);
|
|
1319
|
-
const repos = read(env, "GITTERM_BOT_REPOS");
|
|
1320
|
-
if (repos)
|
|
1321
|
-
options.repos = list(repos);
|
|
1322
|
-
const allowedUsers = read(env, "GITTERM_BOT_ALLOWED_USERS");
|
|
1323
|
-
if (allowedUsers)
|
|
1324
|
-
options.allowedUsers = list(allowedUsers);
|
|
1325
|
-
if (read(env, "GITTERM_BOT_ALLOW_GUESTS") === "true")
|
|
1326
|
-
options.allowGuests = true;
|
|
1327
|
-
const setup = read(env, "GITTERM_BOT_SETUP");
|
|
1328
|
-
if (setup)
|
|
1329
|
-
options.setup = [setup];
|
|
1330
|
-
const sandboxEnv = readSandboxEnv(read(env, "GITTERM_BOT_SANDBOX_ENV_FILE"));
|
|
1331
|
-
if (sandboxEnv)
|
|
1332
|
-
options.env = sandboxEnv;
|
|
1333
|
-
const model = read(env, "GITTERM_BOT_MODEL");
|
|
1334
|
-
const credential = read(env, "GITTERM_BOT_MODEL_CREDENTIAL");
|
|
1335
|
-
const apiKey = read(env, "GITTERM_BOT_MODEL_API_KEY");
|
|
1336
|
-
if ((credential || apiKey) && !model) {
|
|
1337
|
-
throw new Error("GITTERM_BOT_MODEL_CREDENTIAL and GITTERM_BOT_MODEL_API_KEY need GITTERM_BOT_MODEL");
|
|
1338
|
-
}
|
|
1339
|
-
if (model) {
|
|
1340
|
-
options.model = {
|
|
1341
|
-
id: model,
|
|
1342
|
-
...credential ? { credential } : {},
|
|
1343
|
-
...apiKey ? { apiKey } : {}
|
|
1344
|
-
};
|
|
1345
|
-
}
|
|
1346
|
-
const instructions = [
|
|
1347
|
-
read(env, "GITTERM_BOT_INSTRUCTIONS"),
|
|
1348
|
-
readInstructionsFile(read(env, "GITTERM_BOT_INSTRUCTIONS_FILE"))
|
|
1349
|
-
].filter(Boolean).join(`
|
|
1350
|
-
|
|
1351
|
-
`);
|
|
1352
|
-
if (instructions)
|
|
1353
|
-
options.instructions = instructions;
|
|
1354
|
-
const githubToken = read(env, "GITTERM_BOT_GITHUB_TOKEN");
|
|
1355
|
-
if (provider || githubToken) {
|
|
1356
|
-
options.workspace = {
|
|
1357
|
-
...provider ? { provider: { type: provider } } : {},
|
|
1358
|
-
...githubToken ? { repositoryCredentials: { token: githubToken } } : {}
|
|
1359
|
-
};
|
|
1360
|
-
}
|
|
1361
|
-
const stateFile = read(env, "GITTERM_BOT_STATE_FILE");
|
|
1362
|
-
if (stateFile)
|
|
1363
|
-
options.stateFile = stateFile;
|
|
1364
|
-
const runTimeoutMinutes = positive(env, "GITTERM_BOT_RUN_TIMEOUT_MINUTES");
|
|
1365
|
-
if (runTimeoutMinutes)
|
|
1366
|
-
options.runTimeoutMs = runTimeoutMinutes * 60000;
|
|
1367
|
-
return options;
|
|
1368
|
-
}
|
|
1369
|
-
function cliOptions(argv = process.argv.slice(2)) {
|
|
1370
|
-
if (existsSync(".env") && typeof process.loadEnvFile === "function")
|
|
1371
|
-
process.loadEnvFile(".env");
|
|
1372
|
-
const { values, positionals } = parseArgs({
|
|
1373
|
-
args: argv,
|
|
1374
|
-
allowPositionals: true,
|
|
1375
|
-
options: {
|
|
1376
|
-
repo: { type: "string" },
|
|
1377
|
-
model: { type: "string" },
|
|
1378
|
-
"instructions-file": { type: "string" },
|
|
1379
|
-
help: { type: "boolean", short: "h" }
|
|
1380
|
-
}
|
|
1381
|
-
});
|
|
1382
|
-
const flags = {
|
|
1383
|
-
GITTERM_BOT_REPO: values.repo,
|
|
1384
|
-
GITTERM_BOT_MODEL: values.model,
|
|
1385
|
-
GITTERM_BOT_INSTRUCTIONS_FILE: values["instructions-file"]
|
|
1386
|
-
};
|
|
1387
|
-
const env = { ...process.env };
|
|
1388
|
-
for (const [name, value] of Object.entries(flags))
|
|
1389
|
-
if (value)
|
|
1390
|
-
env[name] = value;
|
|
1391
|
-
return { command: values.help ? "help" : positionals[0], options: botOptionsFromEnv(env) };
|
|
1392
|
-
}
|
|
1393
|
-
var BOT_ENV_HELP = ` Saved policy loads at startup; local settings cannot broaden its users, guests, or channels.
|
|
1394
|
-
|
|
1395
|
-
GITTERM_API_TOKEN The bot's GitTerm token (dashboard → Bots creates one)
|
|
1396
|
-
GITTERM_SERVER_URL Self-hosted GitTerm API URL (default: hosted)
|
|
1397
|
-
GITTERM_BOT_REPO Repository for every channel: https://github.com/acme/app[#branch]
|
|
1398
|
-
GITTERM_BOT_REPOS More repositories people can name in a thread: url,url
|
|
1399
|
-
GITTERM_BOT_CHANNELS Per-channel repositories: <channel id>=<url>,…
|
|
1400
|
-
GITTERM_BOT_MODEL provider/model, e.g. anthropic/claude-sonnet-5-5
|
|
1401
|
-
GITTERM_BOT_MODEL_CREDENTIAL Saved credential label (default: the provider's default)
|
|
1402
|
-
GITTERM_BOT_MODEL_API_KEY Or a model API key for this bot only
|
|
1403
|
-
GITTERM_BOT_GITHUB_TOKEN Your own GitHub token instead of a GitTerm GitHub connection
|
|
1404
|
-
GITTERM_BOT_CONNECTIONS Tools besides GitHub, by name: Linear,Sentry
|
|
1405
|
-
GITTERM_BOT_ALLOWED_USERS Only these user ids may use the bot: U012,U034
|
|
1406
|
-
GITTERM_BOT_ID Names an unsaved bot's sandboxes (saved bots use their own id)
|
|
1407
|
-
GITTERM_BOT_ALLOWED_CHANNELS Only these channel ids, whatever the repository routing
|
|
1408
|
-
GITTERM_BOT_APPROVERS Permission approver ids (default: requester only)
|
|
1409
|
-
GITTERM_BOT_ALLOW_ALWAYS true enables persistent approvals (default: disabled)
|
|
1410
|
-
GITTERM_BOT_SHARE_CHANNELS true opts equally trusted channels into a shared sandbox
|
|
1411
|
-
GITTERM_BOT_MAX_PENDING[_PER_USER] Queue bounds (defaults: 20 total, 3 per user)
|
|
1412
|
-
GITTERM_BOT_ALLOW_GUESTS true lets guests and external people use the bot
|
|
1413
|
-
GITTERM_BOT_INSTRUCTIONS[_FILE] What the agent should know and how to behave
|
|
1414
|
-
GITTERM_BOT_SETUP Command run before the agent starts, e.g. "pnpm install"
|
|
1415
|
-
GITTERM_BOT_SANDBOX_ENV_FILE dotenv file of variables for the sandbox (test credentials)
|
|
1416
|
-
GITTERM_BOT_PROVIDER Compute provider for new sandboxes (railway, e2b, …)
|
|
1417
|
-
GITTERM_BOT_STATE_FILE, GITTERM_BOT_RUN_TIMEOUT_MINUTES`;
|
|
1418
|
-
var hasRepository = (options) => Boolean(options.repo || options.repos?.length || Object.keys(options.channels ?? {}).length);
|
|
1419
|
-
|
|
1420
|
-
// src/saved.ts
|
|
1421
|
-
var isClient2 = (value) => typeof value === "object" && value !== null && ("runs" in value);
|
|
1422
|
-
async function withSavedConfig(options, platform, log = console) {
|
|
1423
|
-
const gitterm = isClient2(options.gitterm) ? options.gitterm : createGittermClient2(options.gitterm);
|
|
1424
|
-
const base = { ...options, gitterm };
|
|
1425
|
-
const saved = await gitterm.bots.self();
|
|
1426
|
-
if (!saved)
|
|
1427
|
-
return base;
|
|
1428
|
-
if (saved.platform !== platform) {
|
|
1429
|
-
throw new Error(`This GitTerm token belongs to the ${saved.platform} bot "${saved.name}"; run that bot with it`);
|
|
1430
|
-
}
|
|
1431
|
-
const merged = { ...base };
|
|
1432
|
-
const localRepos = [
|
|
1433
|
-
options.repo,
|
|
1434
|
-
...options.repos ?? [],
|
|
1435
|
-
...Object.values(options.channels ?? {})
|
|
1436
|
-
].filter((repo) => repo !== undefined);
|
|
1437
|
-
if (localRepos.some((repo) => repoKey(parseRepo(repo)) !== repoKey(parseRepo(saved.repo)))) {
|
|
1438
|
-
throw new Error("Local repository routing exceeds the saved bot policy. Change its repository in GitTerm first.");
|
|
1439
|
-
}
|
|
1440
|
-
const localModel = typeof options.model === "string" ? options.model : options.model?.id;
|
|
1441
|
-
if (localModel && localModel !== saved.model)
|
|
1442
|
-
throw new Error("Local model exceeds the saved bot policy. Change its model in GitTerm first.");
|
|
1443
|
-
if (options.connections?.some((reference) => !saved.connections.includes(reference))) {
|
|
1444
|
-
throw new Error("Local connections exceed the saved bot policy. Select them in GitTerm first.");
|
|
1445
|
-
}
|
|
1446
|
-
merged.botId = saved.id;
|
|
1447
|
-
if (saved.channels.length) {
|
|
1448
|
-
merged.allowedChannels = options.allowedChannels ? options.allowedChannels.filter((id) => saved.channels.includes(id)) : saved.channels;
|
|
1449
|
-
}
|
|
1450
|
-
if (!hasRepository(options)) {
|
|
1451
|
-
if (saved.channels.length) {
|
|
1452
|
-
merged.channels = Object.fromEntries(saved.channels.map((id) => [id, saved.repo]));
|
|
1453
|
-
} else {
|
|
1454
|
-
merged.repo = saved.repo;
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
merged.model ??= {
|
|
1458
|
-
id: saved.model,
|
|
1459
|
-
...saved.credential ? { credential: saved.credential } : {}
|
|
1460
|
-
};
|
|
1461
|
-
merged.connections ??= saved.connections;
|
|
1462
|
-
if (saved.provider && !options.workspace?.provider) {
|
|
1463
|
-
merged.workspace = {
|
|
1464
|
-
...options.workspace,
|
|
1465
|
-
provider: { type: saved.provider }
|
|
1466
|
-
};
|
|
1467
|
-
}
|
|
1468
|
-
if (saved.allowedUsers.length) {
|
|
1469
|
-
merged.allowedUsers = options.allowedUsers ? options.allowedUsers.filter((id) => saved.allowedUsers.includes(id)) : saved.allowedUsers;
|
|
1470
|
-
}
|
|
1471
|
-
merged.allowGuests = saved.allowGuests && (options.allowGuests ?? true);
|
|
1472
|
-
if (saved.instructions)
|
|
1473
|
-
merged.instructions ??= saved.instructions;
|
|
1474
|
-
if (saved.setup)
|
|
1475
|
-
merged.setup ??= [saved.setup];
|
|
1476
|
-
if (saved.githubAccess === "token" && !merged.workspace?.repositoryCredentials) {
|
|
1477
|
-
log.warn(`"${saved.name}" uses your own GitHub token, but GITTERM_BOT_GITHUB_TOKEN is not set. Public repositories still work.`);
|
|
1478
|
-
}
|
|
1479
|
-
log.info(`Using the settings saved for "${saved.name}" in GitTerm.`);
|
|
1480
|
-
log.info("Effective bot authorization", {
|
|
1481
|
-
botId: saved.id,
|
|
1482
|
-
source: "saved policy, intersected with local restrictions",
|
|
1483
|
-
channels: merged.allowedChannels ?? "all invited channels",
|
|
1484
|
-
users: merged.allowedUsers ?? "channel members",
|
|
1485
|
-
allowGuests: merged.allowGuests
|
|
1486
|
-
});
|
|
1487
|
-
return merged;
|
|
1488
|
-
}
|
|
1489
1542
|
// src/markdown.ts
|
|
1490
1543
|
var TABLE_SEPARATOR = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/;
|
|
1491
1544
|
function renderTable(lines) {
|
package/dist/saved.d.ts
CHANGED
|
@@ -4,7 +4,10 @@ type Options = Omit<BotOptions, "adapter">;
|
|
|
4
4
|
/**
|
|
5
5
|
* Adds the settings saved for this bot in the GitTerm dashboard (Bots), found by its API token.
|
|
6
6
|
* Routing can be overridden locally; saved authorization is an upper bound. Without saved settings,
|
|
7
|
-
* for example a token that isn't a bot's, the options come back unchanged.
|
|
7
|
+
* for example a token that isn't a bot's, the options come back unchanged. The options given are
|
|
8
|
+
* kept as `localOptions`, so the running bot can apply settings saved later the same way.
|
|
8
9
|
*/
|
|
9
10
|
export declare function withSavedConfig(options: Options, platform: SavedBot["platform"], log?: BotLogger): Promise<Options>;
|
|
11
|
+
/** Saved settings on top of local options; throws when the local options exceed them. */
|
|
12
|
+
export declare function applySavedConfig(options: Options, saved: SavedBot, log?: BotLogger): Options;
|
|
10
13
|
export {};
|
package/dist/types.d.ts
CHANGED
|
@@ -208,6 +208,11 @@ export type BotOptions = {
|
|
|
208
208
|
/** How long a question or permission prompt waits for an answer. Default 30 min. */
|
|
209
209
|
inputTimeoutMs?: number;
|
|
210
210
|
logger?: BotLogger;
|
|
211
|
+
/**
|
|
212
|
+
* Set by `withSavedConfig`: the options before saved settings were added. A running bot applies
|
|
213
|
+
* settings saved later in the dashboard to these, without a restart.
|
|
214
|
+
*/
|
|
215
|
+
localOptions?: Omit<BotOptions, "adapter" | "localOptions">;
|
|
211
216
|
};
|
|
212
217
|
export type Bot = {
|
|
213
218
|
start(): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitterm/bot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Chat-platform-agnostic engine for GitTerm agent bots: one sandbox per repository, one agent session per thread",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"check-types": "tsc --noEmit"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@gitterm/sdk": "^0.
|
|
36
|
+
"@gitterm/sdk": "^0.11.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/bun": "^1.2.6",
|