@gitterm/bot 0.1.1 → 0.2.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/dist/bot.d.ts +1 -1
- package/dist/index.js +365 -303
- 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";
|
|
@@ -94,8 +94,17 @@ function agentInstructions(platform, extra) {
|
|
|
94
94
|
`- Use the question tool only when a decision genuinely blocks you; the person answers with buttons in ${platform}. Do not ask for confirmation of routine steps.`,
|
|
95
95
|
`- You cannot send files to ${platform}; describe results or link to them instead.`
|
|
96
96
|
];
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
const owner = extra?.trim();
|
|
98
|
+
if (!owner)
|
|
99
|
+
return lines.join(`
|
|
100
|
+
`);
|
|
101
|
+
return [
|
|
102
|
+
lines.join(`
|
|
103
|
+
`),
|
|
104
|
+
"## Instructions from this bot's owner",
|
|
105
|
+
"These take precedence over the defaults above.",
|
|
106
|
+
owner
|
|
107
|
+
].join(`
|
|
99
108
|
|
|
100
109
|
`);
|
|
101
110
|
}
|
|
@@ -306,6 +315,260 @@ ${SETUP_HINT}`);
|
|
|
306
315
|
}
|
|
307
316
|
}
|
|
308
317
|
|
|
318
|
+
// src/saved.ts
|
|
319
|
+
import {
|
|
320
|
+
createGittermClient
|
|
321
|
+
} from "@gitterm/sdk";
|
|
322
|
+
|
|
323
|
+
// src/env.ts
|
|
324
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
325
|
+
import { parseArgs, parseEnv } from "node:util";
|
|
326
|
+
var read = (env, name) => env[name]?.trim() || undefined;
|
|
327
|
+
function positive(env, name) {
|
|
328
|
+
const value = read(env, name);
|
|
329
|
+
if (!value)
|
|
330
|
+
return;
|
|
331
|
+
const parsed = Number(value);
|
|
332
|
+
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
333
|
+
throw new Error(`${name} must be a positive integer`);
|
|
334
|
+
return parsed;
|
|
335
|
+
}
|
|
336
|
+
function parseChannels(value) {
|
|
337
|
+
const channels = {};
|
|
338
|
+
for (const entry of value.split(",")) {
|
|
339
|
+
const [channel, repo] = entry.split("=", 2).map((part) => part.trim());
|
|
340
|
+
if (!channel || !repo) {
|
|
341
|
+
throw new Error(`GITTERM_BOT_CHANNELS entries look like <channel id>=<repository url>`);
|
|
342
|
+
}
|
|
343
|
+
channels[channel] = repo;
|
|
344
|
+
}
|
|
345
|
+
return channels;
|
|
346
|
+
}
|
|
347
|
+
var list = (value) => value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
348
|
+
function readSandboxEnv(path) {
|
|
349
|
+
if (!path)
|
|
350
|
+
return;
|
|
351
|
+
try {
|
|
352
|
+
const parsed = parseEnv(readFileSync(path, "utf8"));
|
|
353
|
+
return Object.fromEntries(Object.entries(parsed).filter((entry) => entry[1] !== undefined));
|
|
354
|
+
} catch (error) {
|
|
355
|
+
throw new Error(`Could not read the sandbox environment file ${path}`, { cause: error });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function readInstructionsFile(path) {
|
|
359
|
+
if (!path)
|
|
360
|
+
return;
|
|
361
|
+
try {
|
|
362
|
+
return readFileSync(path, "utf8").trim() || undefined;
|
|
363
|
+
} catch (error) {
|
|
364
|
+
throw new Error(`Could not read the instructions file ${path}`, { cause: error });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function botOptionsFromEnv(env = process.env) {
|
|
368
|
+
const connections = read(env, "GITTERM_BOT_CONNECTIONS");
|
|
369
|
+
const provider = read(env, "GITTERM_BOT_PROVIDER");
|
|
370
|
+
const channels = read(env, "GITTERM_BOT_CHANNELS");
|
|
371
|
+
const options = {};
|
|
372
|
+
const botId = read(env, "GITTERM_BOT_ID");
|
|
373
|
+
if (botId)
|
|
374
|
+
options.botId = botId;
|
|
375
|
+
const allowedChannels = read(env, "GITTERM_BOT_ALLOWED_CHANNELS");
|
|
376
|
+
if (allowedChannels)
|
|
377
|
+
options.allowedChannels = list(allowedChannels);
|
|
378
|
+
const approvers = read(env, "GITTERM_BOT_APPROVERS");
|
|
379
|
+
if (approvers)
|
|
380
|
+
options.approvers = list(approvers);
|
|
381
|
+
if (read(env, "GITTERM_BOT_ALLOW_ALWAYS") === "true")
|
|
382
|
+
options.allowAlways = true;
|
|
383
|
+
if (read(env, "GITTERM_BOT_SHARE_CHANNELS") === "true")
|
|
384
|
+
options.shareChannels = true;
|
|
385
|
+
const maxPendingRequests = positive(env, "GITTERM_BOT_MAX_PENDING");
|
|
386
|
+
if (maxPendingRequests)
|
|
387
|
+
options.maxPendingRequests = maxPendingRequests;
|
|
388
|
+
const maxPendingPerUser = positive(env, "GITTERM_BOT_MAX_PENDING_PER_USER");
|
|
389
|
+
if (maxPendingPerUser)
|
|
390
|
+
options.maxPendingPerUser = maxPendingPerUser;
|
|
391
|
+
const repo = read(env, "GITTERM_BOT_REPO");
|
|
392
|
+
if (repo)
|
|
393
|
+
options.repo = repo;
|
|
394
|
+
if (channels)
|
|
395
|
+
options.channels = parseChannels(channels);
|
|
396
|
+
if (connections)
|
|
397
|
+
options.connections = list(connections);
|
|
398
|
+
const repos = read(env, "GITTERM_BOT_REPOS");
|
|
399
|
+
if (repos)
|
|
400
|
+
options.repos = list(repos);
|
|
401
|
+
const allowedUsers = read(env, "GITTERM_BOT_ALLOWED_USERS");
|
|
402
|
+
if (allowedUsers)
|
|
403
|
+
options.allowedUsers = list(allowedUsers);
|
|
404
|
+
if (read(env, "GITTERM_BOT_ALLOW_GUESTS") === "true")
|
|
405
|
+
options.allowGuests = true;
|
|
406
|
+
const setup = read(env, "GITTERM_BOT_SETUP");
|
|
407
|
+
if (setup)
|
|
408
|
+
options.setup = [setup];
|
|
409
|
+
const sandboxEnv = readSandboxEnv(read(env, "GITTERM_BOT_SANDBOX_ENV_FILE"));
|
|
410
|
+
if (sandboxEnv)
|
|
411
|
+
options.env = sandboxEnv;
|
|
412
|
+
const model = read(env, "GITTERM_BOT_MODEL");
|
|
413
|
+
const credential = read(env, "GITTERM_BOT_MODEL_CREDENTIAL");
|
|
414
|
+
const apiKey = read(env, "GITTERM_BOT_MODEL_API_KEY");
|
|
415
|
+
if ((credential || apiKey) && !model) {
|
|
416
|
+
throw new Error("GITTERM_BOT_MODEL_CREDENTIAL and GITTERM_BOT_MODEL_API_KEY need GITTERM_BOT_MODEL");
|
|
417
|
+
}
|
|
418
|
+
if (model) {
|
|
419
|
+
options.model = {
|
|
420
|
+
id: model,
|
|
421
|
+
...credential ? { credential } : {},
|
|
422
|
+
...apiKey ? { apiKey } : {}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const instructions = [
|
|
426
|
+
read(env, "GITTERM_BOT_INSTRUCTIONS"),
|
|
427
|
+
readInstructionsFile(read(env, "GITTERM_BOT_INSTRUCTIONS_FILE"))
|
|
428
|
+
].filter(Boolean).join(`
|
|
429
|
+
|
|
430
|
+
`);
|
|
431
|
+
if (instructions)
|
|
432
|
+
options.instructions = instructions;
|
|
433
|
+
const githubToken = read(env, "GITTERM_BOT_GITHUB_TOKEN");
|
|
434
|
+
if (provider || githubToken) {
|
|
435
|
+
options.workspace = {
|
|
436
|
+
...provider ? { provider: { type: provider } } : {},
|
|
437
|
+
...githubToken ? { repositoryCredentials: { token: githubToken } } : {}
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const stateFile = read(env, "GITTERM_BOT_STATE_FILE");
|
|
441
|
+
if (stateFile)
|
|
442
|
+
options.stateFile = stateFile;
|
|
443
|
+
const runTimeoutMinutes = positive(env, "GITTERM_BOT_RUN_TIMEOUT_MINUTES");
|
|
444
|
+
if (runTimeoutMinutes)
|
|
445
|
+
options.runTimeoutMs = runTimeoutMinutes * 60000;
|
|
446
|
+
return options;
|
|
447
|
+
}
|
|
448
|
+
function cliOptions(argv = process.argv.slice(2)) {
|
|
449
|
+
if (existsSync(".env") && typeof process.loadEnvFile === "function")
|
|
450
|
+
process.loadEnvFile(".env");
|
|
451
|
+
const { values, positionals } = parseArgs({
|
|
452
|
+
args: argv,
|
|
453
|
+
allowPositionals: true,
|
|
454
|
+
options: {
|
|
455
|
+
repo: { type: "string" },
|
|
456
|
+
model: { type: "string" },
|
|
457
|
+
"instructions-file": { type: "string" },
|
|
458
|
+
help: { type: "boolean", short: "h" }
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
const flags = {
|
|
462
|
+
GITTERM_BOT_REPO: values.repo,
|
|
463
|
+
GITTERM_BOT_MODEL: values.model,
|
|
464
|
+
GITTERM_BOT_INSTRUCTIONS_FILE: values["instructions-file"]
|
|
465
|
+
};
|
|
466
|
+
const env = { ...process.env };
|
|
467
|
+
for (const [name, value] of Object.entries(flags))
|
|
468
|
+
if (value)
|
|
469
|
+
env[name] = value;
|
|
470
|
+
return { command: values.help ? "help" : positionals[0], options: botOptionsFromEnv(env) };
|
|
471
|
+
}
|
|
472
|
+
var BOT_ENV_HELP = ` Saved policy loads at startup; local settings cannot broaden its users, guests, or channels.
|
|
473
|
+
|
|
474
|
+
GITTERM_API_TOKEN The bot's GitTerm token (dashboard → Bots creates one)
|
|
475
|
+
GITTERM_SERVER_URL Self-hosted GitTerm API URL (default: hosted)
|
|
476
|
+
GITTERM_BOT_REPO Repository for every channel: https://github.com/acme/app[#branch]
|
|
477
|
+
GITTERM_BOT_REPOS More repositories people can name in a thread: url,url
|
|
478
|
+
GITTERM_BOT_CHANNELS Per-channel repositories: <channel id>=<url>,…
|
|
479
|
+
GITTERM_BOT_MODEL provider/model, e.g. anthropic/claude-sonnet-5-5
|
|
480
|
+
GITTERM_BOT_MODEL_CREDENTIAL Saved credential label (default: the provider's default)
|
|
481
|
+
GITTERM_BOT_MODEL_API_KEY Or a model API key for this bot only
|
|
482
|
+
GITTERM_BOT_GITHUB_TOKEN Your own GitHub token instead of a GitTerm GitHub connection
|
|
483
|
+
GITTERM_BOT_CONNECTIONS Tools besides GitHub, by name: Linear,Sentry
|
|
484
|
+
GITTERM_BOT_ALLOWED_USERS Only these user ids may use the bot: U012,U034
|
|
485
|
+
GITTERM_BOT_ID Names an unsaved bot's sandboxes (saved bots use their own id)
|
|
486
|
+
GITTERM_BOT_ALLOWED_CHANNELS Only these channel ids, whatever the repository routing
|
|
487
|
+
GITTERM_BOT_APPROVERS Permission approver ids (default: requester only)
|
|
488
|
+
GITTERM_BOT_ALLOW_ALWAYS true enables persistent approvals (default: disabled)
|
|
489
|
+
GITTERM_BOT_SHARE_CHANNELS true opts equally trusted channels into a shared sandbox
|
|
490
|
+
GITTERM_BOT_MAX_PENDING[_PER_USER] Queue bounds (defaults: 20 total, 3 per user)
|
|
491
|
+
GITTERM_BOT_ALLOW_GUESTS true lets guests and external people use the bot
|
|
492
|
+
GITTERM_BOT_INSTRUCTIONS[_FILE] What the agent should know and how to behave
|
|
493
|
+
GITTERM_BOT_SETUP Command run before the agent starts, e.g. "pnpm install"
|
|
494
|
+
GITTERM_BOT_SANDBOX_ENV_FILE dotenv file of variables for the sandbox (test credentials)
|
|
495
|
+
GITTERM_BOT_PROVIDER Compute provider for new sandboxes (railway, e2b, …)
|
|
496
|
+
GITTERM_BOT_STATE_FILE, GITTERM_BOT_RUN_TIMEOUT_MINUTES`;
|
|
497
|
+
var hasRepository = (options) => Boolean(options.repo || options.repos?.length || Object.keys(options.channels ?? {}).length);
|
|
498
|
+
|
|
499
|
+
// src/saved.ts
|
|
500
|
+
var isClient = (value) => typeof value === "object" && value !== null && ("runs" in value);
|
|
501
|
+
async function withSavedConfig(options, platform, log = console) {
|
|
502
|
+
const gitterm = isClient(options.gitterm) ? options.gitterm : createGittermClient(options.gitterm);
|
|
503
|
+
const base = { ...options, gitterm };
|
|
504
|
+
const saved = await gitterm.bots.self();
|
|
505
|
+
if (!saved)
|
|
506
|
+
return base;
|
|
507
|
+
if (saved.platform !== platform) {
|
|
508
|
+
throw new Error(`This GitTerm token belongs to the ${saved.platform} bot "${saved.name}"; run that bot with it`);
|
|
509
|
+
}
|
|
510
|
+
log.info(`Using the settings saved for "${saved.name}" in GitTerm.`);
|
|
511
|
+
return { ...applySavedConfig(base, saved, log), localOptions: base };
|
|
512
|
+
}
|
|
513
|
+
function applySavedConfig(options, saved, log = console) {
|
|
514
|
+
const merged = { ...options };
|
|
515
|
+
const localRepos = [
|
|
516
|
+
options.repo,
|
|
517
|
+
...options.repos ?? [],
|
|
518
|
+
...Object.values(options.channels ?? {})
|
|
519
|
+
].filter((repo) => repo !== undefined);
|
|
520
|
+
if (localRepos.some((repo) => repoKey(parseRepo(repo)) !== repoKey(parseRepo(saved.repo)))) {
|
|
521
|
+
throw new Error("Local repository routing exceeds the saved bot policy. Change its repository in GitTerm first.");
|
|
522
|
+
}
|
|
523
|
+
const localModel = typeof options.model === "string" ? options.model : options.model?.id;
|
|
524
|
+
if (localModel && localModel !== saved.model)
|
|
525
|
+
throw new Error("Local model exceeds the saved bot policy. Change its model in GitTerm first.");
|
|
526
|
+
if (options.connections?.some((reference) => !saved.connections.includes(reference))) {
|
|
527
|
+
throw new Error("Local connections exceed the saved bot policy. Select them in GitTerm first.");
|
|
528
|
+
}
|
|
529
|
+
merged.botId = saved.id;
|
|
530
|
+
if (saved.channels.length) {
|
|
531
|
+
merged.allowedChannels = options.allowedChannels ? options.allowedChannels.filter((id) => saved.channels.includes(id)) : saved.channels;
|
|
532
|
+
}
|
|
533
|
+
if (!hasRepository(options)) {
|
|
534
|
+
if (saved.channels.length) {
|
|
535
|
+
merged.channels = Object.fromEntries(saved.channels.map((id) => [id, saved.repo]));
|
|
536
|
+
} else {
|
|
537
|
+
merged.repo = saved.repo;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
merged.model ??= {
|
|
541
|
+
id: saved.model,
|
|
542
|
+
...saved.credential ? { credential: saved.credential } : {}
|
|
543
|
+
};
|
|
544
|
+
merged.connections ??= saved.connections;
|
|
545
|
+
if (saved.provider && !options.workspace?.provider) {
|
|
546
|
+
merged.workspace = {
|
|
547
|
+
...options.workspace,
|
|
548
|
+
provider: { type: saved.provider }
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
if (saved.allowedUsers.length) {
|
|
552
|
+
merged.allowedUsers = options.allowedUsers ? options.allowedUsers.filter((id) => saved.allowedUsers.includes(id)) : saved.allowedUsers;
|
|
553
|
+
}
|
|
554
|
+
merged.allowGuests = saved.allowGuests && (options.allowGuests ?? true);
|
|
555
|
+
if (saved.instructions)
|
|
556
|
+
merged.instructions ??= saved.instructions;
|
|
557
|
+
if (saved.setup)
|
|
558
|
+
merged.setup ??= [saved.setup];
|
|
559
|
+
if (saved.githubAccess === "token" && !merged.workspace?.repositoryCredentials) {
|
|
560
|
+
log.warn(`"${saved.name}" uses your own GitHub token, but GITTERM_BOT_GITHUB_TOKEN is not set. Public repositories still work.`);
|
|
561
|
+
}
|
|
562
|
+
log.info("Effective bot authorization", {
|
|
563
|
+
botId: saved.id,
|
|
564
|
+
source: "saved policy, intersected with local restrictions",
|
|
565
|
+
channels: merged.allowedChannels ?? "all invited channels",
|
|
566
|
+
users: merged.allowedUsers ?? "channel members",
|
|
567
|
+
allowGuests: merged.allowGuests
|
|
568
|
+
});
|
|
569
|
+
return merged;
|
|
570
|
+
}
|
|
571
|
+
|
|
309
572
|
// src/state.ts
|
|
310
573
|
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
311
574
|
import { dirname } from "node:path";
|
|
@@ -393,12 +656,14 @@ async function openStatusLine(adapter, thread, messageId) {
|
|
|
393
656
|
const ms = Date.now() - startedAt;
|
|
394
657
|
return ms >= TICK_MS ? ` (${minutes(ms)} min)` : "";
|
|
395
658
|
};
|
|
396
|
-
|
|
659
|
+
let queue = Promise.resolve();
|
|
660
|
+
const send = (update) => queue = queue.then(update).catch(noop);
|
|
661
|
+
const render = () => send(() => {
|
|
397
662
|
if (id)
|
|
398
|
-
return adapter.edit(thread, id, `${current.message}${elapsed()}`)
|
|
663
|
+
return adapter.edit(thread, id, `${current.message}${elapsed()}`);
|
|
399
664
|
const text = current.indicator && `${current.indicator}${elapsed()}`;
|
|
400
|
-
return adapter.indicate?.(thread, text)
|
|
401
|
-
};
|
|
665
|
+
return adapter.indicate?.(thread, text);
|
|
666
|
+
});
|
|
402
667
|
const timer = setInterval(() => {
|
|
403
668
|
if (id || current.indicator)
|
|
404
669
|
render();
|
|
@@ -418,18 +683,15 @@ async function openStatusLine(adapter, thread, messageId) {
|
|
|
418
683
|
async finish(text) {
|
|
419
684
|
end();
|
|
420
685
|
if (id) {
|
|
421
|
-
await adapter.edit(thread, id, text)
|
|
686
|
+
await send(() => adapter.edit(thread, id, text));
|
|
422
687
|
return;
|
|
423
688
|
}
|
|
424
|
-
await adapter.indicate?.(thread, "")
|
|
689
|
+
await send(() => adapter.indicate?.(thread, ""));
|
|
425
690
|
await adapter.post(thread, text).catch(noop);
|
|
426
691
|
},
|
|
427
692
|
async remove() {
|
|
428
693
|
end();
|
|
429
|
-
|
|
430
|
-
await adapter.remove(thread, id).catch(noop);
|
|
431
|
-
else
|
|
432
|
-
await adapter.indicate?.(thread, "").catch(noop);
|
|
694
|
+
await send(() => id ? adapter.remove(thread, id) : adapter.indicate?.(thread, ""));
|
|
433
695
|
},
|
|
434
696
|
stop: end
|
|
435
697
|
};
|
|
@@ -466,7 +728,7 @@ var threadKeyOf = (thread) => `${thread.channel}:${thread.thread}`;
|
|
|
466
728
|
var noop2 = () => {
|
|
467
729
|
return;
|
|
468
730
|
};
|
|
469
|
-
var
|
|
731
|
+
var isClient2 = (value) => typeof value === "object" && value !== null && ("runs" in value);
|
|
470
732
|
function describeAnswer(answer) {
|
|
471
733
|
if (answer.kind === "question")
|
|
472
734
|
return answer.labels.join(", ");
|
|
@@ -511,25 +773,9 @@ function recentIds(limit) {
|
|
|
511
773
|
return true;
|
|
512
774
|
};
|
|
513
775
|
}
|
|
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
|
-
}
|
|
776
|
+
function settingsFor(options) {
|
|
529
777
|
const defaultRepo = options.repo ? parseRepo(options.repo) : undefined;
|
|
530
778
|
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
779
|
const channelRepos = new Map(Object.entries(options.channels ?? {}).map(([channel, repo]) => [channel, parseRepo(repo)]));
|
|
534
780
|
const knownRepos = [
|
|
535
781
|
...new Map([defaultRepo, ...channelRepos.values(), ...extraRepos].filter((repo) => Boolean(repo)).map((repo) => [repoKey(repo), repo])).values()
|
|
@@ -537,6 +783,32 @@ function createBot(options) {
|
|
|
537
783
|
if (knownRepos.length === 0) {
|
|
538
784
|
throw new Error("Give the bot a `repo`, `repos` to choose from, or a repository per channel.");
|
|
539
785
|
}
|
|
786
|
+
return {
|
|
787
|
+
options,
|
|
788
|
+
defaultRepo,
|
|
789
|
+
extraRepos,
|
|
790
|
+
channelRepos,
|
|
791
|
+
knownRepos,
|
|
792
|
+
allowedUsers: options.allowedUsers ? new Set(options.allowedUsers) : undefined,
|
|
793
|
+
model: typeof options.model === "string" ? { id: options.model } : options.model
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function createBot(initial) {
|
|
797
|
+
const { adapter } = initial;
|
|
798
|
+
const log = initial.logger ?? console;
|
|
799
|
+
const gitterm = isClient2(initial.gitterm) ? initial.gitterm : createGittermClient2(initial.gitterm);
|
|
800
|
+
const runTimeoutMs = initial.runTimeoutMs ?? 60 * 60000;
|
|
801
|
+
const inputTimeoutMs = initial.inputTimeoutMs ?? 30 * 60000;
|
|
802
|
+
for (const [name, value, maximum] of [
|
|
803
|
+
["runTimeoutMs", runTimeoutMs, 24 * 60 * 60000],
|
|
804
|
+
["inputTimeoutMs", inputTimeoutMs, 24 * 60 * 60000],
|
|
805
|
+
["maxPendingRequests", initial.maxPendingRequests ?? 20, 1000],
|
|
806
|
+
["maxPendingPerUser", initial.maxPendingPerUser ?? 3, 1000]
|
|
807
|
+
]) {
|
|
808
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum)
|
|
809
|
+
throw new Error(`${name} must be a positive integer no greater than ${maximum}`);
|
|
810
|
+
}
|
|
811
|
+
let { options, defaultRepo, extraRepos, channelRepos, knownRepos, allowedUsers, model } = settingsFor(initial);
|
|
540
812
|
const lock = createLocks();
|
|
541
813
|
const fresh = recentIds(RECENT_MESSAGES);
|
|
542
814
|
const active = new Map;
|
|
@@ -550,6 +822,7 @@ function createBot(options) {
|
|
|
550
822
|
let running = false;
|
|
551
823
|
let leaseTimer;
|
|
552
824
|
let leaseId;
|
|
825
|
+
let savedAt;
|
|
553
826
|
const tasks = new Set;
|
|
554
827
|
const trackTask = (task) => {
|
|
555
828
|
tasks.add(task);
|
|
@@ -1088,11 +1361,64 @@ function createBot(options) {
|
|
|
1088
1361
|
if (!(error instanceof GittermError2 && error.code === "CONFLICT") || waited >= 1e5)
|
|
1089
1362
|
throw error;
|
|
1090
1363
|
if (waited === 0)
|
|
1091
|
-
log.warn("Another process is running this bot
|
|
1364
|
+
log.warn("Another process is running this bot, or one stopped without calling bot.stop() and holds it for up to 90 s; waiting…");
|
|
1092
1365
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
1093
1366
|
}
|
|
1094
1367
|
}
|
|
1095
1368
|
}
|
|
1369
|
+
const workspaceManager = () => createWorkspaceManager({
|
|
1370
|
+
gitterm,
|
|
1371
|
+
platform: adapter.platform,
|
|
1372
|
+
scope,
|
|
1373
|
+
botId: options.botId,
|
|
1374
|
+
directRetentionMs: options.directRetentionMs,
|
|
1375
|
+
identityPolicy: {
|
|
1376
|
+
allowedUsers: options.allowedUsers,
|
|
1377
|
+
allowGuests: options.allowGuests,
|
|
1378
|
+
allowedChannels: options.allowedChannels,
|
|
1379
|
+
shareChannels: options.shareChannels
|
|
1380
|
+
},
|
|
1381
|
+
connections: options.connections ?? [],
|
|
1382
|
+
env: options.env ?? {},
|
|
1383
|
+
setup: options.setup ?? [],
|
|
1384
|
+
model,
|
|
1385
|
+
instructions: agentInstructions(adapter.displayName, options.instructions),
|
|
1386
|
+
overrides: options.workspace,
|
|
1387
|
+
log
|
|
1388
|
+
});
|
|
1389
|
+
async function reload() {
|
|
1390
|
+
const local = options.localOptions;
|
|
1391
|
+
if (!local)
|
|
1392
|
+
return;
|
|
1393
|
+
try {
|
|
1394
|
+
const saved = await gitterm.bots.self();
|
|
1395
|
+
if (!saved)
|
|
1396
|
+
return;
|
|
1397
|
+
savedAt = saved.updatedAt;
|
|
1398
|
+
if (saved.platform !== adapter.platform) {
|
|
1399
|
+
log.warn(`"${saved.name}" is now a ${saved.platform} bot; run it on that platform.`);
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
const next = settingsFor({
|
|
1403
|
+
...applySavedConfig(local, saved, log),
|
|
1404
|
+
adapter,
|
|
1405
|
+
localOptions: local
|
|
1406
|
+
});
|
|
1407
|
+
await checkSetup({
|
|
1408
|
+
gitterm,
|
|
1409
|
+
repos: next.knownRepos,
|
|
1410
|
+
connections: next.options.connections ?? [],
|
|
1411
|
+
model: next.model,
|
|
1412
|
+
overrides: next.options.workspace,
|
|
1413
|
+
log
|
|
1414
|
+
});
|
|
1415
|
+
({ options, defaultRepo, extraRepos, channelRepos, knownRepos, allowedUsers, model } = next);
|
|
1416
|
+
workspaces = workspaceManager();
|
|
1417
|
+
log.info(`Applied the settings saved for "${saved.name}". New sandboxes use them; reset replaces an existing one.`);
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
log.warn("Could not apply the newly saved settings; keeping the current ones. Save again in GitTerm to retry.", error);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1096
1422
|
const bot = {
|
|
1097
1423
|
async start() {
|
|
1098
1424
|
if (running)
|
|
@@ -1106,6 +1432,7 @@ function createBot(options) {
|
|
|
1106
1432
|
throw new Error("Load the saved configuration with withSavedConfig() before starting a saved bot");
|
|
1107
1433
|
}
|
|
1108
1434
|
if (savedIdentity) {
|
|
1435
|
+
savedAt = savedIdentity.updatedAt;
|
|
1109
1436
|
const id = randomUUID();
|
|
1110
1437
|
await claimLease(id);
|
|
1111
1438
|
leaseId = id;
|
|
@@ -1115,7 +1442,11 @@ function createBot(options) {
|
|
|
1115
1442
|
if (renewing || shutdown.signal.aborted)
|
|
1116
1443
|
return;
|
|
1117
1444
|
renewing = true;
|
|
1118
|
-
gitterm.bots.acquireLease(id).then(
|
|
1445
|
+
gitterm.bots.acquireLease(id).then(async (lease) => {
|
|
1446
|
+
failures = 0;
|
|
1447
|
+
if (lease.updatedAt && lease.updatedAt !== savedAt)
|
|
1448
|
+
await reload();
|
|
1449
|
+
}).catch((error) => {
|
|
1119
1450
|
const lost = error instanceof GittermError2 && error.code === "CONFLICT";
|
|
1120
1451
|
if (!lost && ++failures < 2)
|
|
1121
1452
|
return;
|
|
@@ -1154,26 +1485,7 @@ function createBot(options) {
|
|
|
1154
1485
|
accepts: servesChannel
|
|
1155
1486
|
});
|
|
1156
1487
|
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
|
-
});
|
|
1488
|
+
workspaces = workspaceManager();
|
|
1177
1489
|
await recover();
|
|
1178
1490
|
markReady();
|
|
1179
1491
|
log.info(`GitTerm ${adapter.displayName} bot is running for ${knownRepos.map(repoLabel).join(", ")}.`);
|
|
@@ -1236,256 +1548,6 @@ async function downloadImage(url, options) {
|
|
|
1236
1548
|
reader.releaseLock();
|
|
1237
1549
|
}
|
|
1238
1550
|
}
|
|
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
1551
|
// src/markdown.ts
|
|
1490
1552
|
var TABLE_SEPARATOR = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/;
|
|
1491
1553
|
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.1",
|
|
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",
|