@agentrific/server 0.6.0 → 0.7.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/cli.js CHANGED
@@ -30,7 +30,7 @@ var init_package = __esm({
30
30
  "package.json"() {
31
31
  package_default = {
32
32
  name: "@agentrific/server",
33
- version: "0.6.0",
33
+ version: "0.7.1",
34
34
  description: "Self-hosted control plane for long-running coding agents",
35
35
  keywords: [
36
36
  "agentrific",
@@ -56,20 +56,28 @@ var init_package = __esm({
56
56
  registry: "https://registry.npmjs.org"
57
57
  },
58
58
  scripts: {
59
- dev: "tsx watch src/index.ts",
59
+ dev: "node scripts/build-claude-bridge.mjs && tsx watch src/index.ts",
60
60
  start: "node dist/index.js",
61
- typecheck: "tsc --noEmit",
61
+ typecheck: "node scripts/check-claude-bridge-generated.mjs && tsc --noEmit",
62
62
  build: "node scripts/build.mjs",
63
- cli: "AGENTRIFIC_DATA_DIR=./data tsx src/cli.ts"
63
+ cli: "node scripts/build-claude-bridge.mjs && AGENTRIFIC_DATA_DIR=./data tsx src/cli.ts",
64
+ "build:claude": "node scripts/build-claude-bridge.mjs"
64
65
  },
65
66
  dependencies: {
67
+ "@anthropic-ai/claude-agent-sdk": "0.3.296",
68
+ "@anthropic-ai/sdk": "^0.124.0",
69
+ "@earendil-works/pi-ai": "1.0.0",
66
70
  "@earendil-works/pi-coding-agent": "1.0.0",
67
71
  "@fastify/cors": "^11.3.0",
68
72
  "@fastify/multipart": "^10.1.2",
69
73
  "@inquirer/select": "^5.2.5",
74
+ "@modelcontextprotocol/sdk": "^1.29.0",
75
+ "cc-session-io": "^0.4.0",
76
+ "change-case": "^5.4.4",
70
77
  diff: "^9.0.0",
71
78
  effect: "4.0.0-rc.117",
72
79
  fastify: "^5.6.2",
80
+ "pi-claude-agent-sdk": "0.9.2",
73
81
  "pino-pretty": "^13.1.3",
74
82
  "qrcode-terminal": "^0.12.0",
75
83
  semver: "^7.8.5",
@@ -786,6 +794,7 @@ var init_control = __esm({
786
794
  serverId: z3.string().uuid(),
787
795
  publicUrl: z3.string().optional(),
788
796
  phoneProvider: ProviderSchema.nullable().optional(),
797
+ phoneProgress: z3.string().nullable().optional(),
789
798
  localUrl: z3.string().optional(),
790
799
  serverVersion: z3.string().optional(),
791
800
  connectedDevices: z3.number().optional(),
@@ -795,7 +804,11 @@ var init_control = __esm({
795
804
  maintenance: MaintenanceSchema.optional()
796
805
  });
797
806
  ControlResultSchema = z3.union([
798
- z3.object({ provider: ProviderSchema.nullable(), publicUrl: z3.string().url().nullable() }),
807
+ z3.object({
808
+ provider: ProviderSchema.nullable(),
809
+ publicUrl: z3.string().url().nullable(),
810
+ progress: z3.string().nullable().optional()
811
+ }),
799
812
  z3.array(z3.object({ provider: ProviderSchema, available: z3.boolean(), reason: z3.string() })),
800
813
  z3.object({
801
814
  pairingId: z3.string(),
@@ -1330,6 +1343,10 @@ var init_opentunnel = __esm({
1330
1343
  client;
1331
1344
  connection;
1332
1345
  stopping = false;
1346
+ stage = "reading-identity";
1347
+ progress() {
1348
+ return this.connection?.status().state ?? this.stage;
1349
+ }
1333
1350
  running() {
1334
1351
  return this.connection?.status().state === "connected" && !this.stopping;
1335
1352
  }
@@ -1359,13 +1376,32 @@ var init_opentunnel = __esm({
1359
1376
  let canceled = () => {
1360
1377
  };
1361
1378
  try {
1362
- const connecting = this.client.tunnel.connect({
1363
- profile: "agentrific",
1364
- routes: {
1365
- [route]: `${endpoint.hostname === "[::1]" ? "[::1]" : "127.0.0.1"}:${endpoint.port || "80"}`
1366
- },
1367
- signal
1368
- });
1379
+ const client = this.client;
1380
+ this.stage = "reading-identity";
1381
+ const connecting = (async () => {
1382
+ const options = {
1383
+ profile: "agentrific",
1384
+ onProgress: (stage) => {
1385
+ this.stage = stage;
1386
+ }
1387
+ };
1388
+ const identity2 = await client.tunnel.get(options);
1389
+ abort.throwIfAborted();
1390
+ if (!identity2) {
1391
+ const resumed = await client.tunnel.resume(options);
1392
+ abort.throwIfAborted();
1393
+ if (!resumed) await client.tunnel.create(options);
1394
+ }
1395
+ abort.throwIfAborted();
1396
+ this.stage = "connecting";
1397
+ return client.tunnel.connect({
1398
+ profile: "agentrific",
1399
+ routes: {
1400
+ [route]: `${endpoint.hostname === "[::1]" ? "[::1]" : "127.0.0.1"}:${endpoint.port || "80"}`
1401
+ },
1402
+ signal
1403
+ });
1404
+ })();
1369
1405
  const cancellation = new Promise((_resolve, reject) => {
1370
1406
  canceled = () => reject(new Error("OpenTunnel connection canceled or timed out."));
1371
1407
  abort.addEventListener("abort", canceled, { once: true });
@@ -2181,7 +2217,11 @@ var init_desktop_phone = __esm({
2181
2217
  publicUrl = null;
2182
2218
  pending = Promise.resolve();
2183
2219
  snapshot() {
2184
- return { provider: this.provider, publicUrl: this.publicUrl };
2220
+ return {
2221
+ provider: this.provider,
2222
+ publicUrl: this.publicUrl,
2223
+ progress: this.tunnel?.progress?.() ?? null
2224
+ };
2185
2225
  }
2186
2226
  async options() {
2187
2227
  return (await this.detect()).map(({ provider, available, reason }) => ({
@@ -3525,14 +3565,17 @@ import { z as z13 } from "zod";
3525
3565
  function deliveryWriter(models2, createSession = createAgentSession) {
3526
3566
  return async (run, input, diff, signal) => {
3527
3567
  signal?.throwIfAborted();
3568
+ const selected = run.model ?? await models2.defaultReference();
3569
+ const sessionOptions = await models2.sessionOptions(run.workspacePath);
3528
3570
  const { session } = await createSession({
3529
3571
  cwd: run.workspacePath,
3530
- model: await models2.resolve(run.model ?? await models2.defaultReference()),
3531
- modelRuntime: models2.runtime,
3572
+ model: await models2.resolve(selected),
3573
+ ...sessionOptions,
3532
3574
  sessionManager: SessionManager.inMemory(),
3533
3575
  tools: [],
3534
3576
  noTools: "all"
3535
3577
  });
3578
+ if (sessionOptions.resourceLoader) await session.bindExtensions({});
3536
3579
  const timer = setTimeout(() => {
3537
3580
  void session.abort();
3538
3581
  }, 6e4);
@@ -3566,12 +3609,116 @@ var init_delivery_writer = __esm({
3566
3609
  }
3567
3610
  });
3568
3611
 
3612
+ // src/claude-code.ts
3613
+ import { execFile as execFile3 } from "node:child_process";
3614
+ import { homedir as homedir3 } from "node:os";
3615
+ import { join as join2 } from "node:path";
3616
+ import { promisify as promisify3 } from "node:util";
3617
+ import { z as z14 } from "zod";
3618
+ import {
3619
+ createAgentSessionServices
3620
+ } from "@earendil-works/pi-coding-agent";
3621
+ async function discoverClaudeCode() {
3622
+ if (process.env.AGENTRIFIC_CLAUDE_CODE === "off") return;
3623
+ const candidates = [
3624
+ "claude",
3625
+ join2(homedir3(), ".local/bin/claude"),
3626
+ join2(homedir3(), ".claude/local/claude")
3627
+ ];
3628
+ for (const executable of candidates) {
3629
+ try {
3630
+ await execute3(executable, ["--version"], { timeout: 5e3, maxBuffer: 64 * 1024 });
3631
+ } catch {
3632
+ continue;
3633
+ }
3634
+ try {
3635
+ const { stdout } = await execute3(executable, ["auth", "status", "--json"], {
3636
+ timeout: 5e3,
3637
+ maxBuffer: 64 * 1024
3638
+ });
3639
+ const status = AuthStatusSchema.parse(JSON.parse(stdout));
3640
+ return { executable, ready: status.loggedIn };
3641
+ } catch {
3642
+ return { executable, ready: false };
3643
+ }
3644
+ }
3645
+ }
3646
+ var claudePlatform, execute3, AuthStatusSchema, ClaudeCode;
3647
+ var init_claude_code = __esm({
3648
+ "src/claude-code.ts"() {
3649
+ "use strict";
3650
+ claudePlatform = "claude-bridge";
3651
+ execute3 = promisify3(execFile3);
3652
+ AuthStatusSchema = z14.object({ loggedIn: z14.boolean() });
3653
+ ClaudeCode = class {
3654
+ constructor(probe = discoverClaudeCode, query) {
3655
+ this.probe = probe;
3656
+ this.query = query;
3657
+ }
3658
+ probe;
3659
+ query;
3660
+ installation;
3661
+ registered = false;
3662
+ get ready() {
3663
+ return this.installation?.ready === true;
3664
+ }
3665
+ get warning() {
3666
+ 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;
3667
+ }
3668
+ async refresh(runtime2) {
3669
+ this.installation = await this.probe();
3670
+ if (this.installation && !this.registered) {
3671
+ await this.services(process.cwd(), runtime2);
3672
+ this.registered = true;
3673
+ }
3674
+ }
3675
+ async services(cwd, modelRuntime2) {
3676
+ const module = await import(new URL("./claude-bridge.generated.js", import.meta.url).href);
3677
+ const extension = module.createClaudeExtension(this.query);
3678
+ const services = await createAgentSessionServices({
3679
+ cwd,
3680
+ modelRuntime: modelRuntime2,
3681
+ resourceLoaderOptions: {
3682
+ // Match Pipeline: do not execute unrelated global Pi extensions.
3683
+ noExtensions: true,
3684
+ extensionFactories: [
3685
+ {
3686
+ name: "agentrific-claude-code",
3687
+ factory: (pi) => extension({
3688
+ ...pi,
3689
+ // SAFETY: The pinned bridge uses only the name/config registration overload.
3690
+ registerProvider: ((id, config) => pi.registerProvider(id, {
3691
+ ...config,
3692
+ // Bind the working directory to this provider,
3693
+ // without changing the process cwd or global Claude configuration.
3694
+ streamSimple: config.streamSimple && ((model, context, options) => {
3695
+ const request = { ...options, cwd };
3696
+ return config.streamSimple(model, context, request);
3697
+ })
3698
+ }))
3699
+ })
3700
+ }
3701
+ ]
3702
+ }
3703
+ });
3704
+ const errors = [
3705
+ ...services.diagnostics.filter((item) => item.type === "error").map((item) => item.message),
3706
+ ...services.resourceLoader.getExtensions().errors.map((item) => item.error)
3707
+ ];
3708
+ if (errors.length) throw new Error(errors.join("\n"));
3709
+ return services;
3710
+ }
3711
+ };
3712
+ }
3713
+ });
3714
+
3569
3715
  // src/models.ts
3570
3716
  import { Effect as Effect11 } from "effect";
3571
3717
  var legacyModel, makers, ServerModels;
3572
3718
  var init_models = __esm({
3573
3719
  "src/models.ts"() {
3574
3720
  "use strict";
3721
+ init_claude_code();
3575
3722
  legacyModel = { platformId: "openai-codex", modelId: "gpt-6-luna" };
3576
3723
  makers = [
3577
3724
  { id: "openai", name: "OpenAI", pattern: /(?:^|[/.:\s-])(?:gpt-|o[134](?:-|$))/i },
@@ -3588,12 +3735,14 @@ var init_models = __esm({
3588
3735
  { id: "amazon", name: "Amazon", pattern: /amazon\.nova|nova-/i }
3589
3736
  ];
3590
3737
  ServerModels = class {
3591
- constructor(runtime2, store2) {
3738
+ constructor(runtime2, store2, claude) {
3592
3739
  this.runtime = runtime2;
3593
3740
  this.store = store2;
3741
+ this.claude = claude;
3594
3742
  }
3595
3743
  runtime;
3596
3744
  store;
3745
+ claude;
3597
3746
  refreshing;
3598
3747
  refreshedAt = (/* @__PURE__ */ new Date()).toISOString();
3599
3748
  warning = null;
@@ -3615,18 +3764,20 @@ var init_models = __esm({
3615
3764
  platformId,
3616
3765
  modelId,
3617
3766
  name: model?.name ?? modelId,
3618
- platformName: this.runtime.getProvider(platformId)?.name ?? platformId,
3767
+ platformName: platformId === claudePlatform ? "Claude Code" : this.runtime.getProvider(platformId)?.name ?? platformId,
3619
3768
  providerId: namespace === "openrouter" ? "unspecified" : maker?.id ?? namespace ?? "unspecified",
3620
3769
  providerName: namespace === "openrouter" ? "Other / custom" : maker?.name ?? publisherName ?? "Other / custom"
3621
3770
  };
3622
3771
  }
3623
3772
  async refresh(network) {
3624
3773
  if (!this.refreshing) {
3625
- this.refreshing = this.runtime.refresh({
3626
- allowNetwork: network,
3627
- force: network,
3628
- signal: AbortSignal.timeout(15e3)
3629
- }).then((result) => {
3774
+ this.refreshing = Promise.resolve(this.claude?.refresh(this.runtime)).then(
3775
+ () => this.runtime.refresh({
3776
+ allowNetwork: network,
3777
+ force: network,
3778
+ signal: AbortSignal.timeout(15e3)
3779
+ })
3780
+ ).then((result) => {
3630
3781
  this.refreshedAt = (/* @__PURE__ */ new Date()).toISOString();
3631
3782
  this.warning = result.aborted || result.errors.size || this.runtime.getError() ? "Some providers could not refresh. Showing the catalogue currently available on this server." : null;
3632
3783
  }).finally(() => {
@@ -3636,7 +3787,10 @@ var init_models = __esm({
3636
3787
  await this.refreshing;
3637
3788
  }
3638
3789
  async defaultReference() {
3639
- return await Effect11.runPromise(this.store.defaultModel()) ?? legacyModel;
3790
+ const saved = await Effect11.runPromise(this.store.defaultModel());
3791
+ if (saved) return saved;
3792
+ const preferred = this.claude?.ready ? this.runtime.getModels().find((model) => model.provider === claudePlatform && /sonnet/i.test(model.id)) : void 0;
3793
+ return preferred ? { platformId: preferred.provider, modelId: preferred.id } : legacyModel;
3640
3794
  }
3641
3795
  async defaultIdentity() {
3642
3796
  const selected = await this.defaultReference();
@@ -3649,17 +3803,24 @@ var init_models = __esm({
3649
3803
  return {
3650
3804
  models: this.runtime.getModels().map((model) => ({
3651
3805
  ...this.identity(model.provider, model.id),
3652
- available: available.has(`${model.provider}\0${model.id}`),
3806
+ available: available.has(`${model.provider}\0${model.id}`) && (model.provider !== claudePlatform || this.claude?.ready === true),
3653
3807
  reasoning: model.reasoning,
3654
3808
  input: model.input,
3655
3809
  contextWindow: model.contextWindow
3656
3810
  })),
3657
3811
  defaultModel: await this.defaultIdentity(),
3658
3812
  refreshedAt: this.refreshedAt,
3659
- warning: this.warning
3813
+ warning: this.claude?.warning ?? this.warning
3660
3814
  };
3661
3815
  }
3662
3816
  async resolve(reference) {
3817
+ if (reference.platformId === claudePlatform && this.claude) {
3818
+ await this.claude.refresh(this.runtime);
3819
+ if (!this.claude.ready)
3820
+ throw new Error(
3821
+ this.claude.warning ?? "Claude Code is unavailable on this server. Install it and run claude auth login as the server user."
3822
+ );
3823
+ }
3663
3824
  const available = await this.runtime.getAvailable(reference.platformId, {
3664
3825
  signal: AbortSignal.timeout(1e4)
3665
3826
  });
@@ -3670,6 +3831,17 @@ var init_models = __esm({
3670
3831
  );
3671
3832
  return model;
3672
3833
  }
3834
+ async sessionOptions(cwd = process.cwd()) {
3835
+ if (this.claude && this.runtime.getProvider(claudePlatform)) {
3836
+ const services = await this.claude.services(cwd);
3837
+ return {
3838
+ modelRuntime: services.modelRuntime,
3839
+ resourceLoader: services.resourceLoader,
3840
+ settingsManager: services.settingsManager
3841
+ };
3842
+ }
3843
+ return { modelRuntime: this.runtime };
3844
+ }
3673
3845
  async setDefault(reference) {
3674
3846
  await this.resolve(reference);
3675
3847
  await Effect11.runPromise(this.store.setDefaultModel(reference));
@@ -3720,7 +3892,7 @@ var init_delivery_tool = __esm({
3720
3892
  import { createWriteToolDefinition, defineTool as defineTool2 } from "@earendil-works/pi-coding-agent";
3721
3893
  import { createTwoFilesPatch } from "diff";
3722
3894
  import { mkdir as mkdir6, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
3723
- import { z as z14 } from "zod";
3895
+ import { z as z15 } from "zod";
3724
3896
  function boundFileChange(change) {
3725
3897
  if ((change.patch?.length ?? 0) + (change.content?.length ?? 0) > previewLimit)
3726
3898
  return { path: change.path, omitted: true };
@@ -3778,8 +3950,8 @@ var init_file_change = __esm({
3778
3950
  "use strict";
3779
3951
  init_src();
3780
3952
  previewLimit = 256e3;
3781
- EditResultSchema = z14.object({
3782
- details: z14.object({ patch: z14.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
3953
+ EditResultSchema = z15.object({
3954
+ details: z15.object({ patch: z15.string().optional(), fileChange: FileChangeSchema.optional() }).optional()
3783
3955
  });
3784
3956
  }
3785
3957
  });
@@ -3791,7 +3963,7 @@ import {
3791
3963
  createAgentSession as createAgentSession2,
3792
3964
  SessionManager as SessionManager2
3793
3965
  } from "@earendil-works/pi-coding-agent";
3794
- import { z as z15 } from "zod";
3966
+ import { z as z16 } from "zod";
3795
3967
  function toolOutput(value) {
3796
3968
  const parsed = ToolResultSchema.safeParse(value);
3797
3969
  const text = parsed.success ? parsed.data.content?.filter((part) => part.type === "text").map((part) => part.text || "").join("\n") || "Completed without text output." : boundedJson(value, 4e3);
@@ -3801,16 +3973,18 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
3801
3973
  return {
3802
3974
  async open(sessionId, cwd, sessionDirectory, selected = legacyModel, run) {
3803
3975
  const model = await models2.resolve(selected);
3976
+ const sessionOptions = await models2.sessionOptions(cwd);
3804
3977
  const { session } = await createSession({
3805
3978
  cwd,
3806
3979
  model,
3807
- modelRuntime: models2.runtime,
3980
+ ...sessionOptions,
3808
3981
  sessionManager: SessionManager2.continueRecent(cwd, sessionDirectory),
3809
3982
  customTools: [
3810
3983
  fileWriteTool(cwd),
3811
3984
  ...run?.gitFlow && delivery2 ? [deliveryTool(sessionId, delivery2)] : []
3812
3985
  ]
3813
3986
  });
3987
+ if (sessionOptions.resourceLoader) await session.bindExtensions({});
3814
3988
  return {
3815
3989
  get model() {
3816
3990
  const current = session.model;
@@ -3905,7 +4079,7 @@ function createPiFactory(files, models2, delivery2, createSession = createAgentS
3905
4079
  return false;
3906
4080
  }
3907
4081
  const filePaths = /* @__PURE__ */ new Map();
3908
- const FileArgsSchema = z15.object({ path: z15.string() });
4082
+ const FileArgsSchema = z16.object({ path: z16.string() });
3909
4083
  function handleTool(event) {
3910
4084
  if (event.type === "tool_execution_start") {
3911
4085
  flushText();
@@ -3970,8 +4144,8 @@ var init_pi = __esm({
3970
4144
  init_models();
3971
4145
  init_delivery_tool();
3972
4146
  init_file_change();
3973
- ToolResultSchema = z15.object({
3974
- content: z15.array(z15.object({ type: z15.string(), text: z15.string().optional() })).optional()
4147
+ ToolResultSchema = z16.object({
4148
+ content: z16.array(z16.object({ type: z16.string(), text: z16.string().optional() })).optional()
3975
4149
  }).passthrough();
3976
4150
  boundedJson = (value, limit) => {
3977
4151
  let text;
@@ -3988,16 +4162,16 @@ var init_pi = __esm({
3988
4162
  // src/projects.ts
3989
4163
  import { access as access2, readdir as readdir2, realpath, stat as stat3, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
3990
4164
  import { randomUUID as randomUUID6 } from "node:crypto";
3991
- import { z as z16 } from "zod";
4165
+ import { z as z17 } from "zod";
3992
4166
  import { constants as constants2 } from "node:fs";
3993
- import { homedir as homedir3 } from "node:os";
4167
+ import { homedir as homedir4 } from "node:os";
3994
4168
  import path10 from "node:path";
3995
4169
  import { Effect as Effect13, Semaphore as Semaphore3 } from "effect";
3996
4170
  function resolveProjectsRoot(input) {
3997
4171
  return Effect13.tryPromise({
3998
4172
  try: async () => {
3999
4173
  const trimmed = input.trim();
4000
- const expanded = trimmed === "~" ? homedir3() : trimmed.startsWith("~/") ? path10.join(homedir3(), trimmed.slice(2)) : trimmed;
4174
+ const expanded = trimmed === "~" ? homedir4() : trimmed.startsWith("~/") ? path10.join(homedir4(), trimmed.slice(2)) : trimmed;
4001
4175
  if (!path10.isAbsolute(expanded))
4002
4176
  throw new Error("Use an absolute server path or a path beginning with ~/.");
4003
4177
  const resolved = await realpath(expanded);
@@ -4035,10 +4209,10 @@ async function identity(directory, preferred) {
4035
4209
  try {
4036
4210
  return ProjectId.parse(await readFile6(filename, "utf8"));
4037
4211
  } catch (error) {
4038
- if (!z16.object({ code: z16.literal("ENOENT") }).safeParse(error).success) throw error;
4212
+ if (!z17.object({ code: z17.literal("ENOENT") }).safeParse(error).success) throw error;
4039
4213
  const legacyFilename = path10.join(await gitDirectory(directory), "chatplans-project-id");
4040
4214
  const legacy = await readFile6(legacyFilename, "utf8").catch((cause) => {
4041
- if (!z16.object({ code: z16.literal("ENOENT") }).safeParse(cause).success) throw cause;
4215
+ if (!z17.object({ code: z17.literal("ENOENT") }).safeParse(cause).success) throw cause;
4042
4216
  return null;
4043
4217
  });
4044
4218
  const id = legacy === null ? preferred ?? randomUUID6() : ProjectId.parse(legacy);
@@ -4046,7 +4220,7 @@ async function identity(directory, preferred) {
4046
4220
  await writeFile5(filename, id + "\n", { flag: "wx" });
4047
4221
  return id;
4048
4222
  } catch (writeError) {
4049
- if (!z16.object({ code: z16.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
4223
+ if (!z17.object({ code: z17.literal("EEXIST") }).safeParse(writeError).success) throw writeError;
4050
4224
  return ProjectId.parse(await readFile6(filename, "utf8"));
4051
4225
  }
4052
4226
  }
@@ -4123,13 +4297,13 @@ var init_projects = __esm({
4123
4297
  "src/projects.ts"() {
4124
4298
  "use strict";
4125
4299
  locks2 = /* @__PURE__ */ new WeakMap();
4126
- ProjectId = z16.string().trim().min(1).max(255);
4300
+ ProjectId = z17.string().trim().min(1).max(255);
4127
4301
  }
4128
4302
  });
4129
4303
 
4130
4304
  // src/project-git.ts
4131
- import { execFile as execFile3 } from "node:child_process";
4132
- import { promisify as promisify3 } from "node:util";
4305
+ import { execFile as execFile4 } from "node:child_process";
4306
+ import { promisify as promisify4 } from "node:util";
4133
4307
  function repositoryIdentity(remote) {
4134
4308
  const input = remote.trim();
4135
4309
  if (!input || input.startsWith("/") || input.startsWith(".")) return null;
@@ -4150,7 +4324,7 @@ function repositoryIdentity(remote) {
4150
4324
  return { key, display: key };
4151
4325
  }
4152
4326
  async function git(directory, args) {
4153
- const { stdout } = await execute3("git", ["-C", directory, ...args], {
4327
+ const { stdout } = await execute4("git", ["-C", directory, ...args], {
4154
4328
  timeout: 3e3,
4155
4329
  maxBuffer: 1024 * 1024,
4156
4330
  env: { ...workflowEnvironment(), GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" }
@@ -4176,7 +4350,7 @@ async function inspect(directory) {
4176
4350
  git(directory, ["symbolic-ref", "--quiet", "--short", "HEAD"]).catch(() => null),
4177
4351
  // NUL-separated records avoid ambiguous filenames/newlines. --no-renames
4178
4352
  // keeps each entry one record, so we only need its two status characters.
4179
- execute3(
4353
+ execute4(
4180
4354
  "git",
4181
4355
  [
4182
4356
  "-C",
@@ -4224,37 +4398,37 @@ function projectGitStatus(directory) {
4224
4398
  observations.set(directory, { expires: Date.now() + 15e3, value });
4225
4399
  return value;
4226
4400
  }
4227
- var execute3, observations;
4401
+ var execute4, observations;
4228
4402
  var init_project_git = __esm({
4229
4403
  "src/project-git.ts"() {
4230
4404
  "use strict";
4231
4405
  init_workflow_command();
4232
- execute3 = promisify3(execFile3);
4406
+ execute4 = promisify4(execFile4);
4233
4407
  observations = /* @__PURE__ */ new Map();
4234
4408
  }
4235
4409
  });
4236
4410
 
4237
4411
  // src/push-schema.ts
4238
- import { z as z17 } from "zod";
4412
+ import { z as z18 } from "zod";
4239
4413
  var PushBindingSchema, PushStateSchema, PendingPushSchema;
4240
4414
  var init_push_schema = __esm({
4241
4415
  "src/push-schema.ts"() {
4242
4416
  "use strict";
4243
4417
  init_push();
4244
- PushBindingSchema = z17.object({
4245
- bindingId: z17.string().uuid(),
4246
- installationId: z17.string().uuid(),
4247
- deviceId: z17.string().uuid()
4418
+ PushBindingSchema = z18.object({
4419
+ bindingId: z18.string().uuid(),
4420
+ installationId: z18.string().uuid(),
4421
+ deviceId: z18.string().uuid()
4248
4422
  });
4249
- PushStateSchema = z17.object({
4250
- bindings: z17.array(PushBindingSchema).max(10),
4251
- error: z17.string().nullable()
4423
+ PushStateSchema = z18.object({
4424
+ bindings: z18.array(PushBindingSchema).max(10),
4425
+ error: z18.string().nullable()
4252
4426
  });
4253
4427
  PendingPushSchema = PushEventSchema.extend({
4254
- remaining: z17.array(PushBindingSchema),
4255
- createdAt: z17.number(),
4256
- attempts: z17.number().int().nonnegative(),
4257
- nextAt: z17.number()
4428
+ remaining: z18.array(PushBindingSchema),
4429
+ createdAt: z18.number(),
4430
+ attempts: z18.number().int().nonnegative(),
4431
+ nextAt: z18.number()
4258
4432
  });
4259
4433
  }
4260
4434
  });
@@ -4264,7 +4438,7 @@ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "n
4264
4438
  import path11 from "node:path";
4265
4439
  import { DatabaseSync } from "node:sqlite";
4266
4440
  import { Context as Context3, Data as Data3, Effect as Effect14, Layer as Layer3 } from "effect";
4267
- import { z as z18 } from "zod";
4441
+ import { z as z19 } from "zod";
4268
4442
  import { randomUUID as randomUUID7 } from "node:crypto";
4269
4443
  var StorageError, PayloadRow, EventRow, SqliteSessionStore, SessionStore;
4270
4444
  var init_store = __esm({
@@ -4277,8 +4451,8 @@ var init_store = __esm({
4277
4451
  init_src();
4278
4452
  StorageError = class extends Data3.TaggedError("StorageError") {
4279
4453
  };
4280
- PayloadRow = z18.object({ payload: z18.string() });
4281
- EventRow = z18.object({ sequence: z18.number(), recorded_at: z18.string(), payload: z18.string() });
4454
+ PayloadRow = z19.object({ payload: z19.string() });
4455
+ EventRow = z19.object({ sequence: z19.number(), recorded_at: z19.string(), payload: z19.string() });
4282
4456
  SqliteSessionStore = class {
4283
4457
  constructor(db) {
4284
4458
  this.db = db;
@@ -4329,7 +4503,7 @@ var init_store = __esm({
4329
4503
  const row = PayloadRow.parse(
4330
4504
  this.db.prepare("SELECT payload FROM settings WHERE id = 'server-id'").get()
4331
4505
  );
4332
- return z18.string().uuid().parse(JSON.parse(row.payload));
4506
+ return z19.string().uuid().parse(JSON.parse(row.payload));
4333
4507
  });
4334
4508
  }
4335
4509
  saveDevice(device, tokenHash) {
@@ -4417,7 +4591,7 @@ var init_store = __esm({
4417
4591
  `).all();
4418
4592
  const insert = this.db.prepare("INSERT OR IGNORE INTO projects(id, payload) VALUES (?, ?)");
4419
4593
  for (const row of legacy) {
4420
- const id = z18.object({ id: z18.string() }).parse(row).id;
4594
+ const id = z19.object({ id: z19.string() }).parse(row).id;
4421
4595
  insert.run(
4422
4596
  id,
4423
4597
  JSON.stringify({ id, name: id, path: path11.join(root, id), available: false })
@@ -4627,7 +4801,7 @@ var init_store = __esm({
4627
4801
  if (event.type === "message.user") this.claimAttachments(event.attachments ?? []);
4628
4802
  const result = this.db.prepare("INSERT INTO events(session_id, recorded_at, payload) VALUES (?, ?, ?)").run(sessionId, recordedAt, JSON.stringify(event));
4629
4803
  if (notification) {
4630
- const count = z18.object({ total: z18.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
4804
+ const count = z19.object({ total: z19.number() }).parse(this.db.prepare("SELECT COUNT(*) AS total FROM push_outbox").get());
4631
4805
  if (count.total >= 1e3)
4632
4806
  this.db.prepare(
4633
4807
  "DELETE FROM push_outbox WHERE rowid IN (SELECT rowid FROM push_outbox ORDER BY rowid LIMIT 1)"
@@ -4679,7 +4853,7 @@ var init_store = __esm({
4679
4853
  for (const suffix of ["-wal", "-shm"])
4680
4854
  if (existsSync2(filename + suffix)) chmodSync(filename + suffix, 384);
4681
4855
  try {
4682
- const schema = z18.object({ user_version: z18.number() }).parse(db.prepare("PRAGMA user_version").get());
4856
+ const schema = z19.object({ user_version: z19.number() }).parse(db.prepare("PRAGMA user_version").get());
4683
4857
  if (schema.user_version > 1)
4684
4858
  throw new Error("This database requires a newer Agentrific server.");
4685
4859
  db.exec(`
@@ -4831,14 +5005,14 @@ import { styleText as styleText4 } from "node:util";
4831
5005
  import cors from "@fastify/cors";
4832
5006
  import multipart from "@fastify/multipart";
4833
5007
  import { randomUUID as randomUUID9 } from "node:crypto";
4834
- import { execFile as execFile4 } from "node:child_process";
4835
- import { promisify as promisify4 } from "node:util";
5008
+ import { execFile as execFile5 } from "node:child_process";
5009
+ import { promisify as promisify5 } from "node:util";
4836
5010
  import { realpath as realpath2 } from "node:fs/promises";
4837
- import { homedir as homedir4 } from "node:os";
5011
+ import { homedir as homedir5 } from "node:os";
4838
5012
  import path13 from "node:path";
4839
5013
  import { WebSocket } from "ws";
4840
5014
  import { Effect as Effect17, ManagedRuntime } from "effect";
4841
- import { z as z19 } from "zod";
5015
+ import { z as z20 } from "zod";
4842
5016
  import { ModelRuntime } from "@earendil-works/pi-coding-agent";
4843
5017
  function publish(event, workflowUpdate = false) {
4844
5018
  const previous = event.type === "session.updated" ? Effect17.runSync(store.get(event.session.id)) : void 0;
@@ -5046,10 +5220,10 @@ async function withSessionTaskLock(id, operation) {
5046
5220
  return withTaskLock(lockId, operation);
5047
5221
  }
5048
5222
  function localServerUrl() {
5049
- const address = z19.object({ port: z19.number() }).safeParse(app.server.address());
5223
+ const address = z20.object({ port: z20.number() }).safeParse(app.server.address());
5050
5224
  return address.success ? `http://${host}:${address.data.port}` : void 0;
5051
5225
  }
5052
- var host, port, defaultProjectsRoot, worktreesRoot, execFileAsync, app, dataRoot, releaseLock, effects, store, security, sandboxPushUrl, testPushUrl, push, allowedOrigins, attachmentEffects, attachments, attachmentCleanup, models, delivery, preparation, agentEffects, runtime, worktreeCleanup, maxWorktreeAgeDays, pruneWorktrees, worktreeCleanupTimer, updates, maintenance, stopUpdateDiscovery, HistoryQuery, StartTaskBody, automation, automationTimer, phoneOrigin, phone, closeControl;
5226
+ var host, port, defaultProjectsRoot, worktreesRoot, execFileAsync, app, dataRoot, releaseLock, effects, store, security, sandboxPushUrl, testPushUrl, push, allowedOrigins, attachmentEffects, attachments, attachmentCleanup, claudeCode, modelRuntime, models, delivery, preparation, agentEffects, runtime, worktreeCleanup, maxWorktreeAgeDays, pruneWorktrees, worktreeCleanupTimer, updates, maintenance, stopUpdateDiscovery, HistoryQuery, StartTaskBody, automation, automationTimer, phoneOrigin, phone, closeControl;
5053
5227
  var init_index = __esm({
5054
5228
  async "src/index.ts"() {
5055
5229
  "use strict";
@@ -5070,6 +5244,7 @@ var init_index = __esm({
5070
5244
  init_delivery();
5071
5245
  init_preparation();
5072
5246
  init_delivery_writer();
5247
+ init_claude_code();
5073
5248
  init_models();
5074
5249
  init_pi();
5075
5250
  init_projects();
@@ -5082,9 +5257,9 @@ var init_index = __esm({
5082
5257
  port = Number(process.env.AGENTRIFIC_PORT ?? 4318);
5083
5258
  defaultProjectsRoot = path13.resolve(process.env.AGENTRIFIC_PROJECTS_DIR ?? "./projects");
5084
5259
  worktreesRoot = path13.resolve(
5085
- process.env.AGENTRIFIC_WORKTREES_DIR ?? path13.join(homedir4(), ".agentrific", "workspaces")
5260
+ process.env.AGENTRIFIC_WORKTREES_DIR ?? path13.join(homedir5(), ".agentrific", "workspaces")
5086
5261
  );
5087
- execFileAsync = promisify4(execFile4);
5262
+ execFileAsync = promisify5(execFile5);
5088
5263
  if (host !== "127.0.0.1" && host !== "::1")
5089
5264
  throw new Error("Bind Agentrific to loopback and expose it with Tailscale Serve or cloudflared.");
5090
5265
  app = Fastify({
@@ -5185,7 +5360,10 @@ var init_index = __esm({
5185
5360
  return reply.type(preview ? "image/png" : attachment.mimeType).send(attachments.content(attachment, preview));
5186
5361
  }
5187
5362
  );
5188
- models = new ServerModels(await ModelRuntime.create(), store);
5363
+ claudeCode = new ClaudeCode();
5364
+ modelRuntime = await ModelRuntime.create();
5365
+ await claudeCode.refresh(modelRuntime);
5366
+ models = new ServerModels(modelRuntime, store, claudeCode);
5189
5367
  delivery = new DeliveryCoordinator(store, {
5190
5368
  publish: (event) => publish(event, true),
5191
5369
  draft: deliveryWriter(models),
@@ -5365,7 +5543,7 @@ var init_index = __esm({
5365
5543
  return project ?? reply.code(404).send({ error: "Project not found on this server." });
5366
5544
  });
5367
5545
  app.post("/api/projects/:id/reconnect", async (request, reply) => {
5368
- const parsed = z19.object({ path: z19.string().trim().min(1).max(4096) }).safeParse(request.body);
5546
+ const parsed = z20.object({ path: z20.string().trim().min(1).max(4096) }).safeParse(request.body);
5369
5547
  if (!parsed.success)
5370
5548
  return reply.code(400).send({ error: "Enter the project's new folder path." });
5371
5549
  const root = await Effect17.runPromise(store.projectsRoot(defaultProjectsRoot));
@@ -5451,7 +5629,7 @@ var init_index = __esm({
5451
5629
  }
5452
5630
  });
5453
5631
  app.post("/api/tasks/:id/discard-unrun", async (request, reply) => {
5454
- const parsed = z19.object({ updatedAt: z19.string().datetime() }).safeParse(request.body);
5632
+ const parsed = z20.object({ updatedAt: z20.string().datetime() }).safeParse(request.body);
5455
5633
  if (!parsed.success) return reply.code(400).send({ error: "The task revision is required." });
5456
5634
  try {
5457
5635
  await deleteTask(store, request.params.id, void 0, parsed.data.updatedAt);
@@ -5521,9 +5699,9 @@ var init_index = __esm({
5521
5699
  }
5522
5700
  })
5523
5701
  );
5524
- HistoryQuery = z19.object({
5525
- after: z19.coerce.number().int().nonnegative().default(0),
5526
- limit: z19.coerce.number().int().min(1).max(1e3).default(500)
5702
+ HistoryQuery = z20.object({
5703
+ after: z20.coerce.number().int().nonnegative().default(0),
5704
+ limit: z20.coerce.number().int().min(1).max(1e3).default(500)
5527
5705
  });
5528
5706
  app.get("/api/sessions/:id/events", async (request, reply) => {
5529
5707
  const query = HistoryQuery.safeParse(request.query);
@@ -5537,7 +5715,7 @@ var init_index = __esm({
5537
5715
  const location = await resolveSessionProject(request.params.id);
5538
5716
  if (location.error !== void 0)
5539
5717
  return reply.code(location.status).send({ error: location.error });
5540
- const parsed = z19.object({ reset: z19.boolean().optional() }).safeParse(request.body ?? {});
5718
+ const parsed = z20.object({ reset: z20.boolean().optional() }).safeParse(request.body ?? {});
5541
5719
  if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
5542
5720
  try {
5543
5721
  return await openRepoChat(
@@ -5561,7 +5739,7 @@ var init_index = __esm({
5561
5739
  if (!result.session) return reply.code(result.status).send({ error: result.error });
5562
5740
  return reply.code(201).send(result.session);
5563
5741
  });
5564
- StartTaskBody = z19.object({ prompt: z19.string().trim().max(2e4).optional() }).default({});
5742
+ StartTaskBody = z20.object({ prompt: z20.string().trim().max(2e4).optional() }).default({});
5565
5743
  app.post("/api/tasks/:id/start", async (request, reply) => {
5566
5744
  const body = StartTaskBody.safeParse(request.body ?? {});
5567
5745
  if (!body.success) return reply.code(400).send({ error: "The task prompt is too long." });
@@ -5596,7 +5774,7 @@ var init_index = __esm({
5596
5774
  return automation.configure(parsed.data);
5597
5775
  });
5598
5776
  app.patch("/api/automation/queue", async (request, reply) => {
5599
- const parsed = z19.object({ taskId: z19.string().min(1), beforeId: z19.string().min(1).nullable() }).safeParse(request.body);
5777
+ const parsed = z20.object({ taskId: z20.string().min(1), beforeId: z20.string().min(1).nullable() }).safeParse(request.body);
5600
5778
  if (!parsed.success)
5601
5779
  return reply.code(400).send({ error: "Choose a task and a queue position." });
5602
5780
  try {
@@ -5677,6 +5855,7 @@ var init_index = __esm({
5677
5855
  serverId: security.serverId,
5678
5856
  publicUrl: process.env.AGENTRIFIC_PUBLIC_URL,
5679
5857
  phoneProvider: phone?.snapshot().provider,
5858
+ phoneProgress: phone?.snapshot().progress,
5680
5859
  localUrl: localServerUrl(),
5681
5860
  serverVersion,
5682
5861
  connectedDevices: new Set(security.sockets.values()).size,
@@ -5728,7 +5907,7 @@ var init_index = __esm({
5728
5907
  // src/cli.ts
5729
5908
  init_warnings();
5730
5909
  import { mkdir as mkdir7, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
5731
- import { homedir as homedir5 } from "node:os";
5910
+ import { homedir as homedir6 } from "node:os";
5732
5911
  import path14 from "node:path";
5733
5912
  import { fileURLToPath } from "node:url";
5734
5913
 
@@ -5828,7 +6007,7 @@ init_version();
5828
6007
  init_pairing();
5829
6008
  import { spawn as spawn2 } from "node:child_process";
5830
6009
  import { parseArgs } from "node:util";
5831
- import { z as z20 } from "zod";
6010
+ import { z as z21 } from "zod";
5832
6011
 
5833
6012
  // src/terminal.ts
5834
6013
  init_pairing();
@@ -6057,11 +6236,11 @@ function startTerminal(dataDir, publicUrl, tunnel) {
6057
6236
  init_opentunnel();
6058
6237
  init_tunnels();
6059
6238
  init_cli_output();
6060
- var ConfigSchema = z20.object({
6061
- dataDir: z20.string(),
6062
- projectsDir: z20.string(),
6063
- publicUrl: z20.string().url().optional(),
6064
- connection: z20.enum(["opentunnel", "tailscale", "cloudflared", "existing", "local"]).optional()
6239
+ var ConfigSchema = z21.object({
6240
+ dataDir: z21.string(),
6241
+ projectsDir: z21.string(),
6242
+ publicUrl: z21.string().url().optional(),
6243
+ connection: z21.enum(["opentunnel", "tailscale", "cloudflared", "existing", "local"]).optional()
6065
6244
  });
6066
6245
  function parseCommandArgs() {
6067
6246
  return parseArgs({
@@ -6091,14 +6270,14 @@ async function main() {
6091
6270
  return;
6092
6271
  }
6093
6272
  const dataDir = path14.resolve(
6094
- values["data-dir"] ?? process.env.AGENTRIFIC_DATA_DIR ?? path14.join(homedir5(), ".agentrific")
6273
+ values["data-dir"] ?? process.env.AGENTRIFIC_DATA_DIR ?? path14.join(homedir6(), ".agentrific")
6095
6274
  );
6096
6275
  const filename = path14.join(dataDir, "config.json");
6097
6276
  const config = ConfigSchema.parse(
6098
6277
  JSON.parse(
6099
6278
  await readFile7(filename, "utf8").catch((error) => {
6100
- if (!z20.object({ code: z20.literal("ENOENT") }).safeParse(error).success) throw error;
6101
- return JSON.stringify({ dataDir, projectsDir: path14.join(homedir5(), "projects") });
6279
+ if (!z21.object({ code: z21.literal("ENOENT") }).safeParse(error).success) throw error;
6280
+ return JSON.stringify({ dataDir, projectsDir: path14.join(homedir6(), "projects") });
6102
6281
  })
6103
6282
  )
6104
6283
  );
@@ -6133,7 +6312,7 @@ async function startCommand({ values, dataDir, filename, config }) {
6133
6312
  process.stdin.isTTY && process.stdout.isTTY && !values["no-interactive"]
6134
6313
  );
6135
6314
  const explicitUrl = values["public-url"] ?? process.env.AGENTRIFIC_PUBLIC_URL;
6136
- const method = values.connection ? z20.enum(["opentunnel", "tailscale", "cloudflared", "existing", "local"]).parse(values.connection) : explicitUrl ? "existing" : config.connection;
6315
+ const method = values.connection ? z21.enum(["opentunnel", "tailscale", "cloudflared", "existing", "local"]).parse(values.connection) : explicitUrl ? "existing" : config.connection;
6137
6316
  const origin = `http://${process.env.AGENTRIFIC_HOST ?? "127.0.0.1"}:${process.env.AGENTRIFIC_PORT ?? "4318"}`;
6138
6317
  if (interactive) {
6139
6318
  process.env.AGENTRIFIC_INTERACTIVE = "1";
@@ -6225,11 +6404,11 @@ async function pairCommand({ values, dataDir, config }) {
6225
6404
  return;
6226
6405
  }
6227
6406
  async function maintenanceCommand({ values, positionals, dataDir }) {
6228
- const action = z20.enum(["enter", "resume", "drain"]).parse(positionals[1]);
6407
+ const action = z21.enum(["enter", "resume", "drain"]).parse(positionals[1]);
6229
6408
  const result = await controlRequest(dataDir, {
6230
6409
  command: "maintenance",
6231
6410
  action,
6232
- timeoutMs: z20.coerce.number().int().min(0).max(6e4).parse(values["timeout-ms"] ?? 3e4)
6411
+ timeoutMs: z21.coerce.number().int().min(0).max(6e4).parse(values["timeout-ms"] ?? 3e4)
6233
6412
  });
6234
6413
  console.log(JSON.stringify(result, null, 2));
6235
6414
  if (action === "drain" && "drained" in result && !result.drained) process.exitCode = 1;
@@ -6238,7 +6417,7 @@ async function maintenanceCommand({ values, positionals, dataDir }) {
6238
6417
  async function devicesCommand({ dataDir, command }) {
6239
6418
  console.log(
6240
6419
  JSON.stringify(
6241
- await controlRequest(dataDir, { command: z20.enum(["devices", "status"]).parse(command) }),
6420
+ await controlRequest(dataDir, { command: z21.enum(["devices", "status"]).parse(command) }),
6242
6421
  null,
6243
6422
  2
6244
6423
  )
@@ -6246,7 +6425,7 @@ async function devicesCommand({ dataDir, command }) {
6246
6425
  return;
6247
6426
  }
6248
6427
  async function revokeCommand({ positionals, dataDir }) {
6249
- const id = z20.string().uuid().parse(positionals[1]);
6428
+ const id = z21.string().uuid().parse(positionals[1]);
6250
6429
  console.log(JSON.stringify(await controlRequest(dataDir, { command: "revoke", id })));
6251
6430
  return;
6252
6431
  }