@hoststack.dev/mcp 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -419,8 +419,9 @@ defineTool({
419
419
  ' - action: filter to a specific action (e.g. "service.created", "deploy.triggered").',
420
420
  ' - resource_type: filter by resource type (e.g. "service", "deploy", "domain").',
421
421
  " - user_id: filter by acting user numeric ID.",
422
+ ' - since/until: ISO-8601 timestamp OR a relative offset like "-15m" / "-2h" / "-7d" \u2014 narrow to a time window (handy for correlating a deploy with a log spike).',
422
423
  "",
423
- "Returns: { items: ActivityLogEntry[], meta? } \u2014 each entry has id, action, resourceType, resourceId, actorEmail, ipAddress, createdAt, and a context payload.",
424
+ "Returns: { items: ActivityLogEntry[], meta: { page, perPage, total, totalPages } } \u2014 each entry has id, action, resourceType, resourceId, actorEmail, ipAddress, createdAt, and a context payload. Use meta.totalPages to tell whether you have more.",
424
425
  "",
425
426
  'Example: list_activity_log({ resource_type: "deploy", per_page: 10 }) \u2192 { items: [{ action: "deploy.triggered", actorEmail: "ada@\u2026", \u2026 }], meta: { total: 47, page: 1 } }'
426
427
  ].join("\n"),
@@ -429,7 +430,9 @@ defineTool({
429
430
  per_page: z2.number().int().positive().max(100).optional().describe("Items per page, hard cap 100."),
430
431
  action: z2.string().optional().describe('Action filter, e.g. "service.created".'),
431
432
  resource_type: z2.string().optional().describe("Resource type filter."),
432
- user_id: z2.number().int().positive().optional().describe("Numeric acting-user filter.")
433
+ user_id: z2.number().int().positive().optional().describe("Numeric acting-user filter."),
434
+ since: z2.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-15m", "-2h", "-7d").'),
435
+ until: z2.string().optional().describe("ISO-8601 timestamp or relative offset upper bound.")
433
436
  },
434
437
  handler: async (args, ctx) => {
435
438
  const teamId = await ctx.resolveTeamId();
@@ -439,13 +442,24 @@ defineTool({
439
442
  if (args.action !== void 0) params["action"] = args.action;
440
443
  if (args.resource_type !== void 0) params["resourceType"] = args.resource_type;
441
444
  if (args.user_id !== void 0) params["userId"] = String(args.user_id);
445
+ if (args.since !== void 0) params["since"] = args.since;
446
+ if (args.until !== void 0) params["until"] = args.until;
442
447
  const response = await ctx.api.get(
443
448
  `/api/activity-log/${teamId}`,
444
449
  params
445
450
  );
446
451
  const items = Array.isArray(response.data) ? response.data.map(shapeActivity) : [];
447
452
  const data = { items };
448
- if (response.meta !== void 0) data.meta = shape(response.meta);
453
+ if (response.meta !== void 0) {
454
+ data.meta = shape(response.meta);
455
+ } else if (response.page !== void 0 || response.perPage !== void 0 || response.total !== void 0 || response.totalPages !== void 0) {
456
+ data.meta = shape({
457
+ page: response.page,
458
+ perPage: response.perPage,
459
+ total: response.total,
460
+ totalPages: response.totalPages
461
+ });
462
+ }
449
463
  return respond({
450
464
  summary: items.length === 0 ? "No activity log entries match the given filters." : `Returned ${items.length} activity log entr${items.length === 1 ? "y" : "ies"}.`,
451
465
  data
@@ -453,8 +467,51 @@ defineTool({
453
467
  }
454
468
  });
455
469
 
456
- // src/tools/cron.ts
470
+ // src/tools/alerts.ts
457
471
  import { z as z3 } from "zod";
472
+ defineTool({
473
+ name: "list_alerts",
474
+ category: "alerts",
475
+ description: [
476
+ "List recent alert-shaped events for the team: deploy failures, git auth losses, service health failures, auto-restarts, ACME cert failures, resource alerts (high CPU), database backup/restore failures, registrant verification lapses.",
477
+ "",
478
+ "When to use: triage 'what's currently broken or recently broke for this team'. Pairs with list_activity_log for the full audit feed; this tool is the alert-shaped subset.",
479
+ "",
480
+ 'By default events are AGGREGATED by (action, resourceId) so flapping events collapse to one row with a fire count + first/last timestamps \u2014 e.g. "service.auto_restarted on service 31, 8 times in the last hour, last at 14:22". Pass aggregate=false to see every raw row.',
481
+ "",
482
+ "Inputs (all optional):",
483
+ ' - since: ISO-8601 timestamp OR relative offset like "-1h" / "-2d". Default: -24h.',
484
+ " - until: ISO-8601 upper bound (ignored when aggregating \u2014 aggregated view always extends to now).",
485
+ " - limit: max rows (default 100, hard cap 500).",
486
+ " - aggregate: true (default) collapses by (action, resourceId); false returns raw rows.",
487
+ "",
488
+ "Returns: { alerts: Array, aggregated: boolean }. Each aggregated entry includes action, resourceType, resourceId, severity, count, firstFiredAt, lastFiredAt, lastMetadata. Each raw entry includes id, action, resourceType, resourceId, severity, metadata, createdAt.",
489
+ "",
490
+ "Example: list_alerts({ since: '-1h' }) \u2192 { alerts: [{ action: 'deploy.failed', resourceId: 31, severity: 'error', count: 3, lastFiredAt: '\u2026', \u2026 }] }."
491
+ ].join("\n"),
492
+ input: {
493
+ since: z3.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-1h", "-2d"). Default: -24h.'),
494
+ until: z3.string().optional().describe("ISO-8601 upper bound. Only honored when aggregate=false."),
495
+ limit: z3.number().int().positive().max(500).optional().describe("Max rows (default 100, hard cap 500)."),
496
+ aggregate: z3.boolean().optional().describe("Collapse by (action, resourceId). Default true.")
497
+ },
498
+ handler: async (args, ctx) => {
499
+ const teamId = await ctx.resolveTeamId();
500
+ const params = {};
501
+ if (args.since !== void 0) params["since"] = args.since;
502
+ if (args.until !== void 0) params["until"] = args.until;
503
+ if (args.limit !== void 0) params["limit"] = String(args.limit);
504
+ if (args.aggregate === false) params["aggregate"] = "0";
505
+ const response = await ctx.api.get(`/api/alerts/${teamId}`, params);
506
+ const items = Array.isArray(response.alerts) ? response.alerts.map(shape) : [];
507
+ const aggregated = Boolean(response.aggregated);
508
+ const summary = items.length === 0 ? "No alerts in the requested window \u2014 everything is operating normally." : aggregated ? `Returned ${items.length} alert group${items.length === 1 ? "" : "s"} (flapping events collapsed).` : `Returned ${items.length} raw alert event${items.length === 1 ? "" : "s"}.`;
509
+ return respond({ summary, data: { alerts: items, aggregated } });
510
+ }
511
+ });
512
+
513
+ // src/tools/cron.ts
514
+ import { z as z4 } from "zod";
458
515
  defineTool({
459
516
  name: "list_cron_executions",
460
517
  category: "cron",
@@ -472,8 +529,8 @@ defineTool({
472
529
  'Example: list_cron_executions({ service_id: "svc_cron" }) \u2192 { items: [{ status: "succeeded", exitCode: 0, \u2026 }, \u2026] }'
473
530
  ].join("\n"),
474
531
  input: {
475
- service_id: z3.string().describe("Cron service publicId."),
476
- limit: z3.number().int().positive().max(200).optional().describe("Max executions to return (default 50).")
532
+ service_id: z4.string().describe("Cron service publicId."),
533
+ limit: z4.number().int().positive().max(200).optional().describe("Max executions to return (default 50).")
477
534
  },
478
535
  handler: async (args, ctx) => {
479
536
  const teamId = await ctx.resolveTeamId();
@@ -501,8 +558,8 @@ defineTool({
501
558
  'Example: get_cron_execution({ service_id: "svc_cron", execution_id: "exe_xyz" }) \u2192 { execution: { status: "failed", exitCode: 1, \u2026 } }'
502
559
  ].join("\n"),
503
560
  input: {
504
- service_id: z3.string().describe("Cron service publicId."),
505
- execution_id: z3.string().describe("Execution publicId.")
561
+ service_id: z4.string().describe("Cron service publicId."),
562
+ execution_id: z4.string().describe("Execution publicId.")
506
563
  },
507
564
  handler: async (args, ctx) => {
508
565
  const teamId = await ctx.resolveTeamId();
@@ -514,7 +571,7 @@ defineTool({
514
571
  });
515
572
 
516
573
  // src/tools/databases.ts
517
- import { z as z4 } from "zod";
574
+ import { z as z5 } from "zod";
518
575
  defineTool({
519
576
  name: "list_databases",
520
577
  category: "databases",
@@ -531,7 +588,7 @@ defineTool({
531
588
  'Example: list_databases({ project_id: 12 }) \u2192 { items: [{ publicId: "db_\u2026", type: "postgres", status: "running", \u2026 }] }'
532
589
  ].join("\n"),
533
590
  input: {
534
- project_id: z4.number().int().positive().describe("Numeric project ID (from list_projects).")
591
+ project_id: z5.number().int().positive().describe("Numeric project ID (from list_projects).")
535
592
  },
536
593
  handler: async (args, ctx) => {
537
594
  const teamId = await ctx.resolveTeamId();
@@ -541,6 +598,45 @@ defineTool({
541
598
  return respond({ summary, data });
542
599
  }
543
600
  });
601
+ defineTool({
602
+ name: "update_database",
603
+ category: "databases",
604
+ description: [
605
+ "Update a managed database \u2014 rename, change plan tier (memory/CPU), or grow disk size.",
606
+ "",
607
+ "When to use: the user wants to resize a database (disk or plan), or rename it. Disk can only grow \u2014 shrinking is rejected because the underlying filesystem would orphan data.",
608
+ "",
609
+ "Inputs:",
610
+ ' - database_id: publicId of the database (e.g. "db_\u2026").',
611
+ " - name (optional): new database name.",
612
+ ' - plan (optional): "free" | "starter" | "standard" | "pro" \u2014 changes memory tier.',
613
+ " - disk_size_gb (optional): new disk size in GB (must be \u2265 current).",
614
+ "",
615
+ "Returns: { database: Database } \u2014 the updated record.",
616
+ "",
617
+ 'Example: update_database({ database_id: "db_xyz", disk_size_gb: 50 }) \u2192 { database: { diskSizeGb: 50, \u2026 } }'
618
+ ].join("\n"),
619
+ input: {
620
+ database_id: z5.string().describe("Database publicId (e.g. db_xyz)."),
621
+ name: z5.string().min(1).max(100).optional().describe("New database name."),
622
+ plan: z5.enum(["free", "starter", "standard", "pro"]).optional().describe("Plan tier (memory/CPU)."),
623
+ disk_size_gb: z5.number().int().min(1).max(1024).optional().describe("New disk size in GB. Must be \u2265 current.")
624
+ },
625
+ handler: async (args, ctx) => {
626
+ const teamId = await ctx.resolveTeamId();
627
+ const input = {};
628
+ if (args.name !== void 0) input.name = args.name;
629
+ if (args.plan !== void 0) input.plan = args.plan;
630
+ if (args.disk_size_gb !== void 0) input.diskSizeGb = args.disk_size_gb;
631
+ if (Object.keys(input).length === 0) {
632
+ return respond({ summary: "No fields to update.", data: {} });
633
+ }
634
+ const response = await ctx.hoststack.databases.update(teamId, args.database_id, input);
635
+ const data = { database: shapeDatabase(response.database) };
636
+ const fields = Object.keys(input).join(", ");
637
+ return respond({ summary: `Updated ${fields} on database ${args.database_id}.`, data });
638
+ }
639
+ });
544
640
  defineTool({
545
641
  name: "get_database",
546
642
  category: "databases",
@@ -557,7 +653,7 @@ defineTool({
557
653
  'Example: get_database({ database_id: "db_xyz" }) \u2192 { database: { type: "postgres", version: "16", status: "running", \u2026 } }'
558
654
  ].join("\n"),
559
655
  input: {
560
- database_id: z4.string().describe("Database publicId (e.g. db_xyz).")
656
+ database_id: z5.string().describe("Database publicId (e.g. db_xyz).")
561
657
  },
562
658
  handler: async (args, ctx) => {
563
659
  const teamId = await ctx.resolveTeamId();
@@ -570,7 +666,7 @@ defineTool({
570
666
  });
571
667
 
572
668
  // src/tools/deploys.ts
573
- import { z as z5 } from "zod";
669
+ import { z as z6 } from "zod";
574
670
  defineTool({
575
671
  name: "list_deploys",
576
672
  category: "deploys",
@@ -582,12 +678,12 @@ defineTool({
582
678
  "Inputs:",
583
679
  ' - service_id: publicId of the service (e.g. "svc_abc123"). Use list_services to find it.',
584
680
  "",
585
- "Returns: { items: Deploy[] } \u2014 each deploy includes id, publicId, status (pending|building|deploying|live|failed|cancelled), commitSha, commitMessage, branch, triggeredBy, startedAt, finishedAt, durationMs.",
681
+ "Returns: { items: Deploy[] } \u2014 each deploy includes id, publicId, status (pending|building|deploying|live|failed|cancelled), commitSha, commitMessage, branch, triggeredBy, startedAt, finishedAt, buildDurationMs (just the docker build / image-pull step), totalDurationMs (full deploy wall-clock = finishedAt \u2212 startedAt).",
586
682
  "",
587
683
  'Example: list_deploys({ service_id: "svc_abc" }) \u2192 { items: [{ publicId: "dpl_\u2026", status: "live", commitMessage: "Fix login", \u2026 }, \u2026] }'
588
684
  ].join("\n"),
589
685
  input: {
590
- service_id: z5.string().describe("Service publicId (e.g. svc_abc123).")
686
+ service_id: z6.string().describe("Service publicId (e.g. svc_abc123).")
591
687
  },
592
688
  handler: async (args, ctx) => {
593
689
  const teamId = await ctx.resolveTeamId();
@@ -609,13 +705,13 @@ defineTool({
609
705
  " - service_id: publicId of the service.",
610
706
  ' - deploy_id: publicId of the deploy (e.g. "dpl_\u2026").',
611
707
  "",
612
- "Returns: { deploy: Deploy } \u2014 full deploy record (status, commitSha, commitMessage, branch, durationMs, finishedAt, etc).",
708
+ "Returns: { deploy: Deploy } \u2014 full deploy record (status, commitSha, commitMessage, branch, buildDurationMs, totalDurationMs, finishedAt, etc).",
613
709
  "",
614
710
  'Example: get_deploy({ service_id: "svc_abc", deploy_id: "dpl_xyz" }) \u2192 { deploy: { status: "live", \u2026 } }'
615
711
  ].join("\n"),
616
712
  input: {
617
- service_id: z5.string().describe("Service publicId."),
618
- deploy_id: z5.string().describe("Deploy publicId.")
713
+ service_id: z6.string().describe("Service publicId."),
714
+ deploy_id: z6.string().describe("Deploy publicId.")
619
715
  },
620
716
  handler: async (args, ctx) => {
621
717
  const teamId = await ctx.resolveTeamId();
@@ -642,8 +738,8 @@ defineTool({
642
738
  'Example: trigger_deploy({ service_id: "svc_abc" }) \u2192 { deploy: { publicId: "dpl_\u2026", status: "pending", \u2026 } }'
643
739
  ].join("\n"),
644
740
  input: {
645
- service_id: z5.string().describe("Service publicId."),
646
- clear_cache: z5.boolean().optional().describe("Clear the build cache (default false).")
741
+ service_id: z6.string().describe("Service publicId."),
742
+ clear_cache: z6.boolean().optional().describe("Clear the build cache (default false).")
647
743
  },
648
744
  handler: async (args, ctx) => {
649
745
  const teamId = await ctx.resolveTeamId();
@@ -675,8 +771,8 @@ defineTool({
675
771
  'Example: cancel_deploy({ service_id: "svc_abc", deploy_id: "dpl_xyz" }) \u2192 { ok: true }'
676
772
  ].join("\n"),
677
773
  input: {
678
- service_id: z5.string().describe("Service publicId."),
679
- deploy_id: z5.string().describe("Deploy publicId.")
774
+ service_id: z6.string().describe("Service publicId."),
775
+ deploy_id: z6.string().describe("Deploy publicId.")
680
776
  },
681
777
  handler: async (args, ctx) => {
682
778
  const teamId = await ctx.resolveTeamId();
@@ -704,10 +800,10 @@ defineTool({
704
800
  'Example: get_deploy_logs({ service_id: "svc_abc", deploy_id: "dpl_xyz" }) \u2192 { logs: "Building\u2026\\nStep 1/8\u2026\\n\u2026", lineCount: 42, nextAfterId: 1234 }'
705
801
  ].join("\n"),
706
802
  input: {
707
- service_id: z5.string().describe("Service publicId."),
708
- deploy_id: z5.string().describe("Deploy publicId."),
709
- after_id: z5.number().int().optional().describe("Pagination cursor from a previous response\u2019s nextAfterId."),
710
- limit: z5.number().int().min(1).max(5e3).optional().describe("Max log entries to fetch (default 500, hard cap 5000).")
803
+ service_id: z6.string().describe("Service publicId."),
804
+ deploy_id: z6.string().describe("Deploy publicId."),
805
+ after_id: z6.number().int().optional().describe("Pagination cursor from a previous response\u2019s nextAfterId."),
806
+ limit: z6.number().int().min(1).max(5e3).optional().describe("Max log entries to fetch (default 500, hard cap 5000).")
711
807
  },
712
808
  handler: async (args, ctx) => {
713
809
  const teamId = await ctx.resolveTeamId();
@@ -728,9 +824,90 @@ defineTool({
728
824
  });
729
825
  }
730
826
  });
827
+ defineTool({
828
+ name: "diagnose_deploy",
829
+ category: "deploys",
830
+ description: [
831
+ "One-shot diagnostic bundle for a deploy: full deploy record + tail of build logs + tail of runtime logs starting at the deploy's start time. Lets you investigate any deploy failure mode (build-time failure, health-check timeout, post-deploy runtime crash) without juggling two log endpoints and two IDs.",
832
+ "",
833
+ "When to use: a deploy is failed/superseded/stuck and you want the whole story in one call. For just the build output, use get_deploy_logs. For just runtime logs of the running service, use get_service_logs.",
834
+ "",
835
+ "Inputs:",
836
+ " - service_id: publicId of the service.",
837
+ " - deploy_id: publicId of the deploy to diagnose.",
838
+ " - build_log_lines (optional): how many trailing build log lines to include. Default 200, max 2000.",
839
+ " - runtime_log_lines (optional): how many trailing runtime log lines (since the deploy started) to include. Default 100, max 1000. Set to 0 to skip runtime logs (saves a round-trip when investigating a pure build-time failure).",
840
+ "",
841
+ "Returns: { deploy, build: { logs, lineCount }, runtime: { logs, lineCount } } \u2014 runtime is omitted when runtime_log_lines is 0 or the deploy never started a container.",
842
+ "",
843
+ 'Example: diagnose_deploy({ service_id: "svc_abc", deploy_id: "dpl_xyz" }) \u2192 all three sections in one call.'
844
+ ].join("\n"),
845
+ input: {
846
+ service_id: z6.string().describe("Service publicId."),
847
+ deploy_id: z6.string().describe("Deploy publicId."),
848
+ build_log_lines: z6.number().int().min(1).max(2e3).optional().describe("Trailing build log lines. Default 200, max 2000."),
849
+ runtime_log_lines: z6.number().int().min(0).max(1e3).optional().describe(
850
+ "Trailing runtime log lines since deploy start. Default 100, max 1000. 0 = skip."
851
+ )
852
+ },
853
+ handler: async (args, ctx) => {
854
+ const teamId = await ctx.resolveTeamId();
855
+ const buildLimit = args.build_log_lines ?? 200;
856
+ const runtimeLimit = args.runtime_log_lines ?? 100;
857
+ const [deployResp, buildResp] = await Promise.all([
858
+ ctx.hoststack.deploys.get(teamId, args.service_id, args.deploy_id),
859
+ ctx.hoststack.deploys.getLogs(teamId, args.service_id, args.deploy_id, {
860
+ limit: buildLimit
861
+ })
862
+ ]);
863
+ const buildEntries = buildResp.logs;
864
+ const buildLogs = buildEntries.map((e) => e.message).join("\n");
865
+ const deployShape = shapeDeploy(deployResp.deploy);
866
+ const deployStatus = deployShape && "status" in deployShape ? deployShape.status : "unknown";
867
+ const startedAtRaw = deployShape && "startedAt" in deployShape ? deployShape.startedAt : void 0;
868
+ const startedAt = typeof startedAtRaw === "string" ? startedAtRaw : void 0;
869
+ const data = {
870
+ deploy: deployShape,
871
+ build: { logs: buildLogs, lineCount: buildEntries.length }
872
+ };
873
+ const hadContainer = deployStatus !== "pending" && deployStatus !== "building";
874
+ if (runtimeLimit > 0 && hadContainer) {
875
+ const runtimeResp = await ctx.hoststack.services.getRuntimeLogs(
876
+ teamId,
877
+ args.service_id,
878
+ {
879
+ lines: runtimeLimit,
880
+ ...startedAt ? { since: startedAt } : {}
881
+ }
882
+ );
883
+ if ("count" in runtimeResp) {
884
+ data["runtime"] = { logs: "", lineCount: runtimeResp.count };
885
+ } else {
886
+ const runtimeEntries = Array.isArray(runtimeResp.logs) ? runtimeResp.logs : typeof runtimeResp.logs === "string" ? runtimeResp.logs.split("\n").map((line) => ({ message: line })) : [];
887
+ const runtimeText = runtimeEntries.map(
888
+ (e) => typeof e === "object" && "message" in e ? String(e.message) : String(e)
889
+ ).join("\n");
890
+ data["runtime"] = {
891
+ logs: runtimeText,
892
+ lineCount: runtimeEntries.length,
893
+ sinceWindow: startedAt ?? null
894
+ };
895
+ }
896
+ }
897
+ const failureSummary = shape({
898
+ status: deployStatus,
899
+ errorMessage: deployShape && "errorMessage" in deployShape ? deployShape.errorMessage : void 0
900
+ });
901
+ data["summary"] = failureSummary;
902
+ return respond({
903
+ summary: `Diagnostic bundle for deploy ${args.deploy_id} (${deployStatus}): ${buildEntries.length} build log line${buildEntries.length === 1 ? "" : "s"}${runtimeLimit > 0 && hadContainer ? ", runtime log tail included" : ""}.`,
904
+ data
905
+ });
906
+ }
907
+ });
731
908
 
732
909
  // src/tools/domains.ts
733
- import { z as z6 } from "zod";
910
+ import { z as z7 } from "zod";
734
911
  defineTool({
735
912
  name: "list_domains",
736
913
  category: "domains",
@@ -769,8 +946,8 @@ defineTool({
769
946
  'Example: add_domain({ hostname: "api.example.com", service_id: "svc_abc" }) \u2192 { domain: { hostname: "api.example.com", dnsTargets: [...], verified: false, \u2026 } }'
770
947
  ].join("\n"),
771
948
  input: {
772
- hostname: z6.string().min(3).max(253).describe("Fully-qualified hostname (e.g. api.example.com)."),
773
- service_id: z6.string().optional().describe("Optional service publicId to bind to.")
949
+ hostname: z7.string().min(3).max(253).describe("Fully-qualified hostname (e.g. api.example.com)."),
950
+ service_id: z7.string().optional().describe("Optional service publicId to bind to.")
774
951
  },
775
952
  handler: async (args, ctx) => {
776
953
  const teamId = await ctx.resolveTeamId();
@@ -800,12 +977,15 @@ defineTool({
800
977
  'Example: verify_domain({ domain_id: "dom_xyz" }) \u2192 { ok: true }'
801
978
  ].join("\n"),
802
979
  input: {
803
- domain_id: z6.string().describe("Domain publicId.")
980
+ domain_id: z7.string().describe("Domain publicId.")
804
981
  },
805
982
  handler: async (args, ctx) => {
806
983
  const teamId = await ctx.resolveTeamId();
807
984
  await ctx.hoststack.domains.verify(teamId, args.domain_id);
808
- return respond({ summary: `Triggered DNS verification for ${args.domain_id}.`, data: { ok: true } });
985
+ return respond({
986
+ summary: `Triggered DNS verification for ${args.domain_id}.`,
987
+ data: { ok: true }
988
+ });
809
989
  }
810
990
  });
811
991
  defineTool({
@@ -824,7 +1004,7 @@ defineTool({
824
1004
  'Example: remove_domain({ domain_id: "dom_xyz" }) \u2192 { ok: true }'
825
1005
  ].join("\n"),
826
1006
  input: {
827
- domain_id: z6.string().describe("Domain publicId.")
1007
+ domain_id: z7.string().describe("Domain publicId.")
828
1008
  },
829
1009
  handler: async (args, ctx) => {
830
1010
  const teamId = await ctx.resolveTeamId();
@@ -834,7 +1014,7 @@ defineTool({
834
1014
  });
835
1015
 
836
1016
  // src/tools/env-vars.ts
837
- import { z as z7 } from "zod";
1017
+ import { z as z8 } from "zod";
838
1018
  defineTool({
839
1019
  name: "list_env_vars",
840
1020
  category: "env-vars",
@@ -851,7 +1031,7 @@ defineTool({
851
1031
  'Example: list_env_vars({ service_id: "svc_abc" }) \u2192 { items: [{ key: "DATABASE_URL", value: "\u2022\u2022\u2022\u2022\u2022\u2022", isSecret: true }, { key: "PORT", value: "3000", isSecret: false }] }'
852
1032
  ].join("\n"),
853
1033
  input: {
854
- service_id: z7.string().describe("Service publicId.")
1034
+ service_id: z8.string().describe("Service publicId.")
855
1035
  },
856
1036
  handler: async (args, ctx) => {
857
1037
  const teamId = await ctx.resolveTeamId();
@@ -880,10 +1060,10 @@ defineTool({
880
1060
  'Example: set_env_var({ service_id: "svc_abc", key: "DATABASE_URL", value: "postgres://\u2026", is_secret: true }) \u2192 { envVar: { key: "DATABASE_URL", value: "\u2022\u2022\u2022\u2022\u2022\u2022", \u2026 }, action: "updated" }'
881
1061
  ].join("\n"),
882
1062
  input: {
883
- service_id: z7.string().describe("Service publicId."),
884
- key: z7.string().min(1).max(128).describe("Env-var key."),
885
- value: z7.string().describe("New value."),
886
- is_secret: z7.boolean().optional().describe("Mark as secret (encrypted, masked on read). Default true.")
1063
+ service_id: z8.string().describe("Service publicId."),
1064
+ key: z8.string().min(1).max(128).describe("Env-var key."),
1065
+ value: z8.string().describe("New value."),
1066
+ is_secret: z8.boolean().optional().describe("Mark as secret (encrypted, masked on read). Default true.")
887
1067
  },
888
1068
  handler: async (args, ctx) => {
889
1069
  const teamId = await ctx.resolveTeamId();
@@ -891,10 +1071,15 @@ defineTool({
891
1071
  const existing = await ctx.hoststack.envVars.list(teamId, args.service_id);
892
1072
  const match = existing.envVars.find((v) => v.key === args.key);
893
1073
  if (match) {
894
- const response2 = await ctx.hoststack.envVars.update(teamId, args.service_id, String(match.id), {
895
- value: args.value,
896
- isSecret
897
- });
1074
+ const response2 = await ctx.hoststack.envVars.update(
1075
+ teamId,
1076
+ args.service_id,
1077
+ String(match.id),
1078
+ {
1079
+ value: args.value,
1080
+ isSecret
1081
+ }
1082
+ );
898
1083
  const data2 = {
899
1084
  envVar: shapeEnvVar(response2.envVar),
900
1085
  action: "updated"
@@ -930,18 +1115,18 @@ defineTool({
930
1115
  'Example: delete_env_var({ service_id: "svc_abc", key: "OLD_FLAG" }) \u2192 { ok: true }'
931
1116
  ].join("\n"),
932
1117
  input: {
933
- service_id: z7.string().describe("Service publicId."),
934
- key: z7.string().min(1).max(128).describe("Env-var key to delete.")
1118
+ service_id: z8.string().describe("Service publicId."),
1119
+ key: z8.string().min(1).max(128).describe("Env-var key to delete.")
935
1120
  },
936
1121
  handler: async (args, ctx) => {
937
1122
  const teamId = await ctx.resolveTeamId();
938
1123
  const existing = await ctx.hoststack.envVars.list(teamId, args.service_id);
939
1124
  const match = existing.envVars.find((v) => v.key === args.key);
940
1125
  if (!match) {
941
- return respondError(
942
- `Env var "${args.key}" not found on service ${args.service_id}.`,
943
- { key: args.key, service_id: args.service_id }
944
- );
1126
+ return respondError(`Env var "${args.key}" not found on service ${args.service_id}.`, {
1127
+ key: args.key,
1128
+ service_id: args.service_id
1129
+ });
945
1130
  }
946
1131
  await ctx.hoststack.envVars.delete(teamId, args.service_id, String(match.id));
947
1132
  return respond({
@@ -967,19 +1152,19 @@ defineTool({
967
1152
  'Example: bulk_set_env_vars({ service_id: "svc_abc", env_vars: [{ key: "PORT", value: "3000", is_secret: false }, { key: "DATABASE_URL", value: "\u2026", is_secret: true }] }) \u2192 { ok: true }'
968
1153
  ].join("\n"),
969
1154
  input: {
970
- service_id: z7.string().describe("Service publicId."),
971
- env_vars: z7.array(
972
- z7.object({
973
- key: z7.string().min(1).max(128),
974
- value: z7.string(),
975
- is_secret: z7.boolean().optional()
1155
+ service_id: z8.string().describe("Service publicId."),
1156
+ env_vars: z8.array(
1157
+ z8.object({
1158
+ key: z8.string().min(1).max(128),
1159
+ value: z8.string(),
1160
+ is_secret: z8.boolean().optional()
976
1161
  })
977
1162
  ).max(500).describe("Array of env-var rows. Hard cap 500.")
978
1163
  },
979
1164
  handler: async (args, ctx) => {
980
1165
  const teamId = await ctx.resolveTeamId();
981
1166
  const payload = {
982
- envVars: args.env_vars.map((v) => {
1167
+ vars: args.env_vars.map((v) => {
983
1168
  const row = {
984
1169
  key: v.key,
985
1170
  value: v.value
@@ -997,7 +1182,7 @@ defineTool({
997
1182
  });
998
1183
 
999
1184
  // src/tools/environments.ts
1000
- import { z as z8 } from "zod";
1185
+ import { z as z9 } from "zod";
1001
1186
  defineTool({
1002
1187
  name: "list_environments",
1003
1188
  category: "environments",
@@ -1014,7 +1199,7 @@ defineTool({
1014
1199
  'Example: list_environments({ project_id: "prj_abc" }) \u2192 { items: [{ name: "Production", type: "production", isDefault: true }, { name: "Staging", type: "staging" }] }'
1015
1200
  ].join("\n"),
1016
1201
  input: {
1017
- project_id: z8.string().describe("Project publicId.")
1202
+ project_id: z9.string().describe("Project publicId.")
1018
1203
  },
1019
1204
  handler: async (args, ctx) => {
1020
1205
  const teamId = await ctx.resolveTeamId();
@@ -1043,10 +1228,10 @@ defineTool({
1043
1228
  'Example: create_environment({ project_id: "prj_abc", name: "Staging", type: "staging" }) \u2192 { environment: { id: 7, publicId: "environment_\u2026", name: "Staging", type: "staging" } }'
1044
1229
  ].join("\n"),
1045
1230
  input: {
1046
- project_id: z8.string().describe("Project publicId."),
1047
- name: z8.string().min(1).max(64).describe("Environment name (1\u201364 chars)."),
1048
- type: z8.enum(["production", "staging", "development", "preview"]).describe("Environment type \u2014 drives the hostname suffix."),
1049
- is_protected: z8.boolean().optional().describe("Require admin role for destructive actions. Default false.")
1231
+ project_id: z9.string().describe("Project publicId."),
1232
+ name: z9.string().min(1).max(64).describe("Environment name (1\u201364 chars)."),
1233
+ type: z9.enum(["production", "staging", "development", "preview"]).describe("Environment type \u2014 drives the hostname suffix."),
1234
+ is_protected: z9.boolean().optional().describe("Require admin role for destructive actions. Default false.")
1050
1235
  },
1051
1236
  handler: async (args, ctx) => {
1052
1237
  const teamId = await ctx.resolveTeamId();
@@ -1080,8 +1265,8 @@ defineTool({
1080
1265
  'Example: delete_environment({ project_id: "prj_abc", environment_id: "environment_xyz" }) \u2192 { success: true }'
1081
1266
  ].join("\n"),
1082
1267
  input: {
1083
- project_id: z8.string().describe("Project publicId."),
1084
- environment_id: z8.union([z8.string(), z8.number()]).describe("Environment publicId or numeric id.")
1268
+ project_id: z9.string().describe("Project publicId."),
1269
+ environment_id: z9.union([z9.string(), z9.number()]).describe("Environment publicId or numeric id.")
1085
1270
  },
1086
1271
  handler: async (args, ctx) => {
1087
1272
  const teamId = await ctx.resolveTeamId();
@@ -1112,9 +1297,9 @@ defineTool({
1112
1297
  'Example: promote_deploy({ service_id: "svc_staging", deploy_id: "dpl_built", target_environment_id: "environment_prod" }) \u2192 { deploy: { id: 99, status: "pending", trigger: "rollback", dockerImageId: "sha256:\u2026" } }'
1113
1298
  ].join("\n"),
1114
1299
  input: {
1115
- service_id: z8.string().describe("Source service publicId."),
1116
- deploy_id: z8.string().describe("Source deploy publicId (must have a built image)."),
1117
- target_environment_id: z8.union([z8.string(), z8.number()]).describe("Target environment publicId or numeric id.")
1300
+ service_id: z9.string().describe("Source service publicId."),
1301
+ deploy_id: z9.string().describe("Source deploy publicId (must have a built image)."),
1302
+ target_environment_id: z9.union([z9.string(), z9.number()]).describe("Target environment publicId or numeric id.")
1118
1303
  },
1119
1304
  handler: async (args, ctx) => {
1120
1305
  const teamId = await ctx.resolveTeamId();
@@ -1162,8 +1347,190 @@ defineTool({
1162
1347
  }
1163
1348
  });
1164
1349
 
1350
+ // src/tools/notifications.ts
1351
+ import { z as z10 } from "zod";
1352
+ var NOTIFICATION_EVENTS = [
1353
+ "deploy.started",
1354
+ "deploy.succeeded",
1355
+ "deploy.failed",
1356
+ "deploy.failed_consecutive",
1357
+ "service.created",
1358
+ "service.deleted",
1359
+ "service.suspended",
1360
+ "service.resumed",
1361
+ "service.restart_failed",
1362
+ "service.auto_suspended",
1363
+ "service.acme_cert_failed",
1364
+ "git.auth_failed"
1365
+ ];
1366
+ defineTool({
1367
+ name: "list_notification_channels",
1368
+ category: "alerts",
1369
+ description: [
1370
+ "List configured notification channels for the team (Slack/Discord webhooks + email recipients). Each channel has a list of events it subscribes to \u2014 fire only happens when an event in that list occurs.",
1371
+ "",
1372
+ "When to use: setting up alerts ('which channels are notifying us on deploy failures?'), auditing why someone got/didn't get paged, or before subscribing a new event.",
1373
+ "",
1374
+ "Returns: { items: Channel[] } \u2014 each channel includes id, type, name, webhookUrl (masked), active, events, createdAt.",
1375
+ "",
1376
+ "Example: list_notification_channels() \u2192 { items: [{ id: 3, type: 'slack', name: 'eng-alerts', events: ['deploy.failed', 'git.auth_failed'], \u2026 }] }"
1377
+ ].join("\n"),
1378
+ input: {},
1379
+ handler: async (_args, ctx) => {
1380
+ const teamId = await ctx.resolveTeamId();
1381
+ const response = await ctx.hoststack.notifications.listChannels(teamId);
1382
+ const data = shapeList(response, "channels", shape);
1383
+ const summary = data.items.length === 0 ? "No notification channels yet \u2014 create one with create_notification_channel." : `Found ${data.items.length} notification channel${data.items.length === 1 ? "" : "s"}.`;
1384
+ return respond({ summary, data });
1385
+ }
1386
+ });
1387
+ defineTool({
1388
+ name: "create_notification_channel",
1389
+ category: "alerts",
1390
+ description: [
1391
+ "Create a Slack, Discord, or email notification channel and subscribe it to a list of events.",
1392
+ "",
1393
+ "When to use: setting up alert delivery for a team \u2014 paging the on-call when a deploy fails, posting to #incidents when git auth breaks, emailing the owner on ACME renewal failure.",
1394
+ "",
1395
+ "For type=email, `webhook_url` is the recipient email address. For type=slack or type=discord, it is the incoming webhook URL. The URL is stored encrypted server-side and masked in list responses.",
1396
+ "",
1397
+ "Inputs:",
1398
+ ' - type: "slack" | "discord" | "email".',
1399
+ " - name: human-readable label (1\u2013128 chars).",
1400
+ " - webhook_url: Slack/Discord webhook URL OR email address.",
1401
+ " - events: list of event names to subscribe to. Pass an empty list to create a channel that fires for nothing (manual subscribe later with update_notification_channel).",
1402
+ "",
1403
+ "Valid events: deploy.started, deploy.succeeded, deploy.failed, deploy.failed_consecutive, service.created, service.deleted, service.suspended, service.resumed, service.restart_failed, service.auto_suspended, service.acme_cert_failed, git.auth_failed.",
1404
+ "",
1405
+ "Returns: { channel: Channel }.",
1406
+ "",
1407
+ "Example: create_notification_channel({ type: 'slack', name: 'eng-alerts', webhook_url: 'https://hooks.slack.com/\u2026', events: ['deploy.failed', 'git.auth_failed', 'service.restart_failed'] })"
1408
+ ].join("\n"),
1409
+ input: {
1410
+ type: z10.enum(["slack", "discord", "email"]).describe("Channel type."),
1411
+ name: z10.string().min(1).max(128).describe("Human-readable label."),
1412
+ webhook_url: z10.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
1413
+ events: z10.array(z10.enum(NOTIFICATION_EVENTS)).describe("List of events the channel subscribes to. Empty list = subscribe to nothing.")
1414
+ },
1415
+ handler: async (args, ctx) => {
1416
+ const teamId = await ctx.resolveTeamId();
1417
+ const response = await ctx.hoststack.notifications.createChannel(teamId, {
1418
+ type: args.type,
1419
+ name: args.name,
1420
+ webhookUrl: args.webhook_url,
1421
+ events: args.events
1422
+ });
1423
+ const data = { channel: shape(response.channel) };
1424
+ return respond({
1425
+ summary: `Created ${args.type} channel "${args.name}" subscribed to ${args.events.length} event${args.events.length === 1 ? "" : "s"}.`,
1426
+ data
1427
+ });
1428
+ }
1429
+ });
1430
+ defineTool({
1431
+ name: "update_notification_channel",
1432
+ category: "alerts",
1433
+ description: [
1434
+ "Update a notification channel: rename, enable/disable, or change its event subscription list. The webhook URL and type are immutable \u2014 create a new channel if those need to change.",
1435
+ "",
1436
+ "When to use: silencing a noisy channel (active=false), adding a new event to an existing list, or relabeling.",
1437
+ "",
1438
+ "Inputs:",
1439
+ " - channel_id: numeric id of the channel.",
1440
+ " - name (optional): new label.",
1441
+ " - active (optional): false to silence, true to re-enable.",
1442
+ " - events (optional): replace the full subscription list. Passing [] silences all events without disabling the channel.",
1443
+ "",
1444
+ "Returns: { channel: Channel }.",
1445
+ "",
1446
+ "Example: update_notification_channel({ channel_id: 3, events: ['deploy.failed', 'service.restart_failed', 'git.auth_failed'] })"
1447
+ ].join("\n"),
1448
+ input: {
1449
+ channel_id: z10.number().int().positive().describe("Numeric channel id from list_notification_channels."),
1450
+ name: z10.string().min(1).max(128).optional().describe("New label."),
1451
+ active: z10.boolean().optional().describe("false silences without deleting."),
1452
+ events: z10.array(z10.enum(NOTIFICATION_EVENTS)).optional().describe("Replaces the full subscription list.")
1453
+ },
1454
+ handler: async (args, ctx) => {
1455
+ const teamId = await ctx.resolveTeamId();
1456
+ const update = {};
1457
+ if (args.name !== void 0) update.name = args.name;
1458
+ if (args.active !== void 0) update.active = args.active;
1459
+ if (args.events !== void 0) update.events = args.events;
1460
+ if (Object.keys(update).length === 0) {
1461
+ return respond({ summary: "No fields to update.", data: {} });
1462
+ }
1463
+ const response = await ctx.hoststack.notifications.updateChannel(
1464
+ teamId,
1465
+ args.channel_id,
1466
+ update
1467
+ );
1468
+ const data = { channel: shape(response.channel) };
1469
+ return respond({
1470
+ summary: `Updated notification channel #${args.channel_id}.`,
1471
+ data
1472
+ });
1473
+ }
1474
+ });
1475
+ defineTool({
1476
+ name: "delete_notification_channel",
1477
+ category: "alerts",
1478
+ description: [
1479
+ "Permanently delete a notification channel. Use update_notification_channel({ active: false }) to silence without deleting if you might want it back later.",
1480
+ "",
1481
+ "When to use: a channel is no longer needed (decommissioned Slack workspace, team member left, webhook leaked) and you want it gone from the team config entirely.",
1482
+ "",
1483
+ "Inputs:",
1484
+ " - channel_id: numeric id of the channel.",
1485
+ "",
1486
+ "Returns: { ok: true }.",
1487
+ "",
1488
+ "Example: delete_notification_channel({ channel_id: 3 }) \u2192 { ok: true }"
1489
+ ].join("\n"),
1490
+ input: {
1491
+ channel_id: z10.number().int().positive().describe("Numeric channel id.")
1492
+ },
1493
+ handler: async (args, ctx) => {
1494
+ const teamId = await ctx.resolveTeamId();
1495
+ await ctx.hoststack.notifications.deleteChannel(teamId, args.channel_id);
1496
+ return respond({
1497
+ summary: `Deleted notification channel #${args.channel_id}.`,
1498
+ data: { ok: true }
1499
+ });
1500
+ }
1501
+ });
1502
+ defineTool({
1503
+ name: "test_notification_channel",
1504
+ category: "alerts",
1505
+ description: [
1506
+ 'Fire a synthetic test event to the channel so the user can confirm the webhook is wired up correctly. The webhook receives an unmistakeable "test message" payload.',
1507
+ "",
1508
+ "When to use: after creating a channel, or when alerts stop arriving \u2014 quickest way to tell whether the issue is on HostStack's side or the channel's.",
1509
+ "",
1510
+ "Inputs:",
1511
+ " - channel_id: numeric id of the channel.",
1512
+ "",
1513
+ "Returns: { success: boolean, error?: string } \u2014 false + error string when the webhook returned non-2xx.",
1514
+ "",
1515
+ "Example: test_notification_channel({ channel_id: 3 }) \u2192 { success: true }"
1516
+ ].join("\n"),
1517
+ input: {
1518
+ channel_id: z10.number().int().positive().describe("Numeric channel id.")
1519
+ },
1520
+ handler: async (args, ctx) => {
1521
+ const teamId = await ctx.resolveTeamId();
1522
+ const response = await ctx.hoststack.notifications.testChannel(teamId, args.channel_id);
1523
+ const data = shape(response);
1524
+ const okFlag = data && typeof data === "object" && "success" in data ? data.success : false;
1525
+ return respond({
1526
+ summary: okFlag ? `Test event delivered to channel #${args.channel_id}.` : `Test event FAILED for channel #${args.channel_id}: ${data.error ?? "unknown error"}.`,
1527
+ data
1528
+ });
1529
+ }
1530
+ });
1531
+
1165
1532
  // src/tools/projects.ts
1166
- import { z as z9 } from "zod";
1533
+ import { z as z11 } from "zod";
1167
1534
  defineTool({
1168
1535
  name: "list_projects",
1169
1536
  category: "projects",
@@ -1203,9 +1570,9 @@ defineTool({
1203
1570
  'Example: create_project({ name: "billing-api", description: "Stripe webhooks", region: "fsn1" }) \u2192 { project: { id: 12, publicId: "prj_\u2026", \u2026 } }'
1204
1571
  ].join("\n"),
1205
1572
  input: {
1206
- name: z9.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
1207
- description: z9.string().max(500).optional().describe("Short description (\u2264500 chars)."),
1208
- region: z9.enum(["fsn1", "nbg1", "hel1"]).optional().describe("Hetzner region: fsn1 | nbg1 | hel1.")
1573
+ name: z11.string().min(1).max(60).describe("Project name (1\u201360 chars)."),
1574
+ description: z11.string().max(500).optional().describe("Short description (\u2264500 chars)."),
1575
+ region: z11.enum(["fsn1", "nbg1", "hel1"]).optional().describe("Hetzner region: fsn1 | nbg1 | hel1.")
1209
1576
  },
1210
1577
  handler: async (args, ctx) => {
1211
1578
  const teamId = await ctx.resolveTeamId();
@@ -1236,9 +1603,9 @@ defineTool({
1236
1603
  'Example: update_project({ project_id: "prj_abc", name: "billing-prod" }) \u2192 { project: { name: "billing-prod", \u2026 } }'
1237
1604
  ].join("\n"),
1238
1605
  input: {
1239
- project_id: z9.string().describe("Project publicId."),
1240
- name: z9.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
1241
- description: z9.string().max(500).optional().describe("New description (\u2264500 chars).")
1606
+ project_id: z11.string().describe("Project publicId."),
1607
+ name: z11.string().min(1).max(60).optional().describe("New name (1\u201360 chars)."),
1608
+ description: z11.string().max(500).optional().describe("New description (\u2264500 chars).")
1242
1609
  },
1243
1610
  handler: async (args, ctx) => {
1244
1611
  if (args.name === void 0 && args.description === void 0) {
@@ -1272,7 +1639,7 @@ defineTool({
1272
1639
  'Example: get_project({ project_id: "prj_abc" }) \u2192 { project: { id: 12, name: "billing", \u2026 } }'
1273
1640
  ].join("\n"),
1274
1641
  input: {
1275
- project_id: z9.string().describe("Project publicId (e.g. prj_abc123).")
1642
+ project_id: z11.string().describe("Project publicId (e.g. prj_abc123).")
1276
1643
  },
1277
1644
  handler: async (args, ctx) => {
1278
1645
  const teamId = await ctx.resolveTeamId();
@@ -1284,25 +1651,59 @@ defineTool({
1284
1651
  });
1285
1652
 
1286
1653
  // src/tools/services.ts
1287
- import { z as z10 } from "zod";
1654
+ import { z as z12 } from "zod";
1288
1655
  defineTool({
1289
1656
  name: "list_services",
1290
1657
  category: "services",
1291
1658
  description: [
1292
- "List every service (web, worker, cron, private) in the active HostStack team.",
1659
+ "List services (web, worker, cron, private, static) in the active HostStack team.",
1293
1660
  "",
1294
1661
  "When to use: the agent needs to find a service by name, check what is deployed, or pick a target for a follow-up tool (logs, deploys, env vars). This is the canonical way to resolve a publicId from a human-friendly name.",
1295
1662
  "",
1663
+ "Inputs (all optional):",
1664
+ ' - project_id: narrow to one project (numeric id or publicId "prj_\u2026").',
1665
+ ' - environment_id: narrow to one environment (numeric id or publicId "env_\u2026").',
1666
+ ' - status: "active" | "deploying" | "suspended" | "failed" | "not_deployed".',
1667
+ ' - type: "web_service" | "private_service" | "worker" | "cron_job" | "static_site".',
1668
+ "",
1296
1669
  "Returns: { items: Service[] } \u2014 each service includes id, publicId, name, type, status, projectId, repoUrl, branch, runtime, createdAt.",
1297
1670
  "",
1298
- 'Example: list_services() \u2192 { items: [{ id: 31, publicId: "svc_\u2026", name: "api", type: "web", status: "running", \u2026 }] }'
1671
+ 'Example: list_services({ status: "failed" }) \u2192 only services that need attention.'
1299
1672
  ].join("\n"),
1300
- input: {},
1301
- handler: async (_args, ctx) => {
1673
+ input: {
1674
+ project_id: z12.union([z12.number().int().positive(), z12.string()]).optional().describe("Project filter \u2014 numeric id or publicId."),
1675
+ environment_id: z12.union([z12.number().int().positive(), z12.string()]).optional().describe("Environment filter \u2014 numeric id or publicId."),
1676
+ status: z12.enum(["active", "deploying", "suspended", "failed", "not_deployed"]).optional().describe("Filter by current runtime status."),
1677
+ type: z12.enum(["web_service", "private_service", "worker", "cron_job", "static_site"]).optional().describe("Filter by service type.")
1678
+ },
1679
+ handler: async (args, ctx) => {
1302
1680
  const teamId = await ctx.resolveTeamId();
1303
- const response = await ctx.hoststack.services.list(teamId);
1681
+ const filters = {};
1682
+ if (args.project_id !== void 0) {
1683
+ const resolved = await ctx.hoststack.resolveId(args.project_id, {
1684
+ kind: "project",
1685
+ teamId
1686
+ });
1687
+ filters.projectId = resolved;
1688
+ }
1689
+ if (args.environment_id !== void 0) {
1690
+ const resolved = await ctx.hoststack.resolveId(args.environment_id, {
1691
+ kind: "environment",
1692
+ teamId
1693
+ });
1694
+ filters.environmentId = resolved;
1695
+ }
1696
+ if (args.status) filters.status = args.status;
1697
+ if (args.type) filters.type = args.type;
1698
+ const response = await ctx.hoststack.services.list(teamId, filters);
1304
1699
  const data = shapeList(response, "services", shapeService);
1305
- const summary = data.items.length === 0 ? "No services yet \u2014 create one with create_service or via the dashboard." : `Found ${data.items.length} service${data.items.length === 1 ? "" : "s"}.`;
1700
+ const filterDesc = [
1701
+ args.project_id !== void 0 ? `project=${args.project_id}` : null,
1702
+ args.environment_id !== void 0 ? `env=${args.environment_id}` : null,
1703
+ args.status ? `status=${args.status}` : null,
1704
+ args.type ? `type=${args.type}` : null
1705
+ ].filter(Boolean).join(", ");
1706
+ const summary = data.items.length === 0 ? filterDesc ? `No services match the given filters (${filterDesc}).` : "No services yet \u2014 create one with create_service or via the dashboard." : `Found ${data.items.length} service${data.items.length === 1 ? "" : "s"}${filterDesc ? ` matching ${filterDesc}` : ""}.`;
1306
1707
  return respond({ summary, data });
1307
1708
  }
1308
1709
  });
@@ -1322,7 +1723,7 @@ defineTool({
1322
1723
  'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 } }'
1323
1724
  ].join("\n"),
1324
1725
  input: {
1325
- service_id: z10.string().describe("Service publicId (e.g. svc_abc123).")
1726
+ service_id: z12.string().describe("Service publicId (e.g. svc_abc123).")
1326
1727
  },
1327
1728
  handler: async (args, ctx) => {
1328
1729
  const teamId = await ctx.resolveTeamId();
@@ -1338,7 +1739,7 @@ defineTool({
1338
1739
  description: [
1339
1740
  "Fetch the latest CPU, memory, network, and request-rate metrics for a service.",
1340
1741
  "",
1341
- "When to use: a service is reportedly slow, you suspect resource pressure, or you want a quick health snapshot before scaling. Returns a single point-in-time sample, not a time series.",
1742
+ 'When to use: a service is reportedly slow, you suspect resource pressure, or you want a quick health snapshot before scaling. Returns a single point-in-time sample. For a time series \u2014 "is memory growing?", "did CPU spike during that deploy?" \u2014 use get_service_metrics_history instead.',
1342
1743
  "",
1343
1744
  "Inputs:",
1344
1745
  " - service_id: publicId of the service.",
@@ -1348,7 +1749,7 @@ defineTool({
1348
1749
  'Example: get_service_metrics({ service_id: "svc_abc" }) \u2192 { metrics: { cpu: 0.42, memory: 0.71, \u2026 } }'
1349
1750
  ].join("\n"),
1350
1751
  input: {
1351
- service_id: z10.string().describe("Service publicId.")
1752
+ service_id: z12.string().describe("Service publicId.")
1352
1753
  },
1353
1754
  handler: async (args, ctx) => {
1354
1755
  const teamId = await ctx.resolveTeamId();
@@ -1357,6 +1758,55 @@ defineTool({
1357
1758
  return respond({ summary: `Metrics snapshot for service ${args.service_id}.`, data });
1358
1759
  }
1359
1760
  });
1761
+ defineTool({
1762
+ name: "get_service_metrics_history",
1763
+ category: "services",
1764
+ description: [
1765
+ "Fetch a metrics time series (CPU, memory, network, disk) for a service.",
1766
+ "",
1767
+ 'When to use: answering "is memory growing over the last hour?", "did CPU spike during deploy X?", or "what does normal look like for this service?". Pairs well with get_deploy to align spikes against deploy times.',
1768
+ "",
1769
+ "Inputs (all optional \u2014 omit both for the trailing hour):",
1770
+ " - service_id: publicId of the service.",
1771
+ ' - from: ISO-8601 lower bound OR relative offset like "-15m" / "-2h" / "-7d".',
1772
+ " - to: ISO-8601 upper bound (or relative offset). Defaults to now.",
1773
+ "",
1774
+ "Resolution: \u22647d \u2192 raw samples (~minute granularity), \u226430d \u2192 hourly pre-aggregates, >30d \u2192 daily. Up to ~500 points returned.",
1775
+ "",
1776
+ "Returns: { history: Array<{ timestamp, cpuPercent, memoryUsedMb, memoryLimitMb, networkRxBytes, networkTxBytes, diskUsedMb }> }.",
1777
+ "",
1778
+ 'Example: get_service_metrics_history({ service_id: "svc_abc", from: "-1h" }) \u2192 60-ish points for the last hour.'
1779
+ ].join("\n"),
1780
+ input: {
1781
+ service_id: z12.string().describe("Service publicId."),
1782
+ from: z12.string().optional().describe('ISO-8601 lower bound or relative offset (e.g. "-1h", "-2d").'),
1783
+ to: z12.string().optional().describe("ISO-8601 upper bound; defaults to now.")
1784
+ },
1785
+ handler: async (args, ctx) => {
1786
+ const teamId = await ctx.resolveTeamId();
1787
+ const opts = {};
1788
+ const resolveRel = (raw) => {
1789
+ const rel = /^-(\d+)(s|m|h|d)$/.exec(raw.trim());
1790
+ if (!rel) return raw;
1791
+ const amount = Number.parseInt(rel[1], 10);
1792
+ const unit = rel[2];
1793
+ const ms = unit === "s" ? amount * 1e3 : unit === "m" ? amount * 6e4 : unit === "h" ? amount * 36e5 : amount * 864e5;
1794
+ return new Date(Date.now() - ms).toISOString();
1795
+ };
1796
+ if (args.from) opts.from = resolveRel(args.from);
1797
+ if (args.to) opts.to = resolveRel(args.to);
1798
+ const response = await ctx.hoststack.services.getMetricsHistory(
1799
+ teamId,
1800
+ args.service_id,
1801
+ opts
1802
+ );
1803
+ const points = Array.isArray(response.history) ? response.history.length : 0;
1804
+ return respond({
1805
+ summary: `Returned ${points} metric point${points === 1 ? "" : "s"} for service ${args.service_id}.`,
1806
+ data: { history: response.history }
1807
+ });
1808
+ }
1809
+ });
1360
1810
  defineTool({
1361
1811
  name: "update_service",
1362
1812
  category: "services",
@@ -1374,8 +1824,8 @@ defineTool({
1374
1824
  'Example: update_service({ service_id: "svc_abc", name: "api-prod" }) \u2192 { service: { name: "api-prod", \u2026 } }'
1375
1825
  ].join("\n"),
1376
1826
  input: {
1377
- service_id: z10.string().describe("Service publicId."),
1378
- name: z10.string().min(1).max(60).describe("New service name (1\u201360 chars).")
1827
+ service_id: z12.string().describe("Service publicId."),
1828
+ name: z12.string().min(1).max(60).describe("New service name (1\u201360 chars).")
1379
1829
  },
1380
1830
  handler: async (args, ctx) => {
1381
1831
  const teamId = await ctx.resolveTeamId();
@@ -1402,21 +1852,30 @@ defineTool({
1402
1852
  " - dockerfile_path (optional): path to Dockerfile relative to root_directory. Pass null to clear.",
1403
1853
  " - auto_deploy (optional): boolean \u2014 auto-deploy on git push.",
1404
1854
  " - instance_count (optional): integer \u22651 \u2014 pin both min and max instances to this value.",
1855
+ ' - log_filter_rules (optional): list of { pattern, action } rules applied to runtime logs at query time. Pattern matches the message by case-insensitive substring; action is "drop" (filter out) or "downgrade" (flip stderr \u2192 stdout so it stops looking like an error). Pass [] to clear all rules. Capped at 50 rules.',
1405
1856
  "",
1406
1857
  "Returns: { service?: Service, config?: ServiceConfig } \u2014 whichever rows were touched.",
1407
1858
  "",
1408
1859
  'Example: update_service_config({ service_id: "svc_abc", start_command: "bun apps/api/src/index.ts" }) \u2192 { service: { startCommand: "bun apps/api/src/index.ts", \u2026 } }'
1409
1860
  ].join("\n"),
1410
1861
  input: {
1411
- service_id: z10.string().describe("Service publicId."),
1412
- install_command: z10.string().nullable().optional().describe("Install shell command. Null clears."),
1413
- build_command: z10.string().nullable().optional().describe("Build shell command. Null clears."),
1414
- start_command: z10.string().nullable().optional().describe("Start shell command. Null clears."),
1415
- branch: z10.string().optional().describe("Git branch to track."),
1416
- root_directory: z10.string().optional().describe("Build context root."),
1417
- dockerfile_path: z10.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
1418
- auto_deploy: z10.boolean().optional().describe("Auto-deploy on push."),
1419
- instance_count: z10.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350).")
1862
+ service_id: z12.string().describe("Service publicId."),
1863
+ install_command: z12.string().nullable().optional().describe("Install shell command. Null clears."),
1864
+ build_command: z12.string().nullable().optional().describe("Build shell command. Null clears."),
1865
+ start_command: z12.string().nullable().optional().describe("Start shell command. Null clears."),
1866
+ branch: z12.string().optional().describe("Git branch to track."),
1867
+ root_directory: z12.string().optional().describe("Build context root."),
1868
+ dockerfile_path: z12.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
1869
+ auto_deploy: z12.boolean().optional().describe("Auto-deploy on push."),
1870
+ instance_count: z12.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
1871
+ log_filter_rules: z12.array(
1872
+ z12.object({
1873
+ pattern: z12.string().min(1).max(200),
1874
+ action: z12.enum(["drop", "downgrade"])
1875
+ })
1876
+ ).max(50).optional().describe(
1877
+ "Runtime-log filter rules. Empty array [] clears all rules. Each pattern is case-insensitive substring match against the message."
1878
+ )
1420
1879
  },
1421
1880
  handler: async (args, ctx) => {
1422
1881
  const teamId = await ctx.resolveTeamId();
@@ -1428,14 +1887,16 @@ defineTool({
1428
1887
  if (args.dockerfile_path !== void 0)
1429
1888
  serviceUpdate["dockerfilePath"] = args.dockerfile_path;
1430
1889
  if (args.branch !== void 0) serviceUpdate["branch"] = args.branch;
1431
- if (args.root_directory !== void 0)
1432
- serviceUpdate["rootDirectory"] = args.root_directory;
1890
+ if (args.root_directory !== void 0) serviceUpdate["rootDirectory"] = args.root_directory;
1433
1891
  if (args.auto_deploy !== void 0) serviceUpdate["autoDeploy"] = args.auto_deploy;
1434
1892
  const configUpdate = {};
1435
1893
  if (args.instance_count !== void 0) {
1436
1894
  configUpdate["minInstances"] = args.instance_count;
1437
1895
  configUpdate["maxInstances"] = args.instance_count;
1438
1896
  }
1897
+ if (args.log_filter_rules !== void 0) {
1898
+ configUpdate["logFilterRules"] = args.log_filter_rules;
1899
+ }
1439
1900
  if (Object.keys(serviceUpdate).length === 0 && Object.keys(configUpdate).length === 0) {
1440
1901
  return respond({ summary: "No fields to update.", data: {} });
1441
1902
  }
@@ -1481,7 +1942,7 @@ defineTool({
1481
1942
  'Example: suspend_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
1482
1943
  ].join("\n"),
1483
1944
  input: {
1484
- service_id: z10.string().describe("Service publicId.")
1945
+ service_id: z12.string().describe("Service publicId.")
1485
1946
  },
1486
1947
  handler: async (args, ctx) => {
1487
1948
  const teamId = await ctx.resolveTeamId();
@@ -1505,7 +1966,7 @@ defineTool({
1505
1966
  'Example: resume_service({ service_id: "svc_dev" }) \u2192 { ok: true }'
1506
1967
  ].join("\n"),
1507
1968
  input: {
1508
- service_id: z10.string().describe("Service publicId.")
1969
+ service_id: z12.string().describe("Service publicId.")
1509
1970
  },
1510
1971
  handler: async (args, ctx) => {
1511
1972
  const teamId = await ctx.resolveTeamId();
@@ -1526,11 +1987,11 @@ defineTool({
1526
1987
  " - lines (optional): tail size (default 200, max 1000).",
1527
1988
  ' - since/until (optional): ISO-8601 timestamp OR a relative offset like "-5m", "-1h", "-2d".',
1528
1989
  ' - stream (optional): "stdout" | "stderr". Omit to combine.',
1529
- " - level (optional): friendly synonym \u2014 info/debug \u2192 stdout, warn/error \u2192 stderr.",
1990
+ " - level (optional): real log level \u2014 trace/debug/info/warn/error/fatal. For structured JSON logs (pino, bunyan, OpenTelemetry severity) this filters on the parsed inner level field. For plain text logs it falls back to a stream-alias hint (info/debug \u2192 stdout, warn/error/fatal \u2192 stderr).",
1530
1991
  " - search (optional): case-insensitive substring grep, \u2264100 chars.",
1531
1992
  ' - count_only (optional): when true, returns { count } only \u2014 much cheaper for "how many error lines in last 5m" polling.',
1532
1993
  "",
1533
- "Returns: { logs: LogEntry[] | string } when count_only is false (each entry has { timestamp, level?, stream?, message }), or { count: number } when count_only is true.",
1994
+ "Returns: { logs: LogEntry[] | string } when count_only is false. Each entry has { timestamp, level?, stream, message }. `level` is the parsed inner level when the message is a structured JSON envelope (pino numeric or string), and undefined for plain-text logs. `stream` is always one of stdout/stderr. Or { count: number } when count_only is true.",
1534
1995
  "",
1535
1996
  'Example: get_service_logs({ service_id: "svc_abc", search: "OOM", since: "-15m" }) \u2192 { logs: [...] }.',
1536
1997
  "",
@@ -1539,14 +2000,16 @@ defineTool({
1539
2000
  ' - Just count error lines without fetching them: get_service_logs({ service_id: "svc_abc", level: "error", since: "-5m", count_only: true }) \u2192 { count: 47 }'
1540
2001
  ].join("\n"),
1541
2002
  input: {
1542
- service_id: z10.string().describe("Service publicId."),
1543
- lines: z10.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
1544
- since: z10.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
1545
- until: z10.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
1546
- stream: z10.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
1547
- level: z10.enum(["stdout", "stderr", "info", "warn", "error", "debug"]).optional().describe("Friendly stream alias: info/debug\u2192stdout, warn/error\u2192stderr."),
1548
- search: z10.string().max(100).optional().describe("Case-insensitive substring filter."),
1549
- count_only: z10.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
2003
+ service_id: z12.string().describe("Service publicId."),
2004
+ lines: z12.number().int().positive().max(1e3).optional().describe("Tail size; default 200, hard cap 1000."),
2005
+ since: z12.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
2006
+ until: z12.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
2007
+ stream: z12.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
2008
+ level: z12.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe(
2009
+ "Filter by structured JSON log level (pino/bunyan/severity). Falls back to a stream-alias hint for plain-text logs (info/debug\u2192stdout, warn/error/fatal\u2192stderr)."
2010
+ ),
2011
+ search: z12.string().max(100).optional().describe("Case-insensitive substring filter."),
2012
+ count_only: z12.boolean().optional().describe("When true, return only { count } \u2014 skips the log payload.")
1550
2013
  },
1551
2014
  handler: async (args, ctx) => {
1552
2015
  const teamId = await ctx.resolveTeamId();
@@ -1573,9 +2036,82 @@ defineTool({
1573
2036
  });
1574
2037
  }
1575
2038
  });
2039
+ defineTool({
2040
+ name: "get_service_logs_bulk",
2041
+ category: "logs",
2042
+ description: [
2043
+ "Fetch runtime logs for multiple services in parallel and return one response keyed by service_id. Single round-trip from the MCP client perspective.",
2044
+ "",
2045
+ 'When to use: investigating something that spans a project \u2014 "what does every service in the auth-fleet say about the OOM at 14:00?", "did anything log an error in the last 5 minutes across these 4 services?". Pair with list_services (filter by project/status) to get the service_ids list first.',
2046
+ "",
2047
+ "Inputs:",
2048
+ " - service_ids: list of service publicIds (svc_\u2026). Hard cap 10 to bound work \u2014 for larger fleets, partition the call.",
2049
+ " - lines_per_service (optional): tail size per service (default 100, max 500). Smaller default than get_service_logs since you are fanning out.",
2050
+ ' - since/until (optional): same shape as get_service_logs (ISO-8601 or "-5m", "-1h").',
2051
+ ' - stream (optional): "stdout" | "stderr".',
2052
+ " - level (optional): structured-log level filter (trace/debug/info/warn/error/fatal).",
2053
+ " - search (optional): case-insensitive substring filter applied to every service.",
2054
+ ' - count_only (optional): when true, return { results: { [service_id]: { count } } } \u2014 much cheaper for "how many error lines per service in the last 5m" surveys.',
2055
+ "",
2056
+ "Returns: { results: { [service_id]: { logs?, count?, error? } } } \u2014 per-service result, with `error` populated when one service failed (the rest still succeed).",
2057
+ "",
2058
+ 'Example: get_service_logs_bulk({ service_ids: ["svc_api", "svc_worker"], level: "error", since: "-15m", count_only: true }) \u2192 { results: { svc_api: { count: 0 }, svc_worker: { count: 12 } } }.'
2059
+ ].join("\n"),
2060
+ input: {
2061
+ service_ids: z12.array(z12.string()).min(1).max(10).describe("Service publicIds (1\u201310). Hard cap 10 to bound parallel work."),
2062
+ lines_per_service: z12.number().int().positive().max(500).optional().describe("Tail size per service; default 100, hard cap 500."),
2063
+ since: z12.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-5m", "-1h").'),
2064
+ until: z12.string().optional().describe("ISO-8601 timestamp or relative offset upper bound."),
2065
+ stream: z12.enum(["stdout", "stderr"]).optional().describe("Restrict to one stream."),
2066
+ level: z12.enum(["stdout", "stderr", "trace", "debug", "info", "warn", "error", "fatal"]).optional().describe("Structured log level filter (same as get_service_logs)."),
2067
+ search: z12.string().max(100).optional().describe("Case-insensitive substring filter."),
2068
+ count_only: z12.boolean().optional().describe("When true, return only counts per service \u2014 skips the log payload.")
2069
+ },
2070
+ handler: async (args, ctx) => {
2071
+ const teamId = await ctx.resolveTeamId();
2072
+ const linesPerService = args.lines_per_service ?? 100;
2073
+ const baseOpts = { lines: linesPerService };
2074
+ if (args.since) baseOpts.since = args.since;
2075
+ if (args.until) baseOpts.until = args.until;
2076
+ if (args.stream) baseOpts.stream = args.stream;
2077
+ if (args.level) baseOpts.level = args.level;
2078
+ if (args.search) baseOpts.search = args.search;
2079
+ if (args.count_only) baseOpts.countOnly = args.count_only;
2080
+ const settled = await Promise.allSettled(
2081
+ args.service_ids.map(async (sid) => {
2082
+ const response = await ctx.hoststack.services.getRuntimeLogs(teamId, sid, baseOpts);
2083
+ return { sid, response };
2084
+ })
2085
+ );
2086
+ const results = {};
2087
+ let okCount = 0;
2088
+ let errorCount = 0;
2089
+ for (let i = 0; i < settled.length; i += 1) {
2090
+ const outcome = settled[i];
2091
+ const sid = args.service_ids[i];
2092
+ if (outcome.status === "fulfilled") {
2093
+ const { response } = outcome.value;
2094
+ if ("count" in response) {
2095
+ results[sid] = { count: response.count };
2096
+ } else {
2097
+ results[sid] = { logs: response.logs };
2098
+ }
2099
+ okCount += 1;
2100
+ } else {
2101
+ const reason = outcome.reason;
2102
+ results[sid] = {
2103
+ error: typeof reason === "string" ? reason : reason?.message ?? "Unknown error"
2104
+ };
2105
+ errorCount += 1;
2106
+ }
2107
+ }
2108
+ const summary = errorCount === 0 ? `Fetched logs from ${okCount} service${okCount === 1 ? "" : "s"}.` : `Fetched logs from ${okCount} of ${args.service_ids.length} service${args.service_ids.length === 1 ? "" : "s"} (${errorCount} failed \u2014 see per-service .error field).`;
2109
+ return respond({ summary, data: { results } });
2110
+ }
2111
+ });
1576
2112
 
1577
2113
  // src/tools/volumes.ts
1578
- import { z as z11 } from "zod";
2114
+ import { z as z13 } from "zod";
1579
2115
  defineTool({
1580
2116
  name: "list_volumes",
1581
2117
  category: "volumes",
@@ -1592,7 +2128,7 @@ defineTool({
1592
2128
  'Example: list_volumes({ service_id: "svc_abc" }) \u2192 { items: [{ name: "data", mountPath: "/var/data", sizeGb: 10, status: "active" }] }'
1593
2129
  ].join("\n"),
1594
2130
  input: {
1595
- service_id: z11.string().describe("Service publicId (e.g. svc_abc123).")
2131
+ service_id: z13.string().describe("Service publicId (e.g. svc_abc123).")
1596
2132
  },
1597
2133
  handler: async (args, ctx) => {
1598
2134
  const teamId = await ctx.resolveTeamId();
@@ -1621,10 +2157,10 @@ defineTool({
1621
2157
  'Example: create_volume({ service_id: "svc_abc", name: "data", mount_path: "/var/data", size_gb: 10 }) \u2192 { volume: { name: "data", mountPath: "/var/data", sizeGb: 10, status: "active" } }'
1622
2158
  ].join("\n"),
1623
2159
  input: {
1624
- service_id: z11.string().describe("Service publicId."),
1625
- name: z11.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
1626
- mount_path: z11.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
1627
- size_gb: z11.number().int().min(1).max(100).optional().describe("Disk size in GB (default 1, max 100).")
2160
+ service_id: z13.string().describe("Service publicId."),
2161
+ name: z13.string().min(1).max(64).regex(/^[a-z0-9-]+$/).describe("Volume name (lowercase alphanumeric + hyphens)."),
2162
+ mount_path: z13.string().startsWith("/").max(500).describe("In-container mount path (absolute)."),
2163
+ size_gb: z13.number().int().min(1).max(100).optional().describe("Disk size in GB (default 1, max 100).")
1628
2164
  },
1629
2165
  handler: async (args, ctx) => {
1630
2166
  const teamId = await ctx.resolveTeamId();
@@ -1660,10 +2196,10 @@ defineTool({
1660
2196
  'Example: update_volume({ service_id: "svc_abc", volume_id: "vol_xyz", size_gb: 20 }) \u2192 { volume: { sizeGb: 20, \u2026 } }'
1661
2197
  ].join("\n"),
1662
2198
  input: {
1663
- service_id: z11.string().describe("Service publicId."),
1664
- volume_id: z11.string().describe("Volume publicId (e.g. vol_\u2026)."),
1665
- mount_path: z11.string().startsWith("/").max(500).optional().describe("New mount path."),
1666
- size_gb: z11.number().int().min(1).max(100).optional().describe("New size in GB.")
2199
+ service_id: z13.string().describe("Service publicId."),
2200
+ volume_id: z13.string().describe("Volume publicId (e.g. vol_\u2026)."),
2201
+ mount_path: z13.string().startsWith("/").max(500).optional().describe("New mount path."),
2202
+ size_gb: z13.number().int().min(1).max(100).optional().describe("New size in GB.")
1667
2203
  },
1668
2204
  handler: async (args, ctx) => {
1669
2205
  const teamId = await ctx.resolveTeamId();
@@ -1701,8 +2237,8 @@ defineTool({
1701
2237
  'Example: delete_volume({ service_id: "svc_abc", volume_id: "vol_xyz" }) \u2192 { ok: true }'
1702
2238
  ].join("\n"),
1703
2239
  input: {
1704
- service_id: z11.string().describe("Service publicId."),
1705
- volume_id: z11.string().describe("Volume publicId.")
2240
+ service_id: z13.string().describe("Service publicId."),
2241
+ volume_id: z13.string().describe("Volume publicId.")
1706
2242
  },
1707
2243
  handler: async (args, ctx) => {
1708
2244
  const teamId = await ctx.resolveTeamId();