@agentrific/server 0.6.0 → 0.7.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/index.js CHANGED
@@ -885,10 +885,10 @@ var Attachments = class extends Context.Service()(
885
885
 
886
886
  // src/index.ts
887
887
  import { randomUUID as randomUUID9 } from "node:crypto";
888
- import { execFile as execFile3 } from "node:child_process";
889
- import { promisify as promisify3 } from "node:util";
888
+ import { execFile as execFile4 } from "node:child_process";
889
+ import { promisify as promisify4 } from "node:util";
890
890
  import { realpath as realpath2 } from "node:fs/promises";
891
- import { homedir as homedir3 } from "node:os";
891
+ import { homedir as homedir4 } from "node:os";
892
892
  import path11 from "node:path";
893
893
  import { WebSocket } from "ws";
894
894
 
@@ -993,6 +993,7 @@ var ControlStatusSchema = z4.object({
993
993
  serverId: z4.string().uuid(),
994
994
  publicUrl: z4.string().optional(),
995
995
  phoneProvider: ProviderSchema.nullable().optional(),
996
+ phoneProgress: z4.string().nullable().optional(),
996
997
  localUrl: z4.string().optional(),
997
998
  serverVersion: z4.string().optional(),
998
999
  connectedDevices: z4.number().optional(),
@@ -1002,7 +1003,11 @@ var ControlStatusSchema = z4.object({
1002
1003
  maintenance: MaintenanceSchema.optional()
1003
1004
  });
1004
1005
  var ControlResultSchema = z4.union([
1005
- z4.object({ provider: ProviderSchema.nullable(), publicUrl: z4.string().url().nullable() }),
1006
+ z4.object({
1007
+ provider: ProviderSchema.nullable(),
1008
+ publicUrl: z4.string().url().nullable(),
1009
+ progress: z4.string().nullable().optional()
1010
+ }),
1006
1011
  z4.array(z4.object({ provider: ProviderSchema, available: z4.boolean(), reason: z4.string() })),
1007
1012
  z4.object({
1008
1013
  pairingId: z4.string(),
@@ -1173,7 +1178,7 @@ import { styleText } from "node:util";
1173
1178
  // package.json
1174
1179
  var package_default = {
1175
1180
  name: "@agentrific/server",
1176
- version: "0.6.0",
1181
+ version: "0.7.0",
1177
1182
  description: "Self-hosted control plane for long-running coding agents",
1178
1183
  keywords: [
1179
1184
  "agentrific",
@@ -1199,20 +1204,28 @@ var package_default = {
1199
1204
  registry: "https://registry.npmjs.org"
1200
1205
  },
1201
1206
  scripts: {
1202
- dev: "tsx watch src/index.ts",
1207
+ dev: "node scripts/build-claude-bridge.mjs && tsx watch src/index.ts",
1203
1208
  start: "node dist/index.js",
1204
1209
  typecheck: "tsc --noEmit",
1205
1210
  build: "node scripts/build.mjs",
1206
- cli: "AGENTRIFIC_DATA_DIR=./data tsx src/cli.ts"
1211
+ cli: "node scripts/build-claude-bridge.mjs && AGENTRIFIC_DATA_DIR=./data tsx src/cli.ts",
1212
+ "build:claude": "node scripts/build-claude-bridge.mjs"
1207
1213
  },
1208
1214
  dependencies: {
1215
+ "@anthropic-ai/claude-agent-sdk": "0.3.296",
1216
+ "@anthropic-ai/sdk": "^0.124.0",
1217
+ "@earendil-works/pi-ai": "1.0.0",
1209
1218
  "@earendil-works/pi-coding-agent": "1.0.0",
1210
1219
  "@fastify/cors": "^11.3.0",
1211
1220
  "@fastify/multipart": "^10.1.2",
1212
1221
  "@inquirer/select": "^5.2.5",
1222
+ "@modelcontextprotocol/sdk": "^1.29.0",
1223
+ "cc-session-io": "^0.4.0",
1224
+ "change-case": "^5.4.4",
1213
1225
  diff: "^9.0.0",
1214
1226
  effect: "4.0.0-rc.117",
1215
1227
  fastify: "^5.6.2",
1228
+ "pi-claude-agent-sdk": "0.9.2",
1216
1229
  "pino-pretty": "^13.1.3",
1217
1230
  "qrcode-terminal": "^0.12.0",
1218
1231
  semver: "^7.8.5",
@@ -1712,6 +1725,10 @@ var OpenTunnel = class {
1712
1725
  client;
1713
1726
  connection;
1714
1727
  stopping = false;
1728
+ stage = "reading-identity";
1729
+ progress() {
1730
+ return this.connection?.status().state ?? this.stage;
1731
+ }
1715
1732
  running() {
1716
1733
  return this.connection?.status().state === "connected" && !this.stopping;
1717
1734
  }
@@ -1741,13 +1758,32 @@ var OpenTunnel = class {
1741
1758
  let canceled = () => {
1742
1759
  };
1743
1760
  try {
1744
- const connecting = this.client.tunnel.connect({
1745
- profile: "agentrific",
1746
- routes: {
1747
- [route]: `${endpoint.hostname === "[::1]" ? "[::1]" : "127.0.0.1"}:${endpoint.port || "80"}`
1748
- },
1749
- signal
1750
- });
1761
+ const client = this.client;
1762
+ this.stage = "reading-identity";
1763
+ const connecting = (async () => {
1764
+ const options = {
1765
+ profile: "agentrific",
1766
+ onProgress: (stage) => {
1767
+ this.stage = stage;
1768
+ }
1769
+ };
1770
+ const identity2 = await client.tunnel.get(options);
1771
+ abort.throwIfAborted();
1772
+ if (!identity2) {
1773
+ const resumed = await client.tunnel.resume(options);
1774
+ abort.throwIfAborted();
1775
+ if (!resumed) await client.tunnel.create(options);
1776
+ }
1777
+ abort.throwIfAborted();
1778
+ this.stage = "connecting";
1779
+ return client.tunnel.connect({
1780
+ profile: "agentrific",
1781
+ routes: {
1782
+ [route]: `${endpoint.hostname === "[::1]" ? "[::1]" : "127.0.0.1"}:${endpoint.port || "80"}`
1783
+ },
1784
+ signal
1785
+ });
1786
+ })();
1751
1787
  const cancellation = new Promise((_resolve, reject) => {
1752
1788
  canceled = () => reject(new Error("OpenTunnel connection canceled or timed out."));
1753
1789
  abort.addEventListener("abort", canceled, { once: true });
@@ -1799,7 +1835,11 @@ var DesktopPhoneConnection = class {
1799
1835
  publicUrl = null;
1800
1836
  pending = Promise.resolve();
1801
1837
  snapshot() {
1802
- return { provider: this.provider, publicUrl: this.publicUrl };
1838
+ return {
1839
+ provider: this.provider,
1840
+ publicUrl: this.publicUrl,
1841
+ progress: this.tunnel?.progress?.() ?? null
1842
+ };
1803
1843
  }
1804
1844
  async options() {
1805
1845
  return (await this.detect()).map(({ provider, available, reason }) => ({
@@ -2013,7 +2053,7 @@ var UpdateDiscovery = class {
2013
2053
 
2014
2054
  // src/index.ts
2015
2055
  import { Effect as Effect17, ManagedRuntime } from "effect";
2016
- import { z as z19 } from "zod";
2056
+ import { z as z20 } from "zod";
2017
2057
 
2018
2058
  // src/automation.ts
2019
2059
  import { Effect as Effect7 } from "effect";
@@ -3179,14 +3219,17 @@ var DraftSchema = z13.object({ title: z13.string().trim().min(1).max(120), body:
3179
3219
  function deliveryWriter(models2, createSession = createAgentSession) {
3180
3220
  return async (run, input, diff, signal) => {
3181
3221
  signal?.throwIfAborted();
3222
+ const selected = run.model ?? await models2.defaultReference();
3223
+ const sessionOptions = await models2.sessionOptions(run.workspacePath);
3182
3224
  const { session } = await createSession({
3183
3225
  cwd: run.workspacePath,
3184
- model: await models2.resolve(run.model ?? await models2.defaultReference()),
3185
- modelRuntime: models2.runtime,
3226
+ model: await models2.resolve(selected),
3227
+ ...sessionOptions,
3186
3228
  sessionManager: SessionManager.inMemory(),
3187
3229
  tools: [],
3188
3230
  noTools: "all"
3189
3231
  });
3232
+ if (sessionOptions.resourceLoader) await session.bindExtensions({});
3190
3233
  const timer = setTimeout(() => {
3191
3234
  void session.abort();
3192
3235
  }, 6e4);
@@ -3216,6 +3259,103 @@ ${diff}`
3216
3259
  // src/index.ts
3217
3260
  import { ModelRuntime } from "@earendil-works/pi-coding-agent";
3218
3261
 
3262
+ // src/claude-code.ts
3263
+ import { execFile as execFile2 } from "node:child_process";
3264
+ import { homedir as homedir2 } from "node:os";
3265
+ import { join as join2 } from "node:path";
3266
+ import { promisify as promisify2 } from "node:util";
3267
+ import { z as z14 } from "zod";
3268
+ import {
3269
+ createAgentSessionServices
3270
+ } from "@earendil-works/pi-coding-agent";
3271
+ var claudePlatform = "claude-bridge";
3272
+ var execute2 = promisify2(execFile2);
3273
+ var AuthStatusSchema = z14.object({ loggedIn: z14.boolean() });
3274
+ async function discoverClaudeCode() {
3275
+ if (process.env.AGENTRIFIC_CLAUDE_CODE === "off") return;
3276
+ const candidates = [
3277
+ "claude",
3278
+ join2(homedir2(), ".local/bin/claude"),
3279
+ join2(homedir2(), ".claude/local/claude")
3280
+ ];
3281
+ for (const executable of candidates) {
3282
+ try {
3283
+ await execute2(executable, ["--version"], { timeout: 5e3, maxBuffer: 64 * 1024 });
3284
+ } catch {
3285
+ continue;
3286
+ }
3287
+ try {
3288
+ const { stdout } = await execute2(executable, ["auth", "status", "--json"], {
3289
+ timeout: 5e3,
3290
+ maxBuffer: 64 * 1024
3291
+ });
3292
+ const status = AuthStatusSchema.parse(JSON.parse(stdout));
3293
+ return { executable, ready: status.loggedIn };
3294
+ } catch {
3295
+ return { executable, ready: false };
3296
+ }
3297
+ }
3298
+ }
3299
+ var ClaudeCode = class {
3300
+ constructor(probe = discoverClaudeCode, query) {
3301
+ this.probe = probe;
3302
+ this.query = query;
3303
+ }
3304
+ probe;
3305
+ query;
3306
+ installation;
3307
+ registered = false;
3308
+ get ready() {
3309
+ return this.installation?.ready === true;
3310
+ }
3311
+ get warning() {
3312
+ return this.installation && !this.ready ? "Claude Code is installed but its login could not be verified. Run claude auth login as the server user, then refresh the model list." : null;
3313
+ }
3314
+ async refresh(runtime2) {
3315
+ this.installation = await this.probe();
3316
+ if (this.installation && !this.registered) {
3317
+ await this.services(process.cwd(), runtime2);
3318
+ this.registered = true;
3319
+ }
3320
+ }
3321
+ async services(cwd, modelRuntime2) {
3322
+ const module = await import(new URL("./claude-bridge.js", import.meta.url).href);
3323
+ const extension = module.createClaudeExtension(this.query);
3324
+ const services = await createAgentSessionServices({
3325
+ cwd,
3326
+ modelRuntime: modelRuntime2,
3327
+ resourceLoaderOptions: {
3328
+ // Match Pipeline: do not execute unrelated global Pi extensions.
3329
+ noExtensions: true,
3330
+ extensionFactories: [
3331
+ {
3332
+ name: "agentrific-claude-code",
3333
+ factory: (pi) => extension({
3334
+ ...pi,
3335
+ // SAFETY: The pinned bridge uses only the name/config registration overload.
3336
+ registerProvider: ((id, config) => pi.registerProvider(id, {
3337
+ ...config,
3338
+ // Bind the working directory to this provider,
3339
+ // without changing the process cwd or global Claude configuration.
3340
+ streamSimple: config.streamSimple && ((model, context, options) => {
3341
+ const request = { ...options, cwd };
3342
+ return config.streamSimple(model, context, request);
3343
+ })
3344
+ }))
3345
+ })
3346
+ }
3347
+ ]
3348
+ }
3349
+ });
3350
+ const errors = [
3351
+ ...services.diagnostics.filter((item) => item.type === "error").map((item) => item.message),
3352
+ ...services.resourceLoader.getExtensions().errors.map((item) => item.error)
3353
+ ];
3354
+ if (errors.length) throw new Error(errors.join("\n"));
3355
+ return services;
3356
+ }
3357
+ };
3358
+
3219
3359
  // src/models.ts
3220
3360
  import { Effect as Effect11 } from "effect";
3221
3361
  var legacyModel = { platformId: "openai-codex", modelId: "gpt-6-luna" };
@@ -3234,12 +3374,14 @@ var makers = [
3234
3374
  { id: "amazon", name: "Amazon", pattern: /amazon\.nova|nova-/i }
3235
3375
  ];
3236
3376
  var ServerModels = class {
3237
- constructor(runtime2, store2) {
3377
+ constructor(runtime2, store2, claude) {
3238
3378
  this.runtime = runtime2;
3239
3379
  this.store = store2;
3380
+ this.claude = claude;
3240
3381
  }
3241
3382
  runtime;
3242
3383
  store;
3384
+ claude;
3243
3385
  refreshing;
3244
3386
  refreshedAt = (/* @__PURE__ */ new Date()).toISOString();
3245
3387
  warning = null;
@@ -3261,18 +3403,20 @@ var ServerModels = class {
3261
3403
  platformId,
3262
3404
  modelId,
3263
3405
  name: model?.name ?? modelId,
3264
- platformName: this.runtime.getProvider(platformId)?.name ?? platformId,
3406
+ platformName: platformId === claudePlatform ? "Claude Code" : this.runtime.getProvider(platformId)?.name ?? platformId,
3265
3407
  providerId: namespace === "openrouter" ? "unspecified" : maker?.id ?? namespace ?? "unspecified",
3266
3408
  providerName: namespace === "openrouter" ? "Other / custom" : maker?.name ?? publisherName ?? "Other / custom"
3267
3409
  };
3268
3410
  }
3269
3411
  async refresh(network) {
3270
3412
  if (!this.refreshing) {
3271
- this.refreshing = this.runtime.refresh({
3272
- allowNetwork: network,
3273
- force: network,
3274
- signal: AbortSignal.timeout(15e3)
3275
- }).then((result) => {
3413
+ this.refreshing = Promise.resolve(this.claude?.refresh(this.runtime)).then(
3414
+ () => this.runtime.refresh({
3415
+ allowNetwork: network,
3416
+ force: network,
3417
+ signal: AbortSignal.timeout(15e3)
3418
+ })
3419
+ ).then((result) => {
3276
3420
  this.refreshedAt = (/* @__PURE__ */ new Date()).toISOString();
3277
3421
  this.warning = result.aborted || result.errors.size || this.runtime.getError() ? "Some providers could not refresh. Showing the catalogue currently available on this server." : null;
3278
3422
  }).finally(() => {
@@ -3282,7 +3426,10 @@ var ServerModels = class {
3282
3426
  await this.refreshing;
3283
3427
  }
3284
3428
  async defaultReference() {
3285
- return await Effect11.runPromise(this.store.defaultModel()) ?? legacyModel;
3429
+ const saved = await Effect11.runPromise(this.store.defaultModel());
3430
+ if (saved) return saved;
3431
+ const preferred = this.claude?.ready ? this.runtime.getModels().find((model) => model.provider === claudePlatform && /sonnet/i.test(model.id)) : void 0;
3432
+ return preferred ? { platformId: preferred.provider, modelId: preferred.id } : legacyModel;
3286
3433
  }
3287
3434
  async defaultIdentity() {
3288
3435
  const selected = await this.defaultReference();
@@ -3295,17 +3442,24 @@ var ServerModels = class {
3295
3442
  return {
3296
3443
  models: this.runtime.getModels().map((model) => ({
3297
3444
  ...this.identity(model.provider, model.id),
3298
- available: available.has(`${model.provider}\0${model.id}`),
3445
+ available: available.has(`${model.provider}\0${model.id}`) && (model.provider !== claudePlatform || this.claude?.ready === true),
3299
3446
  reasoning: model.reasoning,
3300
3447
  input: model.input,
3301
3448
  contextWindow: model.contextWindow
3302
3449
  })),
3303
3450
  defaultModel: await this.defaultIdentity(),
3304
3451
  refreshedAt: this.refreshedAt,
3305
- warning: this.warning
3452
+ warning: this.claude?.warning ?? this.warning
3306
3453
  };
3307
3454
  }
3308
3455
  async resolve(reference) {
3456
+ if (reference.platformId === claudePlatform && this.claude) {
3457
+ await this.claude.refresh(this.runtime);
3458
+ if (!this.claude.ready)
3459
+ throw new Error(
3460
+ this.claude.warning ?? "Claude Code is unavailable on this server. Install it and run claude auth login as the server user."
3461
+ );
3462
+ }
3309
3463
  const available = await this.runtime.getAvailable(reference.platformId, {
3310
3464
  signal: AbortSignal.timeout(1e4)
3311
3465
  });
@@ -3316,6 +3470,17 @@ var ServerModels = class {
3316
3470
  );
3317
3471
  return model;
3318
3472
  }
3473
+ async sessionOptions(cwd = process.cwd()) {
3474
+ if (this.claude && this.runtime.getProvider(claudePlatform)) {
3475
+ const services = await this.claude.services(cwd);
3476
+ return {
3477
+ modelRuntime: services.modelRuntime,
3478
+ resourceLoader: services.resourceLoader,
3479
+ settingsManager: services.settingsManager
3480
+ };
3481
+ }
3482
+ return { modelRuntime: this.runtime };
3483
+ }
3319
3484
  async setDefault(reference) {
3320
3485
  await this.resolve(reference);
3321
3486
  await Effect11.runPromise(this.store.setDefaultModel(reference));
@@ -3330,7 +3495,7 @@ import {
3330
3495
  createAgentSession as createAgentSession2,
3331
3496
  SessionManager as SessionManager2
3332
3497
  } from "@earendil-works/pi-coding-agent";
3333
- import { z as z15 } from "zod";
3498
+ import { z as z16 } from "zod";
3334
3499
 
3335
3500
  // src/delivery-tool.ts
3336
3501
  import { defineTool } from "@earendil-works/pi-coding-agent";
@@ -3368,15 +3533,15 @@ function deliveryTool(sessionId, delivery2) {
3368
3533
  import { createWriteToolDefinition, defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
3369
3534
  import { createTwoFilesPatch } from "diff";
3370
3535
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
3371
- import { z as z14 } from "zod";
3536
+ import { z as z15 } from "zod";
3372
3537
  var previewLimit = 256e3;
3373
3538
  function boundFileChange(change) {
3374
3539
  if ((change.patch?.length ?? 0) + (change.content?.length ?? 0) > previewLimit)
3375
3540
  return { path: change.path, omitted: true };
3376
3541
  return change;
3377
3542
  }
3378
- var EditResultSchema = z14.object({
3379
- details: z14.object({ patch: z14.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
3543
+ var EditResultSchema = z15.object({
3544
+ details: z15.object({ patch: z15.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
3380
3545
  });
3381
3546
  function completedFileChange(result, path12) {
3382
3547
  const parsed = EditResultSchema.safeParse(result);
@@ -3426,8 +3591,8 @@ function fileWriteTool(cwd) {
3426
3591
  }
3427
3592
 
3428
3593
  // src/pi.ts
3429
- var ToolResultSchema = z15.object({
3430
- content: z15.array(z15.object({ type: z15.string(), text: z15.string().optional() })).optional()
3594
+ var ToolResultSchema = z16.object({
3595
+ content: z16.array(z16.object({ type: z16.string(), text: z16.string().optional() })).optional()
3431
3596
  }).passthrough();
3432
3597
  var boundedJson = (value, limit) => {
3433
3598
  let text;
@@ -3447,16 +3612,18 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
3447
3612
  return {
3448
3613
  async open(sessionId, cwd, sessionDirectory, selected = legacyModel, run) {
3449
3614
  const model = await models2.resolve(selected);
3615
+ const sessionOptions = await models2.sessionOptions(cwd);
3450
3616
  const { session } = await createSession({
3451
3617
  cwd,
3452
3618
  model,
3453
- modelRuntime: models2.runtime,
3619
+ ...sessionOptions,
3454
3620
  sessionManager: SessionManager2.continueRecent(cwd, sessionDirectory),
3455
3621
  customTools: [
3456
3622
  fileWriteTool(cwd),
3457
3623
  ...run?.gitFlow && delivery2 ? [deliveryTool(sessionId, delivery2)] : []
3458
3624
  ]
3459
3625
  });
3626
+ if (sessionOptions.resourceLoader) await session.bindExtensions({});
3460
3627
  return {
3461
3628
  get model() {
3462
3629
  const current = session.model;
@@ -3551,7 +3718,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
3551
3718
  return false;
3552
3719
  }
3553
3720
  const filePaths = /* @__PURE__ */ new Map();
3554
- const FileArgsSchema = z15.object({ path: z15.string() });
3721
+ const FileArgsSchema = z16.object({ path: z16.string() });
3555
3722
  function handleTool(event) {
3556
3723
  if (event.type === "tool_execution_start") {
3557
3724
  flushText();
@@ -3613,16 +3780,16 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
3613
3780
  // src/projects.ts
3614
3781
  import { access as access2, readdir as readdir2, realpath, stat as stat3, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
3615
3782
  import { randomUUID as randomUUID6 } from "node:crypto";
3616
- import { z as z16 } from "zod";
3783
+ import { z as z17 } from "zod";
3617
3784
  import { constants as constants2 } from "node:fs";
3618
- import { homedir as homedir2 } from "node:os";
3785
+ import { homedir as homedir3 } from "node:os";
3619
3786
  import path8 from "node:path";
3620
3787
  import { Effect as Effect13, Semaphore as Semaphore3 } from "effect";
3621
3788
  function resolveProjectsRoot(input) {
3622
3789
  return Effect13.tryPromise({
3623
3790
  try: async () => {
3624
3791
  const trimmed = input.trim();
3625
- const expanded = trimmed === "~" ? homedir2() : trimmed.startsWith("~/") ? path8.join(homedir2(), trimmed.slice(2)) : trimmed;
3792
+ const expanded = trimmed === "~" ? homedir3() : trimmed.startsWith("~/") ? path8.join(homedir3(), trimmed.slice(2)) : trimmed;
3626
3793
  if (!path8.isAbsolute(expanded))
3627
3794
  throw new Error("Use an absolute server path or a path beginning with ~/.");
3628
3795
  const resolved = await realpath(expanded);
@@ -3645,7 +3812,7 @@ function projectLock(store2) {
3645
3812
  }
3646
3813
  return lock;
3647
3814
  }
3648
- var ProjectId = z16.string().trim().min(1).max(255);
3815
+ var ProjectId = z17.string().trim().min(1).max(255);
3649
3816
  async function gitDirectory(directory) {
3650
3817
  const location = path8.join(directory, ".git");
3651
3818
  const info = await stat3(location);
@@ -3662,10 +3829,10 @@ async function identity(directory, preferred) {
3662
3829
  try {
3663
3830
  return ProjectId.parse(await readFile6(filename, "utf8"));
3664
3831
  } catch (error) {
3665
- if (!z16.object({ code: z16.literal("ENOENT") }).safeParse(error).success) throw error;
3832
+ if (!z17.object({ code: z17.literal("ENOENT") }).safeParse(error).success) throw error;
3666
3833
  const legacyFilename = path8.join(await gitDirectory(directory), "chatplans-project-id");
3667
3834
  const legacy = await readFile6(legacyFilename, "utf8").catch((cause) => {
3668
- if (!z16.object({ code: z16.literal("ENOENT") }).safeParse(cause).success) throw cause;
3835
+ if (!z17.object({ code: z17.literal("ENOENT") }).safeParse(cause).success) throw cause;
3669
3836
  return null;
3670
3837
  });
3671
3838
  const id = legacy === null ? preferred ?? randomUUID6() : ProjectId.parse(legacy);
@@ -3673,7 +3840,7 @@ async function identity(directory, preferred) {
3673
3840
  await writeFile4(filename, id + "\n", { flag: "wx" });
3674
3841
  return id;
3675
3842
  } catch (writeError) {
3676
- if (!z16.object({ code: z16.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
3843
+ if (!z17.object({ code: z17.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
3677
3844
  return ProjectId.parse(await readFile6(filename, "utf8"));
3678
3845
  }
3679
3846
  }
@@ -3747,9 +3914,9 @@ function reconnectProject(root, id, input, store2) {
3747
3914
  }
3748
3915
 
3749
3916
  // src/project-git.ts
3750
- import { execFile as execFile2 } from "node:child_process";
3751
- import { promisify as promisify2 } from "node:util";
3752
- var execute2 = promisify2(execFile2);
3917
+ import { execFile as execFile3 } from "node:child_process";
3918
+ import { promisify as promisify3 } from "node:util";
3919
+ var execute3 = promisify3(execFile3);
3753
3920
  function repositoryIdentity(remote) {
3754
3921
  const input = remote.trim();
3755
3922
  if (!input || input.startsWith("/") || input.startsWith(".")) return null;
@@ -3770,7 +3937,7 @@ function repositoryIdentity(remote) {
3770
3937
  return { key, display: key };
3771
3938
  }
3772
3939
  async function git(directory, args) {
3773
- const { stdout } = await execute2("git", ["-C", directory, ...args], {
3940
+ const { stdout } = await execute3("git", ["-C", directory, ...args], {
3774
3941
  timeout: 3e3,
3775
3942
  maxBuffer: 1024 * 1024,
3776
3943
  env: { ...workflowEnvironment(), GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
@@ -3796,7 +3963,7 @@ async function inspect(directory) {
3796
3963
  git(directory, ["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => null),
3797
3964
  // NUL-separated records avoid ambiguous filenames/newlines. --no-renames
3798
3965
  // keeps each entry one record, so we only need its two status characters.
3799
- execute2(
3966
+ execute3(
3800
3967
  "git",
3801
3968
  [
3802
3969
  "-C",
@@ -3851,32 +4018,32 @@ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "n
3851
4018
  import path9 from "node:path";
3852
4019
 
3853
4020
  // src/push-schema.ts
3854
- import { z as z17 } from "zod";
3855
- var PushBindingSchema = z17.object({
3856
- bindingId: z17.string().uuid(),
3857
- installationId: z17.string().uuid(),
3858
- deviceId: z17.string().uuid()
4021
+ import { z as z18 } from "zod";
4022
+ var PushBindingSchema = z18.object({
4023
+ bindingId: z18.string().uuid(),
4024
+ installationId: z18.string().uuid(),
4025
+ deviceId: z18.string().uuid()
3859
4026
  });
3860
- var PushStateSchema = z17.object({
3861
- bindings: z17.array(PushBindingSchema).max(10),
3862
- error: z17.string().nullable()
4027
+ var PushStateSchema = z18.object({
4028
+ bindings: z18.array(PushBindingSchema).max(10),
4029
+ error: z18.string().nullable()
3863
4030
  });
3864
4031
  var PendingPushSchema = PushEventSchema.extend({
3865
- remaining: z17.array(PushBindingSchema),
3866
- createdAt: z17.number(),
3867
- attempts: z17.number().int().nonnegative(),
3868
- nextAt: z17.number()
4032
+ remaining: z18.array(PushBindingSchema),
4033
+ createdAt: z18.number(),
4034
+ attempts: z18.number().int().nonnegative(),
4035
+ nextAt: z18.number()
3869
4036
  });
3870
4037
 
3871
4038
  // src/store.ts
3872
4039
  import { DatabaseSync } from "node:sqlite";
3873
4040
  import { Context as Context3, Data as Data3, Effect as Effect14, Layer as Layer3 } from "effect";
3874
- import { z as z18 } from "zod";
4041
+ import { z as z19 } from "zod";
3875
4042
  import { randomUUID as randomUUID7 } from "node:crypto";
3876
4043
  var StorageError = class extends Data3.TaggedError("StorageError") {
3877
4044
  };
3878
- var PayloadRow = z18.object({ payload: z18.string() });
3879
- var EventRow = z18.object({ sequence: z18.number(), recorded_at: z18.string(), payload: z18.string() });
4045
+ var PayloadRow = z19.object({ payload: z19.string() });
4046
+ var EventRow = z19.object({ sequence: z19.number(), recorded_at: z19.string(), payload: z19.string() });
3880
4047
  var SqliteSessionStore = class {
3881
4048
  constructor(db) {
3882
4049
  this.db = db;
@@ -3927,7 +4094,7 @@ var SqliteSessionStore = class {
3927
4094
  const row = PayloadRow.parse(
3928
4095
  this.db.prepare("SELECT payload FROM settings WHERE id = 'server-id'").get()
3929
4096
  );
3930
- return z18.string().uuid().parse(JSON.parse(row.payload));
4097
+ return z19.string().uuid().parse(JSON.parse(row.payload));
3931
4098
  });
3932
4099
  }
3933
4100
  saveDevice(device, tokenHash) {
@@ -4015,7 +4182,7 @@ var SqliteSessionStore = class {
4015
4182
  `).all();
4016
4183
  const insert = this.db.prepare("INSERT OR IGNORE INTO projects(id, payload) VALUES (?, ?)");
4017
4184
  for (const row of legacy) {
4018
- const id = z18.object({ id: z18.string() }).parse(row).id;
4185
+ const id = z19.object({ id: z19.string() }).parse(row).id;
4019
4186
  insert.run(
4020
4187
  id,
4021
4188
  JSON.stringify({ id, name: id, path: path9.join(root, id), available: false })
@@ -4225,7 +4392,7 @@ var SqliteSessionStore = class {
4225
4392
  if (event.type === "message.user") this.claimAttachments(event.attachments ?? []);
4226
4393
  const result = this.db.prepare("INSERT INTO events(session_id, recorded_at, payload) VALUES (?, ?, ?)").run(sessionId, recordedAt, JSON.stringify(event));
4227
4394
  if (notification) {
4228
- const count = z18.object({ total: z18.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
4395
+ const count = z19.object({ total: z19.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
4229
4396
  if (count.total >= 1e3)
4230
4397
  this.db.prepare(
4231
4398
  "DELETE FROM push_outbox WHERE rowid IN (SELECT rowid FROM push_outbox ORDER BY rowid LIMIT 1)"
@@ -4277,7 +4444,7 @@ var SessionStore = class extends Context3.Service()(
4277
4444
  for (const suffix of ["-wal", "-shm"])
4278
4445
  if (existsSync2(filename + suffix)) chmodSync(filename + suffix, 384);
4279
4446
  try {
4280
- const schema = z18.object({ user_version: z18.number() }).parse(db.prepare("PRAGMA user_version").get());
4447
+ const schema = z19.object({ user_version: z19.number() }).parse(db.prepare("PRAGMA user_version").get());
4281
4448
  if (schema.user_version > 1)
4282
4449
  throw new Error("This database requires a newer Agentrific server.");
4283
4450
  db.exec(`
@@ -4410,9 +4577,9 @@ var host = process.env.AGENTRIFIC_HOST ?? "127.0.0.1";
4410
4577
  var port = Number(process.env.AGENTRIFIC_PORT ?? 4318);
4411
4578
  var defaultProjectsRoot = path11.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "./projects");
4412
4579
  var worktreesRoot = path11.resolve(
4413
- process.env.AGENTRIFIC_WORKTREES_DIR ?? path11.join(homedir3(), ".agentrific", "workspaces")
4580
+ process.env.AGENTRIFIC_WORKTREES_DIR ?? path11.join(homedir4(), ".agentrific", "workspaces")
4414
4581
  );
4415
- var execFileAsync = promisify3(execFile3);
4582
+ var execFileAsync = promisify4(execFile4);
4416
4583
  if (host !== "127.0.0.1" && host !== "::1")
4417
4584
  throw new Error("Bind Agentrific to loopback and expose it with Tailscale Serve or cloudflared.");
4418
4585
  var app = Fastify({
@@ -4532,7 +4699,10 @@ function publish(event, workflowUpdate = false) {
4532
4699
  else client.send(message);
4533
4700
  }
4534
4701
  }
4535
- var models = new ServerModels(await ModelRuntime.create(), store);
4702
+ var claudeCode = new ClaudeCode();
4703
+ var modelRuntime = await ModelRuntime.create();
4704
+ await claudeCode.refresh(modelRuntime);
4705
+ var models = new ServerModels(modelRuntime, store, claudeCode);
4536
4706
  var delivery = new DeliveryCoordinator(store, {
4537
4707
  publish: (event) => publish(event, true),
4538
4708
  draft: deliveryWriter(models),
@@ -4726,7 +4896,7 @@ app.get("/api/projects/:id", async (request, reply) => {
4726
4896
  return project ?? reply.code(404).send({ error: "Project not found on this server." });
4727
4897
  });
4728
4898
  app.post("/api/projects/:id/reconnect", async (request, reply) => {
4729
- const parsed = z19.object({ path: z19.string().trim().min(1).max(4096) }).safeParse(request.body);
4899
+ const parsed = z20.object({ path: z20.string().trim().min(1).max(4096) }).safeParse(request.body);
4730
4900
  if (!parsed.success)
4731
4901
  return reply.code(400).send({ error: "Enter the project's new folder path." });
4732
4902
  const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
@@ -4812,7 +4982,7 @@ app.patch("/api/tasks/:id", async (request, reply) => {
4812
4982
  }
4813
4983
  });
4814
4984
  app.post("/api/tasks/:id/discard-unrun", async (request, reply) => {
4815
- const parsed = z19.object({ updatedAt: z19.string().datetime() }).safeParse(request.body);
4985
+ const parsed = z20.object({ updatedAt: z20.string().datetime() }).safeParse(request.body);
4816
4986
  if (!parsed.success) return reply.code(400).send({ error: "The task revision is required." });
4817
4987
  try {
4818
4988
  await deleteTask(store, request.params.id, void 0, parsed.data.updatedAt);
@@ -4882,9 +5052,9 @@ app.post(
4882
5052
  }
4883
5053
  })
4884
5054
  );
4885
- var HistoryQuery = z19.object({
4886
- after: z19.coerce.number().int().nonnegative().default(0),
4887
- limit: z19.coerce.number().int().min(1).max(1e3).default(500)
5055
+ var HistoryQuery = z20.object({
5056
+ after: z20.coerce.number().int().nonnegative().default(0),
5057
+ limit: z20.coerce.number().int().min(1).max(1e3).default(500)
4888
5058
  });
4889
5059
  app.get("/api/sessions/:id/events", async (request, reply) => {
4890
5060
  const query = HistoryQuery.safeParse(request.query);
@@ -5022,7 +5192,7 @@ app.post("/api/projects/:id/chat", async (request, reply) => {
5022
5192
  const location = await resolveSessionProject(request.params.id);
5023
5193
  if (location.error !== void 0)
5024
5194
  return reply.code(location.status).send({ error: location.error });
5025
- const parsed = z19.object({ reset: z19.boolean().optional() }).safeParse(request.body ?? {});
5195
+ const parsed = z20.object({ reset: z20.boolean().optional() }).safeParse(request.body ?? {});
5026
5196
  if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
5027
5197
  try {
5028
5198
  return await openRepoChat(
@@ -5046,7 +5216,7 @@ app.post("/api/sessions", async (request, reply) => {
5046
5216
  if (!result.session) return reply.code(result.status).send({ error: result.error });
5047
5217
  return reply.code(201).send(result.session);
5048
5218
  });
5049
- var StartTaskBody = z19.object({ prompt: z19.string().trim().max(2e4).optional() }).default({});
5219
+ var StartTaskBody = z20.object({ prompt: z20.string().trim().max(2e4).optional() }).default({});
5050
5220
  function startTask(id, instructionsOverride, automated = false) {
5051
5221
  return withTaskLock(
5052
5222
  "agent-dispatch",
@@ -5124,7 +5294,7 @@ app.patch("/api/automation", async (request, reply) => {
5124
5294
  return automation.configure(parsed.data);
5125
5295
  });
5126
5296
  app.patch("/api/automation/queue", async (request, reply) => {
5127
- const parsed = z19.object({ taskId: z19.string().min(1), beforeId: z19.string().min(1).nullable() }).safeParse(request.body);
5297
+ const parsed = z20.object({ taskId: z20.string().min(1), beforeId: z20.string().min(1).nullable() }).safeParse(request.body);
5128
5298
  if (!parsed.success)
5129
5299
  return reply.code(400).send({ error: "Choose a task and a queue position." });
5130
5300
  try {
@@ -5187,7 +5357,7 @@ attachEventServer(app, security, allowedOrigins, () => ({
5187
5357
  sessions: Effect17.runSync(store.list())
5188
5358
  }));
5189
5359
  function localServerUrl() {
5190
- const address = z19.object({ port: z19.number() }).safeParse(app.server.address());
5360
+ const address = z20.object({ port: z20.number() }).safeParse(app.server.address());
5191
5361
  return address.success ? `http://${host}:${address.data.port}` : void 0;
5192
5362
  }
5193
5363
  var phoneOrigin;
@@ -5215,6 +5385,7 @@ var closeControl = await startControl(
5215
5385
  serverId: security.serverId,
5216
5386
  publicUrl: process.env.AGENTRIFIC_PUBLIC_URL,
5217
5387
  phoneProvider: phone?.snapshot().provider,
5388
+ phoneProgress: phone?.snapshot().progress,
5218
5389
  localUrl: localServerUrl(),
5219
5390
  serverVersion,
5220
5391
  connectedDevices: new Set(security.sockets.values()).size,