@artil-ai/server 0.2.1 → 0.2.3

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 CHANGED
@@ -31,23 +31,28 @@ The first run sets everything up. Files need an S3-compatible bucket, and
31
31
  reaching the server from outside needs Tailscale. Both can wait for
32
32
  `artil-server setup`.
33
33
 
34
- **2. Sign a client in**
34
+ **2. Sign in the accounts the setup made**
35
35
 
36
36
  ```bash
37
- artil auth login --base-url http://localhost:4747
37
+ artil auth login --base-url http://localhost:4747 --account claude
38
+ artil auth login --base-url http://localhost:4747 --account codex
38
39
  ```
39
40
 
40
- The CLI remembers the server from here on.
41
+ `--base-url` needs artil 0.42.0 or later; `artil update` gets it. The CLI
42
+ remembers the server from here on, and works as `claude`, the first account,
43
+ unless you [choose another](https://docs.artil.ai/accounts).
41
44
 
42
- **3. Message another agent**
45
+ **3. Connect a client to each account**
46
+
47
+ Start Claude Code and Codex
48
+ [as Artil Connect describes](https://docs.artil.ai/connect), each as its own
49
+ account. A message reaches an account only while its client listens.
50
+
51
+ **4. Message another agent**
43
52
 
44
53
  ```bash
45
54
  artil url send codex "Review the release branch"
46
55
  ```
47
56
 
48
- That is `claude` writing to `codex`, the other account the setup made.
49
- Everything else works [as with Artil Cloud](https://docs.artil.ai/connect).
50
-
51
- ## TODO
52
-
53
- - [ ] Message URL as a regular channel, not a transport built into the server
57
+ That is `claude` writing to `codex`. Everything else works as with Artil
58
+ Cloud.
package/bin/main.js CHANGED
@@ -22326,7 +22326,7 @@ var channel = {
22326
22326
  create: ({ webhookUrl, secret, fetch: fetch2 }) => new TelegramChannel(createBotApi({ webhook: webhookUrl ? { url: webhookUrl, secret } : undefined, fetch: fetch2 }))
22327
22327
  };
22328
22328
  // package.json
22329
- var version2 = "0.2.1";
22329
+ var version2 = "0.2.3";
22330
22330
 
22331
22331
  // src/app/cli.ts
22332
22332
  import { parseArgs } from "util";
@@ -52142,7 +52142,9 @@ async function startServer({
52142
52142
  store = config2.s3 ? createS3Store(config2.s3) : undefined,
52143
52143
  fetch: fetch2
52144
52144
  }) {
52145
+ await using startup = new AsyncDisposableStack;
52145
52146
  const database = openDatabase({ path: databasePath, migrationsFolder, secret: config2.secret });
52147
+ startup.defer(() => database.close());
52146
52148
  let handleRequest = startingResponse;
52147
52149
  const server = Bun.serve({
52148
52150
  hostname: "127.0.0.1",
@@ -52150,12 +52152,11 @@ async function startServer({
52150
52152
  fetch: (request) => handleRequest(request),
52151
52153
  idleTimeout: 60
52152
52154
  });
52155
+ startup.defer(() => server.stop(true));
52153
52156
  const port = server.port ?? config2.port;
52154
- const publicUrl = config2.public && tailscale ? await tailscale.open(port).catch(async (cause) => {
52155
- await server.stop(true);
52156
- database.close();
52157
- throw cause;
52158
- }) : undefined;
52157
+ const publicUrl = config2.public && tailscale ? await tailscale.open(port) : undefined;
52158
+ if (publicUrl)
52159
+ startup.defer(() => tailscale?.close());
52159
52160
  const baseUrl = publicUrl ?? `http://localhost:${port}`;
52160
52161
  const channels = createChannels({
52161
52162
  definitions,
@@ -52190,6 +52191,10 @@ async function startServer({
52190
52191
  for (const channelReceivers of receivers)
52191
52192
  channelReceivers.sync();
52192
52193
  }
52194
+ startup.defer(() => {
52195
+ for (const channelReceivers of receivers)
52196
+ channelReceivers.stop();
52197
+ });
52193
52198
  syncReceivers();
52194
52199
  let dataVersion = database.dataVersion();
52195
52200
  const databaseWatcher = setInterval(() => {
@@ -52199,28 +52204,193 @@ async function startServer({
52199
52204
  syncReceivers();
52200
52205
  }
52201
52206
  }, 2 * second);
52207
+ startup.defer(() => clearInterval(databaseWatcher));
52202
52208
  function removeExpiredFiles() {
52203
52209
  services.files.removeExpired().catch((cause) => log2(`Could not remove expired files: ${messageOf(cause, String(cause))}`));
52204
52210
  }
52205
52211
  removeExpiredFiles();
52206
52212
  const expiredFilesTimer = setInterval(removeExpiredFiles, hour);
52213
+ startup.defer(() => clearInterval(expiredFilesTimer));
52214
+ const resources = startup.move();
52207
52215
  return {
52208
52216
  baseUrl,
52209
52217
  port,
52210
52218
  services,
52211
- async stop() {
52212
- clearInterval(databaseWatcher);
52213
- clearInterval(expiredFilesTimer);
52214
- for (const channelReceivers of receivers)
52215
- channelReceivers.stop();
52216
- await server.stop(true);
52217
- if (publicUrl)
52218
- await tailscale?.close();
52219
- database.close();
52220
- }
52219
+ stop: () => resources.disposeAsync()
52221
52220
  };
52222
52221
  }
52223
52222
 
52223
+ // src/app/cli.ts
52224
+ var usage = `artil-server: a self-hosted Artil Server
52225
+
52226
+ Usage:
52227
+ artil-server Start the server, setting it up first if it is new
52228
+ artil-server setup Set up the accounts, channels, files and who reaches the server
52229
+ artil-server account create <name> Create an agent account
52230
+ artil-server account list List the accounts
52231
+ artil-server account remove <name> Remove an account with its messages
52232
+ artil-server account token <name> Make a token, for a computer without a browser
52233
+ artil-server account revoke <name> Sign the account out of every client
52234
+ artil-server <channel> connect <account> <credentials> Connect an address of a channel to an account, with what the channel asks for
52235
+ artil-server <channel> disconnect <account> Disconnect the account's address of the channel
52236
+ artil-server access add <account> <channel> <sender> Let someone wake the agent on the channel
52237
+ artil-server access remove <account> <channel> <sender> Take them off the list
52238
+ artil-server access set <account> <channel> <who> Choose who wakes the agent on the channel: anyone, team or list
52239
+ artil-server password Set the password the sign-in page asks for
52240
+
52241
+ Options:
52242
+ --public Open the server to the internet through Tailscale Funnel
52243
+ --local Keep the server on this computer
52244
+ -h, --help Show this help
52245
+ -v, --version Show the version`;
52246
+ function usageError(message) {
52247
+ return new AppError({ code: "invalid_input", message: `${message}
52248
+
52249
+ ${usage}` });
52250
+ }
52251
+ async function serve({ paths, config: config2, environment }) {
52252
+ const { migrationsFolder, tailscale, out } = environment;
52253
+ if (config2.public && !tailscale) {
52254
+ throw new AppError({
52255
+ code: "unavailable",
52256
+ message: "Public mode needs Tailscale: install it from https://tailscale.com/download, or start with --local"
52257
+ });
52258
+ }
52259
+ if (config2.public && config2.password === null) {
52260
+ throw new AppError({
52261
+ code: "invalid_input",
52262
+ message: "Public mode needs a password for the sign-in page: set one with artil-server password"
52263
+ });
52264
+ }
52265
+ const server = await startServer({
52266
+ config: config2,
52267
+ databasePath: paths.database,
52268
+ migrationsFolder,
52269
+ tailscale,
52270
+ channels: await loadChannels({
52271
+ specifiers: config2.channels,
52272
+ dataDir: paths.dataDir,
52273
+ builtInChannels: environment.builtInChannels
52274
+ }),
52275
+ password: async () => (await readConfig(paths))?.password ?? null,
52276
+ log: out
52277
+ });
52278
+ out(`Artil Server is at ${server.baseUrl}${config2.public ? " (public, through Tailscale Funnel)" : ""}.`);
52279
+ out(`Sign an agent in with: ${loginCommand(server.baseUrl)}`);
52280
+ out(`MCP clients connect to ${server.baseUrl}/mcp`);
52281
+ await environment.waitForStop();
52282
+ out("Stopping");
52283
+ await server.stop();
52284
+ }
52285
+ async function setPassword({ paths, config: config2, out }) {
52286
+ const newPassword = await password({
52287
+ message: "A password the sign-in page asks for",
52288
+ validate: validatePassword
52289
+ });
52290
+ if (isCancel(newPassword))
52291
+ return;
52292
+ await writeConfig(paths, { ...config2, password: await hashPassword(newPassword) });
52293
+ out("The sign-in page asks for it from now on.");
52294
+ }
52295
+ async function runCli(environment) {
52296
+ const { values, positionals } = parseArgs({
52297
+ args: environment.argv,
52298
+ allowPositionals: true,
52299
+ options: {
52300
+ public: { type: "boolean" },
52301
+ local: { type: "boolean" },
52302
+ help: { type: "boolean", short: "h" },
52303
+ version: { type: "boolean", short: "v" }
52304
+ }
52305
+ });
52306
+ if (values.help)
52307
+ return environment.out(usage);
52308
+ if (values.version)
52309
+ return environment.out(environment.version);
52310
+ const paths = pathsIn(environment.dataDir);
52311
+ const savedConfig = await readConfig(paths);
52312
+ async function applyFlags(config3) {
52313
+ const changed = {
52314
+ ...config3,
52315
+ ...values.public && { public: true },
52316
+ ...values.local && { public: false }
52317
+ };
52318
+ if (changed.public !== config3.public)
52319
+ await writeConfig(paths, changed);
52320
+ return changed;
52321
+ }
52322
+ const [command = "", subcommand = "", ...args] = positionals;
52323
+ const { migrationsFolder, tailscale, artil, runSetup, out, interactive, builtInChannels } = environment;
52324
+ const builtInKinds = builtInChannels.map(({ kind }) => kind);
52325
+ async function hasAgents(config3) {
52326
+ const context2 = await openCommandContext({
52327
+ paths,
52328
+ config: config3,
52329
+ builtInChannels,
52330
+ migrationsFolder,
52331
+ tailscale,
52332
+ out
52333
+ });
52334
+ try {
52335
+ return context2.agents.list().length > 0;
52336
+ } finally {
52337
+ context2.close();
52338
+ }
52339
+ }
52340
+ if (command === "" || command === "start") {
52341
+ const config3 = interactive && (!savedConfig || !await hasAgents(savedConfig)) ? await runSetup({
52342
+ paths,
52343
+ config: savedConfig,
52344
+ builtInChannels,
52345
+ artil,
52346
+ migrationsFolder,
52347
+ tailscale,
52348
+ out
52349
+ }) : savedConfig ?? newConfig({ channels: builtInKinds });
52350
+ if (!savedConfig && !interactive)
52351
+ await writeConfig(paths, config3);
52352
+ return serve({ paths, config: await applyFlags(config3), environment });
52353
+ }
52354
+ if (command === "setup") {
52355
+ await runSetup({
52356
+ paths,
52357
+ config: savedConfig,
52358
+ builtInChannels,
52359
+ artil,
52360
+ migrationsFolder,
52361
+ tailscale,
52362
+ out
52363
+ });
52364
+ return;
52365
+ }
52366
+ const config2 = savedConfig ?? newConfig({ channels: builtInKinds });
52367
+ if (!savedConfig)
52368
+ await writeConfig(paths, config2);
52369
+ if (command === "password")
52370
+ return setPassword({ paths, config: config2, out });
52371
+ const context = await openCommandContext({
52372
+ paths,
52373
+ config: config2,
52374
+ builtInChannels,
52375
+ migrationsFolder,
52376
+ tailscale,
52377
+ out
52378
+ });
52379
+ try {
52380
+ const run2 = new Map([...createAgentCommands(context), ...createAccessCommands(context)]).get(`${command} ${subcommand}`) ?? createAddressCommands(context).get(subcommand)?.(command);
52381
+ if (!run2) {
52382
+ throw usageError(`There is no command ${[command, subcommand].filter(Boolean).join(" ")}`);
52383
+ }
52384
+ if (args.length !== run2.takes.length) {
52385
+ const expected = run2.takes.map((name) => `<${name}>`).join(" ") || "no arguments";
52386
+ throw usageError(`This command takes ${expected}`);
52387
+ }
52388
+ await run2.run(Object.fromEntries(run2.takes.map((name, index2) => [name, args[index2] ?? ""])));
52389
+ } finally {
52390
+ context.close();
52391
+ }
52392
+ }
52393
+
52224
52394
  // src/app/setup.ts
52225
52395
  class SetupStopped extends Error {
52226
52396
  name = "SetupStopped";
@@ -52456,165 +52626,6 @@ async function runSetup({
52456
52626
  }
52457
52627
  }
52458
52628
 
52459
- // src/app/cli.ts
52460
- var usage = `artil-server: a self-hosted Artil Server
52461
-
52462
- Usage:
52463
- artil-server Start the server, setting it up first if it is new
52464
- artil-server setup Set up the accounts, channels, files and who reaches the server
52465
- artil-server account create <name> Create an agent account
52466
- artil-server account list List the accounts
52467
- artil-server account remove <name> Remove an account with its messages
52468
- artil-server account token <name> Make a token, for a computer without a browser
52469
- artil-server account revoke <name> Sign the account out of every client
52470
- artil-server <channel> connect <account> <credentials> Connect an address of a channel to an account, with what the channel asks for
52471
- artil-server <channel> disconnect <account> Disconnect the account's address of the channel
52472
- artil-server access add <account> <channel> <sender> Let someone wake the agent on the channel
52473
- artil-server access remove <account> <channel> <sender> Take them off the list
52474
- artil-server access set <account> <channel> <who> Choose who wakes the agent on the channel: anyone, team or list
52475
- artil-server password Set the password the sign-in page asks for
52476
-
52477
- Options:
52478
- --public Open the server to the internet through Tailscale Funnel
52479
- --local Keep the server on this computer
52480
- -h, --help Show this help
52481
- -v, --version Show the version`;
52482
- function usageError(message) {
52483
- return new AppError({ code: "invalid_input", message: `${message}
52484
-
52485
- ${usage}` });
52486
- }
52487
- async function serve({ paths, config: config2, environment }) {
52488
- const { migrationsFolder, tailscale, out } = environment;
52489
- if (config2.public && !tailscale) {
52490
- throw new AppError({
52491
- code: "unavailable",
52492
- message: "Public mode needs Tailscale: install it from https://tailscale.com/download, or start with --local"
52493
- });
52494
- }
52495
- if (config2.public && config2.password === null) {
52496
- throw new AppError({
52497
- code: "invalid_input",
52498
- message: "Public mode needs a password for the sign-in page: set one with artil-server password"
52499
- });
52500
- }
52501
- const server = await startServer({
52502
- config: config2,
52503
- databasePath: paths.database,
52504
- migrationsFolder,
52505
- tailscale,
52506
- channels: await loadChannels({
52507
- specifiers: config2.channels,
52508
- dataDir: paths.dataDir,
52509
- builtInChannels: environment.builtInChannels
52510
- }),
52511
- password: async () => (await readConfig(paths))?.password ?? null,
52512
- log: out
52513
- });
52514
- out(`Artil Server is at ${server.baseUrl}${config2.public ? " (public, through Tailscale Funnel)" : ""}.`);
52515
- out(`Sign an agent in with: ${loginCommand(server.baseUrl)}`);
52516
- out(`MCP clients connect to ${server.baseUrl}/mcp`);
52517
- const { promise: promise3, resolve } = Promise.withResolvers();
52518
- process.once("SIGINT", resolve);
52519
- process.once("SIGTERM", resolve);
52520
- await promise3;
52521
- out("Stopping");
52522
- await server.stop();
52523
- }
52524
- async function setPassword({ paths, config: config2, out }) {
52525
- const newPassword = await password({
52526
- message: "A password the sign-in page asks for",
52527
- validate: validatePassword
52528
- });
52529
- if (isCancel(newPassword))
52530
- return;
52531
- await writeConfig(paths, { ...config2, password: await hashPassword(newPassword) });
52532
- out("The sign-in page asks for it from now on.");
52533
- }
52534
- async function runCli(environment) {
52535
- const { values, positionals } = parseArgs({
52536
- args: environment.argv,
52537
- allowPositionals: true,
52538
- options: {
52539
- public: { type: "boolean" },
52540
- local: { type: "boolean" },
52541
- help: { type: "boolean", short: "h" },
52542
- version: { type: "boolean", short: "v" }
52543
- }
52544
- });
52545
- if (values.help)
52546
- return environment.out(usage);
52547
- if (values.version)
52548
- return environment.out(environment.version);
52549
- const paths = pathsIn(environment.dataDir);
52550
- const savedConfig = await readConfig(paths);
52551
- async function applyFlags(config3) {
52552
- const changed = {
52553
- ...config3,
52554
- ...values.public && { public: true },
52555
- ...values.local && { public: false }
52556
- };
52557
- if (changed.public !== config3.public)
52558
- await writeConfig(paths, changed);
52559
- return changed;
52560
- }
52561
- const [command = "", subcommand = "", ...args] = positionals;
52562
- const { migrationsFolder, tailscale, artil, out, interactive, builtInChannels } = environment;
52563
- const builtInKinds = builtInChannels.map(({ kind }) => kind);
52564
- if (command === "" || command === "start") {
52565
- const config3 = savedConfig ?? (interactive ? await runSetup({
52566
- paths,
52567
- config: undefined,
52568
- builtInChannels,
52569
- artil,
52570
- migrationsFolder,
52571
- tailscale,
52572
- out
52573
- }) : newConfig({ channels: builtInKinds }));
52574
- if (!savedConfig && !interactive)
52575
- await writeConfig(paths, config3);
52576
- return serve({ paths, config: await applyFlags(config3), environment });
52577
- }
52578
- if (command === "setup") {
52579
- await runSetup({
52580
- paths,
52581
- config: savedConfig,
52582
- builtInChannels,
52583
- artil,
52584
- migrationsFolder,
52585
- tailscale,
52586
- out
52587
- });
52588
- return;
52589
- }
52590
- const config2 = savedConfig ?? newConfig({ channels: builtInKinds });
52591
- if (!savedConfig)
52592
- await writeConfig(paths, config2);
52593
- if (command === "password")
52594
- return setPassword({ paths, config: config2, out });
52595
- const context = await openCommandContext({
52596
- paths,
52597
- config: config2,
52598
- builtInChannels,
52599
- migrationsFolder,
52600
- tailscale,
52601
- out
52602
- });
52603
- try {
52604
- const run2 = new Map([...createAgentCommands(context), ...createAccessCommands(context)]).get(`${command} ${subcommand}`) ?? createAddressCommands(context).get(subcommand)?.(command);
52605
- if (!run2) {
52606
- throw usageError(`There is no command ${[command, subcommand].filter(Boolean).join(" ")}`);
52607
- }
52608
- if (args.length !== run2.takes.length) {
52609
- const expected = run2.takes.map((name) => `<${name}>`).join(" ") || "no arguments";
52610
- throw usageError(`This command takes ${expected}`);
52611
- }
52612
- await run2.run(Object.fromEntries(run2.takes.map((name, index2) => [name, args[index2] ?? ""])));
52613
- } finally {
52614
- context.close();
52615
- }
52616
- }
52617
-
52618
52629
  // src/platform/tailscale.ts
52619
52630
  import { existsSync } from "fs";
52620
52631
  var statusSchema = exports_external2.object({
@@ -52694,6 +52705,12 @@ function runWith(binary) {
52694
52705
  var migrationsFolder = fileURLToPath(new URL("../migrations", import.meta.url));
52695
52706
  process.umask(63);
52696
52707
  var binary = findTailscale(process.platform);
52708
+ function waitForStop() {
52709
+ const { promise: promise3, resolve } = Promise.withResolvers();
52710
+ process.once("SIGINT", resolve);
52711
+ process.once("SIGTERM", resolve);
52712
+ return promise3;
52713
+ }
52697
52714
  try {
52698
52715
  await runCli({
52699
52716
  argv: process.argv.slice(2),
@@ -52704,6 +52721,8 @@ try {
52704
52721
  migrationsFolder,
52705
52722
  tailscale: binary === undefined ? undefined : createTailscale({ run: runWith(binary) }),
52706
52723
  artil: findArtilCli(),
52724
+ runSetup,
52725
+ waitForStop,
52707
52726
  out: (line) => console.log(line)
52708
52727
  });
52709
52728
  } catch (cause) {
@@ -52713,5 +52732,5 @@ try {
52713
52732
  process.exitCode = 1;
52714
52733
  }
52715
52734
 
52716
- //# debugId=3707CE11C3FC2DD064756E2164756E21
52735
+ //# debugId=874EED01CEF8F5FE64756E2164756E21
52717
52736
  //# sourceMappingURL=main.js.map