@codeworksh/cli 0.0.1-dev.20260922184805 → 0.0.1-dev.20260923102823

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.
Files changed (78) hide show
  1. package/README.md +16 -18
  2. package/{add--i7mPR73.mjs → add-D1yPPRz2.mjs} +4 -4
  3. package/{add--i7mPR73.mjs.map → add-D1yPPRz2.mjs.map} +1 -1
  4. package/catalog-BtH_l7f2.mjs +17 -0
  5. package/catalog-BtH_l7f2.mjs.map +1 -0
  6. package/{check-Dq2WOgap.mjs → check-B7Uh_G-l.mjs} +4 -4
  7. package/{check-Dq2WOgap.mjs.map → check-B7Uh_G-l.mjs.map} +1 -1
  8. package/{commonjs-BDiLMfFF.mjs → commonjs-bcl-ngMU.mjs} +3 -3
  9. package/{commonjs-BDiLMfFF.mjs.map → commonjs-bcl-ngMU.mjs.map} +1 -1
  10. package/{definitions-Dfwyyv_K.mjs → definitions-XbglYT7Q.mjs} +3 -3
  11. package/{definitions-Dfwyyv_K.mjs.map → definitions-XbglYT7Q.mjs.map} +1 -1
  12. package/{entries-Gfsq7U4P.mjs → entries-B4tUkrxs.mjs} +2 -2
  13. package/{entries-Gfsq7U4P.mjs.map → entries-B4tUkrxs.mjs.map} +1 -1
  14. package/{error-BrbT9_4g.mjs → error-DG5INUkQ.mjs} +2185 -2090
  15. package/error-DG5INUkQ.mjs.map +1 -0
  16. package/{harness-Bgvrr0ZB.mjs → harness-LSywfXzo.mjs} +2 -2
  17. package/{harness-Bgvrr0ZB.mjs.map → harness-LSywfXzo.mjs.map} +1 -1
  18. package/index.d.mts +1 -5
  19. package/index.mjs +18 -18
  20. package/index.mjs.map +1 -1
  21. package/{install-DoO4D3fk.mjs → install-zXEeTQ8w.mjs} +4 -4
  22. package/{install-DoO4D3fk.mjs.map → install-zXEeTQ8w.mjs.map} +1 -1
  23. package/{lib-CyynPpjH.mjs → lib-BCUUrctP.mjs} +5 -5
  24. package/{lib-CyynPpjH.mjs.map → lib-BCUUrctP.mjs.map} +1 -1
  25. package/{lib-Dw7R2suc.mjs → lib-CNtGmPS7.mjs} +5 -5
  26. package/{lib-Dw7R2suc.mjs.map → lib-CNtGmPS7.mjs.map} +1 -1
  27. package/{lib-JQygPqgO.mjs → lib-CZnS894S.mjs} +3 -3
  28. package/{lib-JQygPqgO.mjs.map → lib-CZnS894S.mjs.map} +1 -1
  29. package/{lib-BkWR53Bz.mjs → lib-DHoZNIet.mjs} +6 -6
  30. package/{lib-BkWR53Bz.mjs.map → lib-DHoZNIet.mjs.map} +1 -1
  31. package/{link-B1zfETsS.mjs → link-DquUj9D6.mjs} +3 -3
  32. package/{link-B1zfETsS.mjs.map → link-DquUj9D6.mjs.map} +1 -1
  33. package/{list-CfPfmfe0.mjs → list-C1ZWDpwI.mjs} +6 -5
  34. package/list-C1ZWDpwI.mjs.map +1 -0
  35. package/{list-3OR68HpY.mjs → list-eorMHI-g.mjs} +4 -4
  36. package/{list-3OR68HpY.mjs.map → list-eorMHI-g.mjs.map} +1 -1
  37. package/{login-GFV3Z9f2.mjs → login-DEjauWkd.mjs} +4 -4
  38. package/{login-GFV3Z9f2.mjs.map → login-DEjauWkd.mjs.map} +1 -1
  39. package/{logout-D17TQUfo.mjs → logout-BUY-ESj_.mjs} +4 -4
  40. package/{logout-D17TQUfo.mjs.map → logout-BUY-ESj_.mjs.map} +1 -1
  41. package/{p-map-C2WSUhyP.mjs → p-map-CMI1i54p.mjs} +2 -2
  42. package/{p-map-C2WSUhyP.mjs.map → p-map-CMI1i54p.mjs.map} +1 -1
  43. package/package.json +3 -3
  44. package/{providers-DLXs9aWZ.mjs → providers-wTGawU9e.mjs} +6 -5
  45. package/providers-wTGawU9e.mjs.map +1 -0
  46. package/refresh-B2jxJmk1.mjs +16 -0
  47. package/refresh-B2jxJmk1.mjs.map +1 -0
  48. package/{refresh-BTNuP1zL.mjs → refresh-C7e_qZit.mjs} +4 -4
  49. package/{refresh-BTNuP1zL.mjs.map → refresh-C7e_qZit.mjs.map} +1 -1
  50. package/{reload-BzwsCBQq.mjs → reload-B5LO2rk9.mjs} +3 -3
  51. package/{reload-BzwsCBQq.mjs.map → reload-B5LO2rk9.mjs.map} +1 -1
  52. package/{remove-DgB3l5JL.mjs → remove-CTVgJKXM.mjs} +4 -4
  53. package/{remove-DgB3l5JL.mjs.map → remove-CTVgJKXM.mjs.map} +1 -1
  54. package/{run-BVo4XsL6.mjs → run-CORFA6op.mjs} +4 -4
  55. package/{run-BVo4XsL6.mjs.map → run-CORFA6op.mjs.map} +1 -1
  56. package/{runtime-Dl621FOp.mjs → runtime-Bp4kILYn.mjs} +12 -19
  57. package/{runtime-Dl621FOp.mjs.map → runtime-Bp4kILYn.mjs.map} +1 -1
  58. package/{serve-Dpep18-A.mjs → serve-DzaOcgKC.mjs} +8 -5
  59. package/{serve-Dpep18-A.mjs.map → serve-DzaOcgKC.mjs.map} +1 -1
  60. package/{settings-C52IrA72.mjs → settings-tMD7Y0ev.mjs} +2 -2
  61. package/{settings-C52IrA72.mjs.map → settings-tMD7Y0ev.mjs.map} +1 -1
  62. package/{shared-Uwjcofff.mjs → shared-D8hbPMPF.mjs} +3 -3
  63. package/{shared-Uwjcofff.mjs.map → shared-D8hbPMPF.mjs.map} +1 -1
  64. package/{status-DPYq2Sx4.mjs → status-CrAKnTHQ.mjs} +4 -4
  65. package/{status-DPYq2Sx4.mjs.map → status-CrAKnTHQ.mjs.map} +1 -1
  66. package/{type-defs-BZxrtlVD.mjs → type-defs-JBxRyl-4.mjs} +3 -3
  67. package/{type-defs-BZxrtlVD.mjs.map → type-defs-JBxRyl-4.mjs.map} +1 -1
  68. package/{update-CJrBbHwq.mjs → update-DK7kAVc3.mjs} +4 -4
  69. package/{update-CJrBbHwq.mjs.map → update-DK7kAVc3.mjs.map} +1 -1
  70. package/{valid-BWgW_TUl.mjs → valid-Bc7tec52.mjs} +2 -2
  71. package/{valid-BWgW_TUl.mjs.map → valid-Bc7tec52.mjs.map} +1 -1
  72. package/catalog-DJxy_lne.mjs +0 -31
  73. package/catalog-DJxy_lne.mjs.map +0 -1
  74. package/error-BrbT9_4g.mjs.map +0 -1
  75. package/generate-DrGYpD3l.mjs +0 -35
  76. package/generate-DrGYpD3l.mjs.map +0 -1
  77. package/list-CfPfmfe0.mjs.map +0 -1
  78. package/providers-DLXs9aWZ.mjs.map +0 -1
@@ -20,6 +20,7 @@ import { Buffer as Buffer$1 } from "node:buffer";
20
20
  import { Bash } from "just-bash";
21
21
  import { SandboxDriver as SandboxDriver$3, SandboxProvider } from "@codeworksh/plugin/sandbox";
22
22
  import { DirectoryNotFoundError, DirectoryNotFoundError as DirectoryNotFoundError$1, Info, NotDirectoryError, NotDirectoryError as NotDirectoryError$1, Service, Service as Service$8 } from "@codeworksh/plugin/location";
23
+ import { generateModels } from "@codeworksh/aikit/modelgen";
23
24
  import { Script, constants } from "node:vm";
24
25
  import * as AikitFailure from "@codeworksh/aikit/failure";
25
26
  import { JsonGitHubCopilotAuthStorage, getGitHubCopilotApiKey } from "@codeworksh/aikit/oauth/github/copilot";
@@ -357,7 +358,7 @@ const setup = Effect.gen(function* () {
357
358
  yield* sql`PRAGMA foreign_keys = ON`;
358
359
  yield* SqliteMigrator.run({ loader: Migrator.fromRecord(migrations) });
359
360
  });
360
- function layer$26(location) {
361
+ function layer$27(location) {
361
362
  return Layer.unwrap(Effect.gen(function* () {
362
363
  if (location !== ":memory:") yield* fileSystem.makeDirectory(posix.dirname(location), { recursive: true }).pipe(Effect.orDie);
363
364
  return SqliteClient.layer({
@@ -376,7 +377,7 @@ const path$1 = Effect.fn("Database.path")(function* (data) {
376
377
  const directory = data ?? (yield* resolve$3()).data;
377
378
  return resolveDatabaseLocation(yield* locationConfig, directory);
378
379
  });
379
- const defaultLayer$1 = Layer.unwrap(path$1().pipe(Effect.map(layer$26)));
380
+ const defaultLayer$1 = Layer.unwrap(path$1().pipe(Effect.map(layer$27)));
380
381
  //#endregion
381
382
  //#region ../harness/src/event/schema.ts
382
383
  var schema_exports$6 = /* @__PURE__ */ __exportAll({});
@@ -555,7 +556,7 @@ var SessionEntryPartRow = class extends Model$1.Class("SessionEntryPartRow")({
555
556
  /**
556
557
  * The public description of a failed execution.
557
558
  *
558
- * Open by design: `type` is a namespaced category string, not a closed union.
559
+ * Open by design: `type` is a kebab-case category ending in `-error`, not a closed union.
559
560
  * A client older than the server degrades to the message instead of failing to
560
561
  * decode, and a new category is an additive change. The typed cause stays
561
562
  * server-side -- this is what crosses the wire.
@@ -568,40 +569,40 @@ const Error$1 = Schema.Struct({
568
569
  });
569
570
  const providerCategory$1 = (tag) => {
570
571
  switch (tag) {
571
- case "Runner.ProviderAuthenticationError": return "provider.auth";
572
- case "Runner.ProviderAuthorizationError": return "provider.forbidden";
573
- case "Runner.ProviderConfigurationError": return "provider.config";
574
- case "Runner.ProviderModelUnavailableError": return "provider.model-unavailable";
575
- case "Runner.ProviderRateLimitError": return "provider.rate-limit";
576
- case "Runner.ProviderQuotaError": return "provider.quota";
577
- case "Runner.ProviderInvalidRequestError": return "provider.invalid-request";
578
- case "Runner.ProviderContentPolicyError": return "provider.content-filter";
579
- case "Runner.ProviderTimeoutError": return "provider.timeout";
580
- case "Runner.ProviderTransportError": return "provider.transport";
581
- case "Runner.ProviderUnavailableError": return "provider.unavailable";
582
- case "Runner.ProviderInvalidResponseError": return "provider.invalid-output";
583
- default: return "provider.unknown";
572
+ case "Runner.ProviderAuthenticationError": return "provider-authentication-error";
573
+ case "Runner.ProviderAuthorizationError": return "provider-authorization-error";
574
+ case "Runner.ProviderConfigurationError": return "provider-configuration-error";
575
+ case "Runner.ProviderModelUnavailableError": return "provider-model-unavailable-error";
576
+ case "Runner.ProviderRateLimitError": return "provider-rate-limit-error";
577
+ case "Runner.ProviderQuotaError": return "provider-quota-error";
578
+ case "Runner.ProviderInvalidRequestError": return "provider-invalid-request-error";
579
+ case "Runner.ProviderContentPolicyError": return "provider-content-filter-error";
580
+ case "Runner.ProviderTimeoutError": return "provider-timeout-error";
581
+ case "Runner.ProviderTransportError": return "provider-transport-error";
582
+ case "Runner.ProviderUnavailableError": return "provider-unavailable-error";
583
+ case "Runner.ProviderInvalidResponseError": return "provider-invalid-response-error";
584
+ default: return "provider-unknown-error";
584
585
  }
585
586
  };
586
587
  const category = (tag) => {
587
588
  switch (tag) {
588
- case "Runner.ModelCatalogError": return "model.catalog";
589
- case "Runner.ModelNotFoundError": return "model.not-found";
590
- case "Runner.LLMStreamError": return "llm.stream";
591
- case "ContextDecodeError": return "context.decode";
592
- case "ContextEncodeError": return "context.encode";
593
- case "SessionNotFoundError": return "session.not-found";
594
- case "SessionLinkedSpaceNotFoundError": return "session.space-not-found";
595
- case "SettingsError": return "settings";
596
- case "State.SnapshotError": return "state.snapshot";
597
- case "Runner.SandboxDirectoryNotFoundError": return "sandbox.directory-not-found";
598
- case "SandboxFileSystemError": return "sandbox.filesystem";
589
+ case "Runner.ModelCatalogError": return "model-catalog-error";
590
+ case "Runner.ModelNotFoundError": return "model-not-found-error";
591
+ case "Runner.LLMStreamError": return "llm-stream-error";
592
+ case "ContextDecodeError": return "context-decode-error";
593
+ case "ContextEncodeError": return "context-encode-error";
594
+ case "SessionNotFoundError": return "session-not-found-error";
595
+ case "SessionLinkedSpaceNotFoundError": return "session-space-not-found-error";
596
+ case "SettingsError": return "settings-error";
597
+ case "State.SnapshotError": return "state-snapshot-error";
598
+ case "Runner.SandboxDirectoryNotFoundError": return "sandbox-directory-not-found-error";
599
+ case "SandboxFileSystemError": return "sandbox-filesystem-error";
599
600
  case "SandboxNotFoundError":
600
601
  case "SandboxDriverNotRegisteredError":
601
602
  case "SandboxUnavailError":
602
603
  case "SandboxRemovedError":
603
- case "SandboxProviderError": return "sandbox.mount";
604
- default: return "unknown";
604
+ case "SandboxProviderError": return "sandbox-mount-error";
605
+ default: return "unknown-error";
605
606
  }
606
607
  };
607
608
  const message = (cause) => cause instanceof globalThis.Error ? cause.message : typeof cause === "string" ? cause : "Session execution failed";
@@ -609,7 +610,7 @@ const message = (cause) => cause instanceof globalThis.Error ? cause.message : t
609
610
  const fromCause = (cause) => {
610
611
  const tag = Predicate.hasProperty(cause, "_tag") && typeof cause._tag === "string" ? cause._tag : void 0;
611
612
  if (tag === void 0) return {
612
- type: "unknown",
613
+ type: "unknown-error",
613
614
  message: message(cause)
614
615
  };
615
616
  if (tag === "Runner.ProviderError") {
@@ -872,7 +873,7 @@ var InvalidDurableEventError = class extends Schema.TaggedError()("InvalidDurabl
872
873
  }) {};
873
874
  var SubscriptionOverflowError = class extends Schema.TaggedError()("SubscriptionOverflowError", { capacity: Schema.Int }) {};
874
875
  var Service$16 = class extends Context.Service()("@codeworksh/harness/event/event/Service") {};
875
- const layer$25 = Layer.effect(Service$16, Effect.gen(function* () {
876
+ const layer$26 = Layer.effect(Service$16, Effect.gen(function* () {
876
877
  const sql = yield* SqlClient.SqlClient;
877
878
  const pubsub = { durable: /* @__PURE__ */ new Map() };
878
879
  const subscriptions = /* @__PURE__ */ new Set();
@@ -1235,7 +1236,7 @@ const archiveEnv = Effect.fn("Space.archiveEnv")(function* (env) {
1235
1236
  `;
1236
1237
  })).pipe(Effect.orDie);
1237
1238
  });
1238
- const layer$24 = Layer.effect(Service$15, Effect.gen(function* () {
1239
+ const layer$25 = Layer.effect(Service$15, Effect.gen(function* () {
1239
1240
  const sql = yield* SqlClient.SqlClient;
1240
1241
  const fs = yield* io_exports.SandboxIO.FileSystem;
1241
1242
  const { id: env } = yield* io_exports.SandboxIO.Current;
@@ -1403,7 +1404,7 @@ const decodeMessageEnvelopeIdentity = Schema.decodeUnknownEffect(MessageEnvelope
1403
1404
  const decodeCompactionData$1 = Schema.decodeUnknownEffect(CompactionData);
1404
1405
  const decodeJsonObject$1 = Schema.decodeUnknownEffect(JsonObject);
1405
1406
  const encodeJsonObject$1 = Schema.encodeEffect(JsonObject);
1406
- const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
1407
+ const layer$24 = Layer.effect(Service$14, Effect.gen(function* () {
1407
1408
  const sql = yield* SqlClient.SqlClient;
1408
1409
  const events = yield* Service$16;
1409
1410
  const findSession = SqlSchema.findOneOption({
@@ -2065,7 +2066,7 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
2065
2066
  setLabel
2066
2067
  });
2067
2068
  }));
2068
- layer$23.pipe(Layer.provide(defaultLayer$1));
2069
+ layer$24.pipe(Layer.provide(defaultLayer$1));
2069
2070
  //#endregion
2070
2071
  //#region ../harness/src/context/errors.ts
2071
2072
  var ContextDecodeError = class extends Schema.TaggedError()("ContextDecodeError", {
@@ -2366,7 +2367,7 @@ const assemblePath = Effect.fn("Context.assemblePath")(function* (sessionId, pat
2366
2367
  //#endregion
2367
2368
  //#region ../harness/src/context/context.ts
2368
2369
  var Service$13 = class extends Context.Service()("@codeworksh/harness/context/context/Service") {};
2369
- const layer$22 = Layer.effect(Service$13, Effect.gen(function* () {
2370
+ const layer$23 = Layer.effect(Service$13, Effect.gen(function* () {
2370
2371
  const sessions = yield* Service$14;
2371
2372
  const assemble = Effect.fn("Context.assemble")(function* (sessionId) {
2372
2373
  const found = yield* sessions.get(sessionId);
@@ -2466,7 +2467,7 @@ const make$22 = (vfs) => {
2466
2467
  };
2467
2468
  };
2468
2469
  /** The `SandboxFileSystem.Service` backed by the local {@link Vfs}. */
2469
- const layer$21 = Layer.effect(filesystem_exports.SandboxFileSystem.Service, Effect.gen(function* () {
2470
+ const layer$22 = Layer.effect(filesystem_exports.SandboxFileSystem.Service, Effect.gen(function* () {
2470
2471
  return filesystem_exports.SandboxFileSystem.fromProvider(make$22(yield* Vfs));
2471
2472
  }));
2472
2473
  //#endregion
@@ -2493,7 +2494,7 @@ var HostProvider = class extends RealFSProvider {
2493
2494
  * reason to keep relative paths above the mount, not below it — nothing here
2494
2495
  * resolves them, and nothing here should.
2495
2496
  */
2496
- const layer$20 = () => {
2497
+ const layer$21 = () => {
2497
2498
  const vfs = create(new HostProvider("/"), {
2498
2499
  moduleHooks: false,
2499
2500
  virtualCwd: true
@@ -2586,7 +2587,7 @@ const make$21 = (drivers) => Effect.gen(function* () {
2586
2587
  encodeRuntimeConfig
2587
2588
  });
2588
2589
  });
2589
- const layer$19 = (...drivers) => Layer.effect(Registry, make$21(drivers));
2590
+ const layer$20 = (...drivers) => Layer.effect(Registry, make$21(drivers));
2590
2591
  //#endregion
2591
2592
  //#region ../harness/src/sandbox/shell/shell.ts
2592
2593
  var shell_exports = /* @__PURE__ */ __exportAll({});
@@ -2595,7 +2596,7 @@ __reExport(shell_exports, import__codeworksh_plugin_sandbox_shell);
2595
2596
  //#endregion
2596
2597
  //#region ../harness/src/sandbox/shell/host.ts
2597
2598
  const DEFAULT_SHELL = "sh";
2598
- const layer$18 = (options = {}) => Layer.effect(shell_exports.Shell, Effect.gen(function* () {
2599
+ const layer$19 = (options = {}) => Layer.effect(shell_exports.Shell, Effect.gen(function* () {
2599
2600
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
2600
2601
  const run = (label, file, args, opts) => Effect.scoped(Effect.gen(function* () {
2601
2602
  const handle = yield* spawner.spawn(ChildProcess.make(file, [...args], {
@@ -2686,7 +2687,7 @@ const make$20 = Effect.gen(function* () {
2686
2687
  //#endregion
2687
2688
  //#region ../harness/src/sandbox/control.ts
2688
2689
  var Controller = class extends Context.Service()("@codeworksh/harness/sandbox/control/Controller") {};
2689
- const hostTransport = Layer.provide(Layer.merge(Layer.fresh(layer$21), layer$18()), layer$20());
2690
+ const hostTransport = Layer.provide(Layer.merge(Layer.fresh(layer$22), layer$19()), layer$21());
2690
2691
  const asDate = DateTime.toDateUtc;
2691
2692
  const optionalDate = Option.map(DateTime.toDateUtc);
2692
2693
  const MetadataJson = Schema.fromJsonString(Schema.Record(Schema.String, Schema.String));
@@ -3306,7 +3307,7 @@ const make$19 = Effect.fn("Sandbox.Controller.make")(function* (options) {
3306
3307
  destroy
3307
3308
  });
3308
3309
  });
3309
- const layer$17 = (options) => Layer.effect(Controller, make$19(options));
3310
+ const layer$18 = (options) => Layer.effect(Controller, make$19(options));
3310
3311
  //#endregion
3311
3312
  //#region ../harness/src/sandbox/utils/seed.ts
3312
3313
  const copyDirectory = async (vfs, sourceDirectory, targetDirectory) => {
@@ -3562,7 +3563,7 @@ const transport$3 = (inner) => Layer.provideMerge(Layer.effect(shell_exports.She
3562
3563
  })), inner);
3563
3564
  //#endregion
3564
3565
  //#region ../harness/src/sandbox/virtual.ts
3565
- const transportLayer = (primitives) => Layer.provideMerge(Layer.merge(Layer.fresh(layer$21), transport$3(primitives)), primitives);
3566
+ const transportLayer = (primitives) => Layer.provideMerge(Layer.merge(Layer.fresh(layer$22), transport$3(primitives)), primitives);
3566
3567
  /** A cwd-neutral FileSystem + just-bash transport over one retained VFS. */
3567
3568
  const transport$2 = (vfs) => {
3568
3569
  const primitives = Layer.merge(Layer.succeed(Vfs, vfs), unsupported);
@@ -3669,7 +3670,7 @@ const make$15 = (location, options) => Effect.tryPromise({
3669
3670
  })
3670
3671
  });
3671
3672
  const vfsLayer = (location, options) => Layer.effect(Vfs, Effect.acquireRelease(make$15(location, options), ({ provider }) => Effect.sync(() => provider.close())).pipe(Effect.map(({ vfs }) => vfs)));
3672
- const layer$16 = (opts) => Layer.merge(vfsLayer(opts?.location, opts?.options), opts?.options?.hostProcess ? host : unsupported);
3673
+ const layer$17 = (opts) => Layer.merge(vfsLayer(opts?.location, opts?.options), opts?.options?.hostProcess ? host : unsupported);
3673
3674
  //#endregion
3674
3675
  //#region ../harness/src/sandbox/drivers/sqldb.ts
3675
3676
  const CreateConfig$2 = Schema.Struct({
@@ -3687,7 +3688,7 @@ const make$14 = () => {
3687
3688
  const memory = /* @__PURE__ */ new Map();
3688
3689
  const runtimeLocation = (input) => input.runtimeConfig.location ?? Option.getOrUndefined(input.providerResourceId);
3689
3690
  const persistentTransport = (location) => {
3690
- const backend = layer$16({ location });
3691
+ const backend = layer$17({ location });
3691
3692
  return transportLayer(Layer.effectContext(Layer.build(backend).pipe(Effect.mapError((cause) => (0, errors_exports.providerError)({
3692
3693
  driver: name,
3693
3694
  operation: "attach",
@@ -4473,7 +4474,7 @@ var AppProcessError = class extends Schema.TaggedError()("AppProcessError", {
4473
4474
  cause: Schema.optional(Schema.Defect())
4474
4475
  }) {};
4475
4476
  var Service$12 = class extends Context.Service()("@codeworksh/harness/git/git/Service") {};
4476
- const layer$15 = Layer.effect(Service$12, Effect.gen(function* () {
4477
+ const layer$16 = Layer.effect(Service$12, Effect.gen(function* () {
4477
4478
  const shell = yield* io_exports.SandboxIO.Shell;
4478
4479
  const execute = (cwd) => (args, options) => shell.execArgv(["git", ...args], {
4479
4480
  cwd,
@@ -4697,7 +4698,7 @@ function parts(host, name) {
4697
4698
  if (!host || !pathname) return void 0;
4698
4699
  return `${host}/${pathname}`.toLowerCase();
4699
4700
  }
4700
- const layer$14 = Layer.effect(Service$11, Effect.gen(function* () {
4701
+ const layer$15 = Layer.effect(Service$11, Effect.gen(function* () {
4701
4702
  const fs = yield* io_exports.SandboxIO.FileSystem;
4702
4703
  const git = yield* Service$12;
4703
4704
  const find = Effect.fn("Repo.find")(function* (input) {
@@ -4754,7 +4755,7 @@ var WorktreeError = class extends Schema.TaggedError()("Worktree.WorktreeError",
4754
4755
  cause: Schema.optional(Schema.Defect())
4755
4756
  }) {};
4756
4757
  var Service$10 = class extends Context.Service()("@codeworksh/harness/worktree/worktree/Service") {};
4757
- const layer$13 = Layer.effect(Service$10, Effect.gen(function* () {
4758
+ const layer$14 = Layer.effect(Service$10, Effect.gen(function* () {
4758
4759
  const fs = yield* io_exports.SandboxIO.FileSystem;
4759
4760
  const git = yield* Service$12;
4760
4761
  const worktree = Effect.fnUntraced(function* (operation, cwd, args, directory) {
@@ -4809,7 +4810,7 @@ const provisional = (env, location) => schema_exports$5.ProjectSchema.ID.make(fa
4809
4810
  location
4810
4811
  ])));
4811
4812
  var Service$9 = class extends Context.Service()("@codeworksh/harness/project/project/Service") {};
4812
- const layer$12 = Layer.effect(Service$9, Effect.gen(function* () {
4813
+ const layer$13 = Layer.effect(Service$9, Effect.gen(function* () {
4813
4814
  const sql = yield* SqlClient.SqlClient;
4814
4815
  const fs = yield* io_exports.SandboxIO.FileSystem;
4815
4816
  const repos = yield* Service$11;
@@ -4943,7 +4944,7 @@ const layer$12 = Layer.effect(Service$9, Effect.gen(function* () {
4943
4944
  }));
4944
4945
  //#endregion
4945
4946
  //#region ../harness/src/location/location.ts
4946
- const layer$11 = (ref = {}) => Layer.effect(Service, Effect.gen(function* () {
4947
+ const layer$12 = (ref = {}) => Layer.effect(Service, Effect.gen(function* () {
4947
4948
  const instance = yield* io_exports.SandboxIO.Current;
4948
4949
  const directory = schema_exports$8.AbsolutePath.make(ref.directory ?? instance.cwd);
4949
4950
  const resolved = yield* (yield* Service$9).resolveOrCreate(directory);
@@ -4958,7 +4959,7 @@ const layer$11 = (ref = {}) => Layer.effect(Service, Effect.gen(function* () {
4958
4959
  * folded in. What remains is exactly the mount and the database — for callers
4959
4960
  * that already hold both, such as the runner and `Session.create`.
4960
4961
  */
4961
- const layerMounted = (ref = {}) => layer$11(ref).pipe(Layer.provide(layer$12.pipe(Layer.provide(Layer.mergeAll(layer$14, layer$13, layer$24)), Layer.provide(layer$15))));
4962
+ const layerMounted = (ref = {}) => layer$12(ref).pipe(Layer.provide(layer$13.pipe(Layer.provide(Layer.mergeAll(layer$15, layer$14, layer$25)), Layer.provide(layer$16))));
4962
4963
  //#endregion
4963
4964
  //#region ../harness/src/runner/run.ts
4964
4965
  var SandboxDirectoryNotFoundError = class extends Schema.TaggedError()("Runner.SandboxDirectoryNotFoundError", {
@@ -5292,7 +5293,7 @@ var PromptConflictError = class extends Schema.TaggedError()("PromptConflictErro
5292
5293
  messageId: Schema.String
5293
5294
  }) {};
5294
5295
  var Service$5 = class extends Context.Service()("@codeworksh/harness/control/Service") {};
5295
- const layer$10 = Layer.effect(Service$5, Effect.gen(function* () {
5296
+ const layer$11 = Layer.effect(Service$5, Effect.gen(function* () {
5296
5297
  const sessions = yield* Service$14;
5297
5298
  const inputs = yield* make$9;
5298
5299
  const execution = yield* Service$6;
@@ -5400,7 +5401,7 @@ const flatten = Effect.fn("EventRegistry.flatten")(function* (plugins) {
5400
5401
  }
5401
5402
  return [...Definitions, ...definitions];
5402
5403
  });
5403
- const layer$9 = (definitions = Definitions) => Layer.sync(Service$4, () => {
5404
+ const layer$10 = (definitions = Definitions) => Layer.sync(Service$4, () => {
5404
5405
  let current = definitions;
5405
5406
  let latest = schema_exports$6.EventSchema.latest(current);
5406
5407
  const replace = Effect.fn("EventRegistry.replace")(function* (plugins) {
@@ -5417,2100 +5418,2211 @@ const layer$9 = (definitions = Definitions) => Layer.sync(Service$4, () => {
5417
5418
  });
5418
5419
  });
5419
5420
  //#endregion
5420
- //#region ../harness/src/settings/merge.ts
5421
+ //#region ../harness/src/model/catalog.ts
5422
+ const filename = "models.gen.json";
5423
+ /** How old the catalog may get before a check downloads a new one. */
5424
+ const ttlConfig = Config.Duration("CODEWORK_MODELS_TTL").pipe(Config.withDefault(Duration.minutes(15)));
5425
+ /** How often a server looks at the file. A look is a `stat`; only a stale file costs a download. */
5426
+ const checkEvery = Duration.minutes(1);
5427
+ var RefreshError = class extends Schema.TaggedError()("ModelCatalog.RefreshError", {
5428
+ path: Schema.String,
5429
+ detail: Schema.String
5430
+ }) {
5431
+ get message() {
5432
+ return `failed to refresh the model catalog at ${this.path}: ${this.detail}`;
5433
+ }
5434
+ };
5435
+ /** Point aikit at `<home>/models.gen.json`, which `CODEWORK_MODELS_FILE` overrides. */
5436
+ const use = (home) => {
5437
+ const own = hostPath.resolve(home, filename);
5438
+ Model.configureCatalog(own);
5439
+ const path = Model.catalogPath();
5440
+ return {
5441
+ path,
5442
+ pinned: path !== own
5443
+ };
5444
+ };
5445
+ const modified = (path) => fileSystem.stat(path).pipe(Effect.map((info) => Option.map(info.mtime, (date) => date.getTime())), Effect.orElseSucceed(() => Option.none()));
5446
+ const stale = Effect.fn("ModelCatalog.stale")(function* (path) {
5447
+ const mtime = yield* modified(path);
5448
+ if (Option.isNone(mtime)) return true;
5449
+ const ttl = yield* ttlConfig;
5450
+ return (yield* Clock.currentTimeMillis) - mtime.value >= Duration.toMillis(ttl);
5451
+ });
5452
+ const download$1 = (path) => Effect.tryPromise({
5453
+ try: () => generateModels({ path }),
5454
+ catch: (cause) => new RefreshError({
5455
+ path,
5456
+ detail: cause instanceof Error ? cause.message : String(cause)
5457
+ })
5458
+ });
5459
+ /** Download a new catalog for the home when it is missing, older than the TTL, or `force` asks. */
5460
+ const refresh$1 = Effect.fn("ModelCatalog.refresh")(function* (home, options = {}) {
5461
+ const { path } = use(home);
5462
+ if (options.force !== true && !(yield* stale(path))) return {
5463
+ path,
5464
+ refreshed: false
5465
+ };
5466
+ yield* download$1(path);
5467
+ return {
5468
+ path,
5469
+ refreshed: true
5470
+ };
5471
+ });
5421
5472
  /**
5422
- * A plain string-keyed record: what a settings patch merges, and what a plugin's `options` block
5423
- * has to be. A Map, a typed array or a class instance is an object to `typeof` and not one of
5424
- * these — `Object.freeze` alone does not tell them apart, and it throws on some of them.
5473
+ * The automatic check: refresh a stale catalog unless it is pinned, and never fail. A failed
5474
+ * download leaves the old file in place; a missing one surfaces later as a
5475
+ * `Runner.ModelCatalogError` from whatever needed it.
5425
5476
  */
5426
- const isRecord$1 = (value) => Predicate.isObject(value) && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
5427
- const copy = (value) => {
5428
- if (Array.isArray(value)) return value.map(copy);
5429
- return isRecord$1(value) ? merge({}, value) : value;
5477
+ const check$1 = Effect.fn("ModelCatalog.check")(function* ({ path, pinned }) {
5478
+ if (pinned || !(yield* stale(path))) return;
5479
+ yield* download$1(path);
5480
+ });
5481
+ const quietly = (effect) => effect.pipe(Effect.asVoid, Effect.catch((error) => Effect.logWarning(`model catalog: ${error.message}`)));
5482
+ /** Check the home's catalog once, for a command that runs and exits; returns the file aikit reads. */
5483
+ const sync = Effect.fn("ModelCatalog.sync")(function* (home) {
5484
+ const target = use(home);
5485
+ yield* quietly(check$1(target));
5486
+ return target.path;
5487
+ });
5488
+ /**
5489
+ * Check at boot, and with `watch`, keep checking for the life of the layer. A file whose mtime
5490
+ * moved -- refreshed here, by `codework models refresh`, or by hand -- is reloaded, so the
5491
+ * process never serves a catalog older than what is on disk.
5492
+ */
5493
+ const layer$9 = (options) => Layer.effectDiscard(Effect.gen(function* () {
5494
+ const target = use(options.home);
5495
+ yield* quietly(check$1(target));
5496
+ if (!options.watch) return;
5497
+ const current = modified(target.path).pipe(Effect.map(Option.getOrUndefined));
5498
+ const seen = yield* Ref.make(yield* current);
5499
+ const tick = Effect.gen(function* () {
5500
+ yield* quietly(check$1(target));
5501
+ const mtime = yield* current;
5502
+ if (mtime !== (yield* Ref.getAndSet(seen, mtime))) Model.reloadCatalog();
5503
+ });
5504
+ yield* Effect.forkScoped(tick.pipe(Effect.repeat(Schedule.spaced(checkEvery))));
5505
+ }));
5506
+ /** Translate aikit's catalog failure, or undefined for anything else. */
5507
+ const loadError = (cause) => {
5508
+ if (typeof cause !== "object" || cause === null || !("name" in cause) || cause.name !== "ModelCatalogLoadError") return;
5509
+ if (!("data" in cause) || typeof cause.data !== "object" || cause.data === null) return void 0;
5510
+ const data = cause.data;
5511
+ if (!("path" in data) || typeof data.path !== "string" || !("message" in data) || typeof data.message !== "string" || !("reason" in data) || data.reason !== "missing" && data.reason !== "unreadable" && data.reason !== "empty" && data.reason !== "invalid") return;
5512
+ return new ModelCatalogError({
5513
+ path: data.path,
5514
+ reason: data.reason,
5515
+ detail: data.message
5516
+ });
5430
5517
  };
5431
- /** Null/undefined properties are absent, objects merge, and arrays replace. Never mutates inputs. */
5432
- function merge(base, ...patches) {
5433
- const result = /* @__PURE__ */ new Map();
5434
- for (const patch of [base, ...patches]) {
5435
- if (patch === void 0) continue;
5436
- for (const [key, value] of Object.entries(patch)) {
5437
- if (value === null || value === void 0) continue;
5438
- const previous = result.get(key);
5439
- result.set(key, isRecord$1(previous) && isRecord$1(value) ? merge(previous, value) : copy(value));
5440
- }
5441
- }
5442
- return Object.fromEntries(result);
5443
- }
5444
- /** Normalize nullable properties before schema validation, without changing array positions. */
5445
- const normalize$1 = copy;
5518
+ const catalogError = (cause) => loadError(cause) ?? new ModelCatalogError({
5519
+ path: Model.catalogPath(),
5520
+ reason: "unreadable",
5521
+ detail: cause instanceof Error ? cause.message : "failed to load the model catalog"
5522
+ });
5523
+ const models = Effect.tryPromise({
5524
+ try: () => Model.getBuiltInModels(),
5525
+ catch: catalogError
5526
+ });
5527
+ const providers = Effect.tryPromise({
5528
+ try: () => Model.getProviders(),
5529
+ catch: catalogError
5530
+ });
5446
5531
  //#endregion
5447
- //#region ../harness/src/util/module.ts
5448
- /*!
5449
- * Adapted from OpenCode packages/util/src/runtime/import.node.ts.
5450
- *
5451
- * MIT License
5452
- *
5453
- * Copyright (c) 2025 opencode
5454
- *
5455
- * Permission is hereby granted, free of charge, to any person obtaining a copy
5456
- * of this software and associated documentation files (the "Software"), to deal
5457
- * in the Software without restriction, including without limitation the rights
5458
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
5459
- * copies of the Software, and to permit persons to whom the Software is
5460
- * furnished to do so, subject to the following conditions:
5461
- *
5462
- * The above copyright notice and this permission notice shall be included in all
5463
- * copies or substantial portions of the Software.
5464
- *
5465
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
5466
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
5467
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
5468
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
5469
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
5470
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
5471
- * SOFTWARE.
5472
- */
5532
+ //#region ../harness/src/plugin/plugin.ts
5533
+ var plugin_exports = /* @__PURE__ */ __exportAll({});
5534
+ import * as import__codeworksh_plugin_plugin from "@codeworksh/plugin/plugin";
5535
+ __reExport(plugin_exports, import__codeworksh_plugin_plugin);
5536
+ //#endregion
5537
+ //#region ../harness/src/plugin/builtin/prompt/default.ts
5538
+ /** The default coding-agent foundation, used unless `promptCustom` replaces it. */
5539
+ const foundation = `You are an expert coding assistant operating inside codework, a coding agent harness.`;
5473
5540
  /**
5474
- * Packages a plugin must share with the harness instead of using the copy installed beside it.
5475
- *
5476
- * A plugin is installed into the store as its own npm tree, so `effect` and the plugin SDK land
5477
- * there a second time. Two copies of the *same version* are still two module instances, and
5478
- * Effect Schema's checks do not survive the crossing: a schema built by one copy and evaluated by
5479
- * the other reports every check as failed, so a tool whose success type is `Schema.Finite` dies
5480
- * on "Expected a finite number" for `22800`. Nothing about that is visible at registration --
5481
- * `Schema.isSchema` passes, the JSON Schema is derived correctly, and the tool reaches the model
5482
- * -- so it surfaces on the first call rather than at boot.
5541
+ * Guidelines that hold regardless of which tools are registered.
5483
5542
  *
5484
- * Resolution is the right place to fix it. A symlink into the store cannot be: a generation is
5485
- * immutable and shared by every project on the machine, while the harness doing the loading is
5486
- * whichever one is running now, and two applications embedding the harness need not agree on
5487
- * where their `effect` lives.
5543
+ * Kept short on purpose. Every line here is spent on every request, so a line
5544
+ * earns its place only if a model measurably behaves worse without it.
5488
5545
  */
5489
- const sharedPackages = /* @__PURE__ */ new Set(["effect", "@codeworksh/plugin"]);
5490
- /** The package a bare specifier names: `effect/Schema` -> `effect`, `@a/b/c` -> `@a/b`. */
5491
- function packageOf(specifier) {
5492
- if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.includes(":")) return "";
5493
- const segments = specifier.split("/");
5494
- return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0] ?? "";
5495
- }
5546
+ const standingGuidelines = [
5547
+ "Be concise. Report what you did and what you found, not what you are about to do.",
5548
+ "Quote exact paths and command output rather than paraphrasing them.",
5549
+ "If a command fails, read the error before retrying."
5550
+ ];
5551
+ /** Collapse whitespace so two spellings of one guideline dedupe against each other. */
5552
+ const normalize$1 = (value) => value.trim().replace(/\s+/g, " ");
5496
5553
  /**
5497
- * Resolve the shared packages as though this module had imported them, for as long as `run`
5498
- * takes. Anything else resolves exactly as it would have.
5554
+ * Tool-contributed guidelines followed by the standing ones, normalized and
5555
+ * deduplicated with first occurrence winning.
5556
+ *
5557
+ * Tool order is bucket order, which is the same order the index renders in and the
5558
+ * same order the provider receives definitions in -- one ordering, not three.
5499
5559
  */
5500
- function withSharedModules(run) {
5501
- const hook = registerHooks({ resolve(specifier, context, nextResolve) {
5502
- return sharedPackages.has(packageOf(specifier)) ? nextResolve(specifier, {
5503
- ...context,
5504
- parentURL: import.meta.url
5505
- }) : nextResolve(specifier, context);
5506
- } });
5507
- return run().finally(() => hook.deregister());
5508
- }
5509
- /** Use Node's main loader even when the caller runs inside a bundler or test VM. */
5510
- async function importModule(specifier) {
5511
- const imported = await withSharedModules(() => new Script(`import(${JSON.stringify(specifier)})`, { importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER }).runInThisContext());
5512
- if (typeof imported !== "object" || imported === null) return imported;
5513
- const module = imported;
5514
- const exports = module["module.exports"];
5515
- if (exports !== module.default || typeof exports !== "object" && typeof exports !== "function" || exports === null) return imported;
5516
- return Object.assign({}, module, exports);
5517
- }
5518
- /** Node resolution scoped synchronously to the caller. */
5519
- function resolveModule(specifier, directory, conditions) {
5520
- const hook = registerHooks({ resolve(target, context, nextResolve) {
5521
- return nextResolve(target, {
5522
- ...context,
5523
- parentURL: pathToFileURL(path.join(directory, "package.json")).href,
5524
- ...conditions === void 0 ? {} : { conditions: [.../* @__PURE__ */ new Set([...context.conditions, ...conditions])] }
5525
- });
5526
- } });
5527
- try {
5528
- const resolve = (target) => {
5529
- const resolved = import.meta.resolve(path.isAbsolute(target) ? pathToFileURL(target).href : target);
5530
- if (resolved.startsWith("file:")) statSync(new URL(resolved));
5531
- return resolved;
5532
- };
5533
- try {
5534
- return resolve(specifier);
5535
- } catch (error) {
5536
- if (path.extname(specifier) || !missing(error)) throw error;
5537
- for (const extension of [
5538
- ".ts",
5539
- ".tsx",
5540
- ".js",
5541
- ".jsx",
5542
- ".mts",
5543
- ".mjs",
5544
- ".cts",
5545
- ".cjs"
5546
- ]) try {
5547
- return resolve(specifier + extension);
5548
- } catch (cause) {
5549
- if (!missing(cause)) throw cause;
5550
- }
5551
- throw error;
5552
- }
5553
- } finally {
5554
- hook.deregister();
5560
+ const guidelines = (tools) => {
5561
+ const seen = /* @__PURE__ */ new Set();
5562
+ const collected = [];
5563
+ for (const line of [...tools.flatMap((tool) => tool.promptGuidelines ?? []), ...standingGuidelines]) {
5564
+ const normalized = normalize$1(line);
5565
+ if (normalized.length === 0 || seen.has(normalized)) continue;
5566
+ seen.add(normalized);
5567
+ collected.push(normalized);
5555
5568
  }
5556
- }
5557
- function missing(error) {
5558
- return error instanceof Error && "code" in error && [
5559
- "ENOENT",
5560
- "ENOTDIR",
5561
- "ERR_MODULE_NOT_FOUND"
5562
- ].includes(String(error.code));
5563
- }
5564
- //#endregion
5565
- //#region ../harness/src/plugin/error.ts
5569
+ return collected;
5570
+ };
5566
5571
  /**
5567
- * The reference as the person wrote it -- the string they will recognise in their settings file,
5568
- * never the resolved path or the canonical spec. Every plugin failure carries one.
5572
+ * The rendered tool index.
5573
+ *
5574
+ * `(none)` rather than an omitted section: a model told it has no tools behaves
5575
+ * better than one left to infer it from silence.
5569
5576
  */
5570
- const Reference = Schema.String;
5577
+ const toolIndex = (tools) => {
5578
+ const listed = tools.filter((tool) => tool.promptSnippet !== void 0 && tool.promptSnippet.length > 0);
5579
+ if (listed.length === 0) return "(none)";
5580
+ return listed.map((tool) => `- ${tool.name}: ${tool.promptSnippet}`).join("\n");
5581
+ };
5582
+ const assemble = (input) => {
5583
+ const sections = [input.promptCustom ?? foundation];
5584
+ sections.push(`Available tools:\n${toolIndex(input.tools)}`);
5585
+ const lines = guidelines(input.tools);
5586
+ if (lines.length > 0) sections.push(`Guidelines:\n${lines.map((line) => `- ${line}`).join("\n")}`);
5587
+ const append = input.promptSystemAppend?.trim();
5588
+ if (append !== void 0 && append.length > 0) sections.push(append);
5589
+ sections.push(`Current working directory: ${input.directory}`);
5590
+ return sections.join("\n\n");
5591
+ };
5571
5592
  /**
5572
- * The settings file that declared the entry, when one did.
5573
- *
5574
- * Absent for an embedder's own list and for anything the CLI was handed directly -- in both cases
5575
- * the caller already knows where the string came from. It is the settings path that needs saying,
5576
- * because several layers accumulate entries and "which file says this" is the question.
5593
+ * A caller slot, awaited lazily. A throw or rejection fails the snapshot, which the
5594
+ * host attributes to this plugin's id.
5577
5595
  */
5578
- const File$1 = Schema.optional(Schema.String);
5596
+ const slot = (ctx, resolver) => Effect.tryPromise(() => Promise.resolve(resolver?.(ctx)));
5597
+ const defaultPromptPlugin = (0, plugin_exports.define)({
5598
+ id: "codework.prompt.default",
5599
+ kind: "prompt",
5600
+ setup: Effect.fn("DefaultPromptPlugin.setup")(function* (ctx) {
5601
+ const custom = yield* slot(ctx, ctx.config.promptCustom);
5602
+ const append = yield* slot(ctx, ctx.config.promptSystemAppend);
5603
+ ctx.plugin.prompt.set(assemble({
5604
+ tools: ctx.plugin.tools.list(),
5605
+ directory: ctx.location.directory,
5606
+ ...custom === void 0 ? {} : { promptCustom: custom },
5607
+ ...append === void 0 ? {} : { promptSystemAppend: append }
5608
+ }));
5609
+ })
5610
+ });
5611
+ //#endregion
5612
+ //#region ../harness/src/tool/truncate.ts
5579
5613
  /**
5580
- * A spec that cannot be installed at all.
5614
+ * Shared truncation utilities for tool outputs.
5581
5615
  *
5582
- * `plugin-not-found` is a local path with nothing at it; `plugin-not-installed` (the store's) is a
5583
- * package spec with no entry. The first is a mistake in a settings file, the second is fixed by
5584
- * `plugin install`, and keeping them apart is most of the value of having slugs.
5585
- */
5586
- var SourceError = class extends Schema.TaggedError()("PluginSourceError", {
5587
- reason: Schema.Literals([
5588
- "plugin-unsupported-source",
5589
- "plugin-not-found",
5590
- "plugin-escapes-root"
5591
- ]),
5592
- reference: Reference,
5593
- file: File$1,
5594
- message: Schema.String
5595
- }) {};
5596
- /** A spec that is installable, and whose install did not produce a usable package. */
5597
- var InstallError = class extends Schema.TaggedError()("PluginInstallError", {
5598
- reason: Schema.Literals([
5599
- "plugin-resolve-failed",
5600
- "plugin-fetch-failed",
5601
- "plugin-no-commit",
5602
- "plugin-no-entrypoint"
5603
- ]),
5604
- reference: Reference,
5605
- file: File$1,
5606
- message: Schema.String,
5607
- cause: Schema.optional(Schema.Defect())
5608
- }) {};
5609
- /**
5610
- * A module that reached the import and did not come back as a plugin.
5616
+ * Truncation is based on two independent limits — whichever is hit first
5617
+ * wins: a line limit and a byte limit. Lines are never split (except the bash
5618
+ * tail edge case where a single line exceeds the byte limit).
5611
5619
  *
5612
- * Distinct from {@link InstallError}: the bytes are here and are fine as *bytes*. What failed is
5613
- * evaluating them, or what they turned out to export -- which is the distinction a caller acts on,
5614
- * because one is fixed by installing and the other by editing code.
5620
+ * The bash tool uses {@link truncateTail} (keep the end — errors / final
5621
+ * results); file readers use {@link truncateHead} (keep the beginning).
5615
5622
  */
5616
- var LoadError = class extends Schema.TaggedError()("PluginLoadError", {
5617
- reason: Schema.Literals([
5618
- "plugin-import-failed",
5619
- "plugin-missing-dependency",
5620
- "plugin-not-compiled",
5621
- "plugin-invalid-definition"
5622
- ]),
5623
- reference: Reference,
5624
- file: File$1,
5625
- message: Schema.String,
5626
- /** The ID the module declared, when it got far enough to declare one. */
5627
- id: Schema.optional(Schema.String),
5628
- cause: Schema.optional(Schema.Defect())
5629
- }) {};
5630
- /** The store's own failures: what is filed, what is not, and what could not be claimed. */
5631
- var StoreError = class extends Schema.TaggedError()("PluginStoreError", {
5632
- reason: Schema.Literals([
5633
- "plugin-not-installed",
5634
- "plugin-lock-timeout",
5635
- "plugin-marker-invalid",
5636
- "plugin-index-invalid",
5637
- "plugin-collect-failed"
5638
- ]),
5639
- reference: Reference,
5640
- file: File$1,
5641
- message: Schema.String,
5642
- cause: Schema.optional(Schema.Defect())
5643
- }) {};
5623
+ const DEFAULT_MAX_LINES = 2e3;
5624
+ const DEFAULT_MAX_BYTES = 51200;
5625
+ function splitLinesForCounting(content) {
5626
+ if (content.length === 0) return [];
5627
+ const lines = content.split("\n");
5628
+ if (content.endsWith("\n")) lines.pop();
5629
+ return lines;
5630
+ }
5631
+ /** Format bytes as a human-readable size. */
5632
+ function formatSize(bytes) {
5633
+ if (bytes < 1024) return `${bytes}B`;
5634
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`;
5635
+ return `${(bytes / 1048576).toFixed(1)}MB`;
5636
+ }
5644
5637
  /**
5645
- * Note which settings file declared the entry a failure came from.
5646
- *
5647
- * The store and the installer raise their own failures without knowing that, because neither is
5648
- * told: they are handed a spec, not a settings layer. The loader knows, and this is where it says
5649
- * so -- leaving an existing answer alone, since a more specific one is already better.
5638
+ * Truncate content from the tail (keep the last N lines/bytes). Suitable for
5639
+ * bash output where the end matters (errors, final results). May return a
5640
+ * partial first line if the last line of the original content exceeds the byte
5641
+ * limit.
5650
5642
  */
5651
- const declaredIn = (error, file) => {
5652
- if (error.file !== void 0) return error;
5653
- const shared = {
5654
- reference: error.reference,
5655
- message: error.message,
5656
- file
5643
+ function truncateTail(content, options = {}) {
5644
+ const maxLines = options.maxLines ?? 2e3;
5645
+ const maxBytes = options.maxBytes ?? 51200;
5646
+ const totalBytes = Buffer.byteLength(content, "utf-8");
5647
+ const lines = splitLinesForCounting(content);
5648
+ const totalLines = lines.length;
5649
+ if (totalLines <= maxLines && totalBytes <= maxBytes) return {
5650
+ content,
5651
+ truncated: false,
5652
+ truncatedBy: null,
5653
+ totalLines,
5654
+ totalBytes,
5655
+ outputLines: totalLines,
5656
+ outputBytes: totalBytes,
5657
+ lastLinePartial: false,
5658
+ firstLineExceedsLimit: false,
5659
+ maxLines,
5660
+ maxBytes
5657
5661
  };
5658
- if (Schema.is(SourceError)(error)) return new SourceError({
5659
- ...shared,
5660
- reason: error.reason
5661
- });
5662
- if (Schema.is(InstallError)(error)) return new InstallError({
5663
- ...shared,
5664
- reason: error.reason,
5665
- cause: error.cause
5666
- });
5667
- if (Schema.is(LoadError)(error)) return new LoadError({
5668
- ...shared,
5669
- reason: error.reason,
5670
- id: error.id,
5671
- cause: error.cause
5672
- });
5673
- if (Schema.is(StoreError)(error)) return new StoreError({
5674
- ...shared,
5675
- reason: error.reason,
5676
- cause: error.cause
5677
- });
5678
- return error;
5679
- };
5680
- //#endregion
5681
- //#region ../harness/src/util/path.ts
5662
+ const outputLinesArr = [];
5663
+ let outputBytesCount = 0;
5664
+ let truncatedBy = "lines";
5665
+ let lastLinePartial = false;
5666
+ for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
5667
+ const line = lines[i] ?? "";
5668
+ const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0);
5669
+ if (outputBytesCount + lineBytes > maxBytes) {
5670
+ truncatedBy = "bytes";
5671
+ if (outputLinesArr.length === 0) {
5672
+ const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
5673
+ outputLinesArr.unshift(truncatedLine);
5674
+ outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
5675
+ lastLinePartial = true;
5676
+ }
5677
+ break;
5678
+ }
5679
+ outputLinesArr.unshift(line);
5680
+ outputBytesCount += lineBytes;
5681
+ }
5682
+ if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) truncatedBy = "lines";
5683
+ const outputContent = outputLinesArr.join("\n");
5684
+ const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
5685
+ return {
5686
+ content: outputContent,
5687
+ truncated: true,
5688
+ truncatedBy,
5689
+ totalLines,
5690
+ totalBytes,
5691
+ outputLines: outputLinesArr.length,
5692
+ outputBytes: finalOutputBytes,
5693
+ lastLinePartial,
5694
+ firstLineExceedsLimit: false,
5695
+ maxLines,
5696
+ maxBytes
5697
+ };
5698
+ }
5682
5699
  /**
5683
- * A directory that crosses into the harness is absolute, and a relative one is a **defect** rather
5684
- * than an error.
5685
- *
5686
- * A relative path is a disguised `process.cwd()` call: and every consumer
5687
- * resolves it silently. `path.resolve("x")` anchors to the process directory, npm answers a
5688
- * relative `cwd` with a `localPrefix` of whatever repository the process is inside (measured, not
5689
- * assumed), and `Session.link` would write the *server's* directory into a field whose whole
5690
- * purpose is to be a host path the client declared.
5691
- *
5692
- * For a long-running server the process directory is nobody's project, so the result is a
5693
- * plausible answer to a question no one asked.
5694
- * An absolute path is the one spelling none of them can quietly reinterpret.
5695
- *
5696
- * A defect rather than a typed error because nothing a user types reaches here. Every reference
5697
- * goes through `parse`, and every directory comes from `Global.resolve`, a resolved `cwd` or a
5698
- * session's `hostDir`, all of them already absolute. A relative one means a new call site got it
5699
- * wrong, and a stack trace names that call site; a typed error would ask every caller to handle a
5700
- * case none of them can cause.
5700
+ * Truncate a string to fit within a byte limit (from the end). Handles
5701
+ * multi-byte UTF-8 characters correctly (never splits a codepoint).
5701
5702
  */
5702
- const rooted = (directory, role) => {
5703
- if (hostPath.isAbsolute(directory)) return directory;
5704
- throw new Error(`${role} must be an absolute path, got ${JSON.stringify(directory)}`);
5705
- };
5703
+ function truncateStringToBytesFromEnd(str, maxBytes) {
5704
+ const buf = Buffer.from(str, "utf-8");
5705
+ if (buf.length <= maxBytes) return str;
5706
+ let start = buf.length - maxBytes;
5707
+ while (start < buf.length && (buf[start] & 192) === 128) start++;
5708
+ return buf.subarray(start).toString("utf-8");
5709
+ }
5706
5710
  //#endregion
5707
- //#region ../harness/src/plugin/npm.ts
5708
- const Manifest$1 = Schema.Struct({ version: Schema.optional(Schema.String) });
5709
- const Lockfile = Schema.Struct({ packages: Schema.optional(Schema.Record(Schema.String, Schema.Struct({ resolved: Schema.optional(Schema.String) }))) });
5710
- /**
5711
- * npm's flattened config: registry, scopes, auth, and cache paths.
5712
- *
5713
- * Built in one place, because two call sites deriving it separately is how the cache location
5714
- * drifts between an install and the staleness check that is supposed to describe it.
5715
- *
5716
- * `dir` is where the `.npmrc` chain is read from, and it is the **host directory the caller
5717
- * belongs to** -- never the staging directory and never the cache. A company repo keeps its
5718
- * private registry and its scoped token in its own `.npmrc`, so a chain read anywhere else
5719
- * resolves the plugin against the public registry and fails 404, or worse, installs a public
5720
- * package of the same name. This is the registry half of auth; the git half is below.
5721
- *
5722
- * No `--prefix`. npm pins its local prefix to a `--prefix` on `argv` and stops walking, so
5723
- * passing one would look up exactly `<dir>/.npmrc` and nothing above it -- wrong for a person
5724
- * standing in `packages/x` whose token lives at the repository root. Without it npm walks up from
5725
- * `cwd` to the nearest `package.json` or `node_modules` exactly as `npm` itself would if it were
5726
- * run there, which is the only rule a user can predict.
5727
- *
5728
- * `--cache` is ours and is passed on `argv`, which outranks every `.npmrc`. npm's `flatten`
5729
- * derives `_cacache`, `_npx` and `_tuf` from that one value, so naming the parent is enough --
5730
- * and overriding `flat.cache` afterwards would redirect one of the three and leave `_tuf` pointing
5731
- * at the user's home. `--home` exists to make a run self-contained; anything reached by an ambient
5732
- * default is a test reading the developer's real machine.
5733
- *
5734
- * **Nothing here names an `env`.** npm hands git `opts.env` when one is present and falls back to
5735
- * `{ ...gitDefaults, ...process.env }` when it is not, so leaving it out is what lets a git source
5736
- * reach a private remote at all: `SSH_AUTH_SOCK` finds the agent, `HOME` finds the `~/.gitconfig`
5737
- * that names the credential helper, and `PATH` finds `git`. Setting `env` to anything -- even to
5738
- * a copy of `process.env` -- would be a standing invitation to prune it later and break private
5739
- * git silently, months from the change. §15 Q2.
5740
- */
5741
- const options = (dir, cache) => Effect.suspend(() => {
5742
- const root = rooted(dir, "the directory a plugin install reads its .npmrc chain from");
5743
- const store = rooted(cache, "the npm cache");
5744
- return npmOptions(root, store);
5745
- });
5746
- const npmOptions = (dir, cache) => Effect.gen(function* () {
5747
- const config = yield* Effect.tryPromise(async () => {
5748
- const { default: Config } = await import("./lib-CyynPpjH.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
5749
- const { definitions, flatten, nerfDarts, shorthands } = (await import("./definitions-Dfwyyv_K.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1))).default;
5750
- const created = new Config({
5751
- npmPath: fileURLToPath(new URL("..", import.meta.url)),
5752
- cwd: dir,
5753
- env: { ...process.env },
5754
- argv: [
5755
- process.execPath,
5756
- process.execPath,
5757
- "--cache",
5758
- hostPath.join(cache, "npm")
5759
- ],
5760
- execPath: process.execPath,
5761
- platform: process.platform,
5762
- definitions,
5763
- flatten,
5764
- nerfDarts,
5765
- shorthands,
5766
- warn: false
5711
+ //#region ../harness/src/tool/accumulator.ts
5712
+ function defaultTempFilePath(prefix, suffix) {
5713
+ return posix.join(tmpdir(), `${prefix}-${suffix}.log`);
5714
+ }
5715
+ function byteLength(text) {
5716
+ return Buffer.byteLength(text, "utf-8");
5717
+ }
5718
+ var Accumulator = class {
5719
+ maxLines;
5720
+ maxBytes;
5721
+ maxRollingBytes;
5722
+ tempFilePrefix;
5723
+ decoder = new TextDecoder();
5724
+ rawChunks = [];
5725
+ tailText = "";
5726
+ tailBytes = 0;
5727
+ tailStartsAtLineBoundary = true;
5728
+ totalRawBytes = 0;
5729
+ totalDecodedBytes = 0;
5730
+ completedLines = 0;
5731
+ totalLines = 0;
5732
+ currentLineBytes = 0;
5733
+ hasOpenLine = false;
5734
+ finished = false;
5735
+ tempFilePath;
5736
+ tempFile;
5737
+ constructor(options = {}) {
5738
+ this.maxLines = options.maxLines ?? 2e3;
5739
+ this.maxBytes = options.maxBytes ?? 51200;
5740
+ this.maxRollingBytes = Math.max(this.maxBytes * 2, 1);
5741
+ this.tempFilePrefix = options.tempFilePrefix ?? "codework-output";
5742
+ }
5743
+ append(data) {
5744
+ return Effect.suspend(() => {
5745
+ if (this.finished) return Effect.die(/* @__PURE__ */ new Error("cannot append to a finished output accumulator"));
5746
+ this.totalRawBytes += data.length;
5747
+ this.appendDecodedText(this.decoder.decode(data, { stream: true }));
5748
+ if (this.tempFile !== void 0 || this.shouldUseTempFile()) return Effect.gen({ self: this }, function* () {
5749
+ yield* this.ensureTempFile();
5750
+ yield* this.tempFile.writeAll(data).pipe(Effect.orDie);
5751
+ });
5752
+ if (data.length > 0) this.rawChunks.push(data);
5753
+ return Effect.void;
5767
5754
  });
5768
- await created.load();
5769
- return created;
5770
- }).pipe(Effect.catch((cause) => Effect.logWarning(`cannot load npm configuration from ${dir}; npm defaults apply`, cause).pipe(Effect.as(void 0))));
5771
- if (config === void 0) return { allowGit: "root" };
5772
- for (const kind of [
5773
- "project",
5774
- "user",
5775
- "global"
5776
- ]) {
5777
- const loaded = config.data.get(kind);
5778
- if (loaded?.loadError !== void 0 && loaded.loadError.code !== "ENOENT") yield* Effect.logWarning(`cannot read npm configuration ${loaded.source ?? `for ${kind}`}; npm defaults apply`, loaded.loadError);
5779
5755
  }
5780
- return {
5781
- ...config.flat,
5782
- allowGit: "root"
5783
- };
5784
- });
5785
- const PUBLIC_REGISTRY = "https://registry.npmjs.org/";
5786
- /** Stable, credential-free registry identity used to address registry artifacts in the store. */
5787
- const registry = Effect.fn("PluginNpm.registry")(function* (target, cache, from) {
5788
- const flat = yield* options(from, cache);
5789
- const scope = target.name.startsWith("@") ? target.name.slice(0, target.name.indexOf("/")) : void 0;
5790
- const configured = (scope === void 0 ? void 0 : flat[`${scope}:registry`]) ?? flat.registry ?? PUBLIC_REGISTRY;
5791
- if (typeof configured !== "string") return yield* new InstallError({
5792
- reason: "plugin-resolve-failed",
5793
- reference: target.spec,
5794
- message: `npm registry for ${target.name} is not a URL`
5795
- });
5796
- return yield* Effect.try({
5797
- try: () => {
5798
- const value = new URL(configured);
5799
- value.username = "";
5800
- value.password = "";
5801
- value.hash = "";
5802
- return value.href;
5803
- },
5804
- catch: (cause) => new InstallError({
5805
- reason: "plugin-resolve-failed",
5806
- reference: target.spec,
5807
- message: `npm registry for ${target.name} is not a valid URL: ${configured}`,
5808
- cause
5809
- })
5810
- });
5811
- });
5812
- /** A 40- or 64-hex commit out of the `resolved` URL arborist writes into the lockfile. */
5813
- const commitIn = (resolved) => resolved?.match(/#([a-f0-9]{40}|[a-f0-9]{64})(?=::|$)/i)?.[1];
5756
+ finish() {
5757
+ return Effect.suspend(() => {
5758
+ if (this.finished) return Effect.void;
5759
+ this.finished = true;
5760
+ this.appendDecodedText(this.decoder.decode());
5761
+ return Effect.gen({ self: this }, function* () {
5762
+ if (this.shouldUseTempFile()) yield* this.ensureTempFile();
5763
+ if (this.tempFile !== void 0) yield* this.tempFile.sync.pipe(Effect.orDie);
5764
+ });
5765
+ });
5766
+ }
5767
+ snapshot() {
5768
+ const tailTruncation = truncateTail(this.getSnapshotText(), {
5769
+ maxLines: this.maxLines,
5770
+ maxBytes: this.maxBytes
5771
+ });
5772
+ const truncated = this.totalLines > this.maxLines || this.totalDecodedBytes > this.maxBytes;
5773
+ const truncatedBy = truncated ? tailTruncation.truncatedBy ?? (this.totalDecodedBytes > this.maxBytes ? "bytes" : "lines") : null;
5774
+ const truncation = {
5775
+ ...tailTruncation,
5776
+ truncated,
5777
+ truncatedBy,
5778
+ totalLines: this.totalLines,
5779
+ totalBytes: this.totalDecodedBytes,
5780
+ maxLines: this.maxLines,
5781
+ maxBytes: this.maxBytes
5782
+ };
5783
+ return {
5784
+ content: truncation.content,
5785
+ truncation,
5786
+ ...this.tempFilePath === void 0 ? {} : { fullOutputPath: this.tempFilePath }
5787
+ };
5788
+ }
5789
+ /** Bytes in the final (possibly unterminated) line — for the partial-line footer. */
5790
+ getLastLineBytes() {
5791
+ return this.currentLineBytes;
5792
+ }
5793
+ appendDecodedText(text) {
5794
+ if (text.length === 0) return;
5795
+ const bytes = byteLength(text);
5796
+ this.totalDecodedBytes += bytes;
5797
+ this.tailText += text;
5798
+ this.tailBytes += bytes;
5799
+ if (this.tailBytes > this.maxRollingBytes * 2) this.trimTail();
5800
+ let newlines = 0;
5801
+ let lastNewline = -1;
5802
+ for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) {
5803
+ newlines++;
5804
+ lastNewline = i;
5805
+ }
5806
+ if (newlines === 0) {
5807
+ this.currentLineBytes += bytes;
5808
+ this.hasOpenLine = true;
5809
+ } else {
5810
+ this.completedLines += newlines;
5811
+ const tail = text.slice(lastNewline + 1);
5812
+ this.currentLineBytes = byteLength(tail);
5813
+ this.hasOpenLine = tail.length > 0;
5814
+ }
5815
+ this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);
5816
+ }
5817
+ trimTail() {
5818
+ const buffer = Buffer.from(this.tailText, "utf-8");
5819
+ if (buffer.length <= this.maxRollingBytes) {
5820
+ this.tailBytes = buffer.length;
5821
+ return;
5822
+ }
5823
+ let start = buffer.length - this.maxRollingBytes;
5824
+ while (start < buffer.length && (buffer[start] & 192) === 128) start++;
5825
+ this.tailStartsAtLineBoundary = start === 0 ? this.tailStartsAtLineBoundary : buffer[start - 1] === 10;
5826
+ this.tailText = buffer.subarray(start).toString("utf-8");
5827
+ this.tailBytes = byteLength(this.tailText);
5828
+ }
5829
+ getSnapshotText() {
5830
+ if (this.tailStartsAtLineBoundary) return this.tailText;
5831
+ const firstNewline = this.tailText.indexOf("\n");
5832
+ return firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1);
5833
+ }
5834
+ shouldUseTempFile() {
5835
+ return this.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines;
5836
+ }
5837
+ ensureTempFile() {
5838
+ if (this.tempFile !== void 0) return Effect.void;
5839
+ return Effect.gen({ self: this }, function* () {
5840
+ const suffix = Encoding.encodeHex(yield* crypto.randomBytes(8));
5841
+ this.tempFilePath = defaultTempFilePath(this.tempFilePrefix, suffix);
5842
+ this.tempFile = yield* fileSystem.open(this.tempFilePath, { flag: "w" });
5843
+ for (const chunk of this.rawChunks) yield* this.tempFile.writeAll(chunk);
5844
+ this.rawChunks = [];
5845
+ }).pipe(Effect.orDie);
5846
+ }
5847
+ };
5848
+ //#endregion
5849
+ //#region ../harness/src/tool/progress.ts
5850
+ var progress_exports = /* @__PURE__ */ __exportAll({});
5851
+ import * as import__codeworksh_plugin_tool_progress from "@codeworksh/plugin/tool/progress";
5852
+ __reExport(progress_exports, import__codeworksh_plugin_tool_progress);
5853
+ //#endregion
5854
+ //#region ../harness/src/tool/shell.ts
5814
5855
  /**
5815
- * The commit a git install actually landed on, read back from the lockfile arborist wrote.
5856
+ * `ToolShell` — the tool-owned, cancellable command-execution capability the bash tool depends on.
5816
5857
  *
5817
- * Both spellings are tried because which one exists depends on whether the tree was saved.
5858
+ * It deliberately does **not** reuse `sandbox/Shell` directly: the local OS path
5859
+ * provides no `Shell` at all, and `sandbox/Shell` models neither cancellation
5860
+ * nor a timeout. `ToolShell` makes both part of the contract. Adapters honor the
5861
+ * mid-flight process kill where the SDK allows it (local, Vercel) and fall back
5862
+ * to the timeout where it does not (Daytona) — "kill where it's easy".
5863
+ *
5864
+ * Two modes, and Local has two variants:
5865
+ * - Local / full OS shell → {@link local} (default)
5866
+ * - Local / just-bash → {@link fromSandboxShell} over `EnvBash`
5867
+ * - Remote / provider → {@link fromSandboxShell} over `EnvDaytona` / `EnvVercel`
5818
5868
  */
5819
- const commit = Effect.fn("PluginNpm.commit")(function* (root, name) {
5820
- for (const file of [hostPath.join(root, "package-lock.json"), hostPath.join(root, "node_modules", ".package-lock.json")]) {
5821
- const parsed = yield* fileSystem.readFileString(file).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Lockfile))), Effect.option);
5822
- if (Option.isNone(parsed)) continue;
5823
- const found = commitIn(parsed.value.packages?.[`node_modules/${name}`]?.resolved);
5824
- if (found !== void 0) return found;
5825
- }
5826
- });
5827
- /** Node error codes that mean "nothing here", as opposed to "this package is broken". */
5828
- const notFound = /* @__PURE__ */ new Set([
5829
- "ENOENT",
5830
- "ENOTDIR",
5831
- "MODULE_NOT_FOUND",
5832
- "ERR_MODULE_NOT_FOUND",
5833
- "ERR_PACKAGE_PATH_NOT_EXPORTED",
5834
- "ERR_UNSUPPORTED_DIR_IMPORT"
5835
- ]);
5869
+ /** Infra/spawn failure — not model-actionable; handlers should treat it as a defect. */
5870
+ var ToolShellError = class extends Schema.TaggedError()("ToolShellError", {
5871
+ command: Schema.String,
5872
+ cause: Schema.optional(Schema.Defect())
5873
+ }) {};
5874
+ /** The command exceeded its deadline. Distinct so handlers can map it to a domain timeout. */
5875
+ var ToolShellTimeout = class extends Schema.TaggedError()("ToolShellTimeout", {
5876
+ command: Schema.String,
5877
+ timeoutMillis: Schema.Finite
5878
+ }) {};
5879
+ const widenExecError = (effect) => effect.pipe(Effect.mapError((error) => error));
5880
+ var ToolShell = class extends Context.Service()("@codeworksh/harness/tool/shell/ToolShell") {};
5836
5881
  /**
5837
- * The module to import out of an installed package.
5838
- *
5839
- * Ordered subpaths through the package's own `exports`, so a package that publishes a dedicated
5840
- * `./plugin` entry is honoured and one that does not falls back to its main.
5882
+ * Bridge the existing `sandbox/Shell` into a `ToolShell`. Used for just-bash
5883
+ * (in-process) and remote providers.
5841
5884
  *
5842
- * Only "not found" is tolerated. Swallowing every error would turn a package that is broken --
5843
- * a syntax error in its manifest, a permission problem -- into "no entrypoint", which is the
5844
- * wrong diagnosis in the one place a person needs the right one.
5885
+ * `cwd` and `env` both pass through to the backing `sandbox/Shell`, which
5886
+ * resolves a relative `cwd` against its mount. The buffered `exec`
5887
+ * timeout is enforced here with `Effect.timeoutOption` (interruption always
5888
+ * returns control; the underlying command is only truly killed mid-flight on
5889
+ * signal-aware backends, handled in the provider layer). When the backend exposes
5890
+ * `stream` (e.g. Vercel `Command.logs`), it is bridged to {@link IToolShell.stream}
5891
+ * so the bash tool streams over it too.
5845
5892
  */
5846
- const entrypoint = (directory, name) => Effect.gen(function* () {
5847
- for (const subpath of ["", "plugin"]) {
5848
- const specifier = name === void 0 ? hostPath.join(directory, subpath === "" ? "index" : subpath) : hostPath.join(name, subpath);
5849
- const resolved = yield* Effect.try(() => resolveModule(specifier, directory)).pipe(Effect.asSome, Effect.catchIf((error) => notFound.has(String(error.cause?.code)), () => Effect.succeed(Option.none())), Effect.mapError((error) => new InstallError({
5850
- reason: "plugin-no-entrypoint",
5851
- reference: name ?? directory,
5852
- message: `cannot resolve an entrypoint in ${directory}`,
5853
- cause: error
5893
+ const fromSandboxShell = Layer.effect(ToolShell, Effect.gen(function* () {
5894
+ const shell = yield* shell_exports.Shell;
5895
+ const exec = (command, options) => {
5896
+ const run = shell.exec(command, {
5897
+ ...options?.env ? { env: options.env } : {},
5898
+ ...options?.cwd ? { cwd: options.cwd } : {}
5899
+ }).pipe(Effect.mapError((cause) => new ToolShellError({
5900
+ command,
5901
+ cause
5902
+ })));
5903
+ if (options?.timeout === void 0) return widenExecError(run);
5904
+ const timeout = options.timeout;
5905
+ return run.pipe(Effect.timeoutOption(timeout), Effect.flatMap(Option.match({
5906
+ onNone: () => Effect.fail(new ToolShellTimeout({
5907
+ command,
5908
+ timeoutMillis: Duration.toMillis(timeout)
5909
+ })),
5910
+ onSome: Effect.succeed
5854
5911
  })));
5855
- if (Option.isSome(resolved)) return fileURLToPath(resolved.value);
5856
- }
5857
- return yield* new InstallError({
5858
- reason: "plugin-no-entrypoint",
5859
- reference: name ?? directory,
5860
- message: `no plugin entrypoint in ${directory}`
5861
- });
5862
- });
5863
- const reify = Effect.fn("PluginNpm.reify")(function* ({ target, into, cache, from }) {
5864
- const flat = yield* options(from, cache);
5865
- const { Arborist } = yield* Effect.promise(() => import("./lib-Dw7R2suc.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1)));
5866
- const settings = {
5867
- ...flat,
5868
- ...target.mutable ? {
5869
- preferOnline: true,
5870
- noGitRevCache: true
5871
- } : {},
5872
- audit: false
5873
5912
  };
5874
- const arborist = new Arborist({
5875
- ...settings,
5876
- path: into,
5877
- binLinks: true,
5878
- progress: false,
5879
- savePrefix: "",
5880
- ignoreScripts: true
5881
- });
5882
- return yield* Effect.tryPromise({
5883
- try: () => arborist.reify({
5884
- ...settings,
5885
- add: [target.spec],
5886
- update: target.mutable,
5887
- save: true,
5888
- saveType: "prod"
5889
- }),
5890
- catch: (cause) => new InstallError({
5891
- reason: "plugin-fetch-failed",
5892
- reference: target.spec,
5893
- message: `cannot install ${target.spec}`,
5894
- cause
5895
- })
5896
- });
5897
- });
5913
+ const sandboxStream = shell.stream;
5914
+ const stream = sandboxStream ? (command, options) => sandboxStream(command, {
5915
+ ...options?.env ? { env: options.env } : {},
5916
+ ...options?.cwd ? { cwd: options.cwd } : {}
5917
+ }).pipe(Stream.map((chunk) => chunk._tag === "exit" ? {
5918
+ _tag: "Exit",
5919
+ exitCode: chunk.exitCode
5920
+ } : {
5921
+ _tag: "Output",
5922
+ bytes: chunk.bytes
5923
+ }), Stream.mapError((cause) => new ToolShellError({
5924
+ command,
5925
+ cause
5926
+ }))) : void 0;
5927
+ return ToolShell.of(stream ? {
5928
+ exec,
5929
+ stream
5930
+ } : { exec });
5931
+ }));
5932
+ Duration.seconds(1);
5933
+ //#endregion
5934
+ //#region ../harness/src/tool/tool.ts
5935
+ var tool_exports = /* @__PURE__ */ __exportAll({});
5936
+ import * as import__codeworksh_plugin_tool from "@codeworksh/plugin/tool";
5937
+ __reExport(tool_exports, import__codeworksh_plugin_tool);
5938
+ //#endregion
5939
+ //#region ../harness/src/plugin/builtin/tool/bash.ts
5898
5940
  /**
5899
- * Fill `into` with the package `target` names, and report what landed there.
5941
+ * The self-contained bash plugin — a worked example for tool plugins. The definition is pure data;
5942
+ * the handler depends only on {@link ToolShell} / {@link ToolProgress}, never
5943
+ * `sandbox/Shell`, so the backend (local OS / just-bash / remote provider) is
5944
+ * swapped by changing the provided Layer with no change to the tool.
5900
5945
  *
5901
- * A local target downloads nothing: the files on disk are already the answer, and a local plugin
5902
- * is never copied into the store.
5946
+ * Two execution paths, picked by capability:
5947
+ * - **variant A (buffered)** otherwise: `ToolShell.exec` returns the full output,
5948
+ * truncated once after completion.
5949
+ * - **variant B (streaming)** when the backend offers `ToolShell.stream`: output
5950
+ * flows through an {@link Accumulator} (bounded memory + temp-file spill)
5951
+ * and is reported live via {@link ToolProgress}; on timeout the partial output
5952
+ * produced so far is preserved.
5953
+ *
5954
+ * Success and failure carry the *same* structured shape
5955
+ * (`output`, `exitCode`, `truncated`, `fullOutputPath`)
5956
+ * — a non-zero exit is just `exitCode !== 0`, not a different kind of result.
5903
5957
  */
5904
- const download = Effect.fn("PluginNpm.download")(function* (input) {
5905
- const { target, into, cache, from, runner = reify } = input;
5906
- if (target.kind === "local") return {
5907
- directory: target.path,
5908
- name: hostPath.basename(target.path),
5909
- entrypoint: yield* entrypoint(target.path)
5958
+ const BashParams = Schema.Struct({
5959
+ command: Schema.String.annotate({ description: "The bash command to execute." }),
5960
+ timeout: Schema.optional(Schema.Finite.check(Schema.isGreaterThan(0)).annotate({ description: "Optional timeout in seconds (must be greater than 0)." }))
5961
+ });
5962
+ const outputFields = {
5963
+ /** Combined stdout + stderr, truncated for display (full output is on disk when `truncated`). */
5964
+ output: Schema.String,
5965
+ truncated: Schema.Boolean,
5966
+ fullOutputPath: Schema.optional(Schema.String)
5967
+ };
5968
+ const BashSuccess = Schema.Struct({
5969
+ ...outputFields,
5970
+ exitCode: Schema.Finite
5971
+ });
5972
+ /** Non-zero exit — expected, model-visible. Carries the same shape as success. */
5973
+ var BashFailed = class extends Schema.TaggedError()("BashFailed", {
5974
+ ...outputFields,
5975
+ exitCode: Schema.Finite
5976
+ }) {};
5977
+ /** Deadline exceeded — expected, model-visible. Carries the partial output produced so far. */
5978
+ var BashTimedOut = class extends Schema.TaggedError()("BashTimedOut", {
5979
+ ...outputFields,
5980
+ timeoutSeconds: Schema.Finite
5981
+ }) {};
5982
+ const BashFailure = Schema.Union([BashFailed, BashTimedOut]);
5983
+ const bashDef = tool_exports.define({
5984
+ name: "bash",
5985
+ label: "bash",
5986
+ promptSnippet: "Execute bash commands (ls, grep, find, etc.).",
5987
+ description: `Execute a bash command in the working directory and return its combined stdout/stderr output. Output is truncated to the last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES}KB (whichever is hit first); when truncated, the full output is saved to a temp file. A non-zero exit code is reported as an error carrying the captured output.`,
5988
+ parameters: BashParams,
5989
+ success: BashSuccess,
5990
+ failure: BashFailure,
5991
+ encodeContent: (success) => [{
5992
+ type: "text",
5993
+ text: success.output
5994
+ }],
5995
+ encodeFailureContent: (failure) => [{
5996
+ type: "text",
5997
+ text: failure.output
5998
+ }]
5999
+ });
6000
+ /** Combine stdout and stderr into one stream of text, stderr after stdout. */
6001
+ const combineOutput = (stdout, stderr) => {
6002
+ if (stderr.length === 0) return stdout;
6003
+ if (stdout.length === 0) return stderr;
6004
+ return `${stdout}\n${stderr}`;
6005
+ };
6006
+ /** One-line footer appended to truncated, model-facing output. */
6007
+ const footer = (t, fullOutputPath, lastLineBytes) => {
6008
+ const where = fullOutputPath ? ` Full output: ${fullOutputPath}` : "";
6009
+ const startLine = t.totalLines - t.outputLines + 1;
6010
+ if (t.lastLinePartial) {
6011
+ const lineSize = lastLineBytes !== void 0 ? ` (line is ${formatSize(lastLineBytes)})` : "";
6012
+ return `\n\n[showing last ${formatSize(t.outputBytes)} of line ${t.totalLines}${lineSize}.${where}]`;
6013
+ }
6014
+ if (t.truncatedBy === "lines") return `\n\n[showing lines ${startLine}-${t.totalLines} of ${t.totalLines}.${where}]`;
6015
+ return `\n\n[showing lines ${startLine}-${t.totalLines} of ${t.totalLines} (${formatSize(t.maxBytes)} limit).${where}]`;
6016
+ };
6017
+ /** Write full output to a host temp file (best-effort; undefined on failure). */
6018
+ const spillToTempFile = (content) => Effect.gen(function* () {
6019
+ const suffix = Encoding.encodeHex(yield* crypto.randomBytes(6));
6020
+ const path = posix.join(tmpdir(), `codework-bash-${suffix}.log`);
6021
+ yield* fileSystem.writeFileString(path, content);
6022
+ return path;
6023
+ }).pipe(Effect.orElseSucceed(() => void 0));
6024
+ /** Variant A: truncate the buffered output once, spilling the full output if truncated. */
6025
+ const presentBuffered = (combined) => Effect.gen(function* () {
6026
+ const t = truncateTail(combined);
6027
+ if (!t.truncated) return {
6028
+ output: t.content,
6029
+ truncated: false
5910
6030
  };
5911
- const root = yield* fileSystem.realPath(into).pipe(Effect.mapError((cause) => new InstallError({
5912
- reason: "plugin-fetch-failed",
5913
- reference: target.spec,
5914
- message: `cannot resolve the staging directory ${into}`,
5915
- cause
5916
- })));
5917
- const node = (yield* runner({
5918
- target,
5919
- into: root,
5920
- cache,
5921
- from
5922
- })).edgesOut.values().next().value?.to;
5923
- if (node === void 0) return yield* new InstallError({
5924
- reason: "plugin-fetch-failed",
5925
- reference: target.spec,
5926
- message: `${target.spec} installed nothing`
5927
- });
5928
- const manifest = yield* fileSystem.readFileString(hostPath.join(node.path, "package.json")).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest$1))), Effect.option);
5929
- const version = Option.isNone(manifest) ? void 0 : manifest.value.version;
5930
- const revision = target.kind === "registry" ? version : (yield* commit(root, node.name)) ?? (yield* noCommit(target.spec));
6031
+ const fullOutputPath = yield* spillToTempFile(combined);
5931
6032
  return {
5932
- directory: node.path,
5933
- name: node.name,
5934
- ...version === void 0 ? {} : { version },
5935
- ...revision === void 0 ? {} : { revision },
5936
- entrypoint: yield* entrypoint(node.path, node.name)
6033
+ output: t.content + footer(t, fullOutputPath),
6034
+ truncated: true,
6035
+ ...fullOutputPath !== void 0 ? { fullOutputPath } : {}
5937
6036
  };
5938
6037
  });
5939
- const noCommit = (spec) => new InstallError({
5940
- reason: "plugin-no-commit",
5941
- reference: spec,
5942
- message: `cannot resolve a commit for ${spec}`
5943
- });
5944
- /**
5945
- * What a mutable target points at right now, without installing it.
5946
- *
5947
- * `preferOnline` and `noGitRevCache` are the whole point: without them pacote answers from its own
5948
- * metadata cache, and a staleness check that reads a cache is not a staleness check. An immutable
5949
- * target skips the call entirely -- a version and a SHA cannot move.
5950
- */
5951
- const probe = Effect.fn("PluginNpm.probe")(function* (target, cache, from) {
5952
- if (target.kind === "local" || !target.mutable) return void 0;
5953
- const opts = {
5954
- ...yield* options(from, cache),
5955
- preferOnline: true,
5956
- noGitRevCache: true,
5957
- _isRoot: true
6038
+ /** Variant B: present an accumulator snapshot (the temp file is spilled incrementally). */
6039
+ const presentSnapshot = (snap, lastLineBytes) => {
6040
+ if (!snap.truncation.truncated) return {
6041
+ output: snap.content,
6042
+ truncated: false
6043
+ };
6044
+ return {
6045
+ output: snap.content + footer(snap.truncation, snap.fullOutputPath, lastLineBytes),
6046
+ truncated: true,
6047
+ ...snap.fullOutputPath !== void 0 ? { fullOutputPath: snap.fullOutputPath } : {}
5958
6048
  };
5959
- const { default: pacote } = yield* Effect.promise(() => import("./lib-BkWR53Bz.mjs").then((n) => /* @__PURE__ */ __toESM(n.t(), 1)));
5960
- return yield* Effect.tryPromise({
5961
- try: async () => target.kind === "registry" ? (await pacote.manifest(target.spec, opts)).version : commitIn(await pacote.resolve(target.spec, opts)),
5962
- catch: (cause) => new InstallError({
5963
- reason: "plugin-resolve-failed",
5964
- reference: target.spec,
5965
- message: `cannot reach ${target.spec}`,
5966
- cause
5967
- })
5968
- });
5969
- });
5970
- /** A `file:` URL for an entrypoint, which is what the module loader takes. */
5971
- const url = (entrypoint) => pathToFileURL(entrypoint).href;
5972
- //#endregion
5973
- //#region ../harness/src/plugin/plugin.ts
5974
- var plugin_exports = /* @__PURE__ */ __exportAll({});
5975
- import * as import__codeworksh_plugin_plugin from "@codeworksh/plugin/plugin";
5976
- __reExport(plugin_exports, import__codeworksh_plugin_plugin);
5977
- //#endregion
5978
- //#region ../harness/src/plugin/source.ts
5979
- /** A committish that names one artifact rather than a moving branch: an unabbreviated SHA. */
5980
- const isCommit = (committish) => typeof committish === "string" && /^[a-f0-9]{40}$|^[a-f0-9]{64}$/i.test(committish);
5981
- /**
5982
- * A short, readable name for a git source, used to make a store path recognisable to a human
5983
- * reading `ls`. It is never an identity: two remotes can slug alike, and the digest is what
5984
- * separates them.
5985
- */
5986
- const slug = (spec) => {
5987
- const withoutCommittish = spec.split("#")[0] ?? spec;
5988
- return (() => {
5989
- try {
5990
- return decodeURIComponent(withoutCommittish);
5991
- } catch {
5992
- return withoutCommittish;
5993
- }
5994
- })().replace(/\.git$/i, "").split(/[/:\\]/).at(-1)?.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repository";
5995
6049
  };
5996
- /** Whether a reference names a place on disk rather than something to fetch. */
5997
- const isPath = (reference) => reference.startsWith("file:") || reference.startsWith("./") || reference.startsWith("../") || hostPath.isAbsolute(reference);
5998
- /**
5999
- * `from` is the directory a relative spec resolves against, and the caller picks it:
6000
- *
6001
- * - a **plugin entry** anchors to the directory holding the file that declared it --
6002
- * `<root>/.codework/` for a project entry, `<home>/` for a user one, the named directory for
6003
- * `--user-config-dir`. The rule is about the file, not the project.
6004
- * - a **CLI argument** anchors to the process's startup directory, because that is where the
6005
- * person typed it.
6006
- */
6007
- const parse$2 = (reference, from) => Effect.gen(function* () {
6008
- const fail = (reason, message) => new SourceError({
6009
- reason,
6010
- reference,
6011
- message
6050
+ /** Variant A — buffered: one `exec`, truncate after completion. No live progress. */
6051
+ const runBuffered = (shell, params) => Effect.gen(function* () {
6052
+ const result = yield* shell.exec(params.command, params.timeout !== void 0 ? { timeout: Duration.seconds(params.timeout) } : void 0).pipe(Effect.catchTags({
6053
+ ToolShellTimeout: (timeout) => Effect.fail(new BashTimedOut({
6054
+ timeoutSeconds: timeout.timeoutMillis / 1e3,
6055
+ output: "",
6056
+ truncated: false
6057
+ })),
6058
+ ToolShellError: (cause) => Effect.die(cause)
6059
+ }));
6060
+ const present = yield* presentBuffered(combineOutput(result.stdout, result.stderr));
6061
+ if (result.exitCode !== 0) return yield* new BashFailed({
6062
+ exitCode: result.exitCode,
6063
+ ...present
6012
6064
  });
6013
- rooted(from, "the directory a plugin reference anchors to");
6014
- if (reference.startsWith("file:")) {
6015
- if (!reference.startsWith("file:///")) return yield* fail("plugin-unsupported-source", `not an absolute file URL: ${reference}`);
6016
- return {
6017
- kind: "local",
6018
- path: fileURLToPath(reference)
6019
- };
6020
- }
6021
- const expanded = expandTilde(reference, hostPath);
6022
- if (isPath(expanded)) return {
6023
- kind: "local",
6024
- path: hostPath.resolve(from, expanded)
6065
+ return {
6066
+ exitCode: result.exitCode,
6067
+ ...present
6025
6068
  };
6026
- const parsed = yield* Effect.try({
6027
- try: () => npa(reference),
6028
- catch: () => fail("plugin-unsupported-source", `cannot parse plugin source: ${reference}`)
6029
- });
6030
- if (parsed.type === "git") {
6031
- const { gitSubdir } = parsed;
6032
- return {
6033
- kind: "git",
6034
- slug: slug(reference),
6035
- spec: reference,
6036
- ...parsed.gitCommittish == null ? {} : { committish: parsed.gitCommittish },
6037
- ...gitSubdir == null ? {} : { subdir: gitSubdir },
6038
- mutable: !isCommit(parsed.gitCommittish)
6039
- };
6040
- }
6041
- if (parsed.name !== null && [
6042
- "version",
6043
- "range",
6044
- "tag"
6045
- ].includes(parsed.type)) {
6046
- const omitted = parsed.raw === parsed.name || parsed.raw === `${parsed.name}@`;
6047
- return {
6048
- kind: "registry",
6049
- name: parsed.name,
6050
- spec: `${parsed.name}@${omitted ? "latest" : parsed.rawSpec}`,
6051
- mutable: parsed.type !== "version"
6052
- };
6053
- }
6054
- return yield* fail("plugin-unsupported-source", `unsupported plugin source: ${reference}`);
6055
- });
6056
- /**
6057
- * What two references must share to be the same plugin, without installing either.
6058
- *
6059
- * A local path is the file it resolves to; a package is its name, so `@acme/x@1` and `@acme/x@2`
6060
- * are one package at two versions. Pure -- nothing is installed, imported or read.
6061
- */
6062
- const canonical$1 = (target) => {
6063
- switch (target.kind) {
6064
- case "local": return target.path;
6065
- case "registry": return target.name;
6066
- case "git": return target.spec;
6067
- }
6068
- };
6069
- //#endregion
6070
- //#region ../harness/src/plugin/index.store.ts
6071
- const Record_ = Schema.Struct({
6072
- spec: Schema.String,
6073
- source: Schema.Literals(["registry", "git"]),
6074
- name: Schema.String,
6075
- slug: Schema.String,
6076
- mutable: Schema.Boolean,
6077
- generation: Schema.Finite,
6078
- version: Schema.optional(Schema.String),
6079
- revision: Schema.optional(Schema.String),
6080
- entrypoint: Schema.String,
6081
- installedAt: Schema.Finite,
6082
- checkedAt: Schema.optional(Schema.Finite),
6083
- outdated: Schema.optional(Schema.Boolean),
6084
- /** The remote revision observed at `checkedAt`; distinct from the installed `revision`. */
6085
- available: Schema.optional(Schema.String)
6086
- });
6087
- const File = Schema.Struct({
6088
- version: Schema.Finite,
6089
- entries: Schema.Record(Schema.String, Record_)
6090
- });
6091
- const VERSION = 1;
6092
- const NAME = "index.json";
6093
- /**
6094
- * Read the whole file, or start empty.
6095
- *
6096
- * An unreadable or unparseable index is not an error: it is a cold cache. Failing here would make
6097
- * a corrupt accelerator break installs that do not need it.
6098
- */
6099
- const read = Effect.fn("PluginIndex.read")(function* (root) {
6100
- const parsed = yield* fileSystem.readFileString(hostPath.join(root, NAME)).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(File))), Effect.option);
6101
- if (Option.isNone(parsed) || parsed.value.version !== VERSION) return /* @__PURE__ */ new Map();
6102
- return new Map(Object.entries(parsed.value.entries));
6103
6069
  });
6104
- /** Temp file plus rename, so a reader never sees a half-written index. */
6105
- const write = Effect.fn("PluginIndex.write")(function* (root, entries) {
6106
- const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(File))({
6107
- version: VERSION,
6108
- entries: Object.fromEntries(entries)
6070
+ /** Variant B — streaming: accumulate output, report progress, keep partial output on timeout. */
6071
+ const runStreaming = (stream, params) => Effect.scoped(Effect.gen(function* () {
6072
+ const acc = new Accumulator({ tempFilePrefix: "codework-bash" });
6073
+ const progress = yield* progress_exports.ToolProgress;
6074
+ const exitCode = yield* Ref.make(null);
6075
+ const consume = stream(params.command).pipe(Stream.runForEach((event) => event._tag === "Exit" ? Ref.set(exitCode, event.exitCode) : Effect.gen(function* () {
6076
+ yield* acc.append(Buffer.from(event.bytes));
6077
+ yield* progress.report({ content: [{
6078
+ type: "text",
6079
+ text: acc.snapshot().content
6080
+ }] });
6081
+ })), Effect.catchTag("ToolShellError", (cause) => Effect.die(cause)));
6082
+ let timedOutAfter;
6083
+ if (params.timeout !== void 0) {
6084
+ const finished = yield* consume.pipe(Effect.timeoutOption(Duration.seconds(params.timeout)));
6085
+ if (Option.isNone(finished)) timedOutAfter = params.timeout;
6086
+ } else yield* consume;
6087
+ yield* acc.finish();
6088
+ const present = presentSnapshot(acc.snapshot(), acc.getLastLineBytes());
6089
+ if (timedOutAfter !== void 0) return yield* new BashTimedOut({
6090
+ timeoutSeconds: timedOutAfter,
6091
+ ...present
6109
6092
  });
6110
- yield* fileSystem.makeDirectory(root, { recursive: true });
6111
- const temporary = yield* fileSystem.makeTempFile({
6112
- directory: root,
6113
- prefix: `.${NAME}.`,
6114
- suffix: ".tmp"
6093
+ const code = yield* Ref.get(exitCode);
6094
+ if (code === null || code !== 0) return yield* new BashFailed({
6095
+ exitCode: code ?? -1,
6096
+ ...present
6115
6097
  });
6116
- yield* fileSystem.writeFileString(temporary, encoded).pipe(Effect.andThen(fileSystem.rename(temporary, hostPath.join(root, NAME))), Effect.ensuring(fileSystem.remove(temporary, { force: true }).pipe(Effect.ignore)));
6098
+ return {
6099
+ exitCode: code,
6100
+ ...present
6101
+ };
6102
+ }));
6103
+ const bashHandler = (params) => Effect.gen(function* () {
6104
+ const shell = yield* ToolShell;
6105
+ return shell.stream !== void 0 ? yield* runStreaming(shell.stream, params) : yield* runBuffered(shell, params);
6117
6106
  });
6118
- /**
6119
- * Record one entry, keeping whatever else the file holds.
6120
- *
6121
- * Ignores its own failures. A store that cannot write its accelerator still works; refusing the
6122
- * install would be the accelerator deciding whether the install counted.
6123
- */
6124
- const put = Effect.fn("PluginIndex.put")(function* (root, digest, record) {
6125
- const entries = yield* read(root);
6126
- entries.set(digest, record);
6127
- yield* write(root, entries).pipe(Effect.ignore);
6107
+ /** The bash tool: definition + handler, wired the testable (def/exec split) way. */
6108
+ const bashTool = tool_exports.implement(bashDef, bashHandler);
6109
+ const bashPlugin = (0, plugin_exports.define)({
6110
+ id: "codework.tool.bash",
6111
+ kind: "tool",
6112
+ setup: Effect.fn("BashPlugin.setup")(function* (ctx) {
6113
+ const shell = yield* io_exports.SandboxIO.Shell;
6114
+ const mounted = fromSandboxShell.pipe(Layer.provide(Layer.succeed(io_exports.SandboxIO.Shell, shell)));
6115
+ ctx.plugin.tools.add(tool_exports.provide(bashTool, mounted));
6116
+ })
6128
6117
  });
6118
+ //#endregion
6119
+ //#region ../harness/src/plugin/builtin.ts
6129
6120
  /**
6130
- * Merge fields into one record, if it is there.
6131
- *
6132
- * Unlocked, like every write here but the one that publishes a generation. Two processes checking
6133
- * at once both read the whole file and both rewrite it, so one update is lost and a record added
6134
- * in between can vanish -- and neither matters, because a lost `checkedAt` costs one network call
6135
- * and a vanished record is a miss a scan repairs.
6121
+ * Every plugin the harness ships, and the selection a caller who passes no `plugins` gets. Tool
6122
+ * contributors come first so the Prompt plugin that indexes them runs after they registered.
6136
6123
  */
6137
- const patch = Effect.fn("PluginIndex.patch")(function* (root, digest, fields) {
6138
- const entries = yield* read(root);
6139
- const existing = entries.get(digest);
6140
- if (existing === void 0) return;
6141
- entries.set(digest, {
6142
- ...existing,
6143
- ...fields
6144
- });
6145
- yield* write(root, entries).pipe(Effect.ignore);
6146
- });
6147
- /** Forget one entry, for a store entry that has been removed. */
6148
- const drop = Effect.fn("PluginIndex.drop")(function* (root, digest) {
6149
- const entries = yield* read(root);
6150
- if (!entries.delete(digest)) return;
6151
- yield* write(root, entries).pipe(Effect.ignore);
6152
- });
6124
+ const builtins = Object.freeze([bashPlugin, defaultPromptPlugin]);
6153
6125
  //#endregion
6154
- //#region ../harness/src/plugin/lock.ts
6155
- /** How long to wait for another holder before giving up. */
6156
- const TIMEOUT = Duration.minutes(2);
6157
- /** The holder refreshes the lock's mtime this often; one not refreshed for the timeout is stale. */
6158
- const HEARTBEAT = Duration.seconds(15);
6159
- const abandoned = (directory) => Effect.gen(function* () {
6160
- const info = yield* fileSystem.stat(directory);
6161
- const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
6162
- return Option.exists(info.mtime, (mtime) => now - mtime.getTime() > Duration.toMillis(TIMEOUT));
6163
- }).pipe(Effect.orElseSucceed(() => false));
6164
- const heartbeat = (directory) => Effect.clockWith((clock) => clock.currentTimeMillis).pipe(Effect.flatMap((now) => fileSystem.utimes(directory, now, now)), Effect.ignore, Effect.repeat(Schedule.spaced(HEARTBEAT)));
6126
+ //#region ../harness/src/settings/merge.ts
6165
6127
  /**
6166
- * Claims `directory` by creating it, polling while another process holds it, and releasing it when
6167
- * the surrounding scope closes.
6128
+ * A plain string-keyed record: what a settings patch merges, and what a plugin's `options` block
6129
+ * has to be. A Map, a typed array or a class instance is an object to `typeof` and not one of
6130
+ * these — `Object.freeze` alone does not tell them apart, and it throws on some of them.
6131
+ */
6132
+ const isRecord$1 = (value) => Predicate.isObject(value) && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
6133
+ const copy = (value) => {
6134
+ if (Array.isArray(value)) return value.map(copy);
6135
+ return isRecord$1(value) ? merge({}, value) : value;
6136
+ };
6137
+ /** Null/undefined properties are absent, objects merge, and arrays replace. Never mutates inputs. */
6138
+ function merge(base, ...patches) {
6139
+ const result = /* @__PURE__ */ new Map();
6140
+ for (const patch of [base, ...patches]) {
6141
+ if (patch === void 0) continue;
6142
+ for (const [key, value] of Object.entries(patch)) {
6143
+ if (value === null || value === void 0) continue;
6144
+ const previous = result.get(key);
6145
+ result.set(key, isRecord$1(previous) && isRecord$1(value) ? merge(previous, value) : copy(value));
6146
+ }
6147
+ }
6148
+ return Object.fromEntries(result);
6149
+ }
6150
+ /** Normalize nullable properties before schema validation, without changing array positions. */
6151
+ const normalize = copy;
6152
+ //#endregion
6153
+ //#region ../harness/src/util/module.ts
6154
+ /*!
6155
+ * Adapted from OpenCode packages/util/src/runtime/import.node.ts.
6168
6156
  *
6169
- * One finalizer is registered for the whole wait rather than one per 50ms poll, and a single
6170
- * attempt is `uninterruptible` so `mkdir` and recording ownership cannot be split -- an interrupt
6171
- * between them would leave behind a lock the finalizer does not know to remove.
6157
+ * MIT License
6172
6158
  *
6173
- * `onTimeout` names the failure, because what a caller is waiting for is the caller's to describe.
6159
+ * Copyright (c) 2025 opencode
6160
+ *
6161
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6162
+ * of this software and associated documentation files (the "Software"), to deal
6163
+ * in the Software without restriction, including without limitation the rights
6164
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
6165
+ * copies of the Software, and to permit persons to whom the Software is
6166
+ * furnished to do so, subject to the following conditions:
6167
+ *
6168
+ * The above copyright notice and this permission notice shall be included in all
6169
+ * copies or substantial portions of the Software.
6170
+ *
6171
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
6172
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
6173
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
6174
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
6175
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
6176
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
6177
+ * SOFTWARE.
6174
6178
  */
6175
- const lock = Effect.fn("PluginLock.acquire")(function* (directory, onTimeout) {
6176
- const held = yield* Ref.make(false);
6177
- yield* Effect.acquireRelease(Effect.void, () => Ref.get(held).pipe(Effect.flatMap((owned) => owned ? fileSystem.remove(directory, {
6178
- recursive: true,
6179
- force: true
6180
- }) : Effect.void), Effect.orDie));
6181
- if (!(yield* fileSystem.makeDirectory(directory).pipe(Effect.andThen(Ref.set(held, true)), Effect.as(true), Effect.catchIf((error) => error.reason._tag === "AlreadyExists", () => abandoned(directory).pipe(Effect.flatMap((stale) => stale ? fileSystem.remove(directory, {
6182
- recursive: true,
6183
- force: true
6184
- }).pipe(Effect.as(false)) : Effect.succeed(false)))), Effect.uninterruptible).pipe(Effect.repeat({
6185
- schedule: Schedule.spaced("50 millis"),
6186
- until: (owned) => owned
6187
- }), Effect.timeoutOrElse({
6188
- duration: TIMEOUT,
6189
- orElse: () => Effect.succeed(false)
6190
- })))) return yield* Effect.fail(onTimeout());
6191
- yield* Effect.forkScoped(heartbeat(directory));
6192
- });
6193
- //#endregion
6194
- //#region ../harness/src/plugin/store.ts
6195
- /** Facts about the *package*, and nothing about the plugin inside it. */
6196
- const Marker = Schema.Struct({
6197
- spec: Schema.String,
6198
- name: Schema.String,
6199
- version: Schema.optional(Schema.String),
6200
- revision: Schema.optional(Schema.String),
6201
- /** Relative to the generation directory, so the marker survives the publishing rename. */
6202
- entrypoint: Schema.String,
6203
- createdAt: Schema.Finite
6204
- });
6205
- const MARKER = ".complete.json";
6206
- const STAGING = ".staging-";
6207
- const root = (cache) => hostPath.join(cache, "plugins", "v2");
6208
6179
  /**
6209
- * A readable label, never an identity: two specs that slug alike share a parent directory and
6210
- * nothing more. Worth having because a greppable path is worth real money when debugging, and a
6211
- * bare hash throws away every clue.
6180
+ * Packages a plugin must share with the harness instead of using the copy installed beside it.
6181
+ *
6182
+ * A plugin is installed into the store as its own npm tree, so `effect` and the plugin SDK land
6183
+ * there a second time. Two copies of the *same version* are still two module instances, and
6184
+ * Effect Schema's checks do not survive the crossing: a schema built by one copy and evaluated by
6185
+ * the other reports every check as failed, so a tool whose success type is `Schema.Finite` dies
6186
+ * on "Expected a finite number" for `22800`. Nothing about that is visible at registration --
6187
+ * `Schema.isSchema` passes, the JSON Schema is derived correctly, and the tool reaches the model
6188
+ * -- so it surfaces on the first call rather than at boot.
6189
+ *
6190
+ * Resolution is the right place to fix it. A symlink into the store cannot be: a generation is
6191
+ * immutable and shared by every project on the machine, while the harness doing the loading is
6192
+ * whichever one is running now, and two applications embedding the harness need not agree on
6193
+ * where their `effect` lives.
6212
6194
  */
6213
- const slugOf = (target) => target.kind === "git" ? target.slug : canonical$1(target).replace(/^@/, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "package";
6195
+ const sharedPackages = /* @__PURE__ */ new Set(["effect", "@codeworksh/plugin"]);
6196
+ /** The package a bare specifier names: `effect/Schema` -> `effect`, `@a/b/c` -> `@a/b`. */
6197
+ function packageOf(specifier) {
6198
+ if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.includes(":")) return "";
6199
+ const segments = specifier.split("/");
6200
+ return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0] ?? "";
6201
+ }
6214
6202
  /**
6215
- * The **full** SHA-256 of the source identity: canonical spec plus effective registry for a
6216
- * registry package, and the canonical spec alone for git.
6217
- *
6218
- * Not truncated. The fast path proves a hit by checking that a marker *exists* rather than by
6219
- * decoding it, and the index is keyed by digest alone, so a truncated digest would let a collision
6220
- * return the wrong package silently.
6203
+ * Resolve the shared packages as though this module had imported them, for as long as `run`
6204
+ * takes. Anything else resolves exactly as it would have.
6221
6205
  */
6222
- const digest = Effect.fn("PluginStore.digest")(function* (target, cache, from) {
6223
- const source = target.kind === "registry" ? `${yield* registry(target, cache, from)}\0${target.spec}` : target.spec;
6224
- return Encoding.encodeHex(yield* crypto.digest("SHA-256", new TextEncoder().encode(source)));
6225
- });
6226
- const entryDir = Effect.fn("PluginStore.entryDir")(function* (target, cache, from) {
6227
- const key = yield* digest(target, cache, from);
6228
- return {
6229
- key,
6230
- directory: hostPath.join(root(cache), slugOf(target), key)
6231
- };
6232
- });
6233
- const failure$1 = (target, reason, message, cause) => new StoreError({
6234
- reason,
6235
- reference: target.spec,
6236
- message,
6237
- ...cause === void 0 ? {} : { cause }
6238
- });
6239
- /** Generation directories, oldest first. A name that is not a number is not a generation. */
6240
- const generations = Effect.fn("PluginStore.generations")(function* (directory) {
6241
- return (yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []))).filter((name) => /^\d+$/.test(name)).map((name) => Number(name)).sort((left, right) => left - right);
6242
- });
6243
- const readMarker = (directory) => fileSystem.readFileString(hostPath.join(directory, MARKER)).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Marker))), Effect.option);
6244
- const entryOf = (marker, input) => ({
6245
- ...marker,
6246
- digest: input.digest,
6247
- generation: input.generation,
6248
- directory: input.directory,
6249
- url: url(hostPath.resolve(input.directory, marker.entrypoint))
6250
- });
6251
- /** The newest generation that carries a marker. Unmarked ones are invisible: they never happened. */
6252
- const active = Effect.fn("PluginStore.active")(function* (directory, key) {
6253
- for (const generation of (yield* generations(directory)).reverse()) {
6254
- const where = hostPath.join(directory, String(generation));
6255
- const marker = yield* readMarker(where);
6256
- if (Option.isSome(marker)) return entryOf(marker.value, {
6257
- digest: key,
6258
- generation,
6259
- directory: where
6206
+ function withSharedModules(run) {
6207
+ const hook = registerHooks({ resolve(specifier, context, nextResolve) {
6208
+ return sharedPackages.has(packageOf(specifier)) ? nextResolve(specifier, {
6209
+ ...context,
6210
+ parentURL: import.meta.url
6211
+ }) : nextResolve(specifier, context);
6212
+ } });
6213
+ return run().finally(() => hook.deregister());
6214
+ }
6215
+ /** Use Node's main loader even when the caller runs inside a bundler or test VM. */
6216
+ async function importModule(specifier) {
6217
+ const imported = await withSharedModules(() => new Script(`import(${JSON.stringify(specifier)})`, { importModuleDynamically: constants.USE_MAIN_CONTEXT_DEFAULT_LOADER }).runInThisContext());
6218
+ if (typeof imported !== "object" || imported === null) return imported;
6219
+ const module = imported;
6220
+ const exports = module["module.exports"];
6221
+ if (exports !== module.default || typeof exports !== "object" && typeof exports !== "function" || exports === null) return imported;
6222
+ return Object.assign({}, module, exports);
6223
+ }
6224
+ /** Node resolution scoped synchronously to the caller. */
6225
+ function resolveModule(specifier, directory, conditions) {
6226
+ const hook = registerHooks({ resolve(target, context, nextResolve) {
6227
+ return nextResolve(target, {
6228
+ ...context,
6229
+ parentURL: pathToFileURL(path.join(directory, "package.json")).href,
6230
+ ...conditions === void 0 ? {} : { conditions: [.../* @__PURE__ */ new Set([...context.conditions, ...conditions])] }
6260
6231
  });
6232
+ } });
6233
+ try {
6234
+ const resolve = (target) => {
6235
+ const resolved = import.meta.resolve(path.isAbsolute(target) ? pathToFileURL(target).href : target);
6236
+ if (resolved.startsWith("file:")) statSync(new URL(resolved));
6237
+ return resolved;
6238
+ };
6239
+ try {
6240
+ return resolve(specifier);
6241
+ } catch (error) {
6242
+ if (path.extname(specifier) || !missing(error)) throw error;
6243
+ for (const extension of [
6244
+ ".ts",
6245
+ ".tsx",
6246
+ ".js",
6247
+ ".jsx",
6248
+ ".mts",
6249
+ ".mjs",
6250
+ ".cts",
6251
+ ".cjs"
6252
+ ]) try {
6253
+ return resolve(specifier + extension);
6254
+ } catch (cause) {
6255
+ if (!missing(cause)) throw cause;
6256
+ }
6257
+ throw error;
6258
+ }
6259
+ } finally {
6260
+ hook.deregister();
6261
6261
  }
6262
- });
6263
- /** What the index files about an entry, all of it derivable from the spec and the marker. */
6264
- const record = (target, entry, installedAt) => ({
6265
- spec: target.spec,
6266
- source: target.kind,
6267
- name: entry.name,
6268
- slug: slugOf(target),
6269
- mutable: target.mutable,
6270
- generation: entry.generation,
6271
- ...entry.version === void 0 ? {} : { version: entry.version },
6272
- ...entry.revision === void 0 ? {} : { revision: entry.revision },
6273
- entrypoint: entry.entrypoint,
6274
- installedAt
6275
- });
6262
+ }
6263
+ function missing(error) {
6264
+ return error instanceof Error && "code" in error && [
6265
+ "ENOENT",
6266
+ "ENOTDIR",
6267
+ "ERR_MODULE_NOT_FOUND"
6268
+ ].includes(String(error.code));
6269
+ }
6270
+ //#endregion
6271
+ //#region ../harness/src/plugin/error.ts
6276
6272
  /**
6277
- * Disk-only lookup. No network, no install -- this is what boot uses, and the reason booting
6278
- * offline works.
6273
+ * The reference as the person wrote it -- the string they will recognise in their settings file,
6274
+ * never the resolved path or the canonical spec. Every plugin failure carries one.
6275
+ */
6276
+ const Reference = Schema.String;
6277
+ /**
6278
+ * The settings file that declared the entry, when one did.
6279
6279
  *
6280
- * The index is consulted first and is only ever a hint: a hit is proved by the marker still
6281
- * existing, and anything else falls through to a scan that repairs the index on the way past.
6280
+ * Absent for an embedder's own list and for anything the CLI was handed directly -- in both cases
6281
+ * the caller already knows where the string came from. It is the settings path that needs saying,
6282
+ * because several layers accumulate entries and "which file says this" is the question.
6282
6283
  */
6283
- const resolve$2 = Effect.fn("PluginStore.resolve")(function* (target, cache, from) {
6284
- const { key, directory } = yield* entryDir(target, cache, from);
6285
- const base = root(cache);
6286
- const hinted = (yield* read(base)).get(key);
6287
- if (hinted !== void 0) {
6288
- const where = hostPath.join(directory, String(hinted.generation));
6289
- const marker = yield* readMarker(where);
6290
- if (Option.isSome(marker)) return entryOf(marker.value, {
6291
- digest: key,
6292
- generation: hinted.generation,
6293
- directory: where
6294
- });
6295
- }
6296
- const scanned = yield* active(directory, key);
6297
- if (scanned === void 0) return void 0;
6298
- yield* put(base, key, record(target, scanned, scanned.createdAt));
6299
- return scanned;
6300
- }, Effect.mapError((cause) => Schema.is(StoreError)(cause) || Schema.is(InstallError)(cause) ? cause : new StoreError({
6301
- reason: "plugin-marker-invalid",
6302
- reference: "",
6303
- message: "cannot read the plugin store",
6304
- cause
6305
- })));
6284
+ const File$1 = Schema.optional(Schema.String);
6306
6285
  /**
6307
- * `resolve`, for a caller that needs the entry rather than an answer about it.
6286
+ * A spec that cannot be installed at all.
6308
6287
  *
6309
- * `plugin remove` uses this as its installer: it has to identify what an entry names without
6310
- * fetching anything, and a spec with nothing filed is a real answer there -- the command reports
6311
- * it and moves on.
6288
+ * `plugin-not-found` is a local path with nothing at it; `plugin-not-installed` (the store's) is a
6289
+ * package spec with no entry. The first is a mistake in a settings file, the second is fixed by
6290
+ * `plugin install`, and keeping them apart is most of the value of having slugs.
6312
6291
  */
6313
- const required = Effect.fn("PluginStore.required")(function* (target, cache, from) {
6314
- const found = yield* resolve$2(target, cache, from);
6315
- if (found === void 0) return yield* failure$1(target, "plugin-not-installed", `${target.spec} is not installed`);
6316
- return found;
6317
- });
6292
+ var SourceError = class extends Schema.TaggedError()("PluginSourceError", {
6293
+ reason: Schema.Literals([
6294
+ "plugin-unsupported-source",
6295
+ "plugin-not-found",
6296
+ "plugin-escapes-root"
6297
+ ]),
6298
+ reference: Reference,
6299
+ file: File$1,
6300
+ message: Schema.String
6301
+ }) {};
6302
+ /** A spec that is installable, and whose install did not produce a usable package. */
6303
+ var InstallError = class extends Schema.TaggedError()("PluginInstallError", {
6304
+ reason: Schema.Literals([
6305
+ "plugin-resolve-failed",
6306
+ "plugin-fetch-failed",
6307
+ "plugin-no-commit",
6308
+ "plugin-no-entrypoint"
6309
+ ]),
6310
+ reference: Reference,
6311
+ file: File$1,
6312
+ message: Schema.String,
6313
+ cause: Schema.optional(Schema.Defect())
6314
+ }) {};
6318
6315
  /**
6319
- * Install if absent; hand back what is filed if present.
6316
+ * A module that reached the import and did not come back as a plugin.
6320
6317
  *
6321
- * `refresh` forces past both the fast path and the double-check, which is what `update` needs and
6322
- * nothing else should use.
6318
+ * Distinct from {@link InstallError}: the bytes are here and are fine as *bytes*. What failed is
6319
+ * evaluating them, or what they turned out to export -- which is the distinction a caller acts on,
6320
+ * because one is fixed by installing and the other by editing code.
6323
6321
  */
6324
- const add = Effect.fn("PluginStore.add")(function* (target, cache, options) {
6325
- return yield* Effect.gen(function* () {
6326
- const { key, directory } = yield* entryDir(target, cache, options.from);
6327
- if (options.refresh !== true) {
6328
- const found = yield* resolve$2(target, cache, options.from);
6329
- if (found !== void 0) return { entry: found };
6330
- }
6331
- yield* fileSystem.makeDirectory(directory, { recursive: true });
6332
- yield* lock(hostPath.join(directory, ".lock"), () => failure$1(target, "plugin-lock-timeout", `timed out waiting for another installation of ${target.spec}`));
6333
- const current = yield* active(directory, key);
6334
- if (current !== void 0 && options.refresh !== true) return { entry: current };
6335
- const real = yield* fileSystem.realPath(directory).pipe(Effect.orElseSucceed(() => directory));
6336
- const staging = yield* Effect.acquireRelease(fileSystem.makeTempDirectory({
6337
- directory: real,
6338
- prefix: STAGING
6339
- }), (staged) => fileSystem.remove(staged, {
6340
- recursive: true,
6341
- force: true
6342
- }).pipe(Effect.orDie));
6343
- yield* fileSystem.writeFileString(hostPath.join(staging, "package.json"), "{\"private\":true,\"type\":\"module\"}");
6344
- const fetched = yield* download({
6345
- target,
6346
- into: staging,
6347
- cache,
6348
- from: options.from,
6349
- ...options.runner === void 0 ? {} : { runner: options.runner }
6350
- });
6351
- if (current !== void 0 && current.revision === fetched.revision) return { entry: current };
6352
- const validated = yield* options.validate(fetched);
6353
- const entrypoint = hostPath.relative(yield* fileSystem.realPath(staging), fetched.entrypoint);
6354
- if (entrypoint.startsWith("..") || hostPath.isAbsolute(entrypoint)) return yield* new InstallError({
6355
- reason: "plugin-no-entrypoint",
6356
- reference: target.spec,
6357
- message: `entrypoint escapes its installation: ${entrypoint}`
6358
- });
6359
- const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
6360
- const marker = {
6361
- spec: target.spec,
6362
- name: fetched.name,
6363
- ...fetched.version === void 0 ? {} : { version: fetched.version },
6364
- ...fetched.revision === void 0 ? {} : { revision: fetched.revision },
6365
- entrypoint,
6366
- createdAt: now
6367
- };
6368
- yield* fileSystem.writeFileString(hostPath.join(staging, MARKER), yield* Schema.encodeEffect(Schema.fromJsonString(Marker))(marker));
6369
- const previous = (yield* generations(directory)).at(-1) ?? 0;
6370
- const generation = Math.max(now, previous + 1);
6371
- const published = hostPath.join(directory, String(generation));
6372
- yield* fileSystem.rename(staging, published);
6373
- const entry = entryOf(marker, {
6374
- digest: key,
6375
- generation,
6376
- directory: published
6377
- });
6378
- yield* put(root(cache), key, record(target, entry, now));
6379
- yield* collect$1(directory);
6380
- return {
6381
- entry,
6382
- validated
6383
- };
6384
- }).pipe(Effect.scoped, Effect.mapError((cause) => Schema.is(InstallError)(cause) || Schema.is(StoreError)(cause) || Schema.is(LoadError)(cause) ? cause : failure$1(target, "plugin-marker-invalid", `cannot write the store entry for ${target.spec}`, cause)));
6385
- });
6322
+ var LoadError = class extends Schema.TaggedError()("PluginLoadError", {
6323
+ reason: Schema.Literals([
6324
+ "plugin-import-failed",
6325
+ "plugin-missing-dependency",
6326
+ "plugin-not-compiled",
6327
+ "plugin-invalid-definition"
6328
+ ]),
6329
+ reference: Reference,
6330
+ file: File$1,
6331
+ message: Schema.String,
6332
+ /** The ID the module declared, when it got far enough to declare one. */
6333
+ id: Schema.optional(Schema.String),
6334
+ cause: Schema.optional(Schema.Defect())
6335
+ }) {};
6336
+ /** The store's own failures: what is filed, what is not, and what could not be claimed. */
6337
+ var StoreError = class extends Schema.TaggedError()("PluginStoreError", {
6338
+ reason: Schema.Literals([
6339
+ "plugin-not-installed",
6340
+ "plugin-lock-timeout",
6341
+ "plugin-marker-invalid",
6342
+ "plugin-index-invalid",
6343
+ "plugin-collect-failed"
6344
+ ]),
6345
+ reference: Reference,
6346
+ file: File$1,
6347
+ message: Schema.String,
6348
+ cause: Schema.optional(Schema.Defect())
6349
+ }) {};
6386
6350
  /**
6387
- * Expire what is superseded.
6351
+ * Note which settings file declared the entry a failure came from.
6388
6352
  *
6389
- * The two newest generations are kept regardless of age: the newest is live, and the one before it
6390
- * may still be imported by an exchange in flight. Runs at the end of an `add` that published
6391
- * something -- no timer and no sweeper, because installs are the only thing that creates garbage.
6353
+ * The store and the installer raise their own failures without knowing that, because neither is
6354
+ * told: they are handed a spec, not a settings layer. The loader knows, and this is where it says
6355
+ * so -- leaving an existing answer alone, since a more specific one is already better.
6392
6356
  */
6393
- const collect$1 = Effect.fn("PluginStore.collect")(function* (directory) {
6394
- const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
6395
- const keep = new Set((yield* generations(directory)).slice(-2).map(String));
6396
- const names = yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []));
6397
- for (const name of names) {
6398
- if (keep.has(name) || name === ".lock") continue;
6399
- if (name.startsWith(STAGING)) {
6400
- const started = yield* fileSystem.stat(hostPath.join(directory, name)).pipe(Effect.option);
6401
- if ((Option.isNone(started) ? 0 : Option.match(started.value.mtime, {
6402
- onNone: () => 0,
6403
- onSome: (at) => now - at.getTime()
6404
- })) > HOUR) yield* fileSystem.remove(hostPath.join(directory, name), {
6405
- recursive: true,
6406
- force: true
6407
- }).pipe(Effect.ignore);
6408
- continue;
6409
- }
6410
- if (!/^\d+$/.test(name)) continue;
6411
- if (now - Number(name) > WEEK) yield* fileSystem.remove(hostPath.join(directory, name), {
6412
- recursive: true,
6413
- force: true
6414
- }).pipe(Effect.ignore);
6415
- }
6416
- });
6417
- const HOUR = 36e5;
6418
- const WEEK = 168 * HOUR;
6357
+ const declaredIn = (error, file) => {
6358
+ if (error.file !== void 0) return error;
6359
+ const shared = {
6360
+ reference: error.reference,
6361
+ message: error.message,
6362
+ file
6363
+ };
6364
+ if (Schema.is(SourceError)(error)) return new SourceError({
6365
+ ...shared,
6366
+ reason: error.reason
6367
+ });
6368
+ if (Schema.is(InstallError)(error)) return new InstallError({
6369
+ ...shared,
6370
+ reason: error.reason,
6371
+ cause: error.cause
6372
+ });
6373
+ if (Schema.is(LoadError)(error)) return new LoadError({
6374
+ ...shared,
6375
+ reason: error.reason,
6376
+ id: error.id,
6377
+ cause: error.cause
6378
+ });
6379
+ if (Schema.is(StoreError)(error)) return new StoreError({
6380
+ ...shared,
6381
+ reason: error.reason,
6382
+ cause: error.cause
6383
+ });
6384
+ return error;
6385
+ };
6386
+ //#endregion
6387
+ //#region ../harness/src/util/path.ts
6419
6388
  /**
6420
- * How long a network answer is reused before `check` asks again.
6389
+ * A directory that crosses into the harness is absolute, and a relative one is a **defect** rather
6390
+ * than an error.
6421
6391
  *
6422
- * Persisted in the index rather than held in memory, because the thing that benefits is a command
6423
- * that exits: an in-memory staleness cache is worthless to `codework plugin check`.
6424
- */
6425
- const CHECK_TTL = 36e5;
6426
- /**
6427
- * Is a newer revision available?
6392
+ * A relative path is a disguised `process.cwd()` call: and every consumer
6393
+ * resolves it silently. `path.resolve("x")` anchors to the process directory, npm answers a
6394
+ * relative `cwd` with a `localPrefix` of whatever repository the process is inside (measured, not
6395
+ * assumed), and `Session.link` would write the *server's* directory into a field whose whole
6396
+ * purpose is to be a host path the client declared.
6428
6397
  *
6429
- * The only network call in the store's vocabulary, and it is the caller's to make. A `probe` that
6430
- * fails -- offline, registry down, a remote that refuses -- propagates rather than answering
6431
- * `current`: an outage must not read as "you are up to date".
6398
+ * For a long-running server the process directory is nobody's project, so the result is a
6399
+ * plausible answer to a question no one asked.
6400
+ * An absolute path is the one spelling none of them can quietly reinterpret.
6401
+ *
6402
+ * A defect rather than a typed error because nothing a user types reaches here. Every reference
6403
+ * goes through `parse`, and every directory comes from `Global.resolve`, a resolved `cwd` or a
6404
+ * session's `hostDir`, all of them already absolute. A relative one means a new call site got it
6405
+ * wrong, and a stack trace names that call site; a typed error would ask every caller to handle a
6406
+ * case none of them can cause.
6432
6407
  */
6433
- const check = Effect.fn("PluginStore.check")(function* (target, cache, options) {
6434
- if (!target.mutable) return { _tag: "immutable" };
6435
- const entry = yield* resolve$2(target, cache, options.from);
6436
- if (entry === void 0) return { _tag: "not-installed" };
6437
- const key = yield* digest(target, cache, options.from).pipe(Effect.mapError((cause) => failure$1(target, "plugin-marker-invalid", `cannot address ${target.spec}`, cause)));
6438
- const base = root(cache);
6439
- const record = (yield* read(base)).get(key);
6440
- const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
6441
- if (options.refresh !== true && record?.checkedAt !== void 0 && now - record.checkedAt < CHECK_TTL && (record.outdated !== true || record.available !== void 0)) return record.outdated === true ? {
6442
- _tag: "outdated",
6443
- available: record.available,
6444
- ...entry.revision === void 0 ? {} : { filed: entry.revision }
6445
- } : {
6446
- _tag: "current",
6447
- ...entry.revision === void 0 ? {} : { revision: entry.revision }
6448
- };
6449
- const available = yield* options.probe(target);
6450
- if (available === void 0) return {
6451
- _tag: "current",
6452
- ...entry.revision === void 0 ? {} : { revision: entry.revision }
6453
- };
6454
- const outdated = available !== entry.revision;
6455
- yield* patch(base, key, {
6456
- checkedAt: now,
6457
- outdated,
6458
- available
6459
- });
6460
- return outdated ? {
6461
- _tag: "outdated",
6462
- available,
6463
- ...entry.revision === void 0 ? {} : { filed: entry.revision }
6464
- } : {
6465
- _tag: "current",
6466
- ...entry.revision === void 0 ? {} : { revision: entry.revision }
6467
- };
6468
- });
6408
+ const rooted = (directory, role) => {
6409
+ if (hostPath.isAbsolute(directory)) return directory;
6410
+ throw new Error(`${role} must be an absolute path, got ${JSON.stringify(directory)}`);
6411
+ };
6412
+ //#endregion
6413
+ //#region ../harness/src/plugin/npm.ts
6414
+ const Manifest$1 = Schema.Struct({ version: Schema.optional(Schema.String) });
6415
+ const Lockfile = Schema.Struct({ packages: Schema.optional(Schema.Record(Schema.String, Schema.Struct({ resolved: Schema.optional(Schema.String) }))) });
6469
6416
  /**
6470
- * Refresh a mutable target, if the network says there is anything to refresh.
6417
+ * npm's flattened config: registry, scopes, auth, and cache paths.
6471
6418
  *
6472
- * `add` with `refresh` is what does the work; this is the guard in front of it, so an `update`
6473
- * that finds nothing new neither stages nor publishes.
6419
+ * Built in one place, because two call sites deriving it separately is how the cache location
6420
+ * drifts between an install and the staleness check that is supposed to describe it.
6421
+ *
6422
+ * `dir` is where the `.npmrc` chain is read from, and it is the **host directory the caller
6423
+ * belongs to** -- never the staging directory and never the cache. A company repo keeps its
6424
+ * private registry and its scoped token in its own `.npmrc`, so a chain read anywhere else
6425
+ * resolves the plugin against the public registry and fails 404, or worse, installs a public
6426
+ * package of the same name. This is the registry half of auth; the git half is below.
6427
+ *
6428
+ * No `--prefix`. npm pins its local prefix to a `--prefix` on `argv` and stops walking, so
6429
+ * passing one would look up exactly `<dir>/.npmrc` and nothing above it -- wrong for a person
6430
+ * standing in `packages/x` whose token lives at the repository root. Without it npm walks up from
6431
+ * `cwd` to the nearest `package.json` or `node_modules` exactly as `npm` itself would if it were
6432
+ * run there, which is the only rule a user can predict.
6433
+ *
6434
+ * `--cache` is ours and is passed on `argv`, which outranks every `.npmrc`. npm's `flatten`
6435
+ * derives `_cacache`, `_npx` and `_tuf` from that one value, so naming the parent is enough --
6436
+ * and overriding `flat.cache` afterwards would redirect one of the three and leave `_tuf` pointing
6437
+ * at the user's home. `--home` exists to make a run self-contained; anything reached by an ambient
6438
+ * default is a test reading the developer's real machine.
6439
+ *
6440
+ * **Nothing here names an `env`.** npm hands git `opts.env` when one is present and falls back to
6441
+ * `{ ...gitDefaults, ...process.env }` when it is not, so leaving it out is what lets a git source
6442
+ * reach a private remote at all: `SSH_AUTH_SOCK` finds the agent, `HOME` finds the `~/.gitconfig`
6443
+ * that names the credential helper, and `PATH` finds `git`. Setting `env` to anything -- even to
6444
+ * a copy of `process.env` -- would be a standing invitation to prune it later and break private
6445
+ * git silently, months from the change. §15 Q2.
6474
6446
  */
6475
- const update = Effect.fn("PluginStore.update")(function* (target, cache, options) {
6476
- const current = yield* resolve$2(target, cache, options.from);
6477
- if (current === void 0) return { _tag: "not-installed" };
6478
- if ((yield* check(target, cache, {
6479
- refresh: true,
6480
- probe: options.probe,
6481
- from: options.from
6482
- }))._tag !== "outdated") return {
6483
- _tag: "unchanged",
6484
- entry: current
6485
- };
6486
- const added = yield* add(target, cache, {
6487
- refresh: true,
6488
- validate: options.validate,
6489
- from: options.from,
6490
- ...options.runner === void 0 ? {} : { runner: options.runner }
6491
- });
6492
- return added.entry.generation === current.generation ? {
6493
- _tag: "unchanged",
6494
- entry: current
6495
- } : {
6496
- _tag: "updated",
6497
- entry: added.entry,
6498
- ...added.validated === void 0 ? {} : { validated: added.validated }
6499
- };
6500
- });
6501
- Effect.fn("PluginStore.remove")(function* (target, cache, from) {
6502
- const { key, directory } = yield* entryDir(target, cache, from);
6503
- const existed = yield* fileSystem.exists(directory).pipe(Effect.orElseSucceed(() => false));
6504
- yield* fileSystem.remove(directory, {
6505
- recursive: true,
6506
- force: true
6507
- }).pipe(Effect.ignore);
6508
- yield* drop(root(cache), key);
6509
- return existed;
6447
+ const options = (dir, cache) => Effect.suspend(() => {
6448
+ const root = rooted(dir, "the directory a plugin install reads its .npmrc chain from");
6449
+ const store = rooted(cache, "the npm cache");
6450
+ return npmOptions(root, store);
6510
6451
  });
6511
- Effect.fn("PluginStore.all")(function* (cache) {
6512
- const base = root(cache);
6513
- const slugs = yield* fileSystem.readDirectory(base).pipe(Effect.orElseSucceed(() => []));
6514
- const found = [];
6515
- for (const slug of slugs) {
6516
- const digests = yield* fileSystem.readDirectory(hostPath.join(base, slug)).pipe(Effect.orElseSucceed(() => []));
6517
- for (const key of digests) {
6518
- const entry = yield* active(hostPath.join(base, slug, key), key);
6519
- if (entry !== void 0) found.push(entry);
6520
- }
6452
+ const npmOptions = (dir, cache) => Effect.gen(function* () {
6453
+ const config = yield* Effect.tryPromise(async () => {
6454
+ const { default: Config } = await import("./lib-BCUUrctP.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1));
6455
+ const { definitions, flatten, nerfDarts, shorthands } = (await import("./definitions-XbglYT7Q.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1))).default;
6456
+ const created = new Config({
6457
+ npmPath: fileURLToPath(new URL("..", import.meta.url)),
6458
+ cwd: dir,
6459
+ env: { ...process.env },
6460
+ argv: [
6461
+ process.execPath,
6462
+ process.execPath,
6463
+ "--cache",
6464
+ hostPath.join(cache, "npm")
6465
+ ],
6466
+ execPath: process.execPath,
6467
+ platform: process.platform,
6468
+ definitions,
6469
+ flatten,
6470
+ nerfDarts,
6471
+ shorthands,
6472
+ warn: false
6473
+ });
6474
+ await created.load();
6475
+ return created;
6476
+ }).pipe(Effect.catch((cause) => Effect.logWarning(`cannot load npm configuration from ${dir}; npm defaults apply`, cause).pipe(Effect.as(void 0))));
6477
+ if (config === void 0) return { allowGit: "root" };
6478
+ for (const kind of [
6479
+ "project",
6480
+ "user",
6481
+ "global"
6482
+ ]) {
6483
+ const loaded = config.data.get(kind);
6484
+ if (loaded?.loadError !== void 0 && loaded.loadError.code !== "ENOENT") yield* Effect.logWarning(`cannot read npm configuration ${loaded.source ?? `for ${kind}`}; npm defaults apply`, loaded.loadError);
6521
6485
  }
6522
- return found;
6523
- });
6524
- const Id = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*){2}$/));
6525
- const Definition = Schema.Struct({
6526
- id: Id,
6527
- kind: Schema.Literals(Object.keys(plugin_exports.domains)),
6528
- events: Schema.optional(Schema.Array(Schema.declare((value) => Predicate.hasProperty(value, "type") && Predicate.isString(value.type)))),
6529
- setup: Schema.declare((value) => Predicate.isFunction(value))
6530
- });
6531
- /** Every plugin failure says which file to go and edit, when there is one. */
6532
- const where = (origin) => origin.file === void 0 ? {} : { file: origin.file };
6533
- const detail = (cause) => cause instanceof Error ? cause.message : typeof cause === "string" ? cause : String(cause);
6534
- /** A reference that does not name anything loadable. */
6535
- const sourceFailure = (origin, cause) => Schema.is(SourceError)(cause) ? cause : new SourceError({
6536
- reason: "plugin-not-found",
6537
- reference: origin.reference,
6538
- ...where(origin),
6539
- message: detail(cause)
6486
+ return {
6487
+ ...config.flat,
6488
+ allowGit: "root"
6489
+ };
6540
6490
  });
6541
- /** Node's own code for a failure, when it set one. */
6542
- const codeOf = (cause) => Predicate.hasProperty(cause, "code") && Predicate.isString(cause.code) ? cause.code : void 0;
6491
+ const PUBLIC_REGISTRY = "https://registry.npmjs.org/";
6492
+ /** Stable, credential-free registry identity used to address registry artifacts in the store. */
6493
+ const registry = Effect.fn("PluginNpm.registry")(function* (target, cache, from) {
6494
+ const flat = yield* options(from, cache);
6495
+ const scope = target.name.startsWith("@") ? target.name.slice(0, target.name.indexOf("/")) : void 0;
6496
+ const configured = (scope === void 0 ? void 0 : flat[`${scope}:registry`]) ?? flat.registry ?? PUBLIC_REGISTRY;
6497
+ if (typeof configured !== "string") return yield* new InstallError({
6498
+ reason: "plugin-resolve-failed",
6499
+ reference: target.spec,
6500
+ message: `npm registry for ${target.name} is not a URL`
6501
+ });
6502
+ return yield* Effect.try({
6503
+ try: () => {
6504
+ const value = new URL(configured);
6505
+ value.username = "";
6506
+ value.password = "";
6507
+ value.hash = "";
6508
+ return value.href;
6509
+ },
6510
+ catch: (cause) => new InstallError({
6511
+ reason: "plugin-resolve-failed",
6512
+ reference: target.spec,
6513
+ message: `npm registry for ${target.name} is not a valid URL: ${configured}`,
6514
+ cause
6515
+ })
6516
+ });
6517
+ });
6518
+ /** A 40- or 64-hex commit out of the `resolved` URL arborist writes into the lockfile. */
6519
+ const commitIn = (resolved) => resolved?.match(/#([a-f0-9]{40}|[a-f0-9]{64})(?=::|$)/i)?.[1];
6543
6520
  /**
6544
- * A module that would not import.
6545
- *
6546
- * Two of these get their own reason, because the remedy differs and that is the whole value of a
6547
- * slug. A missing dependency means the plugin is fine and its `node_modules` is not, which a
6548
- * person fixes in the package rather than in their settings. An uncompiled one means the plugin
6549
- * shipped its sources: Node refuses to strip types under `node_modules`, and installing a plugin
6550
- * runs no lifecycle scripts, so a `prepare` build never happens either -- the author has to ship
6551
- * the build output, and nothing the user can do to their settings will help.
6521
+ * The commit a git install actually landed on, read back from the lockfile arborist wrote.
6552
6522
  *
6553
- * `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` is matched by code rather than by message: it is
6554
- * Node's documented identifier and exact, where the wording is free to change under us. Only an
6555
- * installed plugin can reach it -- a local path lives outside `node_modules`, where Node strips
6556
- * types happily, which is what keeps the single-file dev workflow working.
6523
+ * Both spellings are tried because which one exists depends on whether the tree was saved.
6557
6524
  */
6558
- const importFailure = (origin, cause) => new LoadError({
6559
- reason: codeOf(cause) === "ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING" ? "plugin-not-compiled" : /cannot find (package|module)/i.test(detail(cause)) ? "plugin-missing-dependency" : "plugin-import-failed",
6560
- reference: origin.reference,
6561
- ...where(origin),
6562
- message: detail(cause),
6563
- cause
6564
- });
6565
- /** A module that imported and is not a plugin. */
6566
- const definitionFailure = (origin, cause, id) => new LoadError({
6567
- reason: "plugin-invalid-definition",
6568
- reference: origin.reference,
6569
- ...where(origin),
6570
- message: detail(cause),
6571
- ...id === void 0 ? {} : { id },
6572
- cause
6573
- });
6574
- const validate = Effect.fn("PluginLoader.validate")(function* (input, origin, builtin = false) {
6575
- yield* Schema.decodeUnknownEffect(Definition)(input).pipe(Effect.mapError((cause) => definitionFailure(origin, cause)));
6576
- const plugin = input;
6577
- if (!builtin && plugin.id.startsWith("codework.")) return yield* definitionFailure(origin, /* @__PURE__ */ new Error("codework namespace is reserved for builtins"), plugin.id);
6578
- return plugin;
6579
- });
6580
- const Manifest = Schema.Struct({
6581
- name: Schema.optional(Schema.String),
6582
- exports: Schema.optional(Schema.Unknown)
6583
- });
6584
- const localUrl = Effect.fn("PluginLoader.localUrl")(function* (location, origin) {
6585
- const escapes = (message) => new SourceError({
6586
- reason: "plugin-escapes-root",
6587
- reference: origin.reference,
6588
- ...where(origin),
6589
- message
6590
- });
6591
- if ((yield* fileSystem.stat(location)).type !== "Directory") return { url: yield* Effect.try(() => resolveModule(location, hostPath.dirname(location))) };
6592
- const manifestPath = hostPath.join(location, "package.json");
6593
- if (!(yield* fileSystem.exists(manifestPath))) return { url: yield* Effect.try(() => resolveModule("./index", location)) };
6594
- const manifest = yield* fileSystem.readFileString(manifestPath).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest))));
6595
- if (manifest.exports !== void 0) {
6596
- const name = manifest.name;
6597
- if (!name) return yield* escapes("a local package with exports must declare its name");
6598
- const url = yield* Effect.try(() => resolveModule(name, location));
6599
- const resolved = yield* fileSystem.realPath(fileURLToPath(url));
6600
- if (hostPath.relative(yield* fileSystem.realPath(location), resolved).startsWith("..")) return yield* escapes("package export escapes its root");
6601
- return {
6602
- url: pathToFileURL(resolved).href,
6603
- name
6604
- };
6525
+ const commit = Effect.fn("PluginNpm.commit")(function* (root, name) {
6526
+ for (const file of [hostPath.join(root, "package-lock.json"), hostPath.join(root, "node_modules", ".package-lock.json")]) {
6527
+ const parsed = yield* fileSystem.readFileString(file).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Lockfile))), Effect.option);
6528
+ if (Option.isNone(parsed)) continue;
6529
+ const found = commitIn(parsed.value.packages?.[`node_modules/${name}`]?.resolved);
6530
+ if (found !== void 0) return found;
6605
6531
  }
6606
- const url = yield* Effect.try(() => resolveModule(createRequire(pathToFileURL(manifestPath)).resolve(location), location));
6607
- return manifest.name === void 0 ? { url } : {
6608
- url,
6609
- name: manifest.name
6610
- };
6611
6532
  });
6533
+ /** Node error codes that mean "nothing here", as opposed to "this package is broken". */
6534
+ const notFound = /* @__PURE__ */ new Set([
6535
+ "ENOENT",
6536
+ "ENOTDIR",
6537
+ "MODULE_NOT_FOUND",
6538
+ "ERR_MODULE_NOT_FOUND",
6539
+ "ERR_PACKAGE_PATH_NOT_EXPORTED",
6540
+ "ERR_UNSUPPORTED_DIR_IMPORT"
6541
+ ]);
6612
6542
  /**
6613
- * Install a package into the store, importing and validating it **while it is still staged**.
6543
+ * The module to import out of an installed package.
6614
6544
  *
6615
- * The validation is passed down rather than done afterwards for one reason: the store publishes by
6616
- * rename, so a package that turns out not to be a plugin must be rejected before it is published.
6617
- * Publishing first would make the broken generation the newest marked one, which `resolve` returns
6618
- * forever after -- and re-running `add` would hit the fast path and return it again.
6545
+ * Ordered subpaths through the package's own `exports`, so a package that publishes a dedicated
6546
+ * `./plugin` entry is honoured and one that does not falls back to its main.
6547
+ *
6548
+ * Only "not found" is tolerated. Swallowing every error would turn a package that is broken --
6549
+ * a syntax error in its manifest, a permission problem -- into "no entrypoint", which is the
6550
+ * wrong diagnosis in the one place a person needs the right one.
6619
6551
  */
6620
- const install = Effect.fn("PluginLoader.install")(function* (target, cache, origin, options) {
6621
- const added = yield* add(target, cache, {
6622
- from: anchor$1(origin.file, options.hostDir),
6623
- validate: (fetched) => imported(url(fetched.entrypoint), origin, options.import)
6552
+ const entrypoint = (directory, name) => Effect.gen(function* () {
6553
+ for (const subpath of ["", "plugin"]) {
6554
+ const specifier = name === void 0 ? hostPath.join(directory, subpath === "" ? "index" : subpath) : hostPath.join(name, subpath);
6555
+ const resolved = yield* Effect.try(() => resolveModule(specifier, directory)).pipe(Effect.asSome, Effect.catchIf((error) => notFound.has(String(error.cause?.code)), () => Effect.succeed(Option.none())), Effect.mapError((error) => new InstallError({
6556
+ reason: "plugin-no-entrypoint",
6557
+ reference: name ?? directory,
6558
+ message: `cannot resolve an entrypoint in ${directory}`,
6559
+ cause: error
6560
+ })));
6561
+ if (Option.isSome(resolved)) return fileURLToPath(resolved.value);
6562
+ }
6563
+ return yield* new InstallError({
6564
+ reason: "plugin-no-entrypoint",
6565
+ reference: name ?? directory,
6566
+ message: `no plugin entrypoint in ${directory}`
6624
6567
  });
6625
- return {
6626
- url: added.entry.url,
6627
- ...added.entry.version === void 0 ? {} : { version: added.entry.version },
6628
- generation: added.entry.generation,
6629
- ...added.validated === void 0 ? {} : { plugin: added.validated }
6630
- };
6631
6568
  });
6632
- /**
6633
- * The directory whose `.npmrc` chain names the artifact a reference resolves to: the project that
6634
- * declared it. A settings file sits in `<root>/.codework/`, but npm reads a project `.npmrc` at the
6635
- * package boundary discovered upward from the chain's start -- the root itself -- so anchoring at
6636
- * the file's own directory would miss it. Absent a declaring file the host context stands in.
6637
- */
6638
- const anchor$1 = (file, fallback) => {
6639
- if (file === void 0) return fallback;
6640
- const directory = hostPath.dirname(file);
6641
- return hostPath.basename(directory) === ".codework" ? hostPath.dirname(directory) : directory;
6642
- };
6643
- /** The default installer, or the one the caller injected. */
6644
- const installer = (target, origin, options) => options.install === void 0 ? install(target, options.cache, origin, options) : options.install(target, options.cache, anchor$1(origin.file, options.hostDir));
6645
- /** Import a module and confirm it default-exports a plugin. */
6646
- const imported = Effect.fn("PluginLoader.imported")(function* (url, origin, load) {
6647
- const module = yield* Effect.tryPromise({
6648
- try: () => (load ?? importModule)(url),
6649
- catch: (cause) => importFailure(origin, cause)
6569
+ const reify = Effect.fn("PluginNpm.reify")(function* ({ target, into, cache, from }) {
6570
+ const flat = yield* options(from, cache);
6571
+ const { Arborist } = yield* Effect.promise(() => import("./lib-CNtGmPS7.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1)));
6572
+ const settings = {
6573
+ ...flat,
6574
+ ...target.mutable ? {
6575
+ preferOnline: true,
6576
+ noGitRevCache: true
6577
+ } : {},
6578
+ audit: false
6579
+ };
6580
+ const arborist = new Arborist({
6581
+ ...settings,
6582
+ path: into,
6583
+ binLinks: true,
6584
+ progress: false,
6585
+ savePrefix: "",
6586
+ ignoreScripts: true
6587
+ });
6588
+ return yield* Effect.tryPromise({
6589
+ try: () => arborist.reify({
6590
+ ...settings,
6591
+ add: [target.spec],
6592
+ update: target.mutable,
6593
+ save: true,
6594
+ saveType: "prod"
6595
+ }),
6596
+ catch: (cause) => new InstallError({
6597
+ reason: "plugin-fetch-failed",
6598
+ reference: target.spec,
6599
+ message: `cannot install ${target.spec}`,
6600
+ cause
6601
+ })
6650
6602
  });
6651
- return yield* validate(Predicate.isObject(module) && "default" in module ? module.default : void 0, origin);
6652
6603
  });
6653
6604
  /**
6654
- * Import one entrypoint and confirm it is a plugin, without touching the store.
6605
+ * Fill `into` with the package `target` names, and report what landed there.
6655
6606
  *
6656
- * This is what a caller hands the store as its `validate`: the artifact is checked while it is
6657
- * still staged, so a package that turns out not to be a plugin is never published.
6658
- */
6659
- const definition$1 = (entrypoint, reference) => imported(url(entrypoint), {
6660
- index: 0,
6661
- reference
6662
- });
6663
- /**
6664
- * Install and import one reference, and report what it turned out to be, without selecting it.
6665
- * `codework plugin add` uses this to prove a package really is a plugin before it writes the
6666
- * reference into a settings file: the failure a user gets is the one they can act on — a bad
6667
- * spec, an install that did not resolve, a module that exports no plugin — rather than a broken
6668
- * configuration that only fails at the next run.
6607
+ * A local target downloads nothing: the files on disk are already the answer, and a local plugin
6608
+ * is never copied into the store.
6669
6609
  */
6670
- const inspect = Effect.fn("PluginLoader.inspect")(function* (reference, options) {
6671
- const origin = {
6672
- index: 0,
6673
- reference,
6674
- file: options.file
6610
+ const download = Effect.fn("PluginNpm.download")(function* (input) {
6611
+ const { target, into, cache, from, runner = reify } = input;
6612
+ if (target.kind === "local") return {
6613
+ directory: target.path,
6614
+ name: hostPath.basename(target.path),
6615
+ entrypoint: yield* entrypoint(target.path)
6675
6616
  };
6676
- const target = yield* parse$2(reference, options.hostDir);
6677
- const loaded = yield* load$3(target, origin, options);
6617
+ const root = yield* fileSystem.realPath(into).pipe(Effect.mapError((cause) => new InstallError({
6618
+ reason: "plugin-fetch-failed",
6619
+ reference: target.spec,
6620
+ message: `cannot resolve the staging directory ${into}`,
6621
+ cause
6622
+ })));
6623
+ const node = (yield* runner({
6624
+ target,
6625
+ into: root,
6626
+ cache,
6627
+ from
6628
+ })).edgesOut.values().next().value?.to;
6629
+ if (node === void 0) return yield* new InstallError({
6630
+ reason: "plugin-fetch-failed",
6631
+ reference: target.spec,
6632
+ message: `${target.spec} installed nothing`
6633
+ });
6634
+ const manifest = yield* fileSystem.readFileString(hostPath.join(node.path, "package.json")).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest$1))), Effect.option);
6635
+ const version = Option.isNone(manifest) ? void 0 : manifest.value.version;
6636
+ const revision = target.kind === "registry" ? version : (yield* commit(root, node.name)) ?? (yield* noCommit(target.spec));
6678
6637
  return {
6679
- id: loaded.plugin.id,
6680
- ...loaded.version === void 0 ? {} : { version: loaded.version },
6681
- ...loaded.name === void 0 ? {} : { name: loaded.name }
6638
+ directory: node.path,
6639
+ name: node.name,
6640
+ ...version === void 0 ? {} : { version },
6641
+ ...revision === void 0 ? {} : { revision },
6642
+ entrypoint: yield* entrypoint(node.path, node.name)
6682
6643
  };
6683
6644
  });
6684
- const load$3 = Effect.fn("PluginLoader.load")(function* (target, origin, options) {
6685
- const installed = yield* target.kind === "local" ? localUrl(target.path, origin).pipe(Effect.mapError((cause) => sourceFailure(origin, cause))) : installer(target, origin, options).pipe(Effect.mapError((cause) => origin.file === void 0 ? cause : declaredIn(cause, origin.file)));
6686
- const address = target.kind === "local" && options.reload !== void 0 && options.reload > 0 ? `${installed.url}?reload=${options.reload}` : installed.url;
6687
- return {
6688
- plugin: installed.plugin ?? (yield* imported(address, origin, options.import)),
6689
- source: origin.reference,
6690
- ..."version" in installed && installed.version !== void 0 ? { version: installed.version } : {},
6691
- ..."generation" in installed && installed.generation !== void 0 ? { generation: installed.generation } : {},
6692
- ...installed.name === void 0 ? {} : { name: installed.name }
6645
+ const noCommit = (spec) => new InstallError({
6646
+ reason: "plugin-no-commit",
6647
+ reference: spec,
6648
+ message: `cannot resolve a commit for ${spec}`
6649
+ });
6650
+ /**
6651
+ * What a mutable target points at right now, without installing it.
6652
+ *
6653
+ * `preferOnline` and `noGitRevCache` are the whole point: without them pacote answers from its own
6654
+ * metadata cache, and a staleness check that reads a cache is not a staleness check. An immutable
6655
+ * target skips the call entirely -- a version and a SHA cannot move.
6656
+ */
6657
+ const probe = Effect.fn("PluginNpm.probe")(function* (target, cache, from) {
6658
+ if (target.kind === "local" || !target.mutable) return void 0;
6659
+ const opts = {
6660
+ ...yield* options(from, cache),
6661
+ preferOnline: true,
6662
+ noGitRevCache: true,
6663
+ _isRoot: true
6693
6664
  };
6665
+ const { default: pacote } = yield* Effect.promise(() => import("./lib-DHoZNIet.mjs").then((n) => /* @__PURE__ */ __toESM(n.t(), 1)));
6666
+ return yield* Effect.tryPromise({
6667
+ try: async () => target.kind === "registry" ? (await pacote.manifest(target.spec, opts)).version : commitIn(await pacote.resolve(target.spec, opts)),
6668
+ catch: (cause) => new InstallError({
6669
+ reason: "plugin-resolve-failed",
6670
+ reference: target.spec,
6671
+ message: `cannot reach ${target.spec}`,
6672
+ cause
6673
+ })
6674
+ });
6694
6675
  });
6695
- /**
6696
- * The version-free, location-anchored spelling of a reference.
6697
- *
6698
- * Two references naming the same module compare equal through it: `@acme/x@1.2.0` and `@acme/x`
6699
- * are one package, and `./plugins/x.ts` is the file it resolves to from the directory that
6700
- * declared it. Pure -- nothing is installed, imported or read.
6701
- */
6702
- const canonical = (reference, hostDir) => Effect.map(parse$2(reference, hostDir), canonical$1);
6676
+ /** A `file:` URL for an entrypoint, which is what the module loader takes. */
6677
+ const url = (entrypoint) => pathToFileURL(entrypoint).href;
6703
6678
  //#endregion
6704
- //#region ../harness/src/plugin/catalog.ts
6679
+ //#region ../harness/src/plugin/source.ts
6680
+ /** A committish that names one artifact rather than a moving branch: an unabbreviated SHA. */
6681
+ const isCommit = (committish) => typeof committish === "string" && /^[a-f0-9]{40}$|^[a-f0-9]{64}$/i.test(committish);
6705
6682
  /**
6706
- * A definition carries `setup`; a patch carries `plugin` or `package` and no `setup`. Checking
6707
- * `setup` first matters because `Plugin` permits extra properties, so a definition holding its
6708
- * own `plugin` property must not be read as configuration.
6709
- *
6710
- * e.g
6711
- * "acme/tool@1.2.0" → module (load me)
6712
- * define({id, kind, setup, ...}) → module (I AM the definition — embedder code only)
6713
- * { plugin: "x", enabled: false } → config (don't load — adjust something already loaded)
6714
- *
6715
- * setup present → definition, always (even with a stray plugin: key)
6716
- * setup absent + plugin:/package: present → config patch
6717
- * setup absent + neither key → malformed
6683
+ * A short, readable name for a git source, used to make a store path recognisable to a human
6684
+ * reading `ls`. It is never an identity: two remotes can slug alike, and the digest is what
6685
+ * separates them.
6718
6686
  */
6719
- const isPatch = (reference) => Predicate.isObject(reference) && !Predicate.isFunction(reference) && !("setup" in reference) && ("plugin" in reference || "package" in reference);
6687
+ const slug = (spec) => {
6688
+ const withoutCommittish = spec.split("#")[0] ?? spec;
6689
+ return (() => {
6690
+ try {
6691
+ return decodeURIComponent(withoutCommittish);
6692
+ } catch {
6693
+ return withoutCommittish;
6694
+ }
6695
+ })().replace(/\.git$/i, "").split(/[/:\\]/).at(-1)?.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repository";
6696
+ };
6697
+ /** Whether a reference names a place on disk rather than something to fetch. */
6698
+ const isPath = (reference) => reference.startsWith("file:") || reference.startsWith("./") || reference.startsWith("../") || hostPath.isAbsolute(reference);
6720
6699
  /**
6721
- * The **load pass**: store lookup and ESM import, never the network.
6722
- *
6723
- * Runs at boot, at `reload`, and at any exchange that finds a reference it cannot satisfy. It
6724
- * deliberately knows nothing about `enabled`, options or order -- those are data over modules that
6725
- * are already loaded, they cost a walk of a short array, and they belong to every exchange.
6700
+ * `from` is the directory a relative spec resolves against, and the caller picks it:
6726
6701
  *
6727
- * Loading is keyed by what a reference resolves to, so naming one module twice imports it once.
6702
+ * - a **plugin entry** anchors to the directory holding the file that declared it --
6703
+ * `<root>/.codework/` for a project entry, `<home>/` for a user one, the named directory for
6704
+ * `--user-config-dir`. The rule is about the file, not the project.
6705
+ * - a **CLI argument** anchors to the process's startup directory, because that is where the
6706
+ * person typed it.
6728
6707
  */
6729
- const load$2 = Effect.fn("PluginCatalog.load")(function* (references, options) {
6730
- const plugins = /* @__PURE__ */ new Map();
6731
- const aliases = /* @__PURE__ */ new Map();
6732
- const versions = /* @__PURE__ */ new Map();
6733
- const origins = /* @__PURE__ */ new Map();
6734
- const seen = /* @__PURE__ */ new Map();
6735
- const remember = (plugin, names, from) => {
6736
- plugins.set(plugin.id, plugin);
6737
- aliases.set(plugin.id, plugin.id);
6738
- for (const name of names) aliases.set(name, plugin.id);
6739
- if (from?.version !== void 0) versions.set(plugin.id, from.version);
6740
- if (from !== void 0) origins.set(plugin.id, from.origin);
6741
- };
6742
- /** Source metadata exists for diagnostics; a silent ID replacement is where it earns that. */
6743
- const note = (plugin, source) => plugins.has(plugin.id) ? Effect.logDebug(`plugin ${plugin.id} redefined by ${source}; the earlier definition is discarded`) : Effect.void;
6744
- for (const builtin of options.builtins) remember(yield* validate(builtin, {
6745
- index: -1,
6746
- reference: builtin.id
6747
- }, true), []);
6748
- for (const [index, reference] of references.entries()) {
6749
- if (isPatch(reference)) continue;
6750
- const declared = typeof reference === "string" ? reference : Predicate.hasProperty(reference, "id") ? reference.id : void 0;
6751
- const named = typeof declared === "string" ? declared : `<plugin object #${index}>`;
6752
- const origin = {
6753
- index,
6754
- reference: named,
6755
- file: options.declared?.get(named)
6708
+ const parse$2 = (reference, from) => Effect.gen(function* () {
6709
+ const fail = (reason, message) => new SourceError({
6710
+ reason,
6711
+ reference,
6712
+ message
6713
+ });
6714
+ rooted(from, "the directory a plugin reference anchors to");
6715
+ if (reference.startsWith("file:")) {
6716
+ if (!reference.startsWith("file:///")) return yield* fail("plugin-unsupported-source", `not an absolute file URL: ${reference}`);
6717
+ return {
6718
+ kind: "local",
6719
+ path: fileURLToPath(reference)
6756
6720
  };
6757
- if (typeof reference !== "string") {
6758
- if (typeof declared === "string" && plugins.get(declared) === reference) continue;
6759
- const plugin = yield* validate(reference, origin);
6760
- yield* note(plugin, "a supplied object");
6761
- remember(plugin, []);
6762
- continue;
6763
- }
6764
- const target = yield* parse$2(reference, options.hostDir);
6765
- const key = target.kind === "local" ? target.path : target.spec;
6766
- const already = seen.get(key);
6767
- if (already !== void 0) {
6768
- aliases.set(reference, already);
6769
- continue;
6770
- }
6771
- const definition = yield* load$3(target, origin, options);
6772
- yield* note(definition.plugin, definition.version === void 0 ? key : `${key}@${definition.version}`);
6773
- const names = target.kind === "local" ? [target.path, ...definition.name === void 0 ? [] : [definition.name]] : [canonical$1(target), target.spec];
6774
- remember(definition.plugin, [reference, ...names], {
6775
- ...definition.version === void 0 ? {} : { version: definition.version },
6776
- origin: {
6777
- reference,
6778
- ...definition.generation === void 0 ? {} : { generation: definition.generation },
6779
- ...origin.file === void 0 ? {} : { file: origin.file }
6780
- }
6781
- });
6782
- seen.set(key, definition.plugin.id);
6783
6721
  }
6784
- return {
6785
- plugins,
6786
- aliases,
6787
- versions,
6788
- origins
6722
+ const expanded = expandTilde(reference, hostPath);
6723
+ if (isPath(expanded)) return {
6724
+ kind: "local",
6725
+ path: hostPath.resolve(from, expanded)
6789
6726
  };
6727
+ const parsed = yield* Effect.try({
6728
+ try: () => npa(reference),
6729
+ catch: () => fail("plugin-unsupported-source", `cannot parse plugin source: ${reference}`)
6730
+ });
6731
+ if (parsed.type === "git") {
6732
+ const { gitSubdir } = parsed;
6733
+ return {
6734
+ kind: "git",
6735
+ slug: slug(reference),
6736
+ spec: reference,
6737
+ ...parsed.gitCommittish == null ? {} : { committish: parsed.gitCommittish },
6738
+ ...gitSubdir == null ? {} : { subdir: gitSubdir },
6739
+ mutable: !isCommit(parsed.gitCommittish)
6740
+ };
6741
+ }
6742
+ if (parsed.name !== null && [
6743
+ "version",
6744
+ "range",
6745
+ "tag"
6746
+ ].includes(parsed.type)) {
6747
+ const omitted = parsed.raw === parsed.name || parsed.raw === `${parsed.name}@`;
6748
+ return {
6749
+ kind: "registry",
6750
+ name: parsed.name,
6751
+ spec: `${parsed.name}@${omitted ? "latest" : parsed.rawSpec}`,
6752
+ mutable: parsed.type !== "version"
6753
+ };
6754
+ }
6755
+ return yield* fail("plugin-unsupported-source", `unsupported plugin source: ${reference}`);
6790
6756
  });
6791
6757
  /**
6792
- * The **config pass**: pure data over modules that are already loaded, run at every exchange.
6793
- *
6794
- * An array entry is a **module** — a definition object, a path, a `file:` URL, or a package
6795
- * spec. It takes the position it is written at, so naming a module again moves it.
6796
- *
6797
- * An object entry is **configuration** for a plugin an earlier entry (or the built-in list)
6798
- * already selected, addressed by its ID (`plugin`) or the module string (`package`) that selected
6799
- * it. It never loads, installs or reorders anything, and a name matching nothing in the selection
6800
- * is ignored rather than failing the boot — a typo costs a debug line, never a fetch.
6758
+ * What two references must share to be the same plugin, without installing either.
6801
6759
  *
6802
- * Nothing here touches the disk, which is the whole point of the split: an `options` edit, an
6803
- * `enabled` flip or a reordering applies at the next exchange without importing anything.
6760
+ * A local path is the file it resolves to; a package is its name, so `@acme/x@1` and `@acme/x@2`
6761
+ * are one package at two versions. Pure -- nothing is installed, imported or read.
6804
6762
  */
6805
- const select = Effect.fn("PluginCatalog.select")(function* (references, pool) {
6806
- const operations = /* @__PURE__ */ new Map();
6807
- const missing = [];
6808
- const choose = (id, index) => operations.set(id, {
6809
- enabled: true,
6810
- index,
6811
- options: operations.get(id)?.options ?? {}
6812
- });
6813
- for (const [index, reference] of references.entries()) {
6814
- if (isPatch(reference)) {
6815
- const named = reference.plugin ?? reference.package;
6816
- const origin = {
6817
- index,
6818
- reference: typeof named === "string" ? named : `<plugin entry #${index}>`
6819
- };
6820
- const invalid = reference.plugin !== void 0 && reference.package !== void 0 ? "names both `plugin` and `package`" : typeof named !== "string" || named.length === 0 ? "names neither `plugin` nor `package`" : reference.enabled !== void 0 && typeof reference.enabled !== "boolean" ? "`enabled` is not a boolean" : reference.options !== void 0 && !isRecord$1(reference.options) ? "`options` is not a plain object" : void 0;
6821
- if (invalid !== void 0) return yield* definitionFailure(origin, /* @__PURE__ */ new Error(`plugin entry ${invalid}`));
6822
- const id = reference.plugin === void 0 ? pool.aliases.get(reference.package) : reference.plugin;
6823
- const operation = id === void 0 ? void 0 : operations.get(id);
6824
- if (id === void 0 || operation === void 0) {
6825
- yield* Effect.logDebug(`plugin configuration ignored: nothing selected under ${named}`);
6826
- continue;
6827
- }
6828
- operations.set(id, {
6829
- enabled: reference.enabled ?? operation.enabled,
6830
- index: operation.index,
6831
- options: reference.options ?? operation.options
6832
- });
6833
- continue;
6834
- }
6835
- if (typeof reference !== "string") {
6836
- const declared = Predicate.hasProperty(reference, "id") ? reference.id : void 0;
6837
- if (typeof declared === "string" && pool.plugins.has(declared)) choose(declared, index);
6838
- continue;
6839
- }
6840
- const id = pool.aliases.get(reference);
6841
- if (id === void 0) missing.push(reference);
6842
- else choose(id, index);
6843
- }
6844
- const selection = [];
6845
- for (const [id, operation] of operations) {
6846
- if (!operation.enabled) continue;
6847
- const plugin = pool.plugins.get(id);
6848
- if (plugin !== void 0) selection.push({
6849
- plugin,
6850
- options: Object.freeze(operation.options),
6851
- index: operation.index
6852
- });
6763
+ const canonical$1 = (target) => {
6764
+ switch (target.kind) {
6765
+ case "local": return target.path;
6766
+ case "registry": return target.name;
6767
+ case "git": return target.spec;
6853
6768
  }
6854
- selection.sort((a, b) => (0, plugin_exports.rank)(a.plugin.kind) - (0, plugin_exports.rank)(b.plugin.kind) || a.index - b.index);
6855
- return {
6856
- selection: Object.freeze(selection.map(({ plugin, options }) => ({
6857
- plugin,
6858
- options
6859
- }))),
6860
- missing
6861
- };
6769
+ };
6770
+ //#endregion
6771
+ //#region ../harness/src/plugin/index.store.ts
6772
+ const Record_ = Schema.Struct({
6773
+ spec: Schema.String,
6774
+ source: Schema.Literals(["registry", "git"]),
6775
+ name: Schema.String,
6776
+ slug: Schema.String,
6777
+ mutable: Schema.Boolean,
6778
+ generation: Schema.Finite,
6779
+ version: Schema.optional(Schema.String),
6780
+ revision: Schema.optional(Schema.String),
6781
+ entrypoint: Schema.String,
6782
+ installedAt: Schema.Finite,
6783
+ checkedAt: Schema.optional(Schema.Finite),
6784
+ outdated: Schema.optional(Schema.Boolean),
6785
+ /** The remote revision observed at `checkedAt`; distinct from the installed `revision`. */
6786
+ available: Schema.optional(Schema.String)
6787
+ });
6788
+ const File = Schema.Struct({
6789
+ version: Schema.Finite,
6790
+ entries: Schema.Record(Schema.String, Record_)
6862
6791
  });
6792
+ const VERSION = 1;
6793
+ const NAME = "index.json";
6863
6794
  /**
6864
- * The exchange-boundary check: has the store moved under this pool?
6865
- *
6866
- * Every exchange already re-reads every settings layer from disk, and the config pass already
6867
- * walks those entries against the loaded set. Two things can be settled right there, and they are
6868
- * the same question asked twice:
6869
- *
6870
- * 1. a settings entry names a module that is not loaded, and the store holds it;
6871
- * 2. a loaded module is not the newest generation the store has.
6795
+ * Read the whole file, or start empty.
6872
6796
  *
6873
- * The second is what makes `plugin update` finish. Without it the question is "is this entry
6874
- * loaded?", the answer after an update is still yes, and the bytes just fetched sit on disk until
6875
- * something else forces an import.
6797
+ * An unreadable or unparseable index is not an error: it is a cold cache. Failing here would make
6798
+ * a corrupt accelerator break installs that do not need it.
6799
+ */
6800
+ const read = Effect.fn("PluginIndex.read")(function* (root) {
6801
+ const parsed = yield* fileSystem.readFileString(hostPath.join(root, NAME)).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(File))), Effect.option);
6802
+ if (Option.isNone(parsed) || parsed.value.version !== VERSION) return /* @__PURE__ */ new Map();
6803
+ return new Map(Object.entries(parsed.value.entries));
6804
+ });
6805
+ /** Temp file plus rename, so a reader never sees a half-written index. */
6806
+ const write = Effect.fn("PluginIndex.write")(function* (root, entries) {
6807
+ const encoded = yield* Schema.encodeEffect(Schema.fromJsonString(File))({
6808
+ version: VERSION,
6809
+ entries: Object.fromEntries(entries)
6810
+ });
6811
+ yield* fileSystem.makeDirectory(root, { recursive: true });
6812
+ const temporary = yield* fileSystem.makeTempFile({
6813
+ directory: root,
6814
+ prefix: `.${NAME}.`,
6815
+ suffix: ".tmp"
6816
+ });
6817
+ yield* fileSystem.writeFileString(temporary, encoded).pipe(Effect.andThen(fileSystem.rename(temporary, hostPath.join(root, NAME))), Effect.ensuring(fileSystem.remove(temporary, { force: true }).pipe(Effect.ignore)));
6818
+ });
6819
+ /**
6820
+ * Record one entry, keeping whatever else the file holds.
6876
6821
  *
6877
- * Returns the new pool, or nothing when nothing moved -- which is the common case, and costs one
6878
- * store lookup per filed module.
6822
+ * Ignores its own failures. A store that cannot write its accelerator still works; refusing the
6823
+ * install would be the accelerator deciding whether the install counted.
6824
+ */
6825
+ const put = Effect.fn("PluginIndex.put")(function* (root, digest, record) {
6826
+ const entries = yield* read(root);
6827
+ entries.set(digest, record);
6828
+ yield* write(root, entries).pipe(Effect.ignore);
6829
+ });
6830
+ /**
6831
+ * Merge fields into one record, if it is there.
6879
6832
  *
6880
- * **This is not a watcher.** Nothing polls and no fiber exists; it is the observation that we were
6881
- * already paying for the I/O that would tell us, and discarding the answer. Three bounds keep it
6882
- * safe: the store only and never the network, so an exchange cannot block on a registry;
6883
- * resolve-only, so it cannot install; and once, at the top of a snapshot, so an exchange already
6884
- * running is unaffected.
6833
+ * Unlocked, like every write here but the one that publishes a generation. Two processes checking
6834
+ * at once both read the whole file and both rewrite it, so one update is lost and a record added
6835
+ * in between can vanish -- and neither matters, because a lost `checkedAt` costs one network call
6836
+ * and a vanished record is a miss a scan repairs.
6885
6837
  */
6886
- const follow = Effect.fn("PluginCatalog.follow")(function* (references, pool, options, filed) {
6887
- let moved = false;
6888
- for (const reference of references) {
6889
- if (typeof reference !== "string" || pool.aliases.has(reference)) continue;
6890
- if (Option.isSome(yield* filed(reference, anchor$1(options.declared?.get(reference), options.hostDir)))) {
6891
- moved = true;
6892
- break;
6893
- }
6894
- }
6895
- if (!moved) for (const origin of pool.origins.values()) {
6896
- if (origin.generation === void 0) continue;
6897
- const current = yield* filed(origin.reference, anchor$1(origin.file, options.hostDir));
6898
- if (Option.isSome(current) && current.value.generation !== void 0 && current.value.generation > origin.generation) {
6899
- moved = true;
6900
- break;
6901
- }
6902
- }
6903
- if (!moved) return Option.none();
6904
- const retained = [];
6905
- for (const origin of pool.origins.values()) {
6906
- if (references.includes(origin.reference) || Option.isSome(yield* filed(origin.reference, anchor$1(origin.file, options.hostDir)))) {
6907
- retained.push(origin);
6908
- continue;
6909
- }
6910
- yield* Effect.logWarning(`plugin ${origin.reference} no longer resolves; it is dropped from the loaded set`);
6911
- }
6912
- const accumulated = [...Array.from(retained, (origin) => origin.reference), ...references];
6913
- const known = /* @__PURE__ */ new Map();
6914
- for (const origin of retained) if (origin.file !== void 0) known.set(origin.reference, origin.file);
6915
- for (const [reference, file] of options.declared ?? /* @__PURE__ */ new Map()) known.set(reference, file);
6916
- return Option.some(yield* load$2(accumulated, {
6917
- ...options,
6918
- declared: known
6919
- }));
6838
+ const patch = Effect.fn("PluginIndex.patch")(function* (root, digest, fields) {
6839
+ const entries = yield* read(root);
6840
+ const existing = entries.get(digest);
6841
+ if (existing === void 0) return;
6842
+ entries.set(digest, {
6843
+ ...existing,
6844
+ ...fields
6845
+ });
6846
+ yield* write(root, entries).pipe(Effect.ignore);
6920
6847
  });
6921
- Effect.fn("PluginCatalog.prepare")(function* (references, options) {
6922
- const pool = yield* load$2(references, options);
6923
- return (yield* select(references, pool)).selection;
6848
+ /** Forget one entry, for a store entry that has been removed. */
6849
+ const drop = Effect.fn("PluginIndex.drop")(function* (root, digest) {
6850
+ const entries = yield* read(root);
6851
+ if (!entries.delete(digest)) return;
6852
+ yield* write(root, entries).pipe(Effect.ignore);
6924
6853
  });
6925
6854
  //#endregion
6926
- //#region ../harness/src/plugin/builtin/prompt/default.ts
6927
- /** The default coding-agent foundation, used unless `promptCustom` replaces it. */
6928
- const foundation = `You are an expert coding assistant operating inside codework, a coding agent harness.`;
6855
+ //#region ../harness/src/plugin/lock.ts
6856
+ /** How long to wait for another holder before giving up. */
6857
+ const TIMEOUT = Duration.minutes(2);
6858
+ /** The holder refreshes the lock's mtime this often; one not refreshed for the timeout is stale. */
6859
+ const HEARTBEAT = Duration.seconds(15);
6860
+ const abandoned = (directory) => Effect.gen(function* () {
6861
+ const info = yield* fileSystem.stat(directory);
6862
+ const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
6863
+ return Option.exists(info.mtime, (mtime) => now - mtime.getTime() > Duration.toMillis(TIMEOUT));
6864
+ }).pipe(Effect.orElseSucceed(() => false));
6865
+ const heartbeat = (directory) => Effect.clockWith((clock) => clock.currentTimeMillis).pipe(Effect.flatMap((now) => fileSystem.utimes(directory, now, now)), Effect.ignore, Effect.repeat(Schedule.spaced(HEARTBEAT)));
6929
6866
  /**
6930
- * Guidelines that hold regardless of which tools are registered.
6867
+ * Claims `directory` by creating it, polling while another process holds it, and releasing it when
6868
+ * the surrounding scope closes.
6931
6869
  *
6932
- * Kept short on purpose. Every line here is spent on every request, so a line
6933
- * earns its place only if a model measurably behaves worse without it.
6870
+ * One finalizer is registered for the whole wait rather than one per 50ms poll, and a single
6871
+ * attempt is `uninterruptible` so `mkdir` and recording ownership cannot be split -- an interrupt
6872
+ * between them would leave behind a lock the finalizer does not know to remove.
6873
+ *
6874
+ * `onTimeout` names the failure, because what a caller is waiting for is the caller's to describe.
6934
6875
  */
6935
- const standingGuidelines = [
6936
- "Be concise. Report what you did and what you found, not what you are about to do.",
6937
- "Quote exact paths and command output rather than paraphrasing them.",
6938
- "If a command fails, read the error before retrying."
6939
- ];
6940
- /** Collapse whitespace so two spellings of one guideline dedupe against each other. */
6941
- const normalize = (value) => value.trim().replace(/\s+/g, " ");
6876
+ const lock = Effect.fn("PluginLock.acquire")(function* (directory, onTimeout) {
6877
+ const held = yield* Ref.make(false);
6878
+ yield* Effect.acquireRelease(Effect.void, () => Ref.get(held).pipe(Effect.flatMap((owned) => owned ? fileSystem.remove(directory, {
6879
+ recursive: true,
6880
+ force: true
6881
+ }) : Effect.void), Effect.orDie));
6882
+ if (!(yield* fileSystem.makeDirectory(directory).pipe(Effect.andThen(Ref.set(held, true)), Effect.as(true), Effect.catchIf((error) => error.reason._tag === "AlreadyExists", () => abandoned(directory).pipe(Effect.flatMap((stale) => stale ? fileSystem.remove(directory, {
6883
+ recursive: true,
6884
+ force: true
6885
+ }).pipe(Effect.as(false)) : Effect.succeed(false)))), Effect.uninterruptible).pipe(Effect.repeat({
6886
+ schedule: Schedule.spaced("50 millis"),
6887
+ until: (owned) => owned
6888
+ }), Effect.timeoutOrElse({
6889
+ duration: TIMEOUT,
6890
+ orElse: () => Effect.succeed(false)
6891
+ })))) return yield* Effect.fail(onTimeout());
6892
+ yield* Effect.forkScoped(heartbeat(directory));
6893
+ });
6894
+ //#endregion
6895
+ //#region ../harness/src/plugin/store.ts
6896
+ /** Facts about the *package*, and nothing about the plugin inside it. */
6897
+ const Marker = Schema.Struct({
6898
+ spec: Schema.String,
6899
+ name: Schema.String,
6900
+ version: Schema.optional(Schema.String),
6901
+ revision: Schema.optional(Schema.String),
6902
+ /** Relative to the generation directory, so the marker survives the publishing rename. */
6903
+ entrypoint: Schema.String,
6904
+ createdAt: Schema.Finite
6905
+ });
6906
+ const MARKER = ".complete.json";
6907
+ const STAGING = ".staging-";
6908
+ const root = (cache) => hostPath.join(cache, "plugins", "v2");
6942
6909
  /**
6943
- * Tool-contributed guidelines followed by the standing ones, normalized and
6944
- * deduplicated with first occurrence winning.
6910
+ * A readable label, never an identity: two specs that slug alike share a parent directory and
6911
+ * nothing more. Worth having because a greppable path is worth real money when debugging, and a
6912
+ * bare hash throws away every clue.
6913
+ */
6914
+ const slugOf = (target) => target.kind === "git" ? target.slug : canonical$1(target).replace(/^@/, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "package";
6915
+ /**
6916
+ * The **full** SHA-256 of the source identity: canonical spec plus effective registry for a
6917
+ * registry package, and the canonical spec alone for git.
6945
6918
  *
6946
- * Tool order is bucket order, which is the same order the index renders in and the
6947
- * same order the provider receives definitions in -- one ordering, not three.
6919
+ * Not truncated. The fast path proves a hit by checking that a marker *exists* rather than by
6920
+ * decoding it, and the index is keyed by digest alone, so a truncated digest would let a collision
6921
+ * return the wrong package silently.
6948
6922
  */
6949
- const guidelines = (tools) => {
6950
- const seen = /* @__PURE__ */ new Set();
6951
- const collected = [];
6952
- for (const line of [...tools.flatMap((tool) => tool.promptGuidelines ?? []), ...standingGuidelines]) {
6953
- const normalized = normalize(line);
6954
- if (normalized.length === 0 || seen.has(normalized)) continue;
6955
- seen.add(normalized);
6956
- collected.push(normalized);
6923
+ const digest = Effect.fn("PluginStore.digest")(function* (target, cache, from) {
6924
+ const source = target.kind === "registry" ? `${yield* registry(target, cache, from)}\0${target.spec}` : target.spec;
6925
+ return Encoding.encodeHex(yield* crypto.digest("SHA-256", new TextEncoder().encode(source)));
6926
+ });
6927
+ const entryDir = Effect.fn("PluginStore.entryDir")(function* (target, cache, from) {
6928
+ const key = yield* digest(target, cache, from);
6929
+ return {
6930
+ key,
6931
+ directory: hostPath.join(root(cache), slugOf(target), key)
6932
+ };
6933
+ });
6934
+ const failure$1 = (target, reason, message, cause) => new StoreError({
6935
+ reason,
6936
+ reference: target.spec,
6937
+ message,
6938
+ ...cause === void 0 ? {} : { cause }
6939
+ });
6940
+ /** Generation directories, oldest first. A name that is not a number is not a generation. */
6941
+ const generations = Effect.fn("PluginStore.generations")(function* (directory) {
6942
+ return (yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []))).filter((name) => /^\d+$/.test(name)).map((name) => Number(name)).sort((left, right) => left - right);
6943
+ });
6944
+ const readMarker = (directory) => fileSystem.readFileString(hostPath.join(directory, MARKER)).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Marker))), Effect.option);
6945
+ const entryOf = (marker, input) => ({
6946
+ ...marker,
6947
+ digest: input.digest,
6948
+ generation: input.generation,
6949
+ directory: input.directory,
6950
+ url: url(hostPath.resolve(input.directory, marker.entrypoint))
6951
+ });
6952
+ /** The newest generation that carries a marker. Unmarked ones are invisible: they never happened. */
6953
+ const active = Effect.fn("PluginStore.active")(function* (directory, key) {
6954
+ for (const generation of (yield* generations(directory)).reverse()) {
6955
+ const where = hostPath.join(directory, String(generation));
6956
+ const marker = yield* readMarker(where);
6957
+ if (Option.isSome(marker)) return entryOf(marker.value, {
6958
+ digest: key,
6959
+ generation,
6960
+ directory: where
6961
+ });
6957
6962
  }
6958
- return collected;
6959
- };
6963
+ });
6964
+ /** What the index files about an entry, all of it derivable from the spec and the marker. */
6965
+ const record = (target, entry, installedAt) => ({
6966
+ spec: target.spec,
6967
+ source: target.kind,
6968
+ name: entry.name,
6969
+ slug: slugOf(target),
6970
+ mutable: target.mutable,
6971
+ generation: entry.generation,
6972
+ ...entry.version === void 0 ? {} : { version: entry.version },
6973
+ ...entry.revision === void 0 ? {} : { revision: entry.revision },
6974
+ entrypoint: entry.entrypoint,
6975
+ installedAt
6976
+ });
6960
6977
  /**
6961
- * The rendered tool index.
6978
+ * Disk-only lookup. No network, no install -- this is what boot uses, and the reason booting
6979
+ * offline works.
6962
6980
  *
6963
- * `(none)` rather than an omitted section: a model told it has no tools behaves
6964
- * better than one left to infer it from silence.
6981
+ * The index is consulted first and is only ever a hint: a hit is proved by the marker still
6982
+ * existing, and anything else falls through to a scan that repairs the index on the way past.
6965
6983
  */
6966
- const toolIndex = (tools) => {
6967
- const listed = tools.filter((tool) => tool.promptSnippet !== void 0 && tool.promptSnippet.length > 0);
6968
- if (listed.length === 0) return "(none)";
6969
- return listed.map((tool) => `- ${tool.name}: ${tool.promptSnippet}`).join("\n");
6970
- };
6971
- const assemble = (input) => {
6972
- const sections = [input.promptCustom ?? foundation];
6973
- sections.push(`Available tools:\n${toolIndex(input.tools)}`);
6974
- const lines = guidelines(input.tools);
6975
- if (lines.length > 0) sections.push(`Guidelines:\n${lines.map((line) => `- ${line}`).join("\n")}`);
6976
- const append = input.promptSystemAppend?.trim();
6977
- if (append !== void 0 && append.length > 0) sections.push(append);
6978
- sections.push(`Current working directory: ${input.directory}`);
6979
- return sections.join("\n\n");
6980
- };
6984
+ const resolve$2 = Effect.fn("PluginStore.resolve")(function* (target, cache, from) {
6985
+ const { key, directory } = yield* entryDir(target, cache, from);
6986
+ const base = root(cache);
6987
+ const hinted = (yield* read(base)).get(key);
6988
+ if (hinted !== void 0) {
6989
+ const where = hostPath.join(directory, String(hinted.generation));
6990
+ const marker = yield* readMarker(where);
6991
+ if (Option.isSome(marker)) return entryOf(marker.value, {
6992
+ digest: key,
6993
+ generation: hinted.generation,
6994
+ directory: where
6995
+ });
6996
+ }
6997
+ const scanned = yield* active(directory, key);
6998
+ if (scanned === void 0) return void 0;
6999
+ yield* put(base, key, record(target, scanned, scanned.createdAt));
7000
+ return scanned;
7001
+ }, Effect.mapError((cause) => Schema.is(StoreError)(cause) || Schema.is(InstallError)(cause) ? cause : new StoreError({
7002
+ reason: "plugin-marker-invalid",
7003
+ reference: "",
7004
+ message: "cannot read the plugin store",
7005
+ cause
7006
+ })));
6981
7007
  /**
6982
- * A caller slot, awaited lazily. A throw or rejection fails the snapshot, which the
6983
- * host attributes to this plugin's id.
7008
+ * `resolve`, for a caller that needs the entry rather than an answer about it.
7009
+ *
7010
+ * `plugin remove` uses this as its installer: it has to identify what an entry names without
7011
+ * fetching anything, and a spec with nothing filed is a real answer there -- the command reports
7012
+ * it and moves on.
6984
7013
  */
6985
- const slot = (ctx, resolver) => Effect.tryPromise(() => Promise.resolve(resolver?.(ctx)));
6986
- const defaultPromptPlugin = (0, plugin_exports.define)({
6987
- id: "codework.prompt.default",
6988
- kind: "prompt",
6989
- setup: Effect.fn("DefaultPromptPlugin.setup")(function* (ctx) {
6990
- const custom = yield* slot(ctx, ctx.config.promptCustom);
6991
- const append = yield* slot(ctx, ctx.config.promptSystemAppend);
6992
- ctx.plugin.prompt.set(assemble({
6993
- tools: ctx.plugin.tools.list(),
6994
- directory: ctx.location.directory,
6995
- ...custom === void 0 ? {} : { promptCustom: custom },
6996
- ...append === void 0 ? {} : { promptSystemAppend: append }
6997
- }));
6998
- })
7014
+ const required = Effect.fn("PluginStore.required")(function* (target, cache, from) {
7015
+ const found = yield* resolve$2(target, cache, from);
7016
+ if (found === void 0) return yield* failure$1(target, "plugin-not-installed", `${target.spec} is not installed`);
7017
+ return found;
6999
7018
  });
7000
- //#endregion
7001
- //#region ../harness/src/tool/truncate.ts
7002
7019
  /**
7003
- * Shared truncation utilities for tool outputs.
7004
- *
7005
- * Truncation is based on two independent limits — whichever is hit first
7006
- * wins: a line limit and a byte limit. Lines are never split (except the bash
7007
- * tail edge case where a single line exceeds the byte limit).
7020
+ * Install if absent; hand back what is filed if present.
7008
7021
  *
7009
- * The bash tool uses {@link truncateTail} (keep the end — errors / final
7010
- * results); file readers use {@link truncateHead} (keep the beginning).
7011
- */
7012
- const DEFAULT_MAX_LINES = 2e3;
7013
- const DEFAULT_MAX_BYTES = 51200;
7014
- function splitLinesForCounting(content) {
7015
- if (content.length === 0) return [];
7016
- const lines = content.split("\n");
7017
- if (content.endsWith("\n")) lines.pop();
7018
- return lines;
7019
- }
7020
- /** Format bytes as a human-readable size. */
7021
- function formatSize(bytes) {
7022
- if (bytes < 1024) return `${bytes}B`;
7023
- if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`;
7024
- return `${(bytes / 1048576).toFixed(1)}MB`;
7025
- }
7026
- /**
7027
- * Truncate content from the tail (keep the last N lines/bytes). Suitable for
7028
- * bash output where the end matters (errors, final results). May return a
7029
- * partial first line if the last line of the original content exceeds the byte
7030
- * limit.
7022
+ * `refresh` forces past both the fast path and the double-check, which is what `update` needs and
7023
+ * nothing else should use.
7031
7024
  */
7032
- function truncateTail(content, options = {}) {
7033
- const maxLines = options.maxLines ?? 2e3;
7034
- const maxBytes = options.maxBytes ?? 51200;
7035
- const totalBytes = Buffer.byteLength(content, "utf-8");
7036
- const lines = splitLinesForCounting(content);
7037
- const totalLines = lines.length;
7038
- if (totalLines <= maxLines && totalBytes <= maxBytes) return {
7039
- content,
7040
- truncated: false,
7041
- truncatedBy: null,
7042
- totalLines,
7043
- totalBytes,
7044
- outputLines: totalLines,
7045
- outputBytes: totalBytes,
7046
- lastLinePartial: false,
7047
- firstLineExceedsLimit: false,
7048
- maxLines,
7049
- maxBytes
7050
- };
7051
- const outputLinesArr = [];
7052
- let outputBytesCount = 0;
7053
- let truncatedBy = "lines";
7054
- let lastLinePartial = false;
7055
- for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
7056
- const line = lines[i] ?? "";
7057
- const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0);
7058
- if (outputBytesCount + lineBytes > maxBytes) {
7059
- truncatedBy = "bytes";
7060
- if (outputLinesArr.length === 0) {
7061
- const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
7062
- outputLinesArr.unshift(truncatedLine);
7063
- outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
7064
- lastLinePartial = true;
7065
- }
7066
- break;
7025
+ const add = Effect.fn("PluginStore.add")(function* (target, cache, options) {
7026
+ return yield* Effect.gen(function* () {
7027
+ const { key, directory } = yield* entryDir(target, cache, options.from);
7028
+ if (options.refresh !== true) {
7029
+ const found = yield* resolve$2(target, cache, options.from);
7030
+ if (found !== void 0) return { entry: found };
7067
7031
  }
7068
- outputLinesArr.unshift(line);
7069
- outputBytesCount += lineBytes;
7070
- }
7071
- if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) truncatedBy = "lines";
7072
- const outputContent = outputLinesArr.join("\n");
7073
- const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
7074
- return {
7075
- content: outputContent,
7076
- truncated: true,
7077
- truncatedBy,
7078
- totalLines,
7079
- totalBytes,
7080
- outputLines: outputLinesArr.length,
7081
- outputBytes: finalOutputBytes,
7082
- lastLinePartial,
7083
- firstLineExceedsLimit: false,
7084
- maxLines,
7085
- maxBytes
7086
- };
7087
- }
7088
- /**
7089
- * Truncate a string to fit within a byte limit (from the end). Handles
7090
- * multi-byte UTF-8 characters correctly (never splits a codepoint).
7091
- */
7092
- function truncateStringToBytesFromEnd(str, maxBytes) {
7093
- const buf = Buffer.from(str, "utf-8");
7094
- if (buf.length <= maxBytes) return str;
7095
- let start = buf.length - maxBytes;
7096
- while (start < buf.length && (buf[start] & 192) === 128) start++;
7097
- return buf.subarray(start).toString("utf-8");
7098
- }
7099
- //#endregion
7100
- //#region ../harness/src/tool/accumulator.ts
7101
- function defaultTempFilePath(prefix, suffix) {
7102
- return posix.join(tmpdir(), `${prefix}-${suffix}.log`);
7103
- }
7104
- function byteLength(text) {
7105
- return Buffer.byteLength(text, "utf-8");
7106
- }
7107
- var Accumulator = class {
7108
- maxLines;
7109
- maxBytes;
7110
- maxRollingBytes;
7111
- tempFilePrefix;
7112
- decoder = new TextDecoder();
7113
- rawChunks = [];
7114
- tailText = "";
7115
- tailBytes = 0;
7116
- tailStartsAtLineBoundary = true;
7117
- totalRawBytes = 0;
7118
- totalDecodedBytes = 0;
7119
- completedLines = 0;
7120
- totalLines = 0;
7121
- currentLineBytes = 0;
7122
- hasOpenLine = false;
7123
- finished = false;
7124
- tempFilePath;
7125
- tempFile;
7126
- constructor(options = {}) {
7127
- this.maxLines = options.maxLines ?? 2e3;
7128
- this.maxBytes = options.maxBytes ?? 51200;
7129
- this.maxRollingBytes = Math.max(this.maxBytes * 2, 1);
7130
- this.tempFilePrefix = options.tempFilePrefix ?? "codework-output";
7131
- }
7132
- append(data) {
7133
- return Effect.suspend(() => {
7134
- if (this.finished) return Effect.die(/* @__PURE__ */ new Error("cannot append to a finished output accumulator"));
7135
- this.totalRawBytes += data.length;
7136
- this.appendDecodedText(this.decoder.decode(data, { stream: true }));
7137
- if (this.tempFile !== void 0 || this.shouldUseTempFile()) return Effect.gen({ self: this }, function* () {
7138
- yield* this.ensureTempFile();
7139
- yield* this.tempFile.writeAll(data).pipe(Effect.orDie);
7140
- });
7141
- if (data.length > 0) this.rawChunks.push(data);
7142
- return Effect.void;
7143
- });
7144
- }
7145
- finish() {
7146
- return Effect.suspend(() => {
7147
- if (this.finished) return Effect.void;
7148
- this.finished = true;
7149
- this.appendDecodedText(this.decoder.decode());
7150
- return Effect.gen({ self: this }, function* () {
7151
- if (this.shouldUseTempFile()) yield* this.ensureTempFile();
7152
- if (this.tempFile !== void 0) yield* this.tempFile.sync.pipe(Effect.orDie);
7153
- });
7032
+ yield* fileSystem.makeDirectory(directory, { recursive: true });
7033
+ yield* lock(hostPath.join(directory, ".lock"), () => failure$1(target, "plugin-lock-timeout", `timed out waiting for another installation of ${target.spec}`));
7034
+ const current = yield* active(directory, key);
7035
+ if (current !== void 0 && options.refresh !== true) return { entry: current };
7036
+ const real = yield* fileSystem.realPath(directory).pipe(Effect.orElseSucceed(() => directory));
7037
+ const staging = yield* Effect.acquireRelease(fileSystem.makeTempDirectory({
7038
+ directory: real,
7039
+ prefix: STAGING
7040
+ }), (staged) => fileSystem.remove(staged, {
7041
+ recursive: true,
7042
+ force: true
7043
+ }).pipe(Effect.orDie));
7044
+ yield* fileSystem.writeFileString(hostPath.join(staging, "package.json"), "{\"private\":true,\"type\":\"module\"}");
7045
+ const fetched = yield* download({
7046
+ target,
7047
+ into: staging,
7048
+ cache,
7049
+ from: options.from,
7050
+ ...options.runner === void 0 ? {} : { runner: options.runner }
7154
7051
  });
7155
- }
7156
- snapshot() {
7157
- const tailTruncation = truncateTail(this.getSnapshotText(), {
7158
- maxLines: this.maxLines,
7159
- maxBytes: this.maxBytes
7052
+ if (current !== void 0 && current.revision === fetched.revision) return { entry: current };
7053
+ const validated = yield* options.validate(fetched);
7054
+ const entrypoint = hostPath.relative(yield* fileSystem.realPath(staging), fetched.entrypoint);
7055
+ if (entrypoint.startsWith("..") || hostPath.isAbsolute(entrypoint)) return yield* new InstallError({
7056
+ reason: "plugin-no-entrypoint",
7057
+ reference: target.spec,
7058
+ message: `entrypoint escapes its installation: ${entrypoint}`
7160
7059
  });
7161
- const truncated = this.totalLines > this.maxLines || this.totalDecodedBytes > this.maxBytes;
7162
- const truncatedBy = truncated ? tailTruncation.truncatedBy ?? (this.totalDecodedBytes > this.maxBytes ? "bytes" : "lines") : null;
7163
- const truncation = {
7164
- ...tailTruncation,
7165
- truncated,
7166
- truncatedBy,
7167
- totalLines: this.totalLines,
7168
- totalBytes: this.totalDecodedBytes,
7169
- maxLines: this.maxLines,
7170
- maxBytes: this.maxBytes
7060
+ const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
7061
+ const marker = {
7062
+ spec: target.spec,
7063
+ name: fetched.name,
7064
+ ...fetched.version === void 0 ? {} : { version: fetched.version },
7065
+ ...fetched.revision === void 0 ? {} : { revision: fetched.revision },
7066
+ entrypoint,
7067
+ createdAt: now
7171
7068
  };
7069
+ yield* fileSystem.writeFileString(hostPath.join(staging, MARKER), yield* Schema.encodeEffect(Schema.fromJsonString(Marker))(marker));
7070
+ const previous = (yield* generations(directory)).at(-1) ?? 0;
7071
+ const generation = Math.max(now, previous + 1);
7072
+ const published = hostPath.join(directory, String(generation));
7073
+ yield* fileSystem.rename(staging, published);
7074
+ const entry = entryOf(marker, {
7075
+ digest: key,
7076
+ generation,
7077
+ directory: published
7078
+ });
7079
+ yield* put(root(cache), key, record(target, entry, now));
7080
+ yield* collect$1(directory);
7172
7081
  return {
7173
- content: truncation.content,
7174
- truncation,
7175
- ...this.tempFilePath === void 0 ? {} : { fullOutputPath: this.tempFilePath }
7082
+ entry,
7083
+ validated
7176
7084
  };
7177
- }
7178
- /** Bytes in the final (possibly unterminated) line — for the partial-line footer. */
7179
- getLastLineBytes() {
7180
- return this.currentLineBytes;
7181
- }
7182
- appendDecodedText(text) {
7183
- if (text.length === 0) return;
7184
- const bytes = byteLength(text);
7185
- this.totalDecodedBytes += bytes;
7186
- this.tailText += text;
7187
- this.tailBytes += bytes;
7188
- if (this.tailBytes > this.maxRollingBytes * 2) this.trimTail();
7189
- let newlines = 0;
7190
- let lastNewline = -1;
7191
- for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) {
7192
- newlines++;
7193
- lastNewline = i;
7194
- }
7195
- if (newlines === 0) {
7196
- this.currentLineBytes += bytes;
7197
- this.hasOpenLine = true;
7198
- } else {
7199
- this.completedLines += newlines;
7200
- const tail = text.slice(lastNewline + 1);
7201
- this.currentLineBytes = byteLength(tail);
7202
- this.hasOpenLine = tail.length > 0;
7203
- }
7204
- this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);
7205
- }
7206
- trimTail() {
7207
- const buffer = Buffer.from(this.tailText, "utf-8");
7208
- if (buffer.length <= this.maxRollingBytes) {
7209
- this.tailBytes = buffer.length;
7210
- return;
7085
+ }).pipe(Effect.scoped, Effect.mapError((cause) => Schema.is(InstallError)(cause) || Schema.is(StoreError)(cause) || Schema.is(LoadError)(cause) ? cause : failure$1(target, "plugin-marker-invalid", `cannot write the store entry for ${target.spec}`, cause)));
7086
+ });
7087
+ /**
7088
+ * Expire what is superseded.
7089
+ *
7090
+ * The two newest generations are kept regardless of age: the newest is live, and the one before it
7091
+ * may still be imported by an exchange in flight. Runs at the end of an `add` that published
7092
+ * something -- no timer and no sweeper, because installs are the only thing that creates garbage.
7093
+ */
7094
+ const collect$1 = Effect.fn("PluginStore.collect")(function* (directory) {
7095
+ const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
7096
+ const keep = new Set((yield* generations(directory)).slice(-2).map(String));
7097
+ const names = yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []));
7098
+ for (const name of names) {
7099
+ if (keep.has(name) || name === ".lock") continue;
7100
+ if (name.startsWith(STAGING)) {
7101
+ const started = yield* fileSystem.stat(hostPath.join(directory, name)).pipe(Effect.option);
7102
+ if ((Option.isNone(started) ? 0 : Option.match(started.value.mtime, {
7103
+ onNone: () => 0,
7104
+ onSome: (at) => now - at.getTime()
7105
+ })) > HOUR) yield* fileSystem.remove(hostPath.join(directory, name), {
7106
+ recursive: true,
7107
+ force: true
7108
+ }).pipe(Effect.ignore);
7109
+ continue;
7211
7110
  }
7212
- let start = buffer.length - this.maxRollingBytes;
7213
- while (start < buffer.length && (buffer[start] & 192) === 128) start++;
7214
- this.tailStartsAtLineBoundary = start === 0 ? this.tailStartsAtLineBoundary : buffer[start - 1] === 10;
7215
- this.tailText = buffer.subarray(start).toString("utf-8");
7216
- this.tailBytes = byteLength(this.tailText);
7217
- }
7218
- getSnapshotText() {
7219
- if (this.tailStartsAtLineBoundary) return this.tailText;
7220
- const firstNewline = this.tailText.indexOf("\n");
7221
- return firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1);
7222
- }
7223
- shouldUseTempFile() {
7224
- return this.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines;
7225
- }
7226
- ensureTempFile() {
7227
- if (this.tempFile !== void 0) return Effect.void;
7228
- return Effect.gen({ self: this }, function* () {
7229
- const suffix = Encoding.encodeHex(yield* crypto.randomBytes(8));
7230
- this.tempFilePath = defaultTempFilePath(this.tempFilePrefix, suffix);
7231
- this.tempFile = yield* fileSystem.open(this.tempFilePath, { flag: "w" });
7232
- for (const chunk of this.rawChunks) yield* this.tempFile.writeAll(chunk);
7233
- this.rawChunks = [];
7234
- }).pipe(Effect.orDie);
7111
+ if (!/^\d+$/.test(name)) continue;
7112
+ if (now - Number(name) > WEEK) yield* fileSystem.remove(hostPath.join(directory, name), {
7113
+ recursive: true,
7114
+ force: true
7115
+ }).pipe(Effect.ignore);
7235
7116
  }
7236
- };
7237
- //#endregion
7238
- //#region ../harness/src/tool/progress.ts
7239
- var progress_exports = /* @__PURE__ */ __exportAll({});
7240
- import * as import__codeworksh_plugin_tool_progress from "@codeworksh/plugin/tool/progress";
7241
- __reExport(progress_exports, import__codeworksh_plugin_tool_progress);
7242
- //#endregion
7243
- //#region ../harness/src/tool/shell.ts
7117
+ });
7118
+ const HOUR = 36e5;
7119
+ const WEEK = 168 * HOUR;
7244
7120
  /**
7245
- * `ToolShell` — the tool-owned, cancellable command-execution capability the bash tool depends on.
7246
- *
7247
- * It deliberately does **not** reuse `sandbox/Shell` directly: the local OS path
7248
- * provides no `Shell` at all, and `sandbox/Shell` models neither cancellation
7249
- * nor a timeout. `ToolShell` makes both part of the contract. Adapters honor the
7250
- * mid-flight process kill where the SDK allows it (local, Vercel) and fall back
7251
- * to the timeout where it does not (Daytona) — "kill where it's easy".
7121
+ * How long a network answer is reused before `check` asks again.
7252
7122
  *
7253
- * Two modes, and Local has two variants:
7254
- * - Local / full OS shell → {@link local} (default)
7255
- * - Local / just-bash → {@link fromSandboxShell} over `EnvBash`
7256
- * - Remote / provider → {@link fromSandboxShell} over `EnvDaytona` / `EnvVercel`
7123
+ * Persisted in the index rather than held in memory, because the thing that benefits is a command
7124
+ * that exits: an in-memory staleness cache is worthless to `codework plugin check`.
7257
7125
  */
7258
- /** Infra/spawn failure — not model-actionable; handlers should treat it as a defect. */
7259
- var ToolShellError = class extends Schema.TaggedError()("ToolShellError", {
7260
- command: Schema.String,
7261
- cause: Schema.optional(Schema.Defect())
7262
- }) {};
7263
- /** The command exceeded its deadline. Distinct so handlers can map it to a domain timeout. */
7264
- var ToolShellTimeout = class extends Schema.TaggedError()("ToolShellTimeout", {
7265
- command: Schema.String,
7266
- timeoutMillis: Schema.Finite
7267
- }) {};
7268
- const widenExecError = (effect) => effect.pipe(Effect.mapError((error) => error));
7269
- var ToolShell = class extends Context.Service()("@codeworksh/harness/tool/shell/ToolShell") {};
7126
+ const CHECK_TTL = 36e5;
7270
7127
  /**
7271
- * Bridge the existing `sandbox/Shell` into a `ToolShell`. Used for just-bash
7272
- * (in-process) and remote providers.
7128
+ * Is a newer revision available?
7273
7129
  *
7274
- * `cwd` and `env` both pass through to the backing `sandbox/Shell`, which
7275
- * resolves a relative `cwd` against its mount. The buffered `exec`
7276
- * timeout is enforced here with `Effect.timeoutOption` (interruption always
7277
- * returns control; the underlying command is only truly killed mid-flight on
7278
- * signal-aware backends, handled in the provider layer). When the backend exposes
7279
- * `stream` (e.g. Vercel `Command.logs`), it is bridged to {@link IToolShell.stream}
7280
- * so the bash tool streams over it too.
7130
+ * The only network call in the store's vocabulary, and it is the caller's to make. A `probe` that
7131
+ * fails -- offline, registry down, a remote that refuses -- propagates rather than answering
7132
+ * `current`: an outage must not read as "you are up to date".
7281
7133
  */
7282
- const fromSandboxShell = Layer.effect(ToolShell, Effect.gen(function* () {
7283
- const shell = yield* shell_exports.Shell;
7284
- const exec = (command, options) => {
7285
- const run = shell.exec(command, {
7286
- ...options?.env ? { env: options.env } : {},
7287
- ...options?.cwd ? { cwd: options.cwd } : {}
7288
- }).pipe(Effect.mapError((cause) => new ToolShellError({
7289
- command,
7290
- cause
7291
- })));
7292
- if (options?.timeout === void 0) return widenExecError(run);
7293
- const timeout = options.timeout;
7294
- return run.pipe(Effect.timeoutOption(timeout), Effect.flatMap(Option.match({
7295
- onNone: () => Effect.fail(new ToolShellTimeout({
7296
- command,
7297
- timeoutMillis: Duration.toMillis(timeout)
7298
- })),
7299
- onSome: Effect.succeed
7300
- })));
7134
+ const check = Effect.fn("PluginStore.check")(function* (target, cache, options) {
7135
+ if (!target.mutable) return { _tag: "immutable" };
7136
+ const entry = yield* resolve$2(target, cache, options.from);
7137
+ if (entry === void 0) return { _tag: "not-installed" };
7138
+ const key = yield* digest(target, cache, options.from).pipe(Effect.mapError((cause) => failure$1(target, "plugin-marker-invalid", `cannot address ${target.spec}`, cause)));
7139
+ const base = root(cache);
7140
+ const record = (yield* read(base)).get(key);
7141
+ const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
7142
+ if (options.refresh !== true && record?.checkedAt !== void 0 && now - record.checkedAt < CHECK_TTL && (record.outdated !== true || record.available !== void 0)) return record.outdated === true ? {
7143
+ _tag: "outdated",
7144
+ available: record.available,
7145
+ ...entry.revision === void 0 ? {} : { filed: entry.revision }
7146
+ } : {
7147
+ _tag: "current",
7148
+ ...entry.revision === void 0 ? {} : { revision: entry.revision }
7301
7149
  };
7302
- const sandboxStream = shell.stream;
7303
- const stream = sandboxStream ? (command, options) => sandboxStream(command, {
7304
- ...options?.env ? { env: options.env } : {},
7305
- ...options?.cwd ? { cwd: options.cwd } : {}
7306
- }).pipe(Stream.map((chunk) => chunk._tag === "exit" ? {
7307
- _tag: "Exit",
7308
- exitCode: chunk.exitCode
7150
+ const available = yield* options.probe(target);
7151
+ if (available === void 0) return {
7152
+ _tag: "current",
7153
+ ...entry.revision === void 0 ? {} : { revision: entry.revision }
7154
+ };
7155
+ const outdated = available !== entry.revision;
7156
+ yield* patch(base, key, {
7157
+ checkedAt: now,
7158
+ outdated,
7159
+ available
7160
+ });
7161
+ return outdated ? {
7162
+ _tag: "outdated",
7163
+ available,
7164
+ ...entry.revision === void 0 ? {} : { filed: entry.revision }
7165
+ } : {
7166
+ _tag: "current",
7167
+ ...entry.revision === void 0 ? {} : { revision: entry.revision }
7168
+ };
7169
+ });
7170
+ /**
7171
+ * Refresh a mutable target, if the network says there is anything to refresh.
7172
+ *
7173
+ * `add` with `refresh` is what does the work; this is the guard in front of it, so an `update`
7174
+ * that finds nothing new neither stages nor publishes.
7175
+ */
7176
+ const update = Effect.fn("PluginStore.update")(function* (target, cache, options) {
7177
+ const current = yield* resolve$2(target, cache, options.from);
7178
+ if (current === void 0) return { _tag: "not-installed" };
7179
+ if ((yield* check(target, cache, {
7180
+ refresh: true,
7181
+ probe: options.probe,
7182
+ from: options.from
7183
+ }))._tag !== "outdated") return {
7184
+ _tag: "unchanged",
7185
+ entry: current
7186
+ };
7187
+ const added = yield* add(target, cache, {
7188
+ refresh: true,
7189
+ validate: options.validate,
7190
+ from: options.from,
7191
+ ...options.runner === void 0 ? {} : { runner: options.runner }
7192
+ });
7193
+ return added.entry.generation === current.generation ? {
7194
+ _tag: "unchanged",
7195
+ entry: current
7309
7196
  } : {
7310
- _tag: "Output",
7311
- bytes: chunk.bytes
7312
- }), Stream.mapError((cause) => new ToolShellError({
7313
- command,
7314
- cause
7315
- }))) : void 0;
7316
- return ToolShell.of(stream ? {
7317
- exec,
7318
- stream
7319
- } : { exec });
7320
- }));
7321
- Duration.seconds(1);
7322
- //#endregion
7323
- //#region ../harness/src/tool/tool.ts
7324
- var tool_exports = /* @__PURE__ */ __exportAll({});
7325
- import * as import__codeworksh_plugin_tool from "@codeworksh/plugin/tool";
7326
- __reExport(tool_exports, import__codeworksh_plugin_tool);
7327
- //#endregion
7328
- //#region ../harness/src/plugin/builtin/tool/bash.ts
7197
+ _tag: "updated",
7198
+ entry: added.entry,
7199
+ ...added.validated === void 0 ? {} : { validated: added.validated }
7200
+ };
7201
+ });
7202
+ Effect.fn("PluginStore.remove")(function* (target, cache, from) {
7203
+ const { key, directory } = yield* entryDir(target, cache, from);
7204
+ const existed = yield* fileSystem.exists(directory).pipe(Effect.orElseSucceed(() => false));
7205
+ yield* fileSystem.remove(directory, {
7206
+ recursive: true,
7207
+ force: true
7208
+ }).pipe(Effect.ignore);
7209
+ yield* drop(root(cache), key);
7210
+ return existed;
7211
+ });
7212
+ Effect.fn("PluginStore.all")(function* (cache) {
7213
+ const base = root(cache);
7214
+ const slugs = yield* fileSystem.readDirectory(base).pipe(Effect.orElseSucceed(() => []));
7215
+ const found = [];
7216
+ for (const slug of slugs) {
7217
+ const digests = yield* fileSystem.readDirectory(hostPath.join(base, slug)).pipe(Effect.orElseSucceed(() => []));
7218
+ for (const key of digests) {
7219
+ const entry = yield* active(hostPath.join(base, slug, key), key);
7220
+ if (entry !== void 0) found.push(entry);
7221
+ }
7222
+ }
7223
+ return found;
7224
+ });
7225
+ const Id = Schema.String.check(Schema.isPattern(/^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*){2}$/));
7226
+ const Definition = Schema.Struct({
7227
+ id: Id,
7228
+ kind: Schema.Literals(Object.keys(plugin_exports.domains)),
7229
+ events: Schema.optional(Schema.Array(Schema.declare((value) => Predicate.hasProperty(value, "type") && Predicate.isString(value.type)))),
7230
+ setup: Schema.declare((value) => Predicate.isFunction(value))
7231
+ });
7232
+ /** Every plugin failure says which file to go and edit, when there is one. */
7233
+ const where = (origin) => origin.file === void 0 ? {} : { file: origin.file };
7234
+ const detail = (cause) => cause instanceof Error ? cause.message : typeof cause === "string" ? cause : String(cause);
7235
+ /** A reference that does not name anything loadable. */
7236
+ const sourceFailure = (origin, cause) => Schema.is(SourceError)(cause) ? cause : new SourceError({
7237
+ reason: "plugin-not-found",
7238
+ reference: origin.reference,
7239
+ ...where(origin),
7240
+ message: detail(cause)
7241
+ });
7242
+ /** Node's own code for a failure, when it set one. */
7243
+ const codeOf = (cause) => Predicate.hasProperty(cause, "code") && Predicate.isString(cause.code) ? cause.code : void 0;
7329
7244
  /**
7330
- * The self-contained bash plugin — a worked example for tool plugins. The definition is pure data;
7331
- * the handler depends only on {@link ToolShell} / {@link ToolProgress}, never
7332
- * `sandbox/Shell`, so the backend (local OS / just-bash / remote provider) is
7333
- * swapped by changing the provided Layer with no change to the tool.
7245
+ * A module that would not import.
7334
7246
  *
7335
- * Two execution paths, picked by capability:
7336
- * - **variant A (buffered)** otherwise: `ToolShell.exec` returns the full output,
7337
- * truncated once after completion.
7338
- * - **variant B (streaming)** when the backend offers `ToolShell.stream`: output
7339
- * flows through an {@link Accumulator} (bounded memory + temp-file spill)
7340
- * and is reported live via {@link ToolProgress}; on timeout the partial output
7341
- * produced so far is preserved.
7247
+ * Two of these get their own reason, because the remedy differs and that is the whole value of a
7248
+ * slug. A missing dependency means the plugin is fine and its `node_modules` is not, which a
7249
+ * person fixes in the package rather than in their settings. An uncompiled one means the plugin
7250
+ * shipped its sources: Node refuses to strip types under `node_modules`, and installing a plugin
7251
+ * runs no lifecycle scripts, so a `prepare` build never happens either -- the author has to ship
7252
+ * the build output, and nothing the user can do to their settings will help.
7342
7253
  *
7343
- * Success and failure carry the *same* structured shape
7344
- * (`output`, `exitCode`, `truncated`, `fullOutputPath`)
7345
- * — a non-zero exit is just `exitCode !== 0`, not a different kind of result.
7254
+ * `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` is matched by code rather than by message: it is
7255
+ * Node's documented identifier and exact, where the wording is free to change under us. Only an
7256
+ * installed plugin can reach it -- a local path lives outside `node_modules`, where Node strips
7257
+ * types happily, which is what keeps the single-file dev workflow working.
7346
7258
  */
7347
- const BashParams = Schema.Struct({
7348
- command: Schema.String.annotate({ description: "The bash command to execute." }),
7349
- timeout: Schema.optional(Schema.Finite.check(Schema.isGreaterThan(0)).annotate({ description: "Optional timeout in seconds (must be greater than 0)." }))
7259
+ const importFailure = (origin, cause) => new LoadError({
7260
+ reason: codeOf(cause) === "ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING" ? "plugin-not-compiled" : /cannot find (package|module)/i.test(detail(cause)) ? "plugin-missing-dependency" : "plugin-import-failed",
7261
+ reference: origin.reference,
7262
+ ...where(origin),
7263
+ message: detail(cause),
7264
+ cause
7350
7265
  });
7351
- const outputFields = {
7352
- /** Combined stdout + stderr, truncated for display (full output is on disk when `truncated`). */
7353
- output: Schema.String,
7354
- truncated: Schema.Boolean,
7355
- fullOutputPath: Schema.optional(Schema.String)
7356
- };
7357
- const BashSuccess = Schema.Struct({
7358
- ...outputFields,
7359
- exitCode: Schema.Finite
7266
+ /** A module that imported and is not a plugin. */
7267
+ const definitionFailure = (origin, cause, id) => new LoadError({
7268
+ reason: "plugin-invalid-definition",
7269
+ reference: origin.reference,
7270
+ ...where(origin),
7271
+ message: detail(cause),
7272
+ ...id === void 0 ? {} : { id },
7273
+ cause
7360
7274
  });
7361
- /** Non-zero exit — expected, model-visible. Carries the same shape as success. */
7362
- var BashFailed = class extends Schema.TaggedError()("BashFailed", {
7363
- ...outputFields,
7364
- exitCode: Schema.Finite
7365
- }) {};
7366
- /** Deadline exceeded — expected, model-visible. Carries the partial output produced so far. */
7367
- var BashTimedOut = class extends Schema.TaggedError()("BashTimedOut", {
7368
- ...outputFields,
7369
- timeoutSeconds: Schema.Finite
7370
- }) {};
7371
- const BashFailure = Schema.Union([BashFailed, BashTimedOut]);
7372
- const bashDef = tool_exports.define({
7373
- name: "bash",
7374
- label: "bash",
7375
- promptSnippet: "Execute bash commands (ls, grep, find, etc.).",
7376
- description: `Execute a bash command in the working directory and return its combined stdout/stderr output. Output is truncated to the last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES}KB (whichever is hit first); when truncated, the full output is saved to a temp file. A non-zero exit code is reported as an error carrying the captured output.`,
7377
- parameters: BashParams,
7378
- success: BashSuccess,
7379
- failure: BashFailure,
7380
- encodeContent: (success) => [{
7381
- type: "text",
7382
- text: success.output
7383
- }],
7384
- encodeFailureContent: (failure) => [{
7385
- type: "text",
7386
- text: failure.output
7387
- }]
7275
+ const validate = Effect.fn("PluginLoader.validate")(function* (input, origin, builtin = false) {
7276
+ yield* Schema.decodeUnknownEffect(Definition)(input).pipe(Effect.mapError((cause) => definitionFailure(origin, cause)));
7277
+ const plugin = input;
7278
+ if (!builtin && plugin.id.startsWith("codework.")) return yield* definitionFailure(origin, /* @__PURE__ */ new Error("codework namespace is reserved for builtins"), plugin.id);
7279
+ return plugin;
7388
7280
  });
7389
- /** Combine stdout and stderr into one stream of text, stderr after stdout. */
7390
- const combineOutput = (stdout, stderr) => {
7391
- if (stderr.length === 0) return stdout;
7392
- if (stdout.length === 0) return stderr;
7393
- return `${stdout}\n${stderr}`;
7394
- };
7395
- /** One-line footer appended to truncated, model-facing output. */
7396
- const footer = (t, fullOutputPath, lastLineBytes) => {
7397
- const where = fullOutputPath ? ` Full output: ${fullOutputPath}` : "";
7398
- const startLine = t.totalLines - t.outputLines + 1;
7399
- if (t.lastLinePartial) {
7400
- const lineSize = lastLineBytes !== void 0 ? ` (line is ${formatSize(lastLineBytes)})` : "";
7401
- return `\n\n[showing last ${formatSize(t.outputBytes)} of line ${t.totalLines}${lineSize}.${where}]`;
7281
+ const Manifest = Schema.Struct({
7282
+ name: Schema.optional(Schema.String),
7283
+ exports: Schema.optional(Schema.Unknown)
7284
+ });
7285
+ const localUrl = Effect.fn("PluginLoader.localUrl")(function* (location, origin) {
7286
+ const escapes = (message) => new SourceError({
7287
+ reason: "plugin-escapes-root",
7288
+ reference: origin.reference,
7289
+ ...where(origin),
7290
+ message
7291
+ });
7292
+ if ((yield* fileSystem.stat(location)).type !== "Directory") return { url: yield* Effect.try(() => resolveModule(location, hostPath.dirname(location))) };
7293
+ const manifestPath = hostPath.join(location, "package.json");
7294
+ if (!(yield* fileSystem.exists(manifestPath))) return { url: yield* Effect.try(() => resolveModule("./index", location)) };
7295
+ const manifest = yield* fileSystem.readFileString(manifestPath).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Manifest))));
7296
+ if (manifest.exports !== void 0) {
7297
+ const name = manifest.name;
7298
+ if (!name) return yield* escapes("a local package with exports must declare its name");
7299
+ const url = yield* Effect.try(() => resolveModule(name, location));
7300
+ const resolved = yield* fileSystem.realPath(fileURLToPath(url));
7301
+ if (hostPath.relative(yield* fileSystem.realPath(location), resolved).startsWith("..")) return yield* escapes("package export escapes its root");
7302
+ return {
7303
+ url: pathToFileURL(resolved).href,
7304
+ name
7305
+ };
7402
7306
  }
7403
- if (t.truncatedBy === "lines") return `\n\n[showing lines ${startLine}-${t.totalLines} of ${t.totalLines}.${where}]`;
7404
- return `\n\n[showing lines ${startLine}-${t.totalLines} of ${t.totalLines} (${formatSize(t.maxBytes)} limit).${where}]`;
7307
+ const url = yield* Effect.try(() => resolveModule(createRequire(pathToFileURL(manifestPath)).resolve(location), location));
7308
+ return manifest.name === void 0 ? { url } : {
7309
+ url,
7310
+ name: manifest.name
7311
+ };
7312
+ });
7313
+ /**
7314
+ * Install a package into the store, importing and validating it **while it is still staged**.
7315
+ *
7316
+ * The validation is passed down rather than done afterwards for one reason: the store publishes by
7317
+ * rename, so a package that turns out not to be a plugin must be rejected before it is published.
7318
+ * Publishing first would make the broken generation the newest marked one, which `resolve` returns
7319
+ * forever after -- and re-running `add` would hit the fast path and return it again.
7320
+ */
7321
+ const install = Effect.fn("PluginLoader.install")(function* (target, cache, origin, options) {
7322
+ const added = yield* add(target, cache, {
7323
+ from: anchor$1(origin.file, options.hostDir),
7324
+ validate: (fetched) => imported(url(fetched.entrypoint), origin, options.import)
7325
+ });
7326
+ return {
7327
+ url: added.entry.url,
7328
+ ...added.entry.version === void 0 ? {} : { version: added.entry.version },
7329
+ generation: added.entry.generation,
7330
+ ...added.validated === void 0 ? {} : { plugin: added.validated }
7331
+ };
7332
+ });
7333
+ /**
7334
+ * The directory whose `.npmrc` chain names the artifact a reference resolves to: the project that
7335
+ * declared it. A settings file sits in `<root>/.codework/`, but npm reads a project `.npmrc` at the
7336
+ * package boundary discovered upward from the chain's start -- the root itself -- so anchoring at
7337
+ * the file's own directory would miss it. Absent a declaring file the host context stands in.
7338
+ */
7339
+ const anchor$1 = (file, fallback) => {
7340
+ if (file === void 0) return fallback;
7341
+ const directory = hostPath.dirname(file);
7342
+ return hostPath.basename(directory) === ".codework" ? hostPath.dirname(directory) : directory;
7405
7343
  };
7406
- /** Write full output to a host temp file (best-effort; undefined on failure). */
7407
- const spillToTempFile = (content) => Effect.gen(function* () {
7408
- const suffix = Encoding.encodeHex(yield* crypto.randomBytes(6));
7409
- const path = posix.join(tmpdir(), `codework-bash-${suffix}.log`);
7410
- yield* fileSystem.writeFileString(path, content);
7411
- return path;
7412
- }).pipe(Effect.orElseSucceed(() => void 0));
7413
- /** Variant A: truncate the buffered output once, spilling the full output if truncated. */
7414
- const presentBuffered = (combined) => Effect.gen(function* () {
7415
- const t = truncateTail(combined);
7416
- if (!t.truncated) return {
7417
- output: t.content,
7418
- truncated: false
7344
+ /** The default installer, or the one the caller injected. */
7345
+ const installer = (target, origin, options) => options.install === void 0 ? install(target, options.cache, origin, options) : options.install(target, options.cache, anchor$1(origin.file, options.hostDir));
7346
+ /** Import a module and confirm it default-exports a plugin. */
7347
+ const imported = Effect.fn("PluginLoader.imported")(function* (url, origin, load) {
7348
+ const module = yield* Effect.tryPromise({
7349
+ try: () => (load ?? importModule)(url),
7350
+ catch: (cause) => importFailure(origin, cause)
7351
+ });
7352
+ return yield* validate(Predicate.isObject(module) && "default" in module ? module.default : void 0, origin);
7353
+ });
7354
+ /**
7355
+ * Import one entrypoint and confirm it is a plugin, without touching the store.
7356
+ *
7357
+ * This is what a caller hands the store as its `validate`: the artifact is checked while it is
7358
+ * still staged, so a package that turns out not to be a plugin is never published.
7359
+ */
7360
+ const definition$1 = (entrypoint, reference) => imported(url(entrypoint), {
7361
+ index: 0,
7362
+ reference
7363
+ });
7364
+ /**
7365
+ * Install and import one reference, and report what it turned out to be, without selecting it.
7366
+ * `codework plugin add` uses this to prove a package really is a plugin before it writes the
7367
+ * reference into a settings file: the failure a user gets is the one they can act on — a bad
7368
+ * spec, an install that did not resolve, a module that exports no plugin — rather than a broken
7369
+ * configuration that only fails at the next run.
7370
+ */
7371
+ const inspect = Effect.fn("PluginLoader.inspect")(function* (reference, options) {
7372
+ const origin = {
7373
+ index: 0,
7374
+ reference,
7375
+ file: options.file
7419
7376
  };
7420
- const fullOutputPath = yield* spillToTempFile(combined);
7377
+ const target = yield* parse$2(reference, options.hostDir);
7378
+ const loaded = yield* load$3(target, origin, options);
7421
7379
  return {
7422
- output: t.content + footer(t, fullOutputPath),
7423
- truncated: true,
7424
- ...fullOutputPath !== void 0 ? { fullOutputPath } : {}
7380
+ id: loaded.plugin.id,
7381
+ ...loaded.version === void 0 ? {} : { version: loaded.version },
7382
+ ...loaded.name === void 0 ? {} : { name: loaded.name }
7425
7383
  };
7426
7384
  });
7427
- /** Variant B: present an accumulator snapshot (the temp file is spilled incrementally). */
7428
- const presentSnapshot = (snap, lastLineBytes) => {
7429
- if (!snap.truncation.truncated) return {
7430
- output: snap.content,
7431
- truncated: false
7432
- };
7385
+ const load$3 = Effect.fn("PluginLoader.load")(function* (target, origin, options) {
7386
+ const installed = yield* target.kind === "local" ? localUrl(target.path, origin).pipe(Effect.mapError((cause) => sourceFailure(origin, cause))) : installer(target, origin, options).pipe(Effect.mapError((cause) => origin.file === void 0 ? cause : declaredIn(cause, origin.file)));
7387
+ const address = target.kind === "local" && options.reload !== void 0 && options.reload > 0 ? `${installed.url}?reload=${options.reload}` : installed.url;
7433
7388
  return {
7434
- output: snap.content + footer(snap.truncation, snap.fullOutputPath, lastLineBytes),
7435
- truncated: true,
7436
- ...snap.fullOutputPath !== void 0 ? { fullOutputPath: snap.fullOutputPath } : {}
7389
+ plugin: installed.plugin ?? (yield* imported(address, origin, options.import)),
7390
+ source: origin.reference,
7391
+ ..."version" in installed && installed.version !== void 0 ? { version: installed.version } : {},
7392
+ ..."generation" in installed && installed.generation !== void 0 ? { generation: installed.generation } : {},
7393
+ ...installed.name === void 0 ? {} : { name: installed.name }
7437
7394
  };
7438
- };
7439
- /** Variant A — buffered: one `exec`, truncate after completion. No live progress. */
7440
- const runBuffered = (shell, params) => Effect.gen(function* () {
7441
- const result = yield* shell.exec(params.command, params.timeout !== void 0 ? { timeout: Duration.seconds(params.timeout) } : void 0).pipe(Effect.catchTags({
7442
- ToolShellTimeout: (timeout) => Effect.fail(new BashTimedOut({
7443
- timeoutSeconds: timeout.timeoutMillis / 1e3,
7444
- output: "",
7445
- truncated: false
7446
- })),
7447
- ToolShellError: (cause) => Effect.die(cause)
7448
- }));
7449
- const present = yield* presentBuffered(combineOutput(result.stdout, result.stderr));
7450
- if (result.exitCode !== 0) return yield* new BashFailed({
7451
- exitCode: result.exitCode,
7452
- ...present
7453
- });
7395
+ });
7396
+ /**
7397
+ * The version-free, location-anchored spelling of a reference.
7398
+ *
7399
+ * Two references naming the same module compare equal through it: `@acme/x@1.2.0` and `@acme/x`
7400
+ * are one package, and `./plugins/x.ts` is the file it resolves to from the directory that
7401
+ * declared it. Pure -- nothing is installed, imported or read.
7402
+ */
7403
+ const canonical = (reference, hostDir) => Effect.map(parse$2(reference, hostDir), canonical$1);
7404
+ //#endregion
7405
+ //#region ../harness/src/plugin/catalog.ts
7406
+ /**
7407
+ * A definition carries `setup`; a patch carries `plugin` or `package` and no `setup`. Checking
7408
+ * `setup` first matters because `Plugin` permits extra properties, so a definition holding its
7409
+ * own `plugin` property must not be read as configuration.
7410
+ *
7411
+ * e.g
7412
+ * "acme/tool@1.2.0" → module (load me)
7413
+ * define({id, kind, setup, ...}) → module (I AM the definition — embedder code only)
7414
+ * { plugin: "x", enabled: false } → config (don't load — adjust something already loaded)
7415
+ *
7416
+ * setup present → definition, always (even with a stray plugin: key)
7417
+ * setup absent + plugin:/package: present → config patch
7418
+ * setup absent + neither key → malformed
7419
+ */
7420
+ const isPatch = (reference) => Predicate.isObject(reference) && !Predicate.isFunction(reference) && !("setup" in reference) && ("plugin" in reference || "package" in reference);
7421
+ /**
7422
+ * The **load pass**: store lookup and ESM import, never the network.
7423
+ *
7424
+ * Runs at boot, at `reload`, and at any exchange that finds a reference it cannot satisfy. It
7425
+ * deliberately knows nothing about `enabled`, options or order -- those are data over modules that
7426
+ * are already loaded, they cost a walk of a short array, and they belong to every exchange.
7427
+ *
7428
+ * Loading is keyed by what a reference resolves to, so naming one module twice imports it once.
7429
+ */
7430
+ const load$2 = Effect.fn("PluginCatalog.load")(function* (references, options) {
7431
+ const plugins = /* @__PURE__ */ new Map();
7432
+ const aliases = /* @__PURE__ */ new Map();
7433
+ const versions = /* @__PURE__ */ new Map();
7434
+ const origins = /* @__PURE__ */ new Map();
7435
+ const seen = /* @__PURE__ */ new Map();
7436
+ const remember = (plugin, names, from) => {
7437
+ plugins.set(plugin.id, plugin);
7438
+ aliases.set(plugin.id, plugin.id);
7439
+ for (const name of names) aliases.set(name, plugin.id);
7440
+ if (from?.version !== void 0) versions.set(plugin.id, from.version);
7441
+ if (from !== void 0) origins.set(plugin.id, from.origin);
7442
+ };
7443
+ /** Source metadata exists for diagnostics; a silent ID replacement is where it earns that. */
7444
+ const note = (plugin, source) => plugins.has(plugin.id) ? Effect.logDebug(`plugin ${plugin.id} redefined by ${source}; the earlier definition is discarded`) : Effect.void;
7445
+ for (const builtin of options.builtins) remember(yield* validate(builtin, {
7446
+ index: -1,
7447
+ reference: builtin.id
7448
+ }, true), []);
7449
+ for (const [index, reference] of references.entries()) {
7450
+ if (isPatch(reference)) continue;
7451
+ const declared = typeof reference === "string" ? reference : Predicate.hasProperty(reference, "id") ? reference.id : void 0;
7452
+ const named = typeof declared === "string" ? declared : `<plugin object #${index}>`;
7453
+ const origin = {
7454
+ index,
7455
+ reference: named,
7456
+ file: options.declared?.get(named)
7457
+ };
7458
+ if (typeof reference !== "string") {
7459
+ if (typeof declared === "string" && plugins.get(declared) === reference) continue;
7460
+ const plugin = yield* validate(reference, origin);
7461
+ yield* note(plugin, "a supplied object");
7462
+ remember(plugin, []);
7463
+ continue;
7464
+ }
7465
+ const target = yield* parse$2(reference, options.hostDir);
7466
+ const key = target.kind === "local" ? target.path : target.spec;
7467
+ const already = seen.get(key);
7468
+ if (already !== void 0) {
7469
+ aliases.set(reference, already);
7470
+ continue;
7471
+ }
7472
+ const definition = yield* load$3(target, origin, options);
7473
+ yield* note(definition.plugin, definition.version === void 0 ? key : `${key}@${definition.version}`);
7474
+ const names = target.kind === "local" ? [target.path, ...definition.name === void 0 ? [] : [definition.name]] : [canonical$1(target), target.spec];
7475
+ remember(definition.plugin, [reference, ...names], {
7476
+ ...definition.version === void 0 ? {} : { version: definition.version },
7477
+ origin: {
7478
+ reference,
7479
+ ...definition.generation === void 0 ? {} : { generation: definition.generation },
7480
+ ...origin.file === void 0 ? {} : { file: origin.file }
7481
+ }
7482
+ });
7483
+ seen.set(key, definition.plugin.id);
7484
+ }
7454
7485
  return {
7455
- exitCode: result.exitCode,
7456
- ...present
7486
+ plugins,
7487
+ aliases,
7488
+ versions,
7489
+ origins
7457
7490
  };
7458
7491
  });
7459
- /** Variant B — streaming: accumulate output, report progress, keep partial output on timeout. */
7460
- const runStreaming = (stream, params) => Effect.scoped(Effect.gen(function* () {
7461
- const acc = new Accumulator({ tempFilePrefix: "codework-bash" });
7462
- const progress = yield* progress_exports.ToolProgress;
7463
- const exitCode = yield* Ref.make(null);
7464
- const consume = stream(params.command).pipe(Stream.runForEach((event) => event._tag === "Exit" ? Ref.set(exitCode, event.exitCode) : Effect.gen(function* () {
7465
- yield* acc.append(Buffer.from(event.bytes));
7466
- yield* progress.report({ content: [{
7467
- type: "text",
7468
- text: acc.snapshot().content
7469
- }] });
7470
- })), Effect.catchTag("ToolShellError", (cause) => Effect.die(cause)));
7471
- let timedOutAfter;
7472
- if (params.timeout !== void 0) {
7473
- const finished = yield* consume.pipe(Effect.timeoutOption(Duration.seconds(params.timeout)));
7474
- if (Option.isNone(finished)) timedOutAfter = params.timeout;
7475
- } else yield* consume;
7476
- yield* acc.finish();
7477
- const present = presentSnapshot(acc.snapshot(), acc.getLastLineBytes());
7478
- if (timedOutAfter !== void 0) return yield* new BashTimedOut({
7479
- timeoutSeconds: timedOutAfter,
7480
- ...present
7481
- });
7482
- const code = yield* Ref.get(exitCode);
7483
- if (code === null || code !== 0) return yield* new BashFailed({
7484
- exitCode: code ?? -1,
7485
- ...present
7492
+ /**
7493
+ * The **config pass**: pure data over modules that are already loaded, run at every exchange.
7494
+ *
7495
+ * An array entry is a **module** — a definition object, a path, a `file:` URL, or a package
7496
+ * spec. It takes the position it is written at, so naming a module again moves it.
7497
+ *
7498
+ * An object entry is **configuration** for a plugin an earlier entry (or the built-in list)
7499
+ * already selected, addressed by its ID (`plugin`) or the module string (`package`) that selected
7500
+ * it. It never loads, installs or reorders anything, and a name matching nothing in the selection
7501
+ * is ignored rather than failing the boot — a typo costs a debug line, never a fetch.
7502
+ *
7503
+ * Nothing here touches the disk, which is the whole point of the split: an `options` edit, an
7504
+ * `enabled` flip or a reordering applies at the next exchange without importing anything.
7505
+ */
7506
+ const select = Effect.fn("PluginCatalog.select")(function* (references, pool) {
7507
+ const operations = /* @__PURE__ */ new Map();
7508
+ const missing = [];
7509
+ const choose = (id, index) => operations.set(id, {
7510
+ enabled: true,
7511
+ index,
7512
+ options: operations.get(id)?.options ?? {}
7486
7513
  });
7514
+ for (const [index, reference] of references.entries()) {
7515
+ if (isPatch(reference)) {
7516
+ const named = reference.plugin ?? reference.package;
7517
+ const origin = {
7518
+ index,
7519
+ reference: typeof named === "string" ? named : `<plugin entry #${index}>`
7520
+ };
7521
+ const invalid = reference.plugin !== void 0 && reference.package !== void 0 ? "names both `plugin` and `package`" : typeof named !== "string" || named.length === 0 ? "names neither `plugin` nor `package`" : reference.enabled !== void 0 && typeof reference.enabled !== "boolean" ? "`enabled` is not a boolean" : reference.options !== void 0 && !isRecord$1(reference.options) ? "`options` is not a plain object" : void 0;
7522
+ if (invalid !== void 0) return yield* definitionFailure(origin, /* @__PURE__ */ new Error(`plugin entry ${invalid}`));
7523
+ const id = reference.plugin === void 0 ? pool.aliases.get(reference.package) : reference.plugin;
7524
+ const operation = id === void 0 ? void 0 : operations.get(id);
7525
+ if (id === void 0 || operation === void 0) {
7526
+ yield* Effect.logDebug(`plugin configuration ignored: nothing selected under ${named}`);
7527
+ continue;
7528
+ }
7529
+ operations.set(id, {
7530
+ enabled: reference.enabled ?? operation.enabled,
7531
+ index: operation.index,
7532
+ options: reference.options ?? operation.options
7533
+ });
7534
+ continue;
7535
+ }
7536
+ if (typeof reference !== "string") {
7537
+ const declared = Predicate.hasProperty(reference, "id") ? reference.id : void 0;
7538
+ if (typeof declared === "string" && pool.plugins.has(declared)) choose(declared, index);
7539
+ continue;
7540
+ }
7541
+ const id = pool.aliases.get(reference);
7542
+ if (id === void 0) missing.push(reference);
7543
+ else choose(id, index);
7544
+ }
7545
+ const selection = [];
7546
+ for (const [id, operation] of operations) {
7547
+ if (!operation.enabled) continue;
7548
+ const plugin = pool.plugins.get(id);
7549
+ if (plugin !== void 0) selection.push({
7550
+ plugin,
7551
+ options: Object.freeze(operation.options),
7552
+ index: operation.index
7553
+ });
7554
+ }
7555
+ selection.sort((a, b) => (0, plugin_exports.rank)(a.plugin.kind) - (0, plugin_exports.rank)(b.plugin.kind) || a.index - b.index);
7487
7556
  return {
7488
- exitCode: code,
7489
- ...present
7557
+ selection: Object.freeze(selection.map(({ plugin, options }) => ({
7558
+ plugin,
7559
+ options
7560
+ }))),
7561
+ missing
7490
7562
  };
7491
- }));
7492
- const bashHandler = (params) => Effect.gen(function* () {
7493
- const shell = yield* ToolShell;
7494
- return shell.stream !== void 0 ? yield* runStreaming(shell.stream, params) : yield* runBuffered(shell, params);
7495
- });
7496
- /** The bash tool: definition + handler, wired the testable (def/exec split) way. */
7497
- const bashTool = tool_exports.implement(bashDef, bashHandler);
7498
- const bashPlugin = (0, plugin_exports.define)({
7499
- id: "codework.tool.bash",
7500
- kind: "tool",
7501
- setup: Effect.fn("BashPlugin.setup")(function* (ctx) {
7502
- const shell = yield* io_exports.SandboxIO.Shell;
7503
- const mounted = fromSandboxShell.pipe(Layer.provide(Layer.succeed(io_exports.SandboxIO.Shell, shell)));
7504
- ctx.plugin.tools.add(tool_exports.provide(bashTool, mounted));
7505
- })
7506
7563
  });
7507
- //#endregion
7508
- //#region ../harness/src/plugin/builtin.ts
7509
7564
  /**
7510
- * Every plugin the harness ships, and the selection a caller who passes no `plugins` gets. Tool
7511
- * contributors come first so the Prompt plugin that indexes them runs after they registered.
7565
+ * The exchange-boundary check: has the store moved under this pool?
7566
+ *
7567
+ * Every exchange already re-reads every settings layer from disk, and the config pass already
7568
+ * walks those entries against the loaded set. Two things can be settled right there, and they are
7569
+ * the same question asked twice:
7570
+ *
7571
+ * 1. a settings entry names a module that is not loaded, and the store holds it;
7572
+ * 2. a loaded module is not the newest generation the store has.
7573
+ *
7574
+ * The second is what makes `plugin update` finish. Without it the question is "is this entry
7575
+ * loaded?", the answer after an update is still yes, and the bytes just fetched sit on disk until
7576
+ * something else forces an import.
7577
+ *
7578
+ * Returns the new pool, or nothing when nothing moved -- which is the common case, and costs one
7579
+ * store lookup per filed module.
7580
+ *
7581
+ * **This is not a watcher.** Nothing polls and no fiber exists; it is the observation that we were
7582
+ * already paying for the I/O that would tell us, and discarding the answer. Three bounds keep it
7583
+ * safe: the store only and never the network, so an exchange cannot block on a registry;
7584
+ * resolve-only, so it cannot install; and once, at the top of a snapshot, so an exchange already
7585
+ * running is unaffected.
7512
7586
  */
7513
- const builtins = Object.freeze([bashPlugin, defaultPromptPlugin]);
7587
+ const follow = Effect.fn("PluginCatalog.follow")(function* (references, pool, options, filed) {
7588
+ let moved = false;
7589
+ for (const reference of references) {
7590
+ if (typeof reference !== "string" || pool.aliases.has(reference)) continue;
7591
+ if (Option.isSome(yield* filed(reference, anchor$1(options.declared?.get(reference), options.hostDir)))) {
7592
+ moved = true;
7593
+ break;
7594
+ }
7595
+ }
7596
+ if (!moved) for (const origin of pool.origins.values()) {
7597
+ if (origin.generation === void 0) continue;
7598
+ const current = yield* filed(origin.reference, anchor$1(origin.file, options.hostDir));
7599
+ if (Option.isSome(current) && current.value.generation !== void 0 && current.value.generation > origin.generation) {
7600
+ moved = true;
7601
+ break;
7602
+ }
7603
+ }
7604
+ if (!moved) return Option.none();
7605
+ const retained = [];
7606
+ for (const origin of pool.origins.values()) {
7607
+ if (references.includes(origin.reference) || Option.isSome(yield* filed(origin.reference, anchor$1(origin.file, options.hostDir)))) {
7608
+ retained.push(origin);
7609
+ continue;
7610
+ }
7611
+ yield* Effect.logWarning(`plugin ${origin.reference} no longer resolves; it is dropped from the loaded set`);
7612
+ }
7613
+ const accumulated = [...Array.from(retained, (origin) => origin.reference), ...references];
7614
+ const known = /* @__PURE__ */ new Map();
7615
+ for (const origin of retained) if (origin.file !== void 0) known.set(origin.reference, origin.file);
7616
+ for (const [reference, file] of options.declared ?? /* @__PURE__ */ new Map()) known.set(reference, file);
7617
+ return Option.some(yield* load$2(accumulated, {
7618
+ ...options,
7619
+ declared: known
7620
+ }));
7621
+ });
7622
+ Effect.fn("PluginCatalog.prepare")(function* (references, options) {
7623
+ const pool = yield* load$2(references, options);
7624
+ return (yield* select(references, pool)).selection;
7625
+ });
7514
7626
  //#endregion
7515
7627
  //#region ../harness/src/runner/coordinator.ts
7516
7628
  const make$8 = (options) => Effect.gen(function* () {
@@ -7652,6 +7764,82 @@ const layer$8 = Layer.effect(Service$6, Effect.gen(function* () {
7652
7764
  });
7653
7765
  }));
7654
7766
  //#endregion
7767
+ //#region ../harness/src/settings/schema.ts
7768
+ var schema_exports$1 = /* @__PURE__ */ __exportAll({});
7769
+ import * as import__codeworksh_plugin_settings from "@codeworksh/plugin/settings";
7770
+ __reExport(schema_exports$1, import__codeworksh_plugin_settings);
7771
+ //#endregion
7772
+ //#region ../harness/src/settings/resolve.ts
7773
+ const patternMatches = (pattern, id) => new RegExp(`^${pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(id);
7774
+ function collect(settings, provider, id) {
7775
+ const config = settings.model;
7776
+ const blocks = config.providerOptions?.[provider] ?? {};
7777
+ const patterns = Object.keys(blocks).filter((key) => key !== "*" && key !== id && key.includes("*") && patternMatches(key, id)).sort((a, b) => a.indexOf("*") - b.indexOf("*"));
7778
+ return merge({
7779
+ thinkingLevel: config.thinkingLevel,
7780
+ toolExecution: config.toolExecution,
7781
+ ...config.thinkingBudgets === void 0 ? {} : { thinkingBudgets: config.thinkingBudgets }
7782
+ }, config.options, blocks["*"], ...patterns.map((key) => blocks[key]), blocks[id]);
7783
+ }
7784
+ /**
7785
+ * Corrections to the catalog entry itself, applied before lookup.
7786
+ *
7787
+ * `protocol` is here rather than in the request bag because it is model shape: it decides which
7788
+ * wire protocol aikit speaks, and through `Model.optionsKey` which namespace the provider bag is
7789
+ * keyed by. A catalog cannot know it for a local OpenAI-compatible server, and the person
7790
+ * pointing at one does.
7791
+ */
7792
+ const resolveOverrides = (block) => ({
7793
+ ...block.contextWindow === void 0 ? {} : { contextWindow: block.contextWindow },
7794
+ ...block.protocol === void 0 ? {} : { protocol: block.protocol }
7795
+ });
7796
+ /** Protocol-independent request attributes can already be captured by State. */
7797
+ function resolveOptions(block) {
7798
+ return {
7799
+ ...Object.fromEntries(Object.entries(block).filter(([key]) => Object.hasOwn(schema_exports$1.requestFields, key))),
7800
+ ...block.extras === void 0 ? {} : { factoryOptions: merge({}, block.extras) }
7801
+ };
7802
+ }
7803
+ /** The bag key becomes knowable only after lookup at the LLM boundary. */
7804
+ function resolveRequest(block, model) {
7805
+ const bag = Object.fromEntries(Object.entries(block).filter(([key]) => !Object.hasOwn(schema_exports$1.requestFields, key) && ![
7806
+ "thinkingLevel",
7807
+ "toolExecution",
7808
+ "contextWindow",
7809
+ "protocol",
7810
+ "extras"
7811
+ ].includes(key)));
7812
+ return {
7813
+ ...resolveOptions(block),
7814
+ ...Object.keys(bag).length === 0 ? {} : { providerOptions: { [Model.optionsKey(model)]: bag } }
7815
+ };
7816
+ }
7817
+ /**
7818
+ * Harness-owned controls, which never reach a provider.
7819
+ *
7820
+ * Separate from {@link resolveRequest} because these steer the loop itself -- how hard to
7821
+ * think, how to schedule tool calls -- rather than the request body. Runtime bindings
7822
+ * outrank a matched block, which outranks the file's top level.
7823
+ */
7824
+ const resolveControls = (settings, block, runtime) => ({
7825
+ thinkingLevel: runtime.thinkingLevel ?? block.thinkingLevel ?? settings.model.thinkingLevel,
7826
+ toolExecution: runtime.toolExecution ?? block.toolExecution ?? settings.model.toolExecution
7827
+ });
7828
+ /** Pure configuration composition. Domain selections need not exist in the catalog. */
7829
+ function compose(settings, options = {}, session = {}) {
7830
+ const runtime = merge(options, session);
7831
+ const provider = runtime.provider ?? settings.model.provider;
7832
+ const model = runtime.model ?? settings.model.id;
7833
+ const block = collect(settings, provider, model);
7834
+ return {
7835
+ provider,
7836
+ model,
7837
+ ...resolveControls(settings, block, runtime),
7838
+ block,
7839
+ runtime
7840
+ };
7841
+ }
7842
+ //#endregion
7655
7843
  //#region ../harness/src/runner/event.ts
7656
7844
  const make$7 = Effect.fn("LLMEventPublisher.make")(function* (input) {
7657
7845
  const events = yield* Service$16;
@@ -7801,82 +7989,6 @@ const make$7 = Effect.fn("LLMEventPublisher.make")(function* (input) {
7801
7989
  };
7802
7990
  });
7803
7991
  //#endregion
7804
- //#region ../harness/src/settings/schema.ts
7805
- var schema_exports$1 = /* @__PURE__ */ __exportAll({});
7806
- import * as import__codeworksh_plugin_settings from "@codeworksh/plugin/settings";
7807
- __reExport(schema_exports$1, import__codeworksh_plugin_settings);
7808
- //#endregion
7809
- //#region ../harness/src/settings/resolve.ts
7810
- const patternMatches = (pattern, id) => new RegExp(`^${pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(id);
7811
- function collect(settings, provider, id) {
7812
- const config = settings.model;
7813
- const blocks = config.providerOptions?.[provider] ?? {};
7814
- const patterns = Object.keys(blocks).filter((key) => key !== "*" && key !== id && key.includes("*") && patternMatches(key, id)).sort((a, b) => a.indexOf("*") - b.indexOf("*"));
7815
- return merge({
7816
- thinkingLevel: config.thinkingLevel,
7817
- toolExecution: config.toolExecution,
7818
- ...config.thinkingBudgets === void 0 ? {} : { thinkingBudgets: config.thinkingBudgets }
7819
- }, config.options, blocks["*"], ...patterns.map((key) => blocks[key]), blocks[id]);
7820
- }
7821
- /**
7822
- * Corrections to the catalog entry itself, applied before lookup.
7823
- *
7824
- * `protocol` is here rather than in the request bag because it is model shape: it decides which
7825
- * wire protocol aikit speaks, and through `Model.optionsKey` which namespace the provider bag is
7826
- * keyed by. A catalog cannot know it for a local OpenAI-compatible server, and the person
7827
- * pointing at one does.
7828
- */
7829
- const resolveOverrides = (block) => ({
7830
- ...block.contextWindow === void 0 ? {} : { contextWindow: block.contextWindow },
7831
- ...block.protocol === void 0 ? {} : { protocol: block.protocol }
7832
- });
7833
- /** Protocol-independent request attributes can already be captured by State. */
7834
- function resolveOptions(block) {
7835
- return {
7836
- ...Object.fromEntries(Object.entries(block).filter(([key]) => Object.hasOwn(schema_exports$1.requestFields, key))),
7837
- ...block.extras === void 0 ? {} : { factoryOptions: merge({}, block.extras) }
7838
- };
7839
- }
7840
- /** The bag key becomes knowable only after lookup at the LLM boundary. */
7841
- function resolveRequest(block, model) {
7842
- const bag = Object.fromEntries(Object.entries(block).filter(([key]) => !Object.hasOwn(schema_exports$1.requestFields, key) && ![
7843
- "thinkingLevel",
7844
- "toolExecution",
7845
- "contextWindow",
7846
- "protocol",
7847
- "extras"
7848
- ].includes(key)));
7849
- return {
7850
- ...resolveOptions(block),
7851
- ...Object.keys(bag).length === 0 ? {} : { providerOptions: { [Model.optionsKey(model)]: bag } }
7852
- };
7853
- }
7854
- /**
7855
- * Harness-owned controls, which never reach a provider.
7856
- *
7857
- * Separate from {@link resolveRequest} because these steer the loop itself -- how hard to
7858
- * think, how to schedule tool calls -- rather than the request body. Runtime bindings
7859
- * outrank a matched block, which outranks the file's top level.
7860
- */
7861
- const resolveControls = (settings, block, runtime) => ({
7862
- thinkingLevel: runtime.thinkingLevel ?? block.thinkingLevel ?? settings.model.thinkingLevel,
7863
- toolExecution: runtime.toolExecution ?? block.toolExecution ?? settings.model.toolExecution
7864
- });
7865
- /** Pure configuration composition. Domain selections need not exist in the catalog. */
7866
- function compose(settings, options = {}, session = {}) {
7867
- const runtime = merge(options, session);
7868
- const provider = runtime.provider ?? settings.model.provider;
7869
- const model = runtime.model ?? settings.model.id;
7870
- const block = collect(settings, provider, model);
7871
- return {
7872
- provider,
7873
- model,
7874
- ...resolveControls(settings, block, runtime),
7875
- block,
7876
- runtime
7877
- };
7878
- }
7879
- //#endregion
7880
7992
  //#region ../harness/src/runner/llm.ts
7881
7993
  const runtimeOptions = (input, model, signal) => {
7882
7994
  const { activeTools, ...options } = merge(resolveRequest(input.settings ?? {}, model), input.options);
@@ -7926,17 +8038,6 @@ const providerError = (input, failure) => {
7926
8038
  });
7927
8039
  };
7928
8040
  const providerErrorFromUnknown = (input, cause) => providerError(input, AikitFailure.normalize(cause));
7929
- const modelCatalogFailure = (cause) => {
7930
- if (typeof cause !== "object" || cause === null || !("name" in cause) || cause.name !== "ModelCatalogLoadError") return;
7931
- if (!("data" in cause) || typeof cause.data !== "object" || cause.data === null) return void 0;
7932
- const data = cause.data;
7933
- if (!("path" in data) || typeof data.path !== "string" || !("message" in data) || typeof data.message !== "string" || !("reason" in data) || data.reason !== "missing" && data.reason !== "unreadable" && data.reason !== "empty" && data.reason !== "invalid") return;
7934
- return {
7935
- path: data.path,
7936
- reason: data.reason,
7937
- message: data.message
7938
- };
7939
- };
7940
8041
  /** Read structured terminal data while remaining compatible with older Aikit messages. */
7941
8042
  const messageFailure = (message) => {
7942
8043
  const candidate = message.failure;
@@ -7946,14 +8047,7 @@ const messageFailure = (message) => {
7946
8047
  const resolve$1 = Effect.fn("LLM.resolve")(function* (input) {
7947
8048
  const model = yield* Effect.tryPromise({
7948
8049
  try: () => llm(input.provider, input.model, resolveOverrides(input.settings ?? {})),
7949
- catch: (cause) => {
7950
- const catalog = modelCatalogFailure(cause);
7951
- return catalog === void 0 ? providerErrorFromUnknown(input, cause) : new ModelCatalogError({
7952
- path: catalog.path,
7953
- reason: catalog.reason,
7954
- detail: catalog.message
7955
- });
7956
- }
8050
+ catch: (cause) => loadError(cause) ?? providerErrorFromUnknown(input, cause)
7957
8051
  });
7958
8052
  if (model === void 0) return yield* new ModelNotFoundError({
7959
8053
  provider: input.provider,
@@ -8623,7 +8717,7 @@ const parse$1 = Effect.fn("Settings.parse")(function* (path, source) {
8623
8717
  reason: "parse",
8624
8718
  detail: `${printParseErrorCode(first.error)} at ${at(source, first.offset)}`
8625
8719
  });
8626
- const document = normalize$1(json);
8720
+ const document = normalize(json);
8627
8721
  if (Predicate.isObject(document) && Predicate.isObject(json) && "plugins" in json) Object.assign(document, { plugins: json.plugins });
8628
8722
  return yield* Schema.decodeUnknownEffect(schema_exports$1.Patch)(document).pipe(Effect.mapError((error) => new SettingsError({
8629
8723
  path,
@@ -9300,7 +9394,7 @@ const layer$3 = Layer.effectDiscard(Effect.gen(function* () {
9300
9394
  }));
9301
9395
  //#endregion
9302
9396
  //#region ../harness/src/session/live.ts
9303
- const layer$2 = Layer.provideMerge(layer$3, layer$23);
9397
+ const layer$2 = Layer.provideMerge(layer$3, layer$24);
9304
9398
  //#endregion
9305
9399
  //#region ../harness/src/effect/harness.ts
9306
9400
  const layer$1 = (options = {}) => Layer.unwrap(Effect.gen(function* () {
@@ -9361,14 +9455,17 @@ const layer$1 = (options = {}) => Layer.unwrap(Effect.gen(function* () {
9361
9455
  });
9362
9456
  });
9363
9457
  const definitions = yield* flatten([...(yield* Ref.get(pool)).plugins.values()]);
9364
- const database = layer$26(resolveDatabaseLocation(options.database ?? (yield* locationConfig), paths.data));
9458
+ const database = layer$27(resolveDatabaseLocation(options.database ?? (yield* locationConfig), paths.data));
9365
9459
  const configured = yield* loadAll(options.sandboxes ?? [], { hostCwd });
9366
- const drivers = layer$19(driver_exports.SandboxDriver.withSource(make$16().driver, "core"), driver_exports.SandboxDriver.withSource(make$14().driver, "core"), ...configured);
9367
- const sandboxes = layer$17({ hostCwd }).pipe(Layer.provideMerge(drivers), Layer.provideMerge(database));
9460
+ const drivers = layer$20(driver_exports.SandboxDriver.withSource(make$16().driver, "core"), driver_exports.SandboxDriver.withSource(make$14().driver, "core"), ...configured);
9461
+ const sandboxes = layer$18({ hostCwd }).pipe(Layer.provideMerge(drivers), Layer.provideMerge(database));
9368
9462
  const authFile$1 = authFile(options.home === void 0 ? void 0 : paths.home);
9369
9463
  const open = options.llm ?? openWith(authFile$1 === void 0 ? {} : { authFile: authFile$1 });
9370
9464
  const loop = layer$4({ request: make$6(open) });
9371
- return layer$10.pipe(Layer.provideMerge(layer$8.pipe(Layer.provide(loop))), Layer.provideMerge(layer$5({}, pool, references, followStore, rebuild)), Layer.provideMerge(layer$6(settingsOptions)), Layer.provideMerge(layer$7), Layer.provideMerge(sandboxes), Layer.provideMerge(layer$22), Layer.provideMerge(layer$2), Layer.provideMerge(layer$25), Layer.provideMerge(layer$9(definitions)), Layer.provideMerge(database), Layer.provideMerge(global));
9465
+ return layer$11.pipe(Layer.provideMerge(layer$8.pipe(Layer.provide(loop))), Layer.provideMerge(layer$5({}, pool, references, followStore, rebuild)), Layer.provideMerge(layer$6(settingsOptions)), Layer.provideMerge(layer$7), Layer.provideMerge(sandboxes), Layer.provideMerge(layer$23), Layer.provideMerge(layer$2), Layer.provideMerge(layer$26), Layer.provideMerge(layer$10(definitions)), Layer.provideMerge(database), Layer.provideMerge(global), Layer.provideMerge(layer$9({
9466
+ home: paths.home,
9467
+ watch: options.watchModels ?? false
9468
+ })));
9372
9469
  }));
9373
9470
  //#endregion
9374
9471
  //#region ../harness/src/effect/sandbox.ts
@@ -10067,16 +10164,15 @@ const terminalColumns = () => process.stderr.isTTY ? process.stderr.columns ?? 7
10067
10164
  //#region src/cli/error.ts
10068
10165
  /** Bad flag or argument combinations the parser cannot express on its own. */
10069
10166
  var InvalidInputError = class extends Schema.TaggedError()("CLI.InvalidInputError", { message: Schema.String }) {};
10070
- var ModelgenError = class extends Schema.TaggedError()("CLI.ModelgenError", { cause: Schema.Defect() }) {};
10071
10167
  var OAuthError = class extends Schema.TaggedError()("CLI.OAuthError", {
10072
10168
  message: Schema.String,
10073
10169
  cause: Schema.optional(Schema.Defect())
10074
10170
  }) {};
10075
10171
  const isInvalidInputError = Schema.is(InvalidInputError);
10076
- const isModelgenError = Schema.is(ModelgenError);
10077
10172
  const isOAuthError = Schema.is(OAuthError);
10078
10173
  const isProviderError = Schema.is(ProviderError);
10079
10174
  const isModelCatalogError = Schema.is(ModelCatalogError);
10175
+ const isModelCatalogRefreshError = Schema.is(RefreshError);
10080
10176
  const isModelNotFoundError = Schema.is(ModelNotFoundError);
10081
10177
  const isLLMStreamError = Schema.is(LLMStreamError);
10082
10178
  const isSandboxProviderError = Schema.is(SandboxProvider.SandboxProviderError);
@@ -10089,23 +10185,6 @@ const isSettingsError = Schema.is(SettingsError);
10089
10185
  const isSandboxDriverNotRegisteredError = Schema.is(errors_exports.SandboxError.SandboxDriverNotRegisteredError);
10090
10186
  const isSandboxDriverRegistrationError = Schema.is(errors_exports.SandboxError.SandboxDriverRegistrationError);
10091
10187
  const isExecutionError = Schema.is(ExecutionError);
10092
- const providerCategory = (reason) => {
10093
- switch (reason._tag) {
10094
- case "Runner.ProviderAuthenticationError": return "authentication";
10095
- case "Runner.ProviderConfigurationError": return "configuration";
10096
- case "Runner.ProviderAuthorizationError": return "authorization";
10097
- case "Runner.ProviderModelUnavailableError": return "model_unavailable";
10098
- case "Runner.ProviderRateLimitError": return "rate_limited";
10099
- case "Runner.ProviderQuotaError": return "quota";
10100
- case "Runner.ProviderInvalidRequestError": return "invalid_request";
10101
- case "Runner.ProviderContentPolicyError": return "content_policy";
10102
- case "Runner.ProviderTimeoutError": return "timeout";
10103
- case "Runner.ProviderTransportError": return "transport";
10104
- case "Runner.ProviderUnavailableError": return "provider_unavailable";
10105
- case "Runner.ProviderInvalidResponseError": return "invalid_response";
10106
- case "Runner.ProviderUnknownError": return "provider";
10107
- }
10108
- };
10109
10188
  const credentialHint = (provider) => {
10110
10189
  switch (provider) {
10111
10190
  case "openrouter": return "set OPENROUTER_API_KEY and retry";
@@ -10142,6 +10221,23 @@ const unknownMessage = (error) => {
10142
10221
  * usually hand-written in a settings file, so the phase is worth translating.
10143
10222
  */
10144
10223
  /** One hint per reason. Lowercase, like everything else a plugin failure prints. */
10224
+ const providerCategory = (reason) => {
10225
+ switch (reason._tag) {
10226
+ case "Runner.ProviderAuthenticationError": return "provider-authentication-error";
10227
+ case "Runner.ProviderConfigurationError": return "provider-configuration-error";
10228
+ case "Runner.ProviderAuthorizationError": return "provider-authorization-error";
10229
+ case "Runner.ProviderModelUnavailableError": return "provider-model-unavailable-error";
10230
+ case "Runner.ProviderRateLimitError": return "provider-rate-limit-error";
10231
+ case "Runner.ProviderQuotaError": return "provider-quota-error";
10232
+ case "Runner.ProviderInvalidRequestError": return "provider-invalid-request-error";
10233
+ case "Runner.ProviderContentPolicyError": return "provider-content-filter-error";
10234
+ case "Runner.ProviderTimeoutError": return "provider-timeout-error";
10235
+ case "Runner.ProviderTransportError": return "provider-transport-error";
10236
+ case "Runner.ProviderUnavailableError": return "provider-unavailable-error";
10237
+ case "Runner.ProviderInvalidResponseError": return "provider-invalid-response-error";
10238
+ case "Runner.ProviderUnknownError": return "provider-unknown-error";
10239
+ }
10240
+ };
10145
10241
  const pluginReasonHint = (reason) => {
10146
10242
  switch (reason) {
10147
10243
  case "plugin-unsupported-source": return "a path entry starts with `./`, `../`, `~/` or `/`; anything else is a package or a git spec, and a remote tarball is not supported";
@@ -10174,8 +10270,7 @@ const renderError = (error) => {
10174
10270
  if (isSandboxDriverNotRegisteredError(error)) return `error: sandbox driver "${error.driver}" is not registered (available: ${error.registered?.join(", ") ?? "none"})\n`;
10175
10271
  if (isSandboxDriverRegistrationError(error)) return `error: ${error.reason}\n`;
10176
10272
  if (isInvalidInputError(error)) return `error: ${error.message}\n`;
10177
- if (isModelgenError(error)) return ["error[model-catalog]: failed to generate the model catalog", "hint: check the output path and retry"].join("\n") + "\n";
10178
- if (isOAuthError(error)) return [`error[oauth]: ${error.message}`, ...error.cause === void 0 ? [] : [`detail: ${unknownMessage(error.cause)}`]].join("\n") + "\n";
10273
+ if (isOAuthError(error)) return [`error[oauth-credential-error]: ${error.message}`, ...error.cause === void 0 ? [] : [`detail: ${unknownMessage(error.cause)}`]].join("\n") + "\n";
10179
10274
  if (isSandboxProviderError(error)) return [
10180
10275
  `error: SandboxProviderError - ${error.sanitized.message}`,
10181
10276
  `driver: ${error.driver}`,
@@ -10189,8 +10284,7 @@ const renderError = (error) => {
10189
10284
  "hint: run `codework session link <session-id>` from the directory to anchor it to"
10190
10285
  ].join("\n") + "\n";
10191
10286
  if (isSettingsError(error)) return [
10192
- `error[settings]: ${error.path}`,
10193
- `reason: ${error.reason}`,
10287
+ `error[settings-${error.reason}-failed]: ${error.path}`,
10194
10288
  `detail: ${error.detail}`,
10195
10289
  `hint: ${settingsHint(error.reason)}`
10196
10290
  ].join("\n") + "\n";
@@ -10211,9 +10305,10 @@ const renderError = (error) => {
10211
10305
  ...hint === void 0 ? [] : [`hint: ${hint}`]
10212
10306
  ].join("\n") + "\n";
10213
10307
  }
10214
- if (isModelCatalogError(error)) return [`error[model-catalog]: ${error.message}`, "hint: run `codework models generate` or set CODEWORK_MODELS_FILE to a generated catalog"].join("\n") + "\n";
10215
- if (isModelNotFoundError(error)) return [`error[model-not-found]: ${error.message}`, "hint: check the provider/model IDs in models.gen.json or regenerate the catalog"].join("\n") + "\n";
10216
- if (isLLMStreamError(error)) return `error[stream]: ${error.message}\n`;
10308
+ if (isModelCatalogError(error)) return [`error[model-catalog-${error.reason}]: ${error.message}`, "hint: run `codework models refresh`"].join("\n") + "\n";
10309
+ if (isModelCatalogRefreshError(error)) return [`error[catalog-refresh-failed]: ${error.message}`, "hint: check the network connection and retry"].join("\n") + "\n";
10310
+ if (isModelNotFoundError(error)) return [`error[model-not-found-error]: ${error.message}`, "hint: check the provider/model IDs in models.gen.json or regenerate the catalog"].join("\n") + "\n";
10311
+ if (isLLMStreamError(error)) return `error[llm-stream-error]: ${error.message}\n`;
10217
10312
  if (isExecutionError(error)) return [error.type === "aborted" ? `error: ${error.message}` : `error[${error.type}]: ${error.message}`, ...error.status === void 0 ? [] : [`status: ${error.status}`]].join("\n") + "\n";
10218
10313
  return `error: ${unknownMessage(error)}\n`;
10219
10314
  };
@@ -10221,6 +10316,6 @@ const reportFailure = (error) => writeError(renderError(error)).pipe(Effect.andT
10221
10316
  process.exitCode = 1;
10222
10317
  })));
10223
10318
  //#endregion
10224
- export { Service$5 as $, resolve as A, inspect as B, get as C, __commonJSMin as Ct, drivers as D, __toCommonJS as Dt, create$2 as E, __require as Et, paths as F, lock as G, required as H, projectRoot as I, InstallError as J, parse$2 as K, anchor$1 as L, layer$1 as M, Service$1 as N, list as O, __toESM as Ot, load$1 as P, Service$4 as Q, canonical as R, create$1 as S, schema_exports$8 as St, relink as T, __exportAll as Tt, resolve$2 as U, check as V, update as W, SourceError as X, LoadError as Y, StoreError as Z, OAuthError$1 as _, path$1 as _t, reportFailure as a, SessionNotLinkedError as at, encode as b, resolve$3 as bt, header as c, SubscriptionOverflowError as ct, writeError as d, LLMTextDelta as dt, ModelCatalogError as et, writeOut as f, TurnEnded as ft, Api as g, layer$26 as gt, run as h, schema_exports$6 as ht, renderError as i, SessionNotFoundError as it, stop as j, register as k, terminalColumns as l, layer$25 as lt, make as m, instance_exports as mt, ModelgenError as n, sandbox$1 as nt, addUsage as o, layer$23 as ot, layer as p, Prompt as pt, probe as q, OAuthError as r, Service$14 as rt, emptyUsage as s, Service$16 as st, InvalidInputError as t, sandbox as tt, usage as u, LLMEnded as ut, Connected as v, Service$17 as vt, link as w, __esmMin as wt, attach as x, ID$1 as xt, Core as y, authFile as yt, definition$1 as z };
10319
+ export { refresh$1 as $, stop as A, __toESM as At, check as B, link as C, ID$1 as Ct, list as D, __exportAll as Dt, drivers as E, __esmMin as Et, projectRoot as F, parse$2 as G, resolve$2 as H, anchor$1 as I, LoadError as J, probe as K, canonical as L, Service$1 as M, load$1 as N, register as O, __require as Ot, paths as P, providers as Q, definition$1 as R, get as S, resolve$3 as St, create$2 as T, __commonJSMin as Tt, update as U, required as V, lock as W, StoreError as X, SourceError as Y, models as Z, Connected as _, schema_exports$6 as _t, addUsage as a, Service$14 as at, attach as b, Service$17 as bt, terminalColumns as c, layer$24 as ct, writeOut as d, layer$26 as dt, sync as et, layer as f, LLMEnded as ft, OAuthError$1 as g, instance_exports as gt, Api as h, Prompt as ht, reportFailure as i, sandbox$1 as it, layer$1 as j, resolve as k, __toCommonJS as kt, usage as l, Service$16 as lt, run as m, TurnEnded as mt, OAuthError as n, Service$5 as nt, emptyUsage as o, SessionNotFoundError as ot, make as p, LLMTextDelta as pt, InstallError as q, renderError as r, sandbox as rt, header as s, SessionNotLinkedError as st, InvalidInputError as t, Service$4 as tt, writeError as u, SubscriptionOverflowError as ut, Core as v, layer$27 as vt, relink as w, schema_exports$8 as wt, create$1 as x, authFile as xt, encode as y, path$1 as yt, inspect as z };
10225
10320
 
10226
- //# sourceMappingURL=error-BrbT9_4g.mjs.map
10321
+ //# sourceMappingURL=error-DG5INUkQ.mjs.map