@p4code/cli 0.0.41 → 0.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -54,13 +54,11 @@ import * as Exit from "effect/Exit";
54
54
  import * as SchemaGetter from "effect/SchemaGetter";
55
55
  import * as Config from "effect/Config";
56
56
  import * as NodeReadline from "node:readline";
57
- import * as Scope from "effect/Scope";
58
- import * as ChildProcess$1 from "effect/unstable/process/ChildProcess";
59
- import * as ChildProcessSpawner$1 from "effect/unstable/process/ChildProcessSpawner";
60
57
  import * as Cache from "effect/Cache";
61
58
  import * as Deferred from "effect/Deferred";
62
59
  import * as Equal from "effect/Equal";
63
60
  import * as Semaphore from "effect/Semaphore";
61
+ import * as Scope from "effect/Scope";
64
62
  import { parse } from "yaml";
65
63
  import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
66
64
  import * as Clock from "effect/Clock";
@@ -69,6 +67,8 @@ import * as Queue from "effect/Queue";
69
67
  import { cast, dual } from "effect/Function";
70
68
  import Mime from "@effect/platform-node/Mime";
71
69
  import * as Arr from "effect/Array";
70
+ import * as ChildProcess$1 from "effect/unstable/process/ChildProcess";
71
+ import * as ChildProcessSpawner$1 from "effect/unstable/process/ChildProcessSpawner";
72
72
  import * as Schedule from "effect/Schedule";
73
73
  import * as HttpClient$1 from "effect/unstable/http/HttpClient";
74
74
  import * as HttpClientRequest$1 from "effect/unstable/http/HttpClientRequest";
@@ -120,7 +120,7 @@ const closeServer = (server) => {
120
120
  * NetService - Service tag for startup networking helpers.
121
121
  */
122
122
  var NetService = class extends Context.Service()("@p4code/shared/Net/NetService") {};
123
- const make$70 = () => {
123
+ const make$71 = () => {
124
124
  /**
125
125
  * Returns true when a TCP server can bind to {host, port}.
126
126
  * `EADDRNOTAVAIL` is treated as available so IPv6-absent hosts don't fail
@@ -229,10 +229,10 @@ const make$70 = () => {
229
229
  })
230
230
  };
231
231
  };
232
- const layer$66 = Layer.sync(NetService, make$70);
232
+ const layer$67 = Layer.sync(NetService, make$71);
233
233
  //#endregion
234
234
  //#region package.json
235
- var version = "0.0.40";
235
+ var version = "0.0.42";
236
236
  //#endregion
237
237
  //#region src/config.ts
238
238
  /**
@@ -253,8 +253,8 @@ var ServerConfig$1 = class extends Context.Service()("@p4code/cli/config/ServerC
253
253
  /** @deprecated Import and use `layerTest` from this module. */
254
254
  static layerTest = (cwd, baseDirOrPrefix) => layerTest$2(cwd, baseDirOrPrefix);
255
255
  };
256
- const make$69 = (config) => ServerConfig$1.of(config);
257
- const layer$65 = (config) => Layer.succeed(ServerConfig$1, make$69(config));
256
+ const make$70 = (config) => ServerConfig$1.of(config);
257
+ const layer$66 = (config) => Layer.succeed(ServerConfig$1, make$70(config));
258
258
  const deriveServerPaths = Effect.fn(function* (baseDir, devUrl, options = {}) {
259
259
  const { join } = yield* Path.Path;
260
260
  const stateDir = join(baseDir, devUrl !== void 0 && !options.baseDirIsExplicit ? "dev" : "userdata");
@@ -922,7 +922,7 @@ const resolveLatestVersion = Effect.fn("cli.updateCheck.resolve")(function* (sta
922
922
  * `service update` restarts it onto the new one.
923
923
  */
924
924
  function formatUpdateReminder(input) {
925
- return [`Update available: ${PACKAGE_NAME}@${input.latestVersion} (running ${input.currentVersion}).`, ` npm i -g ${PACKAGE_NAME}@latest && p4 service update`].join("\n");
925
+ return [`Update available: ${PACKAGE_NAME}@${input.latestVersion} (running ${input.currentVersion}).`, ` npm i -g ${PACKAGE_NAME}@latest && p4c service update`].join("\n");
926
926
  }
927
927
  /**
928
928
  * The reminder line, or null when there is nothing to say. Callers print it;
@@ -6177,7 +6177,7 @@ var ServerProviderUpdateError = class extends Schema$1.TaggedErrorClass()("Serve
6177
6177
  }
6178
6178
  };
6179
6179
  const ServerSelfUpdateInput = Schema$1.Struct({
6180
- /** Exact npm version of the `p4` package to install (never a dist-tag, so
6180
+ /** Exact npm version of the `p4c` package to install (never a dist-tag, so
6181
6181
  the server and the acknowledging client agree on what was requested). */
6182
6182
  targetVersion: TrimmedNonEmptyString });
6183
6183
  /** Acknowledgement that the update artifact is installed and the server is
@@ -7312,10 +7312,26 @@ const AGENT_ASSET_TOTAL_MAX_BYTES = 4 * 1024 * 1024;
7312
7312
  * What kind of thing this is.
7313
7313
  *
7314
7314
  * A literal union rather than free text because the kind decides where the
7315
- * asset lands on disk, and an unknown kind has no answer to that. Adding
7316
- * `memory` here is the next step, and it needs a materializer to go with it.
7315
+ * asset lands on disk, and an unknown kind has no answer to that. Each kind
7316
+ * needs a materializer, which is what stops this from being a place to add a
7317
+ * string.
7318
+ *
7319
+ * - `skill` — a directory under `<configDir>/skills`, named by the asset.
7320
+ * - `memory` — a single file directly in `<configDir>`, named by the asset.
7321
+ * The name is the filename, and only the allowlisted ones exist: that
7322
+ * directory also holds `settings.json` and `.credentials.json`, so "write the
7323
+ * file this asset names" has to be bounded by something narrower than the
7324
+ * path schema.
7325
+ * - `mcp` — an external MCP server registration, in p4code's own state
7326
+ * directory rather than the provider's. The definition travels; the bearer
7327
+ * tokens and API keys it references do not, and stay in each machine's own
7328
+ * secret store.
7317
7329
  */
7318
- const AgentAssetKind = Schema$1.Literals(["skill"]);
7330
+ const AgentAssetKind = Schema$1.Literals([
7331
+ "skill",
7332
+ "memory",
7333
+ "mcp"
7334
+ ]);
7319
7335
  /**
7320
7336
  * An asset's name, which for a skill is also its directory name.
7321
7337
  *
@@ -7331,6 +7347,18 @@ const AgentAssetName = TrimmedNonEmptyString.check(Schema$1.isMaxLength(AGENT_AS
7331
7347
  description: "Letters, digits, dot, dash and underscore; must not start with a separator."
7332
7348
  }));
7333
7349
  /**
7350
+ * Which asset, across every kind.
7351
+ *
7352
+ * Kind and name together, because name alone stopped being unique the moment a
7353
+ * second kind existed: `AgentAssetName` permits dots, so a skill directory may
7354
+ * legitimately be called `CLAUDE.md`. A report saying only "published
7355
+ * CLAUDE.md" would be ambiguous about what had just been sent where.
7356
+ */
7357
+ const AgentAssetRef = Schema$1.Struct({
7358
+ kind: AgentAssetKind,
7359
+ name: AgentAssetName
7360
+ });
7361
+ /**
7334
7362
  * A path inside the asset, relative to the asset's own directory.
7335
7363
  *
7336
7364
  * Same reasoning as the name and the same threat, in a more tempting form:
@@ -7428,56 +7456,77 @@ const HubLinkSource = Schema$1.Literals([
7428
7456
  "none"
7429
7457
  ]);
7430
7458
  /**
7431
- * What a skill's three views add up to.
7459
+ * What an asset's three views add up to.
7432
7460
  *
7433
7461
  * `pending` means the next sync will act; `local-edit` means it will publish
7434
7462
  * this machine's copy; `conflict` means it will do nothing and wants a person.
7435
7463
  * `unmanaged` is the important one — p4code did not write it, so it is neither
7436
7464
  * uploaded nor written over.
7465
+ *
7466
+ * `hub-only` is the one that has no local half: the hub holds it and this
7467
+ * machine does not materialize it. That is normal for a memory file belonging
7468
+ * to a provider this machine does not run, and it is the state that makes
7469
+ * hub-side editing worth having — otherwise the only way to reach such an asset
7470
+ * would be to install the provider first.
7437
7471
  */
7438
- const SkillSyncEntryState = Schema$1.Literals([
7472
+ const AgentSyncEntryState = Schema$1.Literals([
7439
7473
  "synced",
7440
7474
  "pending",
7441
7475
  "local-edit",
7442
7476
  "conflict",
7443
- "unmanaged"
7477
+ "unmanaged",
7478
+ "hub-only"
7444
7479
  ]);
7445
- const SkillSyncEntry = Schema$1.Struct({
7480
+ const AgentSyncEntry = Schema$1.Struct({
7481
+ kind: AgentAssetKind,
7446
7482
  name: Schema$1.String,
7447
- state: SkillSyncEntryState,
7483
+ state: AgentSyncEntryState,
7448
7484
  version: Schema$1.NullOr(Schema$1.Int),
7449
7485
  /** Conflict reason or pending action, for a caller that wants to explain. */
7450
7486
  detail: Schema$1.NullOr(Schema$1.String)
7451
7487
  });
7452
- const SkillSyncRefusal = Schema$1.Struct({
7488
+ const AgentSyncRefusal = Schema$1.Struct({
7489
+ kind: AgentAssetKind,
7453
7490
  name: Schema$1.String,
7454
7491
  reason: Schema$1.String
7455
7492
  });
7456
- const SkillSyncConflictReport = Schema$1.Struct({
7493
+ const AgentSyncConflictReport = Schema$1.Struct({
7494
+ kind: AgentAssetKind,
7457
7495
  name: Schema$1.String,
7458
7496
  reason: Schema$1.String
7459
7497
  });
7460
- const SkillSyncStatus = Schema$1.Struct({
7498
+ /**
7499
+ * Where one kind materializes on this machine.
7500
+ *
7501
+ * A list rather than a field per kind, so adding the third kind is data rather
7502
+ * than another optional string every reader has to learn about. Null when the
7503
+ * root cannot be resolved — a provider that is not configured here.
7504
+ */
7505
+ const AgentSyncRoot = Schema$1.Struct({
7506
+ kind: AgentAssetKind,
7507
+ path: Schema$1.NullOr(Schema$1.String)
7508
+ });
7509
+ const AgentSyncStatus = Schema$1.Struct({
7461
7510
  configured: Schema$1.Boolean,
7462
7511
  hubUrl: Schema$1.NullOr(Schema$1.String),
7463
7512
  syncMode: HubSyncMode,
7464
7513
  shareMode: HubShareMode,
7465
7514
  source: HubLinkSource,
7466
- skillsRoot: Schema$1.NullOr(Schema$1.String),
7467
- entries: Schema$1.Array(SkillSyncEntry),
7468
- refused: Schema$1.Array(SkillSyncRefusal),
7515
+ roots: Schema$1.Array(AgentSyncRoot),
7516
+ entries: Schema$1.Array(AgentSyncEntry),
7517
+ refused: Schema$1.Array(AgentSyncRefusal),
7469
7518
  /** Set when the hub could not be reached. Local state is still reported. */
7470
7519
  unavailable: Schema$1.NullOr(Schema$1.String)
7471
7520
  });
7472
- const SkillSyncReport = Schema$1.Struct({
7521
+ const AgentSyncReport = Schema$1.Struct({
7473
7522
  ran: Schema$1.Boolean,
7474
- skillsRoot: Schema$1.NullOr(Schema$1.String),
7475
- materialized: Schema$1.Array(Schema$1.String),
7476
- published: Schema$1.Array(Schema$1.String),
7477
- removed: Schema$1.Array(Schema$1.String),
7478
- conflicts: Schema$1.Array(SkillSyncConflictReport),
7479
- unmanaged: Schema$1.Array(Schema$1.String),
7480
- refused: Schema$1.Array(SkillSyncRefusal),
7523
+ roots: Schema$1.Array(AgentSyncRoot),
7524
+ materialized: Schema$1.Array(AgentAssetRef),
7525
+ published: Schema$1.Array(AgentAssetRef),
7526
+ removed: Schema$1.Array(AgentAssetRef),
7527
+ conflicts: Schema$1.Array(AgentSyncConflictReport),
7528
+ unmanaged: Schema$1.Array(AgentAssetRef),
7529
+ refused: Schema$1.Array(AgentSyncRefusal),
7481
7530
  unavailable: Schema$1.NullOr(Schema$1.String)
7482
7531
  });
7483
7532
  const HubConnectInput = Schema$1.Struct({
@@ -7497,7 +7546,57 @@ const HubMintedToken = Schema$1.Struct({
7497
7546
  token: Schema$1.String,
7498
7547
  digest: Schema$1.String
7499
7548
  });
7500
- const SkillNameInput = Schema$1.Struct({ name: AgentAssetName });
7549
+ const AgentAssetRefInput = AgentAssetRef;
7550
+ /**
7551
+ * One asset as the hub holds it, for editing.
7552
+ *
7553
+ * Answered as `found: false` rather than as an error, because "the hub does not
7554
+ * have this" is a normal answer to a panel that is showing a row another
7555
+ * machine may have deleted a moment ago. `version` is carried back out so the
7556
+ * save that follows can be a compare-and-set rather than a blind overwrite.
7557
+ */
7558
+ const AgentAssetContent = Schema$1.Struct({
7559
+ found: Schema$1.Boolean,
7560
+ kind: AgentAssetKind,
7561
+ name: AgentAssetName,
7562
+ files: Schema$1.Array(AgentAssetFile),
7563
+ version: Schema$1.NullOr(Schema$1.Int),
7564
+ updatedAt: Schema$1.NullOr(IsoDateTime),
7565
+ /** Set when the hub could not be reached, so a caller can say why it is blank. */
7566
+ unavailable: Schema$1.NullOr(Schema$1.String)
7567
+ });
7568
+ /**
7569
+ * Edit an asset in the hub, rather than on disk and upward.
7570
+ *
7571
+ * The hub is the shared copy, so this is the edit that reaches every machine
7572
+ * including ones that are asleep — and the only way to reach an asset this
7573
+ * machine does not materialize. `expectedVersion` is required, not optional:
7574
+ * an unconditional overwrite of shared state is exactly the operation that
7575
+ * loses another machine's edit without telling anyone.
7576
+ */
7577
+ const AgentAssetSaveInput = Schema$1.Struct({
7578
+ kind: AgentAssetKind,
7579
+ name: AgentAssetName,
7580
+ files: AgentAssetFiles,
7581
+ expectedVersion: PositiveInt
7582
+ });
7583
+ /**
7584
+ * Delete an asset from the hub and so from every machine that syncs it.
7585
+ *
7586
+ * Distinct from unpublishing, which only ends *this* machine's share and keeps
7587
+ * the local copy. This one is the destructive one, and it does not require the
7588
+ * asset to be managed here — that restriction is what made a stale row from a
7589
+ * decommissioned machine impossible to clear.
7590
+ *
7591
+ * `expectedVersion` is optional here alone: someone deleting a row they can see
7592
+ * in a list has usually never held a version for it, and refusing on that basis
7593
+ * would make the operation unreachable in exactly the case it exists for.
7594
+ */
7595
+ const AgentAssetDeleteInput = Schema$1.Struct({
7596
+ kind: AgentAssetKind,
7597
+ name: AgentAssetName,
7598
+ expectedVersion: Schema$1.optional(PositiveInt)
7599
+ });
7501
7600
  /**
7502
7601
  * Changing the hub link failed.
7503
7602
  *
@@ -7522,6 +7621,143 @@ const encoder$3 = new TextEncoder();
7522
7621
  */
7523
7622
  const agentAssetTotalBytes = (files) => files.reduce((total, file) => total + encoder$3.encode(file.content).length + encoder$3.encode(file.path).length, 0);
7524
7623
  //#endregion
7624
+ //#region ../../packages/contracts/src/mcpServer.ts
7625
+ /**
7626
+ * External MCP server registrations.
7627
+ *
7628
+ * The servers a person adds themselves — a ticket tracker, a search index, a
7629
+ * build system — as opposed to p4code's own toolkit, which the adapters inject
7630
+ * on every session and which is not registered anywhere.
7631
+ *
7632
+ * **These are p4code's own data, not the provider's.** The obvious place to put
7633
+ * them is `~/.claude.json` under `mcpServers`, which is where the Claude CLI
7634
+ * keeps its own. p4code does not write that file: it also holds `oauthAccount`,
7635
+ * `machineID` and a `projects` map of per-path trust decisions, so owning one
7636
+ * key inside it means round-tripping all of that. The SDK takes `mcpServers` as
7637
+ * a session option and the adapter already passes it, so a registration p4code
7638
+ * stores itself reaches the agent by the same door its own toolkit uses —
7639
+ * without touching the provider's file at all.
7640
+ *
7641
+ * **Secret values are not in here, and that is the point.** A registration
7642
+ * routinely needs a bearer token in a header or an API key in the environment.
7643
+ * Those are named here and stored per machine in `ServerSecretStore`; the
7644
+ * registration itself carries only the name of the slot. That is what lets the
7645
+ * whole registration sync through the hub — which stores asset files verbatim —
7646
+ * without a working credential ever leaving the machine that holds it.
7647
+ *
7648
+ * @module contracts/mcpServer
7649
+ */
7650
+ /**
7651
+ * A server's name, reused from asset names rather than defined again.
7652
+ *
7653
+ * It is the key in the SDK's `mcpServers` record *and* a path segment in
7654
+ * p4code's own store, so it needs exactly the constraint an asset name already
7655
+ * has. Two definitions of the same rule is how one of them ends up laxer.
7656
+ */
7657
+ const McpServerName = AgentAssetName;
7658
+ /**
7659
+ * Where a secret goes when it is resolved.
7660
+ *
7661
+ * The slot, not the value. `header` fills one entry of an http server's
7662
+ * `headers`; `env` fills one entry of a stdio server's environment.
7663
+ */
7664
+ const McpSecretTarget = Schema$1.Literals(["header", "env"]);
7665
+ /**
7666
+ * One secret this server needs, named but not carried.
7667
+ *
7668
+ * There is no `secretName` field: the storage name is derived from the server,
7669
+ * the target and the key. A caller-supplied name would be one more thing to get
7670
+ * wrong, and two servers could name the same slot differently or — worse — the
7671
+ * same name for different slots.
7672
+ */
7673
+ const McpSecretSlot = Schema$1.Struct({
7674
+ target: McpSecretTarget,
7675
+ key: TrimmedNonEmptyString
7676
+ });
7677
+ /**
7678
+ * The storage name for one slot's value.
7679
+ *
7680
+ * Derived in the contract so both ends agree without either importing the
7681
+ * other's helper: the server resolving a session and the panel writing a value
7682
+ * have to compute the same string or the value is written where nothing looks.
7683
+ */
7684
+ const mcpSecretStorageName = (input) => `mcp.${input.server}.${input.target}.${input.key}`;
7685
+ const commonFields = {
7686
+ name: McpServerName,
7687
+ /**
7688
+ * Off means registered and not injected.
7689
+ *
7690
+ * Kept rather than deleted because turning a server off to test something is
7691
+ * the common case, and re-entering a command line and four headers to turn it
7692
+ * back on is how people stop turning it off.
7693
+ */
7694
+ enabled: Schema$1.Boolean,
7695
+ secrets: Schema$1.optional(Schema$1.Array(McpSecretSlot))
7696
+ };
7697
+ /**
7698
+ * A server p4code launches as a child process.
7699
+ *
7700
+ * `env` holds only values that are not secret. Anything that is goes in
7701
+ * `secrets` and is resolved from this machine's own store at session start.
7702
+ */
7703
+ const McpStdioServer = Schema$1.Struct({
7704
+ ...commonFields,
7705
+ transport: Schema$1.Literal("stdio"),
7706
+ command: TrimmedNonEmptyString,
7707
+ args: Schema$1.optional(Schema$1.Array(Schema$1.String)),
7708
+ env: Schema$1.optional(Schema$1.Record(Schema$1.String, Schema$1.String))
7709
+ });
7710
+ /** A server reached over HTTP. `headers` holds only non-secret values. */
7711
+ const McpRemoteServer = Schema$1.Struct({
7712
+ ...commonFields,
7713
+ transport: Schema$1.Literals(["http", "sse"]),
7714
+ url: TrimmedNonEmptyString,
7715
+ headers: Schema$1.optional(Schema$1.Record(Schema$1.String, Schema$1.String))
7716
+ });
7717
+ /**
7718
+ * A registration, as a union rather than one struct of optional fields.
7719
+ *
7720
+ * A stdio server with a `url` and no `command` is not a thing, and a struct
7721
+ * with both optional would let one be stored and then fail at session start —
7722
+ * on a machine that may not be the one where it was typed.
7723
+ */
7724
+ const McpServerRegistration = Schema$1.Union([McpStdioServer, McpRemoteServer]);
7725
+ /**
7726
+ * What a panel needs to show, including what this machine cannot run.
7727
+ *
7728
+ * `missingSecrets` is the field that earns this type. A registration syncs
7729
+ * everywhere; its secrets do not. So a server can be perfectly valid and still
7730
+ * unusable *here*, and saying which slot is empty is the difference between a
7731
+ * fixable state and a mystery.
7732
+ */
7733
+ const McpServerStatus = Schema$1.Struct({
7734
+ registration: McpServerRegistration,
7735
+ missingSecrets: Schema$1.Array(McpSecretSlot),
7736
+ /** True when this machine holds everything needed to start it. */
7737
+ ready: Schema$1.Boolean
7738
+ });
7739
+ const McpServerList = Schema$1.Struct({ servers: Schema$1.Array(McpServerStatus) });
7740
+ const McpServerSaveInput = Schema$1.Struct({ registration: McpServerRegistration });
7741
+ const McpServerNameInput = Schema$1.Struct({ name: McpServerName });
7742
+ /**
7743
+ * Setting one secret value, on this machine only.
7744
+ *
7745
+ * Separate from saving the registration because they go to different places and
7746
+ * only one of them travels. Bundling them would make the panel's save button
7747
+ * the thing that decides whether a credential syncs, which is not a decision to
7748
+ * leave to a button.
7749
+ */
7750
+ const McpServerSecretInput = Schema$1.Struct({
7751
+ name: McpServerName,
7752
+ slot: McpSecretSlot,
7753
+ value: Schema$1.String
7754
+ });
7755
+ var McpRegistryError = class extends Schema$1.TaggedErrorClass()("McpRegistryError", { detail: Schema$1.String }) {
7756
+ get message() {
7757
+ return this.detail;
7758
+ }
7759
+ };
7760
+ //#endregion
7525
7761
  //#region ../../packages/contracts/src/filesystem.ts
7526
7762
  const FILESYSTEM_PATH_MAX_LENGTH = 512;
7527
7763
  const FilesystemBrowseInput = Schema$1.Struct({
@@ -7777,6 +8013,13 @@ const WS_METHODS = {
7777
8013
  skillsPublish: "skills.publish",
7778
8014
  skillsPublishAll: "skills.publishAll",
7779
8015
  skillsUnpublish: "skills.unpublish",
8016
+ assetsRead: "assets.read",
8017
+ assetsSave: "assets.save",
8018
+ assetsDelete: "assets.delete",
8019
+ mcpList: "mcp.list",
8020
+ mcpSave: "mcp.save",
8021
+ mcpRemove: "mcp.remove",
8022
+ mcpSetSecret: "mcp.setSecret",
7780
8023
  sourceControlLookupRepository: "sourceControl.lookupRepository",
7781
8024
  sourceControlCloneRepository: "sourceControl.cloneRepository",
7782
8025
  sourceControlPublishRepository: "sourceControl.publishRepository",
@@ -7846,36 +8089,36 @@ const WsTasksDeleteRpc = Rpc.make(WS_METHODS.tasksDelete, {
7846
8089
  const HubRpcError = Schema$1.Union([HubLinkError, EnvironmentAuthorizationError]);
7847
8090
  const WsHubGetSyncStatusRpc = Rpc.make(WS_METHODS.hubGetSyncStatus, {
7848
8091
  payload: Schema$1.Struct({}),
7849
- success: SkillSyncStatus,
8092
+ success: AgentSyncStatus,
7850
8093
  error: Schema$1.Union([EnvironmentAuthorizationError])
7851
8094
  });
7852
8095
  const WsHubConnectRpc = Rpc.make(WS_METHODS.hubConnect, {
7853
8096
  payload: HubConnectInput,
7854
- success: SkillSyncStatus,
8097
+ success: AgentSyncStatus,
7855
8098
  error: HubRpcError
7856
8099
  });
7857
8100
  const WsHubDisconnectRpc = Rpc.make(WS_METHODS.hubDisconnect, {
7858
8101
  payload: Schema$1.Struct({}),
7859
- success: SkillSyncStatus,
8102
+ success: AgentSyncStatus,
7860
8103
  error: HubRpcError
7861
8104
  });
7862
8105
  const WsHubSetSyncModeRpc = Rpc.make(WS_METHODS.hubSetSyncMode, {
7863
8106
  payload: HubSyncModeInput,
7864
- success: SkillSyncStatus,
8107
+ success: AgentSyncStatus,
7865
8108
  error: HubRpcError
7866
8109
  });
7867
8110
  /**
7868
8111
  * Mint a bearer token for a hub, without contacting one.
7869
8112
  *
7870
8113
  * A hub issues nothing — it only ever holds digests — so this is local
7871
- * generation, the same thing `p4 hub token add` does. It exists as an RPC
8114
+ * generation, the same thing `p4c hub token add` does. It exists as an RPC
7872
8115
  * rather than in the browser because a client reached over plain HTTP on a LAN
7873
8116
  * has no `crypto.subtle`, and a token generator that silently degrades is not
7874
8117
  * a thing to ship.
7875
8118
  */
7876
8119
  const WsHubSetShareModeRpc = Rpc.make(WS_METHODS.hubSetShareMode, {
7877
8120
  payload: HubShareModeInput,
7878
- success: SkillSyncStatus,
8121
+ success: AgentSyncStatus,
7879
8122
  error: HubRpcError
7880
8123
  });
7881
8124
  const WsHubMintTokenRpc = Rpc.make(WS_METHODS.hubMintToken, {
@@ -7886,36 +8129,99 @@ const WsHubMintTokenRpc = Rpc.make(WS_METHODS.hubMintToken, {
7886
8129
  const WsSkillsSyncRpc = Rpc.make(WS_METHODS.skillsSync, {
7887
8130
  payload: Schema$1.Struct({}),
7888
8131
  success: Schema$1.Struct({
7889
- report: SkillSyncReport,
7890
- status: SkillSyncStatus
8132
+ report: AgentSyncReport,
8133
+ status: AgentSyncStatus
7891
8134
  }),
7892
8135
  error: Schema$1.Union([EnvironmentAuthorizationError])
7893
8136
  });
7894
8137
  const WsSkillsPublishRpc = Rpc.make(WS_METHODS.skillsPublish, {
7895
- payload: SkillNameInput,
8138
+ payload: AgentAssetRefInput,
7896
8139
  success: Schema$1.Struct({
7897
- report: SkillSyncReport,
7898
- status: SkillSyncStatus
8140
+ report: AgentSyncReport,
8141
+ status: AgentSyncStatus
7899
8142
  }),
7900
8143
  error: Schema$1.Union([EnvironmentAuthorizationError])
7901
8144
  });
7902
8145
  const WsSkillsPublishAllRpc = Rpc.make(WS_METHODS.skillsPublishAll, {
7903
8146
  payload: Schema$1.Struct({}),
7904
8147
  success: Schema$1.Struct({
7905
- report: SkillSyncReport,
7906
- status: SkillSyncStatus
8148
+ report: AgentSyncReport,
8149
+ status: AgentSyncStatus
7907
8150
  }),
7908
8151
  error: Schema$1.Union([EnvironmentAuthorizationError])
7909
8152
  });
7910
8153
  const WsSkillsUnpublishRpc = Rpc.make(WS_METHODS.skillsUnpublish, {
7911
- payload: SkillNameInput,
8154
+ payload: AgentAssetRefInput,
7912
8155
  success: Schema$1.Struct({
7913
- report: SkillSyncReport,
7914
- status: SkillSyncStatus
8156
+ report: AgentSyncReport,
8157
+ status: AgentSyncStatus
7915
8158
  }),
7916
8159
  error: Schema$1.Union([EnvironmentAuthorizationError])
7917
8160
  });
7918
8161
  /**
8162
+ * Reading, editing and deleting an asset in the hub itself.
8163
+ *
8164
+ * The hub is the shared copy, so these reach every machine — including ones
8165
+ * that are asleep, and including assets this machine does not materialize at
8166
+ * all. Without them the only way to change a shared asset was to edit the local
8167
+ * file and wait for it to propagate, which is no way at all when the asset
8168
+ * belongs to a provider this machine does not run or to a machine you no longer
8169
+ * have.
8170
+ */
8171
+ const WsAssetsReadRpc = Rpc.make(WS_METHODS.assetsRead, {
8172
+ payload: AgentAssetRefInput,
8173
+ success: AgentAssetContent,
8174
+ error: Schema$1.Union([EnvironmentAuthorizationError])
8175
+ });
8176
+ const WsAssetsSaveRpc = Rpc.make(WS_METHODS.assetsSave, {
8177
+ payload: AgentAssetSaveInput,
8178
+ success: Schema$1.Struct({
8179
+ report: AgentSyncReport,
8180
+ status: AgentSyncStatus
8181
+ }),
8182
+ error: Schema$1.Union([EnvironmentAuthorizationError])
8183
+ });
8184
+ const WsAssetsDeleteRpc = Rpc.make(WS_METHODS.assetsDelete, {
8185
+ payload: AgentAssetDeleteInput,
8186
+ success: Schema$1.Struct({
8187
+ report: AgentSyncReport,
8188
+ status: AgentSyncStatus
8189
+ }),
8190
+ error: Schema$1.Union([EnvironmentAuthorizationError])
8191
+ });
8192
+ /**
8193
+ * Registering the user's own MCP servers.
8194
+ *
8195
+ * Every one of these answers with the whole list, for the same reason the sync
8196
+ * calls answer with the whole status: a panel that had to re-read after each
8197
+ * write would show a stale row in between.
8198
+ *
8199
+ * `setSecret` is deliberately its own call rather than a field on `save`. The
8200
+ * two go to different places — one to a file that syncs between machines, one
8201
+ * to a store that never leaves this one — and folding them together would make
8202
+ * a save button the thing that decides whether a credential travels.
8203
+ */
8204
+ const WsMcpListRpc = Rpc.make(WS_METHODS.mcpList, {
8205
+ payload: Schema$1.Struct({}),
8206
+ success: McpServerList,
8207
+ error: Schema$1.Union([EnvironmentAuthorizationError])
8208
+ });
8209
+ const WsMcpSaveRpc = Rpc.make(WS_METHODS.mcpSave, {
8210
+ payload: McpServerSaveInput,
8211
+ success: McpServerList,
8212
+ error: Schema$1.Union([McpRegistryError, EnvironmentAuthorizationError])
8213
+ });
8214
+ const WsMcpRemoveRpc = Rpc.make(WS_METHODS.mcpRemove, {
8215
+ payload: McpServerNameInput,
8216
+ success: McpServerList,
8217
+ error: Schema$1.Union([McpRegistryError, EnvironmentAuthorizationError])
8218
+ });
8219
+ const WsMcpSetSecretRpc = Rpc.make(WS_METHODS.mcpSetSecret, {
8220
+ payload: McpServerSecretInput,
8221
+ success: McpServerList,
8222
+ error: Schema$1.Union([McpRegistryError, EnvironmentAuthorizationError])
8223
+ });
8224
+ /**
7919
8225
  * The board, then every change to it, for as long as the client stays
7920
8226
  * subscribed. The leading snapshot is what makes this sufficient on its own —
7921
8227
  * a client does not list first, so there is no window between the two in which
@@ -8283,7 +8589,7 @@ const WsSubscribeAuthAccessRpc = Rpc.make(WS_METHODS.subscribeAuthAccess, {
8283
8589
  error: Schema$1.Union([AuthAccessStreamError, EnvironmentAuthorizationError]),
8284
8590
  stream: true
8285
8591
  });
8286
- const WsRpcGroup = RpcGroup.make(WsServerProbeRpc, WsServerGetConfigRpc, WsServerRefreshProvidersRpc, WsServerUpdateProviderRpc, WsServerUpdateServerRpc, WsServerUpsertKeybindingRpc, WsServerRemoveKeybindingRpc, WsServerGetSettingsRpc, WsServerUpdateSettingsRpc, WsServerDiscoverSourceControlRpc, WsServerGetTraceDiagnosticsRpc, WsServerGetProcessDiagnosticsRpc, WsServerGetProcessResourceHistoryRpc, WsServerSignalProcessRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsSourceControlPublishRepositoryRpc, WsProjectsListEntriesRpc, WsProjectsReadFileRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsShellOpenInEditorRpc, WsFilesystemBrowseRpc, WsAssetsCreateUrlRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsVcsListRefsRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsReviewGetDiffPreviewRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsPreviewOpenRpc, WsPreviewNavigateRpc, WsPreviewResizeRpc, WsPreviewRefreshRpc, WsPreviewCloseRpc, WsPreviewListRpc, WsPreviewReportStatusRpc, WsPreviewAutomationConnectRpc, WsPreviewAutomationRespondRpc, WsPreviewAutomationFocusHostRpc, WsSubscribePreviewEventsRpc, WsSubscribeDiscoveredLocalServersRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsTasksListRpc, WsTasksGetRpc, WsTasksCreateRpc, WsTasksUpdateRpc, WsTasksDeleteRpc, WsSubscribeTasksRpc, WsHubGetSyncStatusRpc, WsHubConnectRpc, WsHubDisconnectRpc, WsHubSetSyncModeRpc, WsHubSetShareModeRpc, WsHubMintTokenRpc, WsSkillsSyncRpc, WsSkillsPublishRpc, WsSkillsPublishAllRpc, WsSkillsUnpublishRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc);
8592
+ const WsRpcGroup = RpcGroup.make(WsServerProbeRpc, WsServerGetConfigRpc, WsServerRefreshProvidersRpc, WsServerUpdateProviderRpc, WsServerUpdateServerRpc, WsServerUpsertKeybindingRpc, WsServerRemoveKeybindingRpc, WsServerGetSettingsRpc, WsServerUpdateSettingsRpc, WsServerDiscoverSourceControlRpc, WsServerGetTraceDiagnosticsRpc, WsServerGetProcessDiagnosticsRpc, WsServerGetProcessResourceHistoryRpc, WsServerSignalProcessRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsSourceControlPublishRepositoryRpc, WsProjectsListEntriesRpc, WsProjectsReadFileRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsShellOpenInEditorRpc, WsFilesystemBrowseRpc, WsAssetsCreateUrlRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsVcsListRefsRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsReviewGetDiffPreviewRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsPreviewOpenRpc, WsPreviewNavigateRpc, WsPreviewResizeRpc, WsPreviewRefreshRpc, WsPreviewCloseRpc, WsPreviewListRpc, WsPreviewReportStatusRpc, WsPreviewAutomationConnectRpc, WsPreviewAutomationRespondRpc, WsPreviewAutomationFocusHostRpc, WsSubscribePreviewEventsRpc, WsSubscribeDiscoveredLocalServersRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsTasksListRpc, WsTasksGetRpc, WsTasksCreateRpc, WsTasksUpdateRpc, WsTasksDeleteRpc, WsSubscribeTasksRpc, WsHubGetSyncStatusRpc, WsHubConnectRpc, WsHubDisconnectRpc, WsHubSetSyncModeRpc, WsHubSetShareModeRpc, WsHubMintTokenRpc, WsSkillsSyncRpc, WsSkillsPublishRpc, WsSkillsPublishAllRpc, WsSkillsUnpublishRpc, WsAssetsReadRpc, WsAssetsSaveRpc, WsAssetsDeleteRpc, WsMcpListRpc, WsMcpSaveRpc, WsMcpRemoveRpc, WsMcpSetSecretRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc);
8287
8593
  //#endregion
8288
8594
  //#region ../../packages/shared/src/oauthScope.ts
8289
8595
  const OAUTH_SCOPE_TOKEN = /^[\u0021\u0023-\u005b\u005d-\u007e]+$/u;
@@ -8429,7 +8735,7 @@ function deriveAuthClientMetadata(input) {
8429
8735
  //#endregion
8430
8736
  //#region src/auth/EnvironmentAuthPolicy.ts
8431
8737
  var EnvironmentAuthPolicy = class extends Context.Service()("@p4code/cli/auth/EnvironmentAuthPolicy") {};
8432
- const make$68 = Effect.gen(function* () {
8738
+ const make$69 = Effect.gen(function* () {
8433
8739
  const config = yield* ServerConfig$1;
8434
8740
  const isRemoteReachable = isRemoteReachableHost(config.host);
8435
8741
  const policy = config.mode === "desktop" ? isRemoteReachable ? "remote-reachable" : "desktop-managed-local" : isRemoteReachable ? "remote-reachable" : "loopback-browser";
@@ -8447,7 +8753,7 @@ const make$68 = Effect.gen(function* () {
8447
8753
  };
8448
8754
  return EnvironmentAuthPolicy.of({ getDescriptor: () => Effect.succeed(descriptor).pipe(Effect.withSpan("EnvironmentAuthPolicy.getDescriptor")) });
8449
8755
  });
8450
- const layer$64 = Layer.effect(EnvironmentAuthPolicy, make$68);
8756
+ const layer$65 = Layer.effect(EnvironmentAuthPolicy, make$69);
8451
8757
  //#endregion
8452
8758
  //#region src/persistence/Errors.ts
8453
8759
  function summarizeSchemaIssue(issue) {
@@ -8628,7 +8934,7 @@ function toPersistenceSqlOrDecodeError$6(sqlOperation, decodeOperation, correlat
8628
8934
  cause
8629
8935
  });
8630
8936
  }
8631
- const make$67 = Effect.gen(function* () {
8937
+ const make$68 = Effect.gen(function* () {
8632
8938
  const sql = yield* SqlClient.SqlClient;
8633
8939
  const createSessionRow = SqlSchema.void({
8634
8940
  Request: CreateAuthSessionInput,
@@ -8762,7 +9068,7 @@ const make$67 = Effect.gen(function* () {
8762
9068
  setLastConnectedAt
8763
9069
  };
8764
9070
  });
8765
- const layer$63 = Layer.effect(AuthSessionRepository, make$67);
9071
+ const layer$64 = Layer.effect(AuthSessionRepository, make$68);
8766
9072
  //#endregion
8767
9073
  //#region src/auth/ServerSecretStore.ts
8768
9074
  const secretStoreErrorContext = {
@@ -8829,7 +9135,7 @@ const isSecretStoreError = Schema$1.is(SecretStoreError);
8829
9135
  const isPlatformError = (value) => Predicate.isTagged(value, "PlatformError");
8830
9136
  const isSecretAlreadyExistsError = (error) => "cause" in error && isPlatformError(error.cause) && error.cause.reason._tag === "AlreadyExists";
8831
9137
  var ServerSecretStore = class extends Context.Service()("@p4code/cli/auth/ServerSecretStore") {};
8832
- const make$66 = Effect.gen(function* () {
9138
+ const make$67 = Effect.gen(function* () {
8833
9139
  const crypto = yield* Crypto.Crypto;
8834
9140
  const fileSystem = yield* FileSystem.FileSystem;
8835
9141
  const path = yield* Path.Path;
@@ -8899,7 +9205,7 @@ const make$66 = Effect.gen(function* () {
8899
9205
  remove
8900
9206
  });
8901
9207
  });
8902
- const layer$62 = Layer.effect(ServerSecretStore, make$66);
9208
+ const layer$63 = Layer.effect(ServerSecretStore, make$67);
8903
9209
  //#endregion
8904
9210
  //#region src/auth/SessionStore.ts
8905
9211
  var MalformedSessionTokenError = class extends Schema$1.TaggedErrorClass()("MalformedSessionTokenError", {}) {
@@ -9137,7 +9443,7 @@ function toAuthClientSession(input) {
9137
9443
  current: false
9138
9444
  };
9139
9445
  }
9140
- const make$65 = Effect.gen(function* () {
9446
+ const make$66 = Effect.gen(function* () {
9141
9447
  const crypto = yield* Crypto.Crypto;
9142
9448
  const serverConfig = yield* ServerConfig$1;
9143
9449
  const secretStore = yield* ServerSecretStore;
@@ -9451,7 +9757,7 @@ const make$65 = Effect.gen(function* () {
9451
9757
  markDisconnected
9452
9758
  });
9453
9759
  });
9454
- const layer$61 = Layer.effect(SessionStore, make$65).pipe(Layer.provideMerge(layer$63));
9760
+ const layer$62 = Layer.effect(SessionStore, make$66).pipe(Layer.provideMerge(layer$64));
9455
9761
  //#endregion
9456
9762
  //#region src/persistence/AuthPairingLinks.ts
9457
9763
  const AuthPairingLinkRecord = Schema$1.Struct({
@@ -9512,7 +9818,7 @@ function toPersistenceSqlOrDecodeError$5(sqlOperation, decodeOperation, correlat
9512
9818
  cause
9513
9819
  });
9514
9820
  }
9515
- const make$64 = Effect.gen(function* () {
9821
+ const make$65 = Effect.gen(function* () {
9516
9822
  const sql = yield* SqlClient.SqlClient;
9517
9823
  const createPairingLinkRow = SqlSchema.void({
9518
9824
  Request: CreateAuthPairingLinkInput,
@@ -9647,7 +9953,7 @@ const make$64 = Effect.gen(function* () {
9647
9953
  getByCredential
9648
9954
  };
9649
9955
  });
9650
- const layer$60 = Layer.effect(AuthPairingLinkRepository, make$64);
9956
+ const layer$61 = Layer.effect(AuthPairingLinkRepository, make$65);
9651
9957
  //#endregion
9652
9958
  //#region src/auth/PairingGrantStore.ts
9653
9959
  var UnknownBootstrapCredentialError = class extends Schema$1.TaggedErrorClass()("UnknownBootstrapCredentialError", {}) {
@@ -9742,7 +10048,7 @@ const DEV_STARTUP_TTL_HOURS = Duration.hours(24);
9742
10048
  const PAIRING_TOKEN_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
9743
10049
  const PAIRING_TOKEN_LENGTH = 12;
9744
10050
  const PAIRING_TOKEN_REJECTION_LIMIT = Math.floor(256 / 32) * 32;
9745
- const make$63 = Effect.gen(function* () {
10051
+ const make$64 = Effect.gen(function* () {
9746
10052
  const crypto = yield* Crypto.Crypto;
9747
10053
  const config = yield* ServerConfig$1;
9748
10054
  const pairingLinks = yield* AuthPairingLinkRepository;
@@ -9940,7 +10246,7 @@ const make$63 = Effect.gen(function* () {
9940
10246
  consume
9941
10247
  });
9942
10248
  });
9943
- const layer$59 = Layer.effect(PairingGrantStore, make$63).pipe(Layer.provideMerge(layer$60));
10249
+ const layer$60 = Layer.effect(PairingGrantStore, make$64).pipe(Layer.provideMerge(layer$61));
9944
10250
  //#endregion
9945
10251
  //#region src/persistence/Migrations/001_OrchestrationEvents.ts
9946
10252
  var _001_OrchestrationEvents_default = Effect.gen(function* () {
@@ -11555,7 +11861,7 @@ Layer.effectDiscard(runMigrations());
11555
11861
  //#region src/persistence/Layers/Sqlite.ts
11556
11862
  const defaultSqliteClientLoaders = {
11557
11863
  bun: () => import("@effect/sql-sqlite-bun/SqliteClient"),
11558
- node: () => import("./NodeSqliteClient-DGAB9NWW.mjs")
11864
+ node: () => import("./NodeSqliteClient-CRS7rg24.mjs")
11559
11865
  };
11560
11866
  const makeRuntimeSqliteLayer = Effect.fn("makeRuntimeSqliteLayer")(function* (config) {
11561
11867
  const runtime = process.versions.bun !== void 0 ? "bun" : "node";
@@ -11713,7 +12019,7 @@ function parseBearerToken(request) {
11713
12019
  const token = header.slice(7).trim();
11714
12020
  return token.length > 0 ? token : null;
11715
12021
  }
11716
- const make$62 = Effect.gen(function* () {
12022
+ const make$63 = Effect.gen(function* () {
11717
12023
  const policy = yield* EnvironmentAuthPolicy;
11718
12024
  const bootstrapCredentials = yield* PairingGrantStore;
11719
12025
  const sessions = yield* SessionStore;
@@ -11908,9 +12214,9 @@ const make$62 = Effect.gen(function* () {
11908
12214
  issueStartupPairingUrl
11909
12215
  });
11910
12216
  });
11911
- const layer$58 = Layer.effect(EnvironmentAuth, make$62).pipe(Layer.provideMerge(layer$59), Layer.provideMerge(layer$61), Layer.provideMerge(layer$64));
11912
- const storageLayer = Layer.mergeAll(layer$62, layerConfig);
11913
- const runtimeLayer = layer$58.pipe(Layer.provideMerge(storageLayer));
12217
+ const layer$59 = Layer.effect(EnvironmentAuth, make$63).pipe(Layer.provideMerge(layer$60), Layer.provideMerge(layer$62), Layer.provideMerge(layer$65));
12218
+ const storageLayer = Layer.mergeAll(layer$63, layerConfig);
12219
+ const runtimeLayer = layer$59.pipe(Layer.provideMerge(storageLayer));
11914
12220
  //#endregion
11915
12221
  //#region src/cliAuthFormat.ts
11916
12222
  const newline = "\n";
@@ -12665,7 +12971,7 @@ const runWithEnvironmentAuth = (flags, run, options) => Effect.gen(function* ()
12665
12971
  }
12666
12972
  return yield* Effect.gen(function* () {
12667
12973
  return yield* run(yield* EnvironmentAuth);
12668
- }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer).pipe(Layer.provide(layer$65(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
12974
+ }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer).pipe(Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
12669
12975
  });
12670
12976
  const ttlFlag = Flag.string("ttl").pipe(Flag.withSchema(DurationFromString), Flag.withDescription("TTL, for example `5m`, `1h`, `30d`, or `15 minutes`."), Flag.optional);
12671
12977
  const jsonFlag = Flag.boolean("json").pipe(Flag.withDescription("Emit JSON instead of human-readable output."), Flag.withDefault(false));
@@ -12750,422 +13056,6 @@ const sessionCommand = Command.make("session").pipe(Command.withDescription("Man
12750
13056
  ]));
12751
13057
  const authCommand = Command.make("auth").pipe(Command.withDescription("Manage the local auth control plane for headless deployments."), Command.withSubcommands([pairingCommand, sessionCommand]));
12752
13058
  //#endregion
12753
- //#region src/stream/collectUint8StreamText.ts
12754
- const collectUint8StreamText = (input) => {
12755
- const maxBytes = input.maxBytes ?? Number.POSITIVE_INFINITY;
12756
- const truncatedMarker = input.truncatedMarker ?? "";
12757
- return input.stream.pipe(Stream.runFold(() => ({
12758
- chunks: [],
12759
- bytes: 0,
12760
- truncated: false
12761
- }), (state, chunk) => {
12762
- if (state.truncated) return state;
12763
- const remainingBytes = maxBytes - state.bytes;
12764
- if (remainingBytes <= 0) return {
12765
- ...state,
12766
- truncated: true
12767
- };
12768
- const nextChunk = chunk.byteLength > remainingBytes ? chunk.slice(0, remainingBytes) : chunk;
12769
- state.chunks.push(nextChunk);
12770
- const bytes = state.bytes + nextChunk.byteLength;
12771
- const truncated = chunk.byteLength > remainingBytes;
12772
- return {
12773
- chunks: state.chunks,
12774
- bytes,
12775
- truncated
12776
- };
12777
- }), Effect.map((state) => {
12778
- const text = Buffer.concat(state.chunks, state.bytes).toString("utf8");
12779
- return {
12780
- text: state.truncated && truncatedMarker.length > 0 ? `${text}${truncatedMarker}` : text,
12781
- bytes: state.bytes,
12782
- truncated: state.truncated
12783
- };
12784
- }));
12785
- };
12786
- //#endregion
12787
- //#region src/processRunner.ts
12788
- const ProcessInvocationFields = {
12789
- command: Schema$1.String,
12790
- argumentCount: Schema$1.Number,
12791
- cwd: Schema$1.optional(Schema$1.String),
12792
- spawnCwd: Schema$1.optional(Schema$1.String)
12793
- };
12794
- const formatProcessInvocation = (input) => {
12795
- const executionCwd = input.spawnCwd ?? input.cwd;
12796
- return executionCwd === void 0 ? `'${input.command}'` : `'${input.command}' in '${executionCwd}'`;
12797
- };
12798
- var ProcessSpawnError = class extends Schema$1.TaggedErrorClass()("ProcessSpawnError", {
12799
- ...ProcessInvocationFields,
12800
- resolvedCommand: Schema$1.optional(Schema$1.String),
12801
- resolvedArgumentCount: Schema$1.optional(Schema$1.Number),
12802
- shell: Schema$1.optional(Schema$1.Boolean),
12803
- cause: Schema$1.Defect()
12804
- }) {
12805
- get message() {
12806
- return `Failed to spawn process ${formatProcessInvocation(this)}`;
12807
- }
12808
- };
12809
- var ProcessStdinError = class extends Schema$1.TaggedErrorClass()("ProcessStdinError", {
12810
- ...ProcessInvocationFields,
12811
- stdinBytes: Schema$1.Number,
12812
- cause: Schema$1.Defect()
12813
- }) {
12814
- get message() {
12815
- return `Failed to write stdin for process ${formatProcessInvocation(this)}`;
12816
- }
12817
- };
12818
- var ProcessOutputLimitError = class extends Schema$1.TaggedErrorClass()("ProcessOutputLimitError", {
12819
- ...ProcessInvocationFields,
12820
- stream: Schema$1.Literals(["stdout", "stderr"]),
12821
- maxBytes: Schema$1.Number,
12822
- observedBytes: Schema$1.Number
12823
- }) {
12824
- get message() {
12825
- return `Process ${formatProcessInvocation(this)} ${this.stream} produced ${this.observedBytes} bytes, exceeding the ${this.maxBytes} byte limit`;
12826
- }
12827
- };
12828
- var ProcessReadError = class extends Schema$1.TaggedErrorClass()("ProcessReadError", {
12829
- ...ProcessInvocationFields,
12830
- stream: Schema$1.Literals([
12831
- "stdout",
12832
- "stderr",
12833
- "exitCode"
12834
- ]),
12835
- cause: Schema$1.Defect()
12836
- }) {
12837
- get message() {
12838
- return `Failed to read ${this.stream} for process ${formatProcessInvocation(this)}`;
12839
- }
12840
- };
12841
- var ProcessTimeoutError = class extends Schema$1.TaggedErrorClass()("ProcessTimeoutError", {
12842
- ...ProcessInvocationFields,
12843
- timeoutMs: Schema$1.Number
12844
- }) {
12845
- get message() {
12846
- return `Process ${formatProcessInvocation(this)} timed out after ${this.timeoutMs}ms`;
12847
- }
12848
- };
12849
- Schema$1.Union([
12850
- ProcessSpawnError,
12851
- ProcessStdinError,
12852
- ProcessOutputLimitError,
12853
- ProcessReadError,
12854
- ProcessTimeoutError
12855
- ]);
12856
- var ProcessRunner = class extends Context.Service()("@p4code/cli/processRunner") {};
12857
- const DEFAULT_TIMEOUT = "60 seconds";
12858
- const DEFAULT_MAX_OUTPUT_BYTES$2 = 8 * 1024 * 1024;
12859
- const WINDOWS_COMMAND_NOT_FOUND_PATTERNS = [
12860
- /is not recognized as an internal or external command/i,
12861
- /n.o . reconhecido como um comando interno/i,
12862
- /non . riconosciuto come comando interno o esterno/i,
12863
- /n.est pas reconnu en tant que commande interne/i,
12864
- /no se reconoce como un comando interno o externo/i,
12865
- /wird nicht als interner oder externer befehl/i
12866
- ];
12867
- function hasWindowsCommandNotFoundMessage(output) {
12868
- return WINDOWS_COMMAND_NOT_FOUND_PATTERNS.some((pattern) => pattern.test(output));
12869
- }
12870
- const isWindowsCommandNotFound = Effect.fn("processRunner.isWindowsCommandNotFound")(function* (code, stderr) {
12871
- if ((yield* HostProcessPlatform) !== "win32") return false;
12872
- if (code === 9009) return true;
12873
- return hasWindowsCommandNotFoundMessage(stderr);
12874
- });
12875
- const collectText = Effect.fn("processRunner.collectText")(function* (input) {
12876
- const stream = input.stream.pipe(Stream.mapError((cause) => new ProcessReadError({
12877
- command: input.command,
12878
- argumentCount: input.args.length,
12879
- cwd: input.cwd,
12880
- spawnCwd: input.spawnCwd,
12881
- stream: input.streamName,
12882
- cause
12883
- })));
12884
- if (input.outputMode === "truncate") return yield* collectUint8StreamText({
12885
- stream,
12886
- maxBytes: input.maxOutputBytes,
12887
- truncatedMarker: input.truncatedMarker
12888
- });
12889
- return yield* stream.pipe(Stream.runFoldEffect(() => ({
12890
- chunks: [],
12891
- bytes: 0
12892
- }), (state, chunk) => {
12893
- const remainingBytes = input.maxOutputBytes - state.bytes;
12894
- if (chunk.byteLength > remainingBytes) return Effect.fail(new ProcessOutputLimitError({
12895
- command: input.command,
12896
- argumentCount: input.args.length,
12897
- cwd: input.cwd,
12898
- spawnCwd: input.spawnCwd,
12899
- stream: input.streamName,
12900
- maxBytes: input.maxOutputBytes,
12901
- observedBytes: state.bytes + chunk.byteLength
12902
- }));
12903
- state.chunks.push(chunk);
12904
- return Effect.succeed({
12905
- chunks: state.chunks,
12906
- bytes: state.bytes + chunk.byteLength
12907
- });
12908
- }), Effect.map((state) => ({
12909
- text: Buffer.concat(state.chunks, state.bytes).toString("utf8"),
12910
- bytes: state.bytes,
12911
- truncated: false
12912
- })));
12913
- });
12914
- function finalizeRunProcess(effect, input) {
12915
- const timeout = Duration.fromInputUnsafe(input.timeout ?? DEFAULT_TIMEOUT);
12916
- const timeoutBehavior = input.timeoutBehavior ?? "error";
12917
- return effect.pipe(Effect.scoped, Effect.timeoutOption(timeout), Effect.flatMap((result) => {
12918
- if (Option.isSome(result)) return Effect.succeed(result.value);
12919
- if (timeoutBehavior === "timedOutResult") return Effect.succeed({
12920
- stdout: "",
12921
- stderr: "",
12922
- code: null,
12923
- timedOut: true,
12924
- stdoutTruncated: false,
12925
- stderrTruncated: false
12926
- });
12927
- return Effect.fail(new ProcessTimeoutError({
12928
- command: input.command,
12929
- argumentCount: input.args.length,
12930
- cwd: input.cwd,
12931
- spawnCwd: input.spawnCwd,
12932
- timeoutMs: Duration.toMillis(timeout)
12933
- }));
12934
- }));
12935
- }
12936
- const runProcessCore = Effect.fn("processRunner.runProcessCore")(function* (spawner, input) {
12937
- const maxOutputBytes = input.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES$2;
12938
- const outputMode = input.outputMode ?? "error";
12939
- const truncatedMarker = input.truncatedMarker ?? "";
12940
- const extendEnv = input.env !== void 0;
12941
- const spawnCommand = yield* resolveSpawnCommand(input.command, input.args, input.env === void 0 ? {} : {
12942
- env: input.env,
12943
- extendEnv
12944
- });
12945
- const child = yield* spawner.spawn(ChildProcess$1.make(spawnCommand.command, spawnCommand.args, {
12946
- ...input.spawnCwd ?? input.cwd ? { cwd: input.spawnCwd ?? input.cwd } : {},
12947
- ...input.env !== void 0 ? {
12948
- env: input.env,
12949
- extendEnv
12950
- } : {},
12951
- shell: spawnCommand.shell
12952
- })).pipe(Effect.mapError((cause) => new ProcessSpawnError({
12953
- command: input.command,
12954
- argumentCount: input.args.length,
12955
- cwd: input.cwd,
12956
- spawnCwd: input.spawnCwd,
12957
- resolvedCommand: spawnCommand.command,
12958
- resolvedArgumentCount: spawnCommand.args.length,
12959
- shell: spawnCommand.shell,
12960
- cause
12961
- })));
12962
- const stdin = input.stdin;
12963
- const writeStdin = stdin === void 0 ? Effect.void : Stream.run(Stream.encodeText(Stream.make(stdin)), child.stdin).pipe(Effect.mapError((cause) => new ProcessStdinError({
12964
- command: input.command,
12965
- argumentCount: input.args.length,
12966
- cwd: input.cwd,
12967
- spawnCwd: input.spawnCwd,
12968
- stdinBytes: Buffer.byteLength(stdin),
12969
- cause
12970
- })));
12971
- const [stdout, stderr] = yield* Effect.all([
12972
- collectText({
12973
- command: input.command,
12974
- args: input.args,
12975
- cwd: input.cwd,
12976
- spawnCwd: input.spawnCwd,
12977
- streamName: "stdout",
12978
- stream: child.stdout,
12979
- maxOutputBytes,
12980
- outputMode,
12981
- truncatedMarker
12982
- }),
12983
- collectText({
12984
- command: input.command,
12985
- args: input.args,
12986
- cwd: input.cwd,
12987
- spawnCwd: input.spawnCwd,
12988
- streamName: "stderr",
12989
- stream: child.stderr,
12990
- maxOutputBytes,
12991
- outputMode,
12992
- truncatedMarker
12993
- }),
12994
- writeStdin
12995
- ], { concurrency: "unbounded" });
12996
- const exitCode = yield* child.exitCode.pipe(Effect.mapError((cause) => new ProcessReadError({
12997
- command: input.command,
12998
- argumentCount: input.args.length,
12999
- cwd: input.cwd,
13000
- spawnCwd: input.spawnCwd,
13001
- stream: "exitCode",
13002
- cause
13003
- })));
13004
- return {
13005
- stdout: stdout.text,
13006
- stderr: stderr.text,
13007
- code: exitCode,
13008
- timedOut: false,
13009
- stdoutTruncated: stdout.truncated,
13010
- stderrTruncated: stderr.truncated
13011
- };
13012
- });
13013
- const make$61 = Effect.fn("ProcessRunner.make")(function* () {
13014
- const spawner = yield* ChildProcessSpawner$1.ChildProcessSpawner;
13015
- const run = (input) => finalizeRunProcess(runProcessCore(spawner, input), input);
13016
- return ProcessRunner.of({ run });
13017
- });
13018
- const layer$57 = Layer.effect(ProcessRunner, make$61());
13019
- //#endregion
13020
- //#region src/cli/completions.ts
13021
- /**
13022
- * Installs the zsh completion script for the `p4` command.
13023
- *
13024
- * zsh ships `_perforce` with `#compdef p4 p4d`, so out of the box tabbing
13025
- * through our subcommands prints "unhandled perforce command: …" from its
13026
- * fallback message. `compinit` resolves duplicate `#compdef` claims by fpath
13027
- * order — first match wins — and `_perforce` lives in the *last* fpath entry
13028
- * (`/usr/share/zsh/<ver>/functions`). Writing our own `_p4` into any earlier
13029
- * directory therefore replaces it outright, with no shell-config edit.
13030
- */
13031
- const COMPLETION_FILE = "_p4";
13032
- const GENERATE_TIMEOUT_MS = 1e4;
13033
- /**
13034
- * Ordered by how reliably zsh actually scans them.
13035
- *
13036
- * oh-my-zsh comes first and is created on demand: it adds
13037
- * `custom/completions` to fpath unconditionally, it is user-owned so
13038
- * `compinit -i` never skips it as insecure, and it does not depend on a package
13039
- * manager's prefix. The Homebrew and /usr/local directories are only used when
13040
- * they already exist, and even then are not guaranteed to be on fpath —
13041
- * `brew shellenv` contributes Homebrew's, and a plugin that rebuilds fpath can
13042
- * drop it, which is exactly how a completion file lands somewhere zsh never
13043
- * reads.
13044
- */
13045
- function candidateDirectories(input) {
13046
- return [
13047
- {
13048
- dir: input.join(input.homeDir, ".oh-my-zsh", "custom", "completions"),
13049
- createWhenPresent: input.join(input.homeDir, ".oh-my-zsh")
13050
- },
13051
- { dir: "/opt/homebrew/share/zsh/site-functions" },
13052
- { dir: "/usr/local/share/zsh/site-functions" },
13053
- {
13054
- dir: input.join(input.homeDir, ".zfunc"),
13055
- requiresFpathEntry: true
13056
- }
13057
- ];
13058
- }
13059
- /**
13060
- * Asks the running CLI for its own completion script rather than rendering one
13061
- * here, so the output cannot drift from what `--completions` produces.
13062
- */
13063
- const generateZshCompletionScript = Effect.fn("cli.completions.generate")(function* () {
13064
- const runner = yield* ProcessRunner;
13065
- const execPath = yield* HostProcessExecutablePath;
13066
- const entryPath = (yield* HostProcessArguments)[1] ?? "";
13067
- if (entryPath === "") return null;
13068
- const result = yield* runner.run({
13069
- command: execPath,
13070
- args: [
13071
- entryPath,
13072
- "--completions",
13073
- "zsh"
13074
- ],
13075
- timeout: GENERATE_TIMEOUT_MS
13076
- });
13077
- if (result.code !== 0) return null;
13078
- const script = result.stdout.trim();
13079
- return script.length === 0 ? null : `${script}\n`;
13080
- });
13081
- /**
13082
- * Best-effort: returns null when completions are not applicable (Windows, no
13083
- * writable directory) rather than failing the command that asked for them.
13084
- * Installing completions is never the reason an install should fail.
13085
- */
13086
- const installZshCompletion = Effect.fn("cli.completions.install")(function* () {
13087
- if ((yield* HostProcessPlatform) === "win32") return null;
13088
- const fs = yield* FileSystem.FileSystem;
13089
- const path = yield* Path.Path;
13090
- const homeDir = (yield* HostProcessEnvironment).HOME ?? "";
13091
- if (homeDir === "") return null;
13092
- const script = yield* generateZshCompletionScript();
13093
- if (script === null) return null;
13094
- for (const candidate of candidateDirectories({
13095
- homeDir,
13096
- join: path.join
13097
- })) {
13098
- if (candidate.requiresFpathEntry === true || candidate.createWhenPresent !== void 0 && (yield* fs.exists(candidate.createWhenPresent).pipe(Effect.orElseSucceed(() => false)))) {
13099
- if (!(yield* fs.makeDirectory(candidate.dir, { recursive: true }).pipe(Effect.as(true), Effect.orElseSucceed(() => false)))) continue;
13100
- } else if (!(yield* fs.exists(candidate.dir).pipe(Effect.orElseSucceed(() => false)))) continue;
13101
- const target = path.join(candidate.dir, COMPLETION_FILE);
13102
- if (!(yield* fs.writeFileString(target, script).pipe(Effect.as(true), Effect.orElseSucceed(() => false)))) continue;
13103
- return {
13104
- path: target,
13105
- requiresFpathEntry: candidate.requiresFpathEntry === true
13106
- };
13107
- }
13108
- return null;
13109
- });
13110
- const HINT_MARKER_FILE = "completions-hint";
13111
- const HINT_INTERVAL_MS = 1440 * 60 * 1e3;
13112
- const COMPLETIONS_HINT = "Shell completions are not installed, so tab completion shows Perforce's. Run `p4 completions install`.";
13113
- /**
13114
- * A hint when no `_p4` is on the fpath, at most once a day.
13115
- *
13116
- * A stat of a few directories, deliberately not the install itself: generating
13117
- * the script spawns a subprocess, which is far too much to do on every command
13118
- * just in case. Throttled through a marker file so an unactioned hint does not
13119
- * become noise on every invocation.
13120
- */
13121
- const resolveMissingCompletionsHint = Effect.fn("cli.completions.hint")(function* (input) {
13122
- if ((yield* HostProcessPlatform) === "win32") return null;
13123
- const fs = yield* FileSystem.FileSystem;
13124
- const path = yield* Path.Path;
13125
- const homeDir = (yield* HostProcessEnvironment).HOME ?? "";
13126
- if (homeDir === "") return null;
13127
- for (const candidate of candidateDirectories({
13128
- homeDir,
13129
- join: path.join
13130
- })) if (yield* fs.exists(path.join(candidate.dir, COMPLETION_FILE)).pipe(Effect.orElseSucceed(() => false))) return null;
13131
- const markerPath = path.join(input.stateDir, HINT_MARKER_FILE);
13132
- const marker = yield* fs.stat(markerPath).pipe(Effect.asSome, Effect.orElseSucceed(() => null));
13133
- const nowMs = DateTime.toEpochMillis(yield* DateTime.now);
13134
- if (marker !== null && Option.isSome(marker)) {
13135
- const mtime = marker.value.mtime;
13136
- if (Option.isSome(mtime) && nowMs - mtime.value.getTime() < HINT_INTERVAL_MS) return null;
13137
- }
13138
- yield* fs.makeDirectory(input.stateDir, { recursive: true }).pipe(Effect.ignore);
13139
- yield* fs.writeFileString(markerPath, "").pipe(Effect.ignore);
13140
- return COMPLETIONS_HINT;
13141
- });
13142
- /**
13143
- * Standalone so completions never depend on the service. A laptop that only
13144
- * runs clients has no service to install, and a host whose `service install`
13145
- * fails still deserves working completions.
13146
- */
13147
- const completionsCommand = Command.make("completions").pipe(Command.withDescription("Manage shell completions for the p4 command."), Command.withSubcommands([Command.make("install").pipe(Command.withDescription("Install zsh completions, replacing the Perforce completion that claims `p4`."), Command.withHandler(() => Effect.gen(function* () {
13148
- const install = yield* installZshCompletion();
13149
- if (install === null) {
13150
- yield* Console.error([
13151
- "Could not install completions automatically.",
13152
- "Write them yourself with:",
13153
- " p4 --completions zsh > <a directory on your fpath>/_p4"
13154
- ].join("\n"));
13155
- return;
13156
- }
13157
- yield* Console.log(formatZshCompletionInstall(install));
13158
- }).pipe(Effect.provide(layer$57))))]));
13159
- function formatZshCompletionInstall(install) {
13160
- const installed = `Installed zsh completions at ${install.path}.`;
13161
- if (!install.requiresFpathEntry) return `${installed} Open a new shell to use them.`;
13162
- return [
13163
- installed,
13164
- `Add this to ~/.zshrc before compinit runs, then open a new shell:`,
13165
- ` fpath=(${install.path.replace(/\/[^/]+$/u, "")} $fpath)`
13166
- ].join("\n");
13167
- }
13168
- //#endregion
13169
13059
  //#region src/hub/HubConfig.ts
13170
13060
  /**
13171
13061
  * Configuration for the hub process.
@@ -13427,7 +13317,7 @@ const makeAgentAssetRepository = Effect.gen(function* () {
13427
13317
  };
13428
13318
  const existing = yield* findByKey(key);
13429
13319
  if (Option.isNone(existing)) return { _tag: "deleted" };
13430
- if (existing.value.version !== request.expectedVersion) return {
13320
+ if (request.expectedVersion !== void 0 && existing.value.version !== request.expectedVersion) return {
13431
13321
  _tag: "conflict",
13432
13322
  current: Option.some(existing.value)
13433
13323
  };
@@ -13809,29 +13699,34 @@ const putAssetRoute = HttpRouter.add("PUT", "/assets", respondToHubFailures(Effe
13809
13699
  return HttpServerResponse.jsonUnsafe(outcome.asset, { status: outcome.created ? 201 : 200 });
13810
13700
  })));
13811
13701
  /**
13812
- * Deletion is conditional too, unlike a task's. Two machines can both hold a
13813
- * skill and both decide to remove it, and one may be acting on a copy that
13814
- * predates the other's edit — an unconditional delete discards that edit with
13815
- * nothing to notice it by.
13702
+ * Deletion is conditional when `expectedVersion` is supplied, which the sync
13703
+ * always does. Two machines can both hold a skill and both decide to remove it,
13704
+ * and one may be acting on a copy that predates the other's edit — deleting on
13705
+ * that basis discards the edit with nothing to notice it by.
13706
+ *
13707
+ * Omitting it deletes unconditionally, and that is deliberate: a person
13708
+ * clearing a row they can only see in a listing has never held a version for
13709
+ * it. Requiring one would leave an asset published by a decommissioned machine
13710
+ * permanently stuck, which is a worse failure than the race it would prevent.
13816
13711
  */
13817
13712
  const deleteAssetRoute = HttpRouter.add("DELETE", "/assets/:kind/:name", respondToHubFailures(Effect.gen(function* () {
13818
13713
  yield* authenticateHubRequest();
13819
13714
  const params = yield* HttpRouter.params;
13820
13715
  const request = yield* HttpServerRequest.HttpServerRequest;
13821
- const url = new URL(request.url, "http://hub.invalid");
13716
+ const rawExpectedVersion = new URL(request.url, "http://hub.invalid").searchParams.get("expectedVersion");
13822
13717
  const requested = yield* decodeOrInvalid(Schema$1.Struct({
13823
13718
  kind: AgentAssetKind,
13824
13719
  name: AgentAssetName,
13825
- expectedVersion: Schema$1.FiniteFromString.check(Schema$1.isGreaterThanOrEqualTo(1))
13720
+ expectedVersion: Schema$1.optional(Schema$1.FiniteFromString.check(Schema$1.isGreaterThanOrEqualTo(1)))
13826
13721
  }), "Path or expectedVersion query parameter is not valid.")({
13827
13722
  kind: params.kind,
13828
13723
  name: params.name,
13829
- expectedVersion: url.searchParams.get("expectedVersion")
13724
+ ...rawExpectedVersion === null ? {} : { expectedVersion: rawExpectedVersion }
13830
13725
  });
13831
13726
  const outcome = yield* (yield* AgentAssetRepository).deleteByName(requested);
13832
13727
  return outcome._tag === "conflict" ? assetConflict(Option.getOrNull(outcome.current)) : HttpServerResponse.empty({ status: 204 });
13833
13728
  })));
13834
- const layer$56 = Layer.mergeAll(healthRoute, listTasksRoute, getTaskRoute, createTaskRoute, updateTaskRoute, deleteTaskRoute, listAssetsRoute, getAssetRoute, putAssetRoute, deleteAssetRoute);
13729
+ const layer$58 = Layer.mergeAll(healthRoute, listTasksRoute, getTaskRoute, createTaskRoute, updateTaskRoute, deleteTaskRoute, listAssetsRoute, getAssetRoute, putAssetRoute, deleteAssetRoute);
13835
13730
  //#endregion
13836
13731
  //#region src/hub/Migrations/001_Tasks.ts
13837
13732
  /**
@@ -13978,7 +13873,7 @@ const runHubMigrations = Effect.fn("runHubMigrations")(function* () {
13978
13873
  //#region src/hub/Sqlite.ts
13979
13874
  const sqliteClientLoaders = {
13980
13875
  bun: () => import("@effect/sql-sqlite-bun/SqliteClient"),
13981
- node: () => import("./NodeSqliteClient-DGAB9NWW.mjs")
13876
+ node: () => import("./NodeSqliteClient-CRS7rg24.mjs")
13982
13877
  };
13983
13878
  const makeHubSqliteLayer = Effect.fn("makeHubSqliteLayer")(function* (config) {
13984
13879
  const runtime = process.versions.bun !== void 0 ? "bun" : "node";
@@ -14045,7 +13940,7 @@ const announce = Layer.effectDiscard(Effect.gen(function* () {
14045
13940
  });
14046
13941
  if (isRemoteReachableHost(config.host)) yield* Effect.logWarning("Hub is bound beyond loopback; the bearer token and your tailnet ACL are the only controls.", { host: config.host });
14047
13942
  }));
14048
- const makeHubLayer = HttpRouter.serve(layer$56).pipe(Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
13943
+ const makeHubLayer = HttpRouter.serve(layer$58).pipe(Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
14049
13944
  const runHub = Layer.launch(makeHubLayer);
14050
13945
  //#endregion
14051
13946
  //#region src/atomicWrite.ts
@@ -14169,7 +14064,7 @@ function stripDefaultServerSettings(current, defaults) {
14169
14064
  }
14170
14065
  return Object.is(current, defaults) ? void 0 : current;
14171
14066
  }
14172
- const make$60 = Effect.gen(function* () {
14067
+ const make$62 = Effect.gen(function* () {
14173
14068
  const { settingsPath } = yield* ServerConfig$1;
14174
14069
  const fs = yield* FileSystem.FileSystem;
14175
14070
  const pathService = yield* Path.Path;
@@ -14390,7 +14285,252 @@ const make$60 = Effect.gen(function* () {
14390
14285
  }
14391
14286
  };
14392
14287
  });
14393
- const layer$55 = Layer.effect(ServerSettingsService, make$60);
14288
+ const layer$57 = Layer.effect(ServerSettingsService, make$62);
14289
+ //#endregion
14290
+ //#region src/pathExpansion.ts
14291
+ /**
14292
+ * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the
14293
+ * current user's home directory. Spawned processes don't get shell
14294
+ * expansion, so env vars like `CODEX_HOME=~/.codex-work` would be passed
14295
+ * verbatim and treated as relative paths by the receiver.
14296
+ *
14297
+ * Matches the behavior of the other `expandHomePath` helpers in the
14298
+ * workspace layers and CLI bootstrap: `~` alone and both `~/` and `~\`
14299
+ * separators are handled. Returns the input unchanged if it doesn't
14300
+ * start with `~` or is empty. Does not handle `~user` (other-user)
14301
+ * expansion.
14302
+ */
14303
+ function expandHomePath$3(value) {
14304
+ if (!value) return value;
14305
+ if (value === "~") return NodeOS.homedir();
14306
+ if (value.startsWith("~/") || value.startsWith("~\\")) return NodePath.join(NodeOS.homedir(), value.slice(2));
14307
+ return value;
14308
+ }
14309
+ //#endregion
14310
+ //#region src/provider/Drivers/ClaudeSkills.ts
14311
+ /**
14312
+ * ClaudeSkills — filesystem discovery of Claude Code skills for the `$` picker.
14313
+ *
14314
+ * Claude Code loads skills from `<config dir>/skills` (user scope) and
14315
+ * `<cwd>/.claude/skills` (project scope), one directory per skill with a
14316
+ * `SKILL.md` carrying YAML frontmatter. The Agent SDK init handshake surfaces
14317
+ * skills only as slash commands without their filesystem paths, so the
14318
+ * provider snapshot scans the same locations directly, mirroring how the
14319
+ * Codex app-server reports its skills.
14320
+ *
14321
+ * @module provider/Drivers/ClaudeSkills
14322
+ */
14323
+ const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
14324
+ function parseSkillFrontmatter(contents) {
14325
+ const match = FRONTMATTER_PATTERN.exec(contents);
14326
+ if (!match) return { kind: "missing" };
14327
+ let parsed;
14328
+ try {
14329
+ parsed = parse(match[1] ?? "");
14330
+ } catch {
14331
+ return { kind: "malformed" };
14332
+ }
14333
+ if (typeof parsed !== "object" || parsed === null) return { kind: "malformed" };
14334
+ const record = parsed;
14335
+ const name = typeof record.name === "string" ? record.name.trim() : "";
14336
+ const description = typeof record.description === "string" ? record.description.trim() : "";
14337
+ return {
14338
+ kind: "parsed",
14339
+ ...name ? { name } : {},
14340
+ ...description ? { description } : {}
14341
+ };
14342
+ }
14343
+ /**
14344
+ * Resolve the Claude config directory the CLI would use, matching the
14345
+ * precedence the spawned CLI sees: the instance's `homePath` (exported as
14346
+ * `CLAUDE_CONFIG_DIR` by `makeClaudeEnvironment`), then a `CLAUDE_CONFIG_DIR`
14347
+ * already present in the process environment, then `~/.claude`.
14348
+ *
14349
+ * Exported for the memory sync, which materializes `CLAUDE.md` directly into
14350
+ * this directory. Same reasoning as the skills root below: resolving it a
14351
+ * second time somewhere else is how the sync and the runtime come to disagree
14352
+ * about which config directory they are talking about.
14353
+ */
14354
+ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(function* (config, environment, cwd) {
14355
+ const path = yield* Path.Path;
14356
+ const homePath = config.homePath.trim();
14357
+ if (homePath.length > 0) return path.resolve(expandHomePath$3(homePath));
14358
+ const environmentConfigDir = environment.CLAUDE_CONFIG_DIR?.trim() ?? "";
14359
+ if (environmentConfigDir.length > 0) return cwd ? path.resolve(cwd, environmentConfigDir) : path.resolve(environmentConfigDir);
14360
+ return path.join(NodeOS.homedir(), ".claude");
14361
+ });
14362
+ /**
14363
+ * The user-scope skills directory the CLI would read.
14364
+ *
14365
+ * Exported for the hub sync, which materializes into exactly this directory —
14366
+ * the same resolution the discovery below uses, so a synced skill is one the
14367
+ * `$` picker can see. Deriving it a second time is how the two would drift.
14368
+ */
14369
+ const resolveClaudeUserSkillsDir = Effect.fn("resolveClaudeUserSkillsDir")(function* (config, environment, cwd) {
14370
+ const path = yield* Path.Path;
14371
+ const configDirPath = yield* resolveClaudeConfigDirPath(config, environment ?? process.env, cwd);
14372
+ return path.join(configDirPath, "skills");
14373
+ });
14374
+ /**
14375
+ * Enumerate Claude Code skills from the user config dir and the workspace.
14376
+ * Discovery is best-effort: unreadable roots and malformed skill entries are
14377
+ * skipped so a broken skill never degrades the provider snapshot. On name
14378
+ * collisions the project-scoped skill wins, matching Claude Code's
14379
+ * most-specific-wins resolution.
14380
+ */
14381
+ const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* (config, cwd, environment) {
14382
+ const fileSystem = yield* FileSystem.FileSystem;
14383
+ const path = yield* Path.Path;
14384
+ const configDirPath = yield* resolveClaudeConfigDirPath(config, environment ?? process.env, cwd);
14385
+ const roots = [{
14386
+ directory: path.join(configDirPath, "skills"),
14387
+ scope: "user"
14388
+ }, ...cwd ? [{
14389
+ directory: path.join(cwd, ".claude", "skills"),
14390
+ scope: "project"
14391
+ }] : []];
14392
+ const skillsByName = /* @__PURE__ */ new Map();
14393
+ for (const root of roots) {
14394
+ const entries = yield* fileSystem.readDirectory(root.directory).pipe(Effect.orElseSucceed(() => []));
14395
+ for (const entry of [...entries].sort()) {
14396
+ const skillPath = path.join(root.directory, entry, "SKILL.md");
14397
+ const contents = yield* fileSystem.readFileString(skillPath).pipe(Effect.orElseSucceed(() => void 0));
14398
+ if (contents === void 0) continue;
14399
+ const frontmatter = parseSkillFrontmatter(contents);
14400
+ if (frontmatter.kind === "malformed") continue;
14401
+ const name = (frontmatter.kind === "parsed" ? frontmatter.name : void 0) ?? entry.trim();
14402
+ if (!name) continue;
14403
+ skillsByName.set(name, {
14404
+ name,
14405
+ path: skillPath,
14406
+ enabled: true,
14407
+ scope: root.scope,
14408
+ ...frontmatter.kind === "parsed" && frontmatter.description ? { description: frontmatter.description } : {}
14409
+ });
14410
+ }
14411
+ }
14412
+ return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name));
14413
+ });
14414
+ /** Extension for a registration file, so the sync's asset name is `<server>.json`. */
14415
+ const MCP_REGISTRATION_SUFFIX = ".json";
14416
+ const RegistrationFromJson$1 = Schema$1.fromJsonString(McpServerRegistration);
14417
+ const decodeRegistration$1 = Schema$1.decodeUnknownExit(RegistrationFromJson$1);
14418
+ const encodeRegistration = Schema$1.encodeSync(RegistrationFromJson$1);
14419
+ var McpRegistry = class extends Context.Service()("@p4code/cli/mcp/McpRegistry") {};
14420
+ const slotsOf = (registration) => registration.secrets ?? [];
14421
+ const make$61 = Effect.gen(function* () {
14422
+ const config = yield* ServerConfig$1;
14423
+ const secrets = yield* ServerSecretStore;
14424
+ const fileSystem = yield* FileSystem.FileSystem;
14425
+ const path = yield* Path.Path;
14426
+ const services = yield* Effect.context();
14427
+ const root = path.join(config.stateDir, "mcp");
14428
+ const fileFor = (name) => path.join(root, `${name}${MCP_REGISTRATION_SUFFIX}`);
14429
+ const readSecret = Effect.fn("McpRegistry.readSecret")(function* (server, slot) {
14430
+ const stored = yield* secrets.get(mcpSecretStorageName({
14431
+ server,
14432
+ target: slot.target,
14433
+ key: slot.key
14434
+ })).pipe(Effect.orElseSucceed(() => Option.none()));
14435
+ return Option.map(stored, (bytes) => new TextDecoder().decode(bytes));
14436
+ });
14437
+ /**
14438
+ * Every readable registration.
14439
+ *
14440
+ * A file that will not parse is skipped rather than failing the read. One
14441
+ * corrupt registration should cost that one server, not every server and the
14442
+ * panel that lists them.
14443
+ */
14444
+ const readAll = Effect.fn("McpRegistry.readAll")(function* () {
14445
+ const entries = yield* fileSystem.readDirectory(root).pipe(Effect.orElseSucceed(() => []));
14446
+ const found = [];
14447
+ for (const entry of [...entries].sort()) {
14448
+ if (!entry.endsWith(".json")) continue;
14449
+ const raw = yield* fileSystem.readFileString(path.join(root, entry)).pipe(Effect.orElseSucceed(() => void 0));
14450
+ if (raw === void 0) continue;
14451
+ const decoded = decodeRegistration$1(raw);
14452
+ if (decoded._tag !== "Success") continue;
14453
+ found.push(decoded.value);
14454
+ }
14455
+ return found;
14456
+ });
14457
+ const list = Effect.gen(function* () {
14458
+ const registrations = yield* readAll();
14459
+ const statuses = [];
14460
+ for (const registration of registrations) {
14461
+ const missingSecrets = [];
14462
+ for (const slot of slotsOf(registration)) {
14463
+ const value = yield* readSecret(registration.name, slot);
14464
+ if (Option.isNone(value)) missingSecrets.push(slot);
14465
+ }
14466
+ statuses.push({
14467
+ registration,
14468
+ missingSecrets,
14469
+ ready: registration.enabled && missingSecrets.length === 0
14470
+ });
14471
+ }
14472
+ return statuses;
14473
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("mcp registry list failed", { cause }).pipe(Effect.as([]))));
14474
+ const save = (registration) => Effect.gen(function* () {
14475
+ yield* fileSystem.makeDirectory(root, { recursive: true }).pipe(Effect.ignore);
14476
+ yield* writeFileStringAtomically({
14477
+ filePath: fileFor(registration.name),
14478
+ contents: `${encodeRegistration(registration)}\n`
14479
+ });
14480
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("mcp registry save failed", { cause }).pipe(Effect.asVoid)));
14481
+ const remove = (name) => Effect.gen(function* () {
14482
+ const removed = (yield* readAll()).find((entry) => entry.name === name);
14483
+ yield* fileSystem.remove(fileFor(name), { force: true });
14484
+ for (const slot of removed === void 0 ? [] : slotsOf(removed)) yield* secrets.remove(mcpSecretStorageName({
14485
+ server: name,
14486
+ target: slot.target,
14487
+ key: slot.key
14488
+ })).pipe(Effect.ignore);
14489
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("mcp registry remove failed", { cause }).pipe(Effect.asVoid)));
14490
+ const setSecret = ({ name, slot, value }) => secrets.set(mcpSecretStorageName({
14491
+ server: name,
14492
+ target: slot.target,
14493
+ key: slot.key
14494
+ }), new TextEncoder().encode(value)).pipe(Effect.catchCause((cause) => Effect.logWarning("mcp secret write failed", { cause }).pipe(Effect.asVoid)));
14495
+ return {
14496
+ list,
14497
+ save,
14498
+ remove,
14499
+ setSecret,
14500
+ resolveForSession: Effect.gen(function* () {
14501
+ const registrations = yield* readAll();
14502
+ const resolved = {};
14503
+ for (const registration of registrations) {
14504
+ if (!registration.enabled) continue;
14505
+ const headers = { ...registration.transport === "stdio" ? {} : registration.headers ?? {} };
14506
+ const env = { ...registration.transport === "stdio" ? registration.env ?? {} : {} };
14507
+ let complete = true;
14508
+ for (const slot of slotsOf(registration)) {
14509
+ const value = yield* readSecret(registration.name, slot);
14510
+ if (Option.isNone(value)) {
14511
+ complete = false;
14512
+ break;
14513
+ }
14514
+ if (slot.target === "header") headers[slot.key] = value.value;
14515
+ else env[slot.key] = value.value;
14516
+ }
14517
+ if (!complete) continue;
14518
+ resolved[registration.name] = registration.transport === "stdio" ? {
14519
+ type: "stdio",
14520
+ command: registration.command,
14521
+ args: registration.args ?? [],
14522
+ env
14523
+ } : {
14524
+ type: registration.transport,
14525
+ url: registration.url,
14526
+ headers
14527
+ };
14528
+ }
14529
+ return resolved;
14530
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("mcp registry resolve failed", { cause }).pipe(Effect.as({}))))
14531
+ };
14532
+ });
14533
+ const layer$56 = Layer.effect(McpRegistry, make$61);
14394
14534
  //#endregion
14395
14535
  //#region src/sync/HubLink.ts
14396
14536
  /**
@@ -14487,7 +14627,7 @@ const formatHubLink = (input) => encodeStoredHubLink({
14487
14627
  shareMode: input.shareMode
14488
14628
  });
14489
14629
  const fromEnvironment = (environment) => validateHubLink(environment.P4CODE_HUB_URL ?? "", environment.P4CODE_HUB_TOKEN ?? "");
14490
- const make$59 = Effect.fn("HubLink.make")(function* (environment) {
14630
+ const make$60 = Effect.fn("HubLink.make")(function* (environment) {
14491
14631
  const secrets = yield* ServerSecretStore;
14492
14632
  const env = environment ?? process.env;
14493
14633
  const fromEnv = fromEnvironment(env);
@@ -14553,7 +14693,7 @@ const make$59 = Effect.fn("HubLink.make")(function* (environment) {
14553
14693
  })
14554
14694
  };
14555
14695
  });
14556
- const layer$54 = Layer.effect(HubLink, make$59());
14696
+ const layer$55 = Layer.effect(HubLink, make$60());
14557
14697
  //#endregion
14558
14698
  //#region src/sync/HubAssetClient.ts
14559
14699
  /**
@@ -14586,7 +14726,7 @@ const decodeAssetListPage = Schema$1.decodeUnknownEffect(AssetListPage);
14586
14726
  const decodeConflictBody = Schema$1.decodeUnknownEffect(ConflictBody);
14587
14727
  const decodeAsset = Schema$1.decodeUnknownEffect(AgentAsset);
14588
14728
  var HubAssetClient = class extends Context.Service()("@p4code/cli/sync/HubAssetClient") {};
14589
- const make$58 = Effect.gen(function* () {
14729
+ const make$59 = Effect.gen(function* () {
14590
14730
  const http = yield* HttpClient.HttpClient;
14591
14731
  const link = yield* HubLink;
14592
14732
  const requireSettings = Effect.gen(function* () {
@@ -14623,13 +14763,24 @@ const make$58 = Effect.gen(function* () {
14623
14763
  });
14624
14764
  const list = (kind) => Effect.gen(function* () {
14625
14765
  const settings = yield* requireSettings;
14626
- const response = yield* send("list", settings, HttpClientRequest.get(`${settings.baseUrl}/assets?kind=${encodeURIComponent(kind)}`));
14766
+ const query = kind === void 0 ? "" : `?kind=${encodeURIComponent(kind)}`;
14767
+ const response = yield* send("list", settings, HttpClientRequest.get(`${settings.baseUrl}/assets${query}`));
14627
14768
  if (response.status !== 200) return yield* new HubUnavailable({
14628
14769
  operation: "list",
14629
14770
  detail: `status ${response.status}`
14630
14771
  });
14631
14772
  return (yield* decodeBody("list", decodeAssetListPage, response)).assets;
14632
14773
  });
14774
+ const get = ({ kind, name }) => Effect.gen(function* () {
14775
+ const settings = yield* requireSettings;
14776
+ const response = yield* send("get", settings, HttpClientRequest.get(`${settings.baseUrl}/assets/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`));
14777
+ if (response.status === 404) return Option.none();
14778
+ if (response.status !== 200) return yield* new HubUnavailable({
14779
+ operation: "get",
14780
+ detail: `status ${response.status}`
14781
+ });
14782
+ return Option.some(yield* decodeBody("get", decodeAsset, response));
14783
+ });
14633
14784
  const write = (input) => Effect.gen(function* () {
14634
14785
  const settings = yield* requireSettings;
14635
14786
  const response = yield* send("write", settings, HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.put(`${settings.baseUrl}/assets`), input));
@@ -14642,7 +14793,8 @@ const make$58 = Effect.gen(function* () {
14642
14793
  });
14643
14794
  const remove = ({ kind, name, expectedVersion }) => Effect.gen(function* () {
14644
14795
  const settings = yield* requireSettings;
14645
- const response = yield* send("remove", settings, HttpClientRequest.delete(`${settings.baseUrl}/assets/${encodeURIComponent(kind)}/${encodeURIComponent(name)}?expectedVersion=${expectedVersion}`));
14796
+ const query = expectedVersion === void 0 ? "" : `?expectedVersion=${expectedVersion}`;
14797
+ const response = yield* send("remove", settings, HttpClientRequest.delete(`${settings.baseUrl}/assets/${encodeURIComponent(kind)}/${encodeURIComponent(name)}${query}`));
14646
14798
  if (response.status === 409) return yield* yield* conflictFrom(name, response);
14647
14799
  if (response.status !== 204 && response.status !== 404) return yield* new HubUnavailable({
14648
14800
  operation: "remove",
@@ -14651,130 +14803,248 @@ const make$58 = Effect.gen(function* () {
14651
14803
  });
14652
14804
  return {
14653
14805
  list,
14806
+ get,
14654
14807
  write,
14655
14808
  remove
14656
14809
  };
14657
14810
  });
14658
- const layer$53 = Layer.effect(HubAssetClient, make$58);
14811
+ const layer$54 = Layer.effect(HubAssetClient, make$59);
14659
14812
  //#endregion
14660
- //#region src/pathExpansion.ts
14813
+ //#region src/sync/mcpRegistrationFiles.ts
14661
14814
  /**
14662
- * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the
14663
- * current user's home directory. Spawned processes don't get shell
14664
- * expansion, so env vars like `CODEX_HOME=~/.codex-work` would be passed
14665
- * verbatim and treated as relative paths by the receiver.
14815
+ * Reading and writing MCP registration files for the sync.
14666
14816
  *
14667
- * Matches the behavior of the other `expandHomePath` helpers in the
14668
- * workspace layers and CLI bootstrap: `~` alone and both `~/` and `~\`
14669
- * separators are handled. Returns the input unchanged if it doesn't
14670
- * start with `~` or is empty. Does not handle `~user` (other-user)
14671
- * expansion.
14817
+ * The third asset driver's filesystem half. Unlike memory, the directory is
14818
+ * p4code's own nothing else lives in `<stateDir>/mcp` so there is no
14819
+ * allowlist to police and the rules are about shape rather than about which
14820
+ * neighbours must not be touched.
14821
+ *
14822
+ * **What is not here is the point.** A registration references its bearer
14823
+ * tokens and API keys by slot name; the values live in `ServerSecretStore` and
14824
+ * are never written into these files. So a registration can travel through a
14825
+ * hub that stores asset files verbatim without a working credential leaving the
14826
+ * machine that holds it. This module enforces the file shape; `McpRegistry`
14827
+ * enforces the split.
14828
+ *
14829
+ * @module sync/mcpRegistrationFiles
14672
14830
  */
14673
- function expandHomePath$3(value) {
14674
- if (!value) return value;
14675
- if (value === "~") return NodeOS.homedir();
14676
- if (value.startsWith("~/") || value.startsWith("~\\")) return NodePath.join(NodeOS.homedir(), value.slice(2));
14677
- return value;
14678
- }
14679
- //#endregion
14680
- //#region src/provider/Drivers/ClaudeSkills.ts
14831
+ const RegistrationFromJson = Schema$1.fromJsonString(McpServerRegistration);
14832
+ const decodeRegistration = Schema$1.decodeUnknownExit(RegistrationFromJson);
14681
14833
  /**
14682
- * ClaudeSkills filesystem discovery of Claude Code skills for the `$` picker.
14834
+ * Every registration in the directory, as syncable assets.
14683
14835
  *
14684
- * Claude Code loads skills from `<config dir>/skills` (user scope) and
14685
- * `<cwd>/.claude/skills` (project scope), one directory per skill with a
14686
- * `SKILL.md` carrying YAML frontmatter. The Agent SDK init handshake surfaces
14687
- * skills only as slash commands without their filesystem paths, so the
14688
- * provider snapshot scans the same locations directly, mirroring how the
14689
- * Codex app-server reports its skills.
14836
+ * The asset name is the filename `linear.json` for the same reason memory's
14837
+ * is: one string is both the hub key and the path, so there is no mapping to
14838
+ * get wrong in one direction and not the other.
14690
14839
  *
14691
- * @module provider/Drivers/ClaudeSkills
14840
+ * A file that will not parse is refused rather than skipped. Unlike a stray
14841
+ * `.DS_Store`, a malformed `.json` in this directory is something p4code wrote
14842
+ * or something the sync brought in, so it is a fault worth reporting rather
14843
+ * than noise to ignore.
14692
14844
  */
14693
- const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
14694
- function parseSkillFrontmatter(contents) {
14695
- const match = FRONTMATTER_PATTERN.exec(contents);
14696
- if (!match) return { kind: "missing" };
14697
- let parsed;
14698
- try {
14699
- parsed = parse(match[1] ?? "");
14700
- } catch {
14701
- return { kind: "malformed" };
14845
+ const readMcpRegistrations = Effect.fn("mcpRegistrationFiles.read")(function* (registrationsRoot) {
14846
+ const fileSystem = yield* FileSystem.FileSystem;
14847
+ const path = yield* Path.Path;
14848
+ const entries = yield* fileSystem.readDirectory(registrationsRoot).pipe(Effect.orElseSucceed(() => []));
14849
+ const skills = [];
14850
+ const refused = [];
14851
+ for (const entry of [...entries].sort()) {
14852
+ if (!entry.endsWith(".json") || entry.startsWith(".")) continue;
14853
+ const content = yield* fileSystem.readFileString(path.join(registrationsRoot, entry)).pipe(Effect.orElseSucceed(() => void 0));
14854
+ if (content === void 0) {
14855
+ refused.push({
14856
+ name: entry,
14857
+ reason: "unreadable"
14858
+ });
14859
+ continue;
14860
+ }
14861
+ if (decodeRegistration(content)._tag !== "Success") {
14862
+ refused.push({
14863
+ name: entry,
14864
+ reason: "unreadable"
14865
+ });
14866
+ continue;
14867
+ }
14868
+ skills.push({
14869
+ name: entry,
14870
+ files: [{
14871
+ path: entry,
14872
+ content
14873
+ }]
14874
+ });
14702
14875
  }
14703
- if (typeof parsed !== "object" || parsed === null) return { kind: "malformed" };
14704
- const record = parsed;
14705
- const name = typeof record.name === "string" ? record.name.trim() : "";
14706
- const description = typeof record.description === "string" ? record.description.trim() : "";
14707
14876
  return {
14708
- kind: "parsed",
14709
- ...name ? { name } : {},
14710
- ...description ? { description } : {}
14877
+ skills,
14878
+ refused
14711
14879
  };
14712
- }
14880
+ });
14713
14881
  /**
14714
- * Resolve the Claude config directory the CLI would use, matching the
14715
- * precedence the spawned CLI sees: the instance's `homePath` (exported as
14716
- * `CLAUDE_CONFIG_DIR` by `makeClaudeEnvironment`), then a `CLAUDE_CONFIG_DIR`
14717
- * already present in the process environment, then `~/.claude`.
14882
+ * Write a registration that arrived from the hub.
14883
+ *
14884
+ * Refused unless it is exactly one file whose path is the asset's own name and
14885
+ * whose content decodes as a registration. The last check is what stops another
14886
+ * machine — or a hub row edited by hand — from installing something this server
14887
+ * would then feed to an agent as a tool source.
14718
14888
  */
14719
- const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(function* (config, environment, cwd) {
14889
+ const writeMcpRegistration = Effect.fn("mcpRegistrationFiles.write")(function* (input) {
14890
+ const fileSystem = yield* FileSystem.FileSystem;
14720
14891
  const path = yield* Path.Path;
14721
- const homePath = config.homePath.trim();
14722
- if (homePath.length > 0) return path.resolve(expandHomePath$3(homePath));
14723
- const environmentConfigDir = environment.CLAUDE_CONFIG_DIR?.trim() ?? "";
14724
- if (environmentConfigDir.length > 0) return cwd ? path.resolve(cwd, environmentConfigDir) : path.resolve(environmentConfigDir);
14725
- return path.join(NodeOS.homedir(), ".claude");
14892
+ const file = input.files[0];
14893
+ if (!input.name.endsWith(".json") || input.files.length !== 1 || file === void 0 || file.path !== input.name) return "refused";
14894
+ if (decodeRegistration(file.content)._tag !== "Success") return "refused";
14895
+ yield* fileSystem.makeDirectory(input.registrationsRoot, { recursive: true }).pipe(Effect.ignore);
14896
+ return yield* writeFileStringAtomically({
14897
+ filePath: path.join(input.registrationsRoot, input.name),
14898
+ contents: file.content
14899
+ }).pipe(Effect.as("written"), Effect.orElseSucceed(() => "failed"));
14900
+ });
14901
+ const removeMcpRegistration = Effect.fn("mcpRegistrationFiles.remove")(function* (input) {
14902
+ if (!input.name.endsWith(".json")) return;
14903
+ const fileSystem = yield* FileSystem.FileSystem;
14904
+ const path = yield* Path.Path;
14905
+ yield* fileSystem.remove(path.join(input.registrationsRoot, input.name), { force: true });
14726
14906
  });
14907
+ //#endregion
14908
+ //#region src/sync/memoryFile.ts
14727
14909
  /**
14728
- * The user-scope skills directory the CLI would read.
14910
+ * Reading and writing user-scope memory files.
14729
14911
  *
14730
- * Exported for the hub sync, which materializes into exactly this directory
14731
- * the same resolution the discovery below uses, so a synced skill is one the
14732
- * `$` picker can see. Deriving it a second time is how the two would drift.
14912
+ * Memory is the instruction file an agent reads before anything else, so a
14913
+ * machine whose `CLAUDE.md` differs is a machine whose agent behaves
14914
+ * differently which is the whole reason the hub exists. Unlike a skill it is
14915
+ * a single file, and it sits *directly* in the provider's config directory
14916
+ * rather than in a subdirectory the sync could own outright.
14917
+ *
14918
+ * That last part is the entire design constraint. `<configDir>` also holds
14919
+ * `settings.json`, `.claude.json` and `.credentials.json`. "Write the file this
14920
+ * asset names" is therefore not safe on its own: an authorized machine — or a
14921
+ * hub row someone wrote by hand — could name `settings.json` and have every
14922
+ * other machine overwrite its own configuration on the next sync. The path
14923
+ * schema does not save us here, because `settings.json` is a perfectly valid
14924
+ * asset path.
14925
+ *
14926
+ * So the allowlist below is the boundary, and it is checked on the way in as
14927
+ * well as on the way out: a file this module will not write is also one it will
14928
+ * not read, so nothing outside it can reach the hub in the first place.
14929
+ *
14930
+ * @module sync/memoryFile
14733
14931
  */
14734
- const resolveClaudeUserSkillsDir = Effect.fn("resolveClaudeUserSkillsDir")(function* (config, environment, cwd) {
14735
- const path = yield* Path.Path;
14736
- const configDirPath = yield* resolveClaudeConfigDirPath(config, environment ?? process.env, cwd);
14737
- return path.join(configDirPath, "skills");
14738
- });
14739
14932
  /**
14740
- * Enumerate Claude Code skills from the user config dir and the workspace.
14741
- * Discovery is best-effort: unreadable roots and malformed skill entries are
14742
- * skipped so a broken skill never degrades the provider snapshot. On name
14743
- * collisions the project-scoped skill wins, matching Claude Code's
14744
- * most-specific-wins resolution.
14933
+ * The only files in the provider config directory this sync will ever touch.
14934
+ *
14935
+ * Both names, not one: Claude Code reads `CLAUDE.md`, Codex reads `AGENTS.md`,
14936
+ * and a machine that syncs both is the ordinary case rather than a special one.
14937
+ * Nothing else in that directory is memory, and everything else in it is either
14938
+ * a credential or a per-machine setting.
14939
+ *
14940
+ * The asset's name *is* the filename. That keeps the hub key and the on-disk
14941
+ * path the same string, so there is no mapping table to get wrong, and it makes
14942
+ * the allowlist checkable at the boundary rather than after a join.
14745
14943
  */
14746
- const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* (config, cwd, environment) {
14944
+ const MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md"];
14945
+ /**
14946
+ * Whether this name is one of the memory files.
14947
+ *
14948
+ * Exact match, deliberately. Not a suffix test and not case-insensitive: a
14949
+ * check that accepted `claude.md` on a case-insensitive filesystem and rejected
14950
+ * it on a case-sensitive one would make the same hub row materialize on one
14951
+ * machine and refuse on another.
14952
+ */
14953
+ const isMemoryAssetName = (name) => MEMORY_FILE_NAMES.includes(name);
14954
+ /** The same test the skill reader uses: a NUL is what breaks a JSON text column. */
14955
+ const looksBinary$1 = (content) => content.includes("\0");
14956
+ /**
14957
+ * Read whichever memory files exist in this machine's config directory.
14958
+ *
14959
+ * Only the allowlisted names are even stat'd. A missing file is not a refusal:
14960
+ * most machines have a `CLAUDE.md` and no `AGENTS.md`, and reporting the second
14961
+ * as a problem would be reporting the normal case.
14962
+ */
14963
+ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memoryRoot) {
14747
14964
  const fileSystem = yield* FileSystem.FileSystem;
14748
14965
  const path = yield* Path.Path;
14749
- const configDirPath = yield* resolveClaudeConfigDirPath(config, environment ?? process.env, cwd);
14750
- const roots = [{
14751
- directory: path.join(configDirPath, "skills"),
14752
- scope: "user"
14753
- }, ...cwd ? [{
14754
- directory: path.join(cwd, ".claude", "skills"),
14755
- scope: "project"
14756
- }] : []];
14757
- const skillsByName = /* @__PURE__ */ new Map();
14758
- for (const root of roots) {
14759
- const entries = yield* fileSystem.readDirectory(root.directory).pipe(Effect.orElseSucceed(() => []));
14760
- for (const entry of [...entries].sort()) {
14761
- const skillPath = path.join(root.directory, entry, "SKILL.md");
14762
- const contents = yield* fileSystem.readFileString(skillPath).pipe(Effect.orElseSucceed(() => void 0));
14763
- if (contents === void 0) continue;
14764
- const frontmatter = parseSkillFrontmatter(contents);
14765
- if (frontmatter.kind === "malformed") continue;
14766
- const name = (frontmatter.kind === "parsed" ? frontmatter.name : void 0) ?? entry.trim();
14767
- if (!name) continue;
14768
- skillsByName.set(name, {
14966
+ const skills = [];
14967
+ const refused = [];
14968
+ for (const name of MEMORY_FILE_NAMES) {
14969
+ const absolute = path.join(memoryRoot, name);
14970
+ if ((yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "File") continue;
14971
+ const content = yield* fileSystem.readFileString(absolute).pipe(Effect.orElseSucceed(() => void 0));
14972
+ if (content === void 0) {
14973
+ refused.push({
14769
14974
  name,
14770
- path: skillPath,
14771
- enabled: true,
14772
- scope: root.scope,
14773
- ...frontmatter.kind === "parsed" && frontmatter.description ? { description: frontmatter.description } : {}
14975
+ reason: "unreadable"
14976
+ });
14977
+ continue;
14978
+ }
14979
+ if (looksBinary$1(content)) {
14980
+ refused.push({
14981
+ name,
14982
+ reason: "binary-file"
14983
+ });
14984
+ continue;
14985
+ }
14986
+ if (content.length > 524288) {
14987
+ refused.push({
14988
+ name,
14989
+ reason: "file-too-large"
14990
+ });
14991
+ continue;
14992
+ }
14993
+ const files = [{
14994
+ path: name,
14995
+ content
14996
+ }];
14997
+ if (agentAssetTotalBytes(files) > 4194304) {
14998
+ refused.push({
14999
+ name,
15000
+ reason: "asset-too-large"
14774
15001
  });
15002
+ continue;
14775
15003
  }
15004
+ skills.push({
15005
+ name,
15006
+ files
15007
+ });
14776
15008
  }
14777
- return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name));
15009
+ return {
15010
+ skills,
15011
+ refused
15012
+ };
15013
+ });
15014
+ /**
15015
+ * Whether an asset from the hub is a memory file this machine will write.
15016
+ *
15017
+ * Both halves are checked. The name has to be allowlisted, and the files have
15018
+ * to be exactly the one file that name refers to — an asset named `CLAUDE.md`
15019
+ * carrying a second file called `settings.json` would otherwise write it, which
15020
+ * is the same escape by a longer route.
15021
+ */
15022
+ const isWritableMemoryAsset = (input) => isMemoryAssetName(input.name) && input.files.length === 1 && input.files[0]?.path === input.name;
15023
+ /**
15024
+ * Write a memory file, or refuse.
15025
+ *
15026
+ * Answers rather than failing, because the caller's response to "this asset is
15027
+ * not writable here" is to report it alongside the other refusals, not to
15028
+ * abandon the sync. Written atomically: memory is read at the start of every
15029
+ * agent turn, and a half-written instruction file is instructions with the end
15030
+ * missing, which an agent will follow without noticing.
15031
+ */
15032
+ const writeMemoryFile = Effect.fn("memoryFile.writeMemoryFile")(function* (input) {
15033
+ if (!isWritableMemoryAsset(input)) return "refused";
15034
+ const fileSystem = yield* FileSystem.FileSystem;
15035
+ const path = yield* Path.Path;
15036
+ const contents = input.files[0]?.content ?? "";
15037
+ yield* fileSystem.makeDirectory(input.memoryRoot, { recursive: true }).pipe(Effect.ignore);
15038
+ return yield* writeFileStringAtomically({
15039
+ filePath: path.join(input.memoryRoot, input.name),
15040
+ contents
15041
+ }).pipe(Effect.as("written"), Effect.orElseSucceed(() => "failed"));
15042
+ });
15043
+ const removeMemoryFile = Effect.fn("memoryFile.removeMemoryFile")(function* (input) {
15044
+ if (!isMemoryAssetName(input.name)) return;
15045
+ const fileSystem = yield* FileSystem.FileSystem;
15046
+ const path = yield* Path.Path;
15047
+ yield* fileSystem.remove(path.join(input.memoryRoot, input.name), { force: true });
14778
15048
  });
14779
15049
  //#endregion
14780
15050
  //#region src/sync/skillManifest.ts
@@ -15151,10 +15421,10 @@ function planSkillSync(input) {
15151
15421
  };
15152
15422
  }
15153
15423
  //#endregion
15154
- //#region src/sync/SkillSync.ts
15424
+ //#region src/sync/AssetSync.ts
15155
15425
  const EMPTY_REPORT = {
15156
15426
  ran: false,
15157
- skillsRoot: null,
15427
+ roots: [],
15158
15428
  materialized: [],
15159
15429
  published: [],
15160
15430
  removed: [],
@@ -15163,13 +15433,12 @@ const EMPTY_REPORT = {
15163
15433
  refused: [],
15164
15434
  unavailable: null
15165
15435
  };
15166
- var SkillSync = class extends Context.Service()("@p4code/cli/sync/SkillSync") {};
15167
- const make$57 = Effect.gen(function* () {
15436
+ var AssetSync = class extends Context.Service()("@p4code/cli/sync/AssetSync") {};
15437
+ const make$58 = Effect.gen(function* () {
15168
15438
  const client = yield* HubAssetClient;
15169
15439
  const link = yield* HubLink;
15170
15440
  const settingsStore = yield* ServerSettingsService;
15171
15441
  const config = yield* ServerConfig$1;
15172
- const fileSystem = yield* FileSystem.FileSystem;
15173
15442
  const path = yield* Path.Path;
15174
15443
  /**
15175
15444
  * Captured here rather than left in each method's requirements, so the
@@ -15178,267 +15447,446 @@ const make$57 = Effect.gen(function* () {
15178
15447
  * neither should have to carry a filesystem to ask for a sync.
15179
15448
  */
15180
15449
  const services = yield* Effect.context();
15450
+ /**
15451
+ * Still `skill-sync-manifest.json`.
15452
+ *
15453
+ * Renaming it would be tidier and would also declare every skill this machine
15454
+ * manages to be unmanaged, which on the next sync means re-publishing all of
15455
+ * them into collisions. The manifest is keyed by root directory, and the two
15456
+ * kinds have different roots, so one file holds both without ambiguity.
15457
+ */
15181
15458
  const manifestPath = path.join(config.stateDir, "skill-sync-manifest.json");
15182
- const resolveSkillsRoot = Effect.fn("SkillSync.resolveSkillsRoot")(function* () {
15183
- return yield* resolveClaudeUserSkillsDir({ homePath: (yield* settingsStore.getSettings.pipe(Effect.orElseSucceed(() => void 0)))?.providers.claudeAgent.homePath ?? "" });
15459
+ const claudeHome = Effect.fn("AssetSync.claudeHome")(function* () {
15460
+ return { homePath: (yield* settingsStore.getSettings.pipe(Effect.orElseSucceed(() => void 0)))?.providers.claudeAgent.homePath ?? "" };
15184
15461
  });
15462
+ const drivers = [
15463
+ {
15464
+ kind: "skill",
15465
+ resolveRoot: Effect.gen(function* () {
15466
+ return yield* resolveClaudeUserSkillsDir(yield* claudeHome());
15467
+ }),
15468
+ read: (root) => readSkillDirectory(root),
15469
+ write: ({ root, name, files }) => writeSkillDirectory({
15470
+ skillsRoot: root,
15471
+ name,
15472
+ files
15473
+ }).pipe(Effect.as("written"), Effect.orElseSucceed(() => "failed")),
15474
+ remove: ({ root, name }) => removeSkillDirectory({
15475
+ skillsRoot: root,
15476
+ name
15477
+ }).pipe(Effect.ignore)
15478
+ },
15479
+ {
15480
+ kind: "memory",
15481
+ resolveRoot: Effect.gen(function* () {
15482
+ return yield* resolveClaudeConfigDirPath(yield* claudeHome(), process.env);
15483
+ }),
15484
+ read: (root) => readMemoryFiles(root),
15485
+ write: ({ root, name, files }) => writeMemoryFile({
15486
+ memoryRoot: root,
15487
+ name,
15488
+ files
15489
+ }).pipe(Effect.map((outcome) => outcome)),
15490
+ remove: ({ root, name }) => removeMemoryFile({
15491
+ memoryRoot: root,
15492
+ name
15493
+ }).pipe(Effect.ignore)
15494
+ },
15495
+ {
15496
+ kind: "mcp",
15497
+ resolveRoot: Effect.succeed(path.join(config.stateDir, "mcp")),
15498
+ read: (root) => readMcpRegistrations(root),
15499
+ write: ({ root, name, files }) => writeMcpRegistration({
15500
+ registrationsRoot: root,
15501
+ name,
15502
+ files
15503
+ }),
15504
+ remove: ({ root, name }) => removeMcpRegistration({
15505
+ registrationsRoot: root,
15506
+ name
15507
+ }).pipe(Effect.ignore)
15508
+ }
15509
+ ];
15510
+ const driverFor = (kind) => drivers.find((driver) => driver.kind === kind);
15185
15511
  const isConfigured = link.current.pipe(Effect.map((state) => Option.isSome(state.settings)));
15512
+ /**
15513
+ * The three views for one kind, gathered once.
15514
+ *
15515
+ * `status` and `run` both need exactly this and disagreeing about it is how a
15516
+ * panel comes to show something the next sync does not do.
15517
+ */
15518
+ const viewsFor = Effect.fn("AssetSync.viewsFor")(function* (driver, hubAssets) {
15519
+ const root = yield* driver.resolveRoot;
15520
+ const hub = hubAssets.filter((asset) => asset.kind === driver.kind);
15521
+ if (root === null) return {
15522
+ root,
15523
+ hub,
15524
+ local: [],
15525
+ refused: [],
15526
+ managed: []
15527
+ };
15528
+ const local = yield* driver.read(root);
15529
+ const manifest = yield* readSkillManifest(manifestPath);
15530
+ return {
15531
+ root,
15532
+ hub,
15533
+ local: local.skills,
15534
+ refused: local.refused,
15535
+ managed: entriesFor(manifest, root)
15536
+ };
15537
+ });
15538
+ const rootsOf = Effect.fn("AssetSync.rootsOf")(function* () {
15539
+ const roots = [];
15540
+ for (const driver of drivers) roots.push({
15541
+ kind: driver.kind,
15542
+ path: yield* driver.resolveRoot
15543
+ });
15544
+ return roots;
15545
+ });
15186
15546
  const run = Effect.gen(function* () {
15187
15547
  if (!(yield* isConfigured)) return EMPTY_REPORT;
15188
- const skillsRoot = yield* resolveSkillsRoot();
15189
- const hub = yield* client.list("skill").pipe(Effect.option);
15190
- if (Option.isNone(hub)) return {
15548
+ const roots = yield* rootsOf();
15549
+ const hubAssets = yield* client.list().pipe(Effect.option);
15550
+ if (Option.isNone(hubAssets)) return {
15191
15551
  ...EMPTY_REPORT,
15192
15552
  ran: true,
15193
- skillsRoot,
15553
+ roots,
15194
15554
  unavailable: "hub is unreachable"
15195
15555
  };
15196
- const local = yield* readSkillDirectory(skillsRoot);
15197
- const manifest = yield* readSkillManifest(manifestPath);
15198
- const plan = planSkillSync({
15199
- hub: hub.value,
15200
- local: local.skills,
15201
- manifest: entriesFor(manifest, skillsRoot)
15202
- });
15203
- const entries = new Map(entriesFor(manifest, skillsRoot).map((entry) => [entry.name, entry]));
15204
15556
  const materialized = [];
15205
15557
  const published = [];
15206
15558
  const removed = [];
15207
- const conflicts = [...plan.conflicts];
15208
- for (const action of plan.actions) switch (action._tag) {
15209
- case "materialize":
15210
- yield* writeSkillDirectory({
15211
- skillsRoot,
15212
- name: action.asset.name,
15213
- files: action.asset.files
15214
- });
15215
- entries.set(action.asset.name, {
15216
- name: action.asset.name,
15217
- version: action.asset.version,
15218
- digest: digestSkillFiles(action.asset.files)
15219
- });
15220
- materialized.push(action.asset.name);
15221
- break;
15222
- case "publish": {
15223
- const written = yield* client.write({
15224
- kind: "skill",
15225
- name: action.name,
15226
- files: action.files,
15227
- expectedVersion: action.expectedVersion
15228
- }).pipe(Effect.option);
15229
- if (Option.isNone(written)) {
15230
- conflicts.push({
15559
+ const unmanaged = [];
15560
+ const conflicts = [];
15561
+ const refused = [];
15562
+ const shareMode = (yield* link.current).shareMode;
15563
+ for (const driver of drivers) {
15564
+ const views = yield* viewsFor(driver, hubAssets.value);
15565
+ const kind = driver.kind;
15566
+ for (const entry of views.refused) refused.push({
15567
+ kind,
15568
+ name: entry.name,
15569
+ reason: entry.reason
15570
+ });
15571
+ if (views.root === null) continue;
15572
+ const root = views.root;
15573
+ const plan = planSkillSync({
15574
+ hub: views.hub,
15575
+ local: views.local,
15576
+ manifest: views.managed
15577
+ });
15578
+ const entries = new Map(views.managed.map((entry) => [entry.name, entry]));
15579
+ /** One manifest for the whole run, re-read per kind so roots do not clobber. */
15580
+ const commit = Effect.fn("AssetSync.commit")(function* () {
15581
+ const manifest = yield* readSkillManifest(manifestPath);
15582
+ yield* writeSkillManifest(manifestPath, withEntriesFor(manifest, root, [...entries.values()]));
15583
+ });
15584
+ for (const action of plan.actions) switch (action._tag) {
15585
+ case "materialize": {
15586
+ const outcome = yield* driver.write({
15587
+ root,
15588
+ name: action.asset.name,
15589
+ files: action.asset.files
15590
+ });
15591
+ if (outcome !== "written") {
15592
+ refused.push({
15593
+ kind,
15594
+ name: action.asset.name,
15595
+ reason: outcome === "refused" ? "not-writable-here" : "write-failed"
15596
+ });
15597
+ break;
15598
+ }
15599
+ entries.set(action.asset.name, {
15600
+ name: action.asset.name,
15601
+ version: action.asset.version,
15602
+ digest: digestSkillFiles(action.asset.files)
15603
+ });
15604
+ materialized.push({
15605
+ kind,
15606
+ name: action.asset.name
15607
+ });
15608
+ break;
15609
+ }
15610
+ case "publish": {
15611
+ const written = yield* client.write({
15612
+ kind,
15613
+ name: action.name,
15614
+ files: action.files,
15615
+ expectedVersion: action.expectedVersion
15616
+ }).pipe(Effect.option);
15617
+ if (Option.isNone(written)) {
15618
+ conflicts.push({
15619
+ kind,
15620
+ name: action.name,
15621
+ reason: "edited-both-sides"
15622
+ });
15623
+ break;
15624
+ }
15625
+ entries.set(action.name, {
15231
15626
  name: action.name,
15232
- reason: "edited-both-sides"
15627
+ version: written.value.version,
15628
+ digest: digestSkillFiles(written.value.files)
15629
+ });
15630
+ published.push({
15631
+ kind,
15632
+ name: action.name
15233
15633
  });
15234
15634
  break;
15235
15635
  }
15236
- entries.set(action.name, {
15237
- name: action.name,
15238
- version: written.value.version,
15239
- digest: digestSkillFiles(written.value.files)
15240
- });
15241
- published.push(action.name);
15242
- break;
15636
+ case "removeLocal":
15637
+ yield* driver.remove({
15638
+ root,
15639
+ name: action.name
15640
+ });
15641
+ entries.delete(action.name);
15642
+ removed.push({
15643
+ kind,
15644
+ name: action.name
15645
+ });
15646
+ break;
15647
+ case "forget":
15648
+ entries.delete(action.name);
15649
+ break;
15243
15650
  }
15244
- case "removeLocal":
15245
- yield* removeSkillDirectory({
15246
- skillsRoot,
15247
- name: action.name
15248
- });
15249
- entries.delete(action.name);
15250
- removed.push(action.name);
15251
- break;
15252
- case "forget":
15253
- entries.delete(action.name);
15254
- break;
15255
- }
15256
- yield* writeSkillManifest(manifestPath, withEntriesFor(manifest, skillsRoot, [...entries.values()]));
15257
- const shareMode = (yield* link.current).shareMode;
15258
- const autoShared = [];
15259
- if (shareMode === "all") {
15260
- for (const name of plan.unmanaged) {
15261
- const skill = local.skills.find((entry) => entry.name === name);
15262
- if (skill === void 0) continue;
15263
- const written = yield* client.write({
15264
- kind: "skill",
15265
- name,
15266
- files: skill.files
15267
- }).pipe(Effect.option);
15268
- if (Option.isNone(written)) {
15269
- conflicts.push({
15651
+ for (const conflict of plan.conflicts) conflicts.push({
15652
+ kind,
15653
+ name: conflict.name,
15654
+ reason: conflict.reason
15655
+ });
15656
+ yield* commit();
15657
+ const autoShared = /* @__PURE__ */ new Set();
15658
+ if (shareMode === "all") {
15659
+ for (const name of plan.unmanaged) {
15660
+ const local = views.local.find((entry) => entry.name === name);
15661
+ if (local === void 0) continue;
15662
+ const written = yield* client.write({
15663
+ kind,
15270
15664
  name,
15271
- reason: "unmanaged-local-collision"
15665
+ files: local.files
15666
+ }).pipe(Effect.option);
15667
+ if (Option.isNone(written)) {
15668
+ conflicts.push({
15669
+ kind,
15670
+ name,
15671
+ reason: "unmanaged-local-collision"
15672
+ });
15673
+ continue;
15674
+ }
15675
+ entries.set(name, {
15676
+ name,
15677
+ version: written.value.version,
15678
+ digest: digestSkillFiles(written.value.files)
15272
15679
  });
15273
- continue;
15680
+ published.push({
15681
+ kind,
15682
+ name
15683
+ });
15684
+ autoShared.add(name);
15274
15685
  }
15275
- entries.set(name, {
15276
- name,
15277
- version: written.value.version,
15278
- digest: digestSkillFiles(written.value.files)
15279
- });
15280
- autoShared.push(name);
15686
+ yield* commit();
15281
15687
  }
15282
- yield* writeSkillManifest(manifestPath, withEntriesFor(manifest, skillsRoot, [...entries.values()]));
15688
+ for (const name of plan.unmanaged) if (!autoShared.has(name)) unmanaged.push({
15689
+ kind,
15690
+ name
15691
+ });
15283
15692
  }
15284
15693
  return {
15285
15694
  ran: true,
15286
- skillsRoot,
15695
+ roots,
15287
15696
  materialized,
15288
- published: [...published, ...autoShared],
15697
+ published,
15289
15698
  removed,
15290
15699
  conflicts,
15291
- unmanaged: plan.unmanaged.filter((name) => !autoShared.includes(name)),
15292
- refused: local.refused,
15700
+ unmanaged,
15701
+ refused,
15293
15702
  unavailable: null
15294
15703
  };
15295
- }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("skill sync failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15704
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset sync failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15296
15705
  /**
15297
- * The same three views the sync reads, rendered rather than acted on.
15706
+ * The same views the sync reads, rendered rather than acted on.
15298
15707
  *
15299
15708
  * Deliberately its own pass instead of a dry-run flag on `run`: a dry run
15300
15709
  * sharing the executing code path is one `if` away from writing during a
15301
15710
  * preview, and this is the call a panel makes every time it opens.
15302
15711
  */
15303
- const skillStatus = Effect.gen(function* () {
15712
+ const assetStatus = Effect.gen(function* () {
15304
15713
  const state = yield* link.current;
15305
- const skillsRoot = yield* resolveSkillsRoot();
15306
- const local = yield* readSkillDirectory(skillsRoot);
15307
- const managed = entriesFor(yield* readSkillManifest(manifestPath), skillsRoot);
15714
+ const roots = yield* rootsOf();
15308
15715
  const base = {
15309
15716
  syncMode: state.syncMode,
15310
15717
  shareMode: state.shareMode,
15311
15718
  source: state.source,
15312
- skillsRoot,
15313
- refused: local.refused
15719
+ roots
15314
15720
  };
15315
- if (Option.isNone(state.settings)) return {
15316
- ...base,
15317
- configured: false,
15318
- hubUrl: null,
15319
- entries: local.skills.map((skill) => ({
15320
- name: skill.name,
15321
- state: "unmanaged",
15322
- version: null,
15323
- detail: null
15324
- })),
15325
- unavailable: null
15326
- };
15327
- const hubUrl = state.settings.value.baseUrl;
15328
- const hub = yield* client.list("skill").pipe(Effect.option);
15329
- if (Option.isNone(hub)) return {
15330
- ...base,
15331
- configured: true,
15332
- hubUrl,
15333
- entries: managed.map((entry) => ({
15721
+ const hubAssets = Option.isNone(state.settings) ? Option.none() : yield* client.list().pipe(Effect.option);
15722
+ const entries = [];
15723
+ const refused = [];
15724
+ for (const driver of drivers) {
15725
+ const kind = driver.kind;
15726
+ const views = yield* viewsFor(driver, Option.getOrElse(hubAssets, () => []));
15727
+ for (const entry of views.refused) refused.push({
15728
+ kind,
15334
15729
  name: entry.name,
15335
- state: "synced",
15336
- version: entry.version,
15337
- detail: null
15338
- })),
15339
- unavailable: "hub is unreachable"
15340
- };
15341
- const plan = planSkillSync({
15342
- hub: hub.value,
15343
- local: local.skills,
15344
- manifest: managed
15345
- });
15346
- const conflictByName = new Map(plan.conflicts.map((entry) => [entry.name, entry.reason]));
15347
- const pendingByName = new Map(plan.actions.map((action) => [action._tag === "materialize" ? action.asset.name : action.name, action._tag]));
15348
- const managedByName = new Map(managed.map((entry) => [entry.name, entry]));
15349
- const hubByName = new Map(hub.value.map((asset) => [asset.name, asset]));
15350
- const entries = [.../* @__PURE__ */ new Set([
15351
- ...hubByName.keys(),
15352
- ...local.skills.map((skill) => skill.name),
15353
- ...managedByName.keys()
15354
- ])].sort().map((name) => {
15355
- const version = hubByName.get(name)?.version ?? managedByName.get(name)?.version ?? null;
15356
- const conflict = conflictByName.get(name);
15357
- if (conflict !== void 0) return {
15358
- name,
15359
- state: "conflict",
15360
- version,
15361
- detail: conflict
15362
- };
15363
- const action = pendingByName.get(name);
15364
- if (action === "publish") return {
15365
- name,
15366
- state: "local-edit",
15367
- version,
15368
- detail: null
15369
- };
15370
- if (action === "materialize" || action === "removeLocal") return {
15371
- name,
15372
- state: "pending",
15373
- version,
15374
- detail: action
15375
- };
15376
- const isManaged = managedByName.has(name);
15377
- return {
15378
- name,
15379
- state: isManaged ? "synced" : "unmanaged",
15380
- version: isManaged ? version : null,
15381
- detail: null
15382
- };
15383
- });
15730
+ reason: entry.reason
15731
+ });
15732
+ if (Option.isNone(hubAssets)) {
15733
+ for (const entry of views.managed) entries.push({
15734
+ kind,
15735
+ name: entry.name,
15736
+ state: "synced",
15737
+ version: entry.version,
15738
+ detail: null
15739
+ });
15740
+ for (const local of views.local) {
15741
+ if (views.managed.some((entry) => entry.name === local.name)) continue;
15742
+ entries.push({
15743
+ kind,
15744
+ name: local.name,
15745
+ state: "unmanaged",
15746
+ version: null,
15747
+ detail: null
15748
+ });
15749
+ }
15750
+ continue;
15751
+ }
15752
+ const plan = planSkillSync({
15753
+ hub: views.hub,
15754
+ local: views.local,
15755
+ manifest: views.managed
15756
+ });
15757
+ const conflictByName = new Map(plan.conflicts.map((entry) => [entry.name, entry.reason]));
15758
+ const pendingByName = new Map(plan.actions.map((action) => [action._tag === "materialize" ? action.asset.name : action.name, action._tag]));
15759
+ const managedByName = new Map(views.managed.map((entry) => [entry.name, entry]));
15760
+ const hubByName = new Map(views.hub.map((asset) => [asset.name, asset]));
15761
+ const localNames = new Set(views.local.map((entry) => entry.name));
15762
+ const names = [.../* @__PURE__ */ new Set([
15763
+ ...hubByName.keys(),
15764
+ ...localNames,
15765
+ ...managedByName.keys()
15766
+ ])].sort();
15767
+ for (const name of names) {
15768
+ const version = hubByName.get(name)?.version ?? managedByName.get(name)?.version ?? null;
15769
+ const conflict = conflictByName.get(name);
15770
+ if (conflict !== void 0) {
15771
+ entries.push({
15772
+ kind,
15773
+ name,
15774
+ state: "conflict",
15775
+ version,
15776
+ detail: conflict
15777
+ });
15778
+ continue;
15779
+ }
15780
+ if (views.root === null) {
15781
+ entries.push({
15782
+ kind,
15783
+ name,
15784
+ state: "hub-only",
15785
+ version,
15786
+ detail: null
15787
+ });
15788
+ continue;
15789
+ }
15790
+ const action = pendingByName.get(name);
15791
+ if (action === "publish") {
15792
+ entries.push({
15793
+ kind,
15794
+ name,
15795
+ state: "local-edit",
15796
+ version,
15797
+ detail: null
15798
+ });
15799
+ continue;
15800
+ }
15801
+ if (action === "materialize" || action === "removeLocal") {
15802
+ entries.push({
15803
+ kind,
15804
+ name,
15805
+ state: "pending",
15806
+ version,
15807
+ detail: action
15808
+ });
15809
+ continue;
15810
+ }
15811
+ const isManaged = managedByName.has(name);
15812
+ entries.push({
15813
+ kind,
15814
+ name,
15815
+ state: isManaged ? "synced" : "unmanaged",
15816
+ version: isManaged ? version : null,
15817
+ detail: null
15818
+ });
15819
+ }
15820
+ }
15384
15821
  return {
15385
15822
  ...base,
15386
- configured: true,
15387
- hubUrl,
15823
+ configured: Option.isSome(state.settings),
15824
+ hubUrl: Option.match(state.settings, {
15825
+ onNone: () => null,
15826
+ onSome: (settings) => settings.baseUrl
15827
+ }),
15388
15828
  entries,
15389
- unavailable: null
15829
+ refused,
15830
+ unavailable: Option.isSome(state.settings) && Option.isNone(hubAssets) ? "hub is unreachable" : null
15390
15831
  };
15391
- }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("skill sync status failed", { cause }).pipe(Effect.as({
15832
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset sync status failed", { cause }).pipe(Effect.as({
15392
15833
  configured: false,
15393
15834
  hubUrl: null,
15394
15835
  syncMode: "auto",
15395
15836
  shareMode: "all",
15396
15837
  source: "none",
15397
- skillsRoot: null,
15838
+ roots: [],
15398
15839
  entries: [],
15399
15840
  refused: [],
15400
- unavailable: "could not read local skills"
15841
+ unavailable: "could not read local assets"
15401
15842
  }))));
15402
- const publish = (name) => Effect.gen(function* () {
15843
+ const publish = ({ kind, name }) => Effect.gen(function* () {
15403
15844
  if (!(yield* isConfigured)) return EMPTY_REPORT;
15404
- const skillsRoot = yield* resolveSkillsRoot();
15405
- const directory = path.join(skillsRoot, name);
15406
- if (!(yield* fileSystem.exists(directory).pipe(Effect.orElseSucceed(() => false)))) return {
15845
+ const driver = driverFor(kind);
15846
+ if (driver === void 0) return EMPTY_REPORT;
15847
+ const root = yield* driver.resolveRoot;
15848
+ const roots = yield* rootsOf();
15849
+ if (root === null) return {
15407
15850
  ...EMPTY_REPORT,
15408
15851
  ran: true,
15409
- skillsRoot
15852
+ roots
15410
15853
  };
15411
- const local = yield* readSkillDirectory(skillsRoot);
15412
- const refused = local.refused.filter((entry) => entry.name === name);
15854
+ const local = yield* driver.read(root);
15855
+ const refused = local.refused.filter((entry) => entry.name === name).map((entry) => ({
15856
+ kind,
15857
+ name: entry.name,
15858
+ reason: entry.reason
15859
+ }));
15413
15860
  if (refused.length > 0) return {
15414
15861
  ...EMPTY_REPORT,
15415
15862
  ran: true,
15416
- skillsRoot,
15863
+ roots,
15417
15864
  refused
15418
15865
  };
15419
- const skill = local.skills.find((entry) => entry.name === name);
15420
- if (skill === void 0) return {
15866
+ const asset = local.skills.find((entry) => entry.name === name);
15867
+ if (asset === void 0) return {
15421
15868
  ...EMPTY_REPORT,
15422
15869
  ran: true,
15423
- skillsRoot
15870
+ roots
15424
15871
  };
15425
15872
  const written = yield* client.write({
15426
- kind: "skill",
15873
+ kind,
15427
15874
  name,
15428
- files: skill.files
15875
+ files: asset.files
15429
15876
  }).pipe(Effect.option);
15430
15877
  if (Option.isNone(written)) return {
15431
15878
  ...EMPTY_REPORT,
15432
15879
  ran: true,
15433
- skillsRoot,
15880
+ roots,
15434
15881
  conflicts: [{
15882
+ kind,
15435
15883
  name,
15436
15884
  reason: "unmanaged-local-collision"
15437
15885
  }]
15438
15886
  };
15439
15887
  const manifest = yield* readSkillManifest(manifestPath);
15440
- const entries = entriesFor(manifest, skillsRoot).filter((entry) => entry.name !== name);
15441
- yield* writeSkillManifest(manifestPath, withEntriesFor(manifest, skillsRoot, [...entries, {
15888
+ const entries = entriesFor(manifest, root).filter((entry) => entry.name !== name);
15889
+ yield* writeSkillManifest(manifestPath, withEntriesFor(manifest, root, [...entries, {
15442
15890
  name,
15443
15891
  version: written.value.version,
15444
15892
  digest: digestSkillFiles(written.value.files)
@@ -15446,134 +15894,261 @@ const make$57 = Effect.gen(function* () {
15446
15894
  return {
15447
15895
  ...EMPTY_REPORT,
15448
15896
  ran: true,
15449
- skillsRoot,
15450
- published: [name]
15897
+ roots,
15898
+ published: [{
15899
+ kind,
15900
+ name
15901
+ }]
15451
15902
  };
15452
- }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("skill publish failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15903
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset publish failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15453
15904
  const publishAll = Effect.gen(function* () {
15454
15905
  if (!(yield* isConfigured)) return EMPTY_REPORT;
15455
- const skillsRoot = yield* resolveSkillsRoot();
15456
- const status = yield* skillStatus;
15457
- const candidates = status.entries.filter((entry) => entry.state === "unmanaged").map((entry) => entry.name);
15906
+ const status = yield* assetStatus;
15907
+ const candidates = status.entries.filter((entry) => entry.state === "unmanaged");
15458
15908
  const published = [];
15459
15909
  const conflicts = [];
15460
- for (const name of candidates) {
15461
- const report = yield* publish(name);
15910
+ for (const candidate of candidates) {
15911
+ const report = yield* publish({
15912
+ kind: candidate.kind,
15913
+ name: candidate.name
15914
+ });
15462
15915
  published.push(...report.published);
15463
15916
  conflicts.push(...report.conflicts);
15464
15917
  }
15465
15918
  return {
15466
15919
  ...EMPTY_REPORT,
15467
15920
  ran: true,
15468
- skillsRoot,
15921
+ roots: status.roots,
15469
15922
  published,
15470
15923
  conflicts,
15471
15924
  refused: status.refused
15472
15925
  };
15473
- }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("skill publish-all failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15474
- const unpublish = (name) => Effect.gen(function* () {
15926
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset publish-all failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15927
+ const unpublish = ({ kind, name }) => Effect.gen(function* () {
15475
15928
  if (!(yield* isConfigured)) return EMPTY_REPORT;
15476
- const skillsRoot = yield* resolveSkillsRoot();
15929
+ const driver = driverFor(kind);
15930
+ if (driver === void 0) return EMPTY_REPORT;
15931
+ const roots = yield* rootsOf();
15932
+ const root = yield* driver.resolveRoot;
15933
+ if (root === null) return {
15934
+ ...EMPTY_REPORT,
15935
+ ran: true,
15936
+ roots
15937
+ };
15477
15938
  const manifest = yield* readSkillManifest(manifestPath);
15478
- const managed = entriesFor(manifest, skillsRoot);
15939
+ const managed = entriesFor(manifest, root);
15479
15940
  const entry = managed.find((candidate) => candidate.name === name);
15480
15941
  if (entry === void 0) return {
15481
15942
  ...EMPTY_REPORT,
15482
15943
  ran: true,
15483
- skillsRoot
15944
+ roots
15484
15945
  };
15485
15946
  const removedFromHub = yield* client.remove({
15486
- kind: "skill",
15947
+ kind,
15487
15948
  name,
15488
15949
  expectedVersion: entry.version
15489
15950
  }).pipe(Effect.option);
15490
15951
  if (Option.isNone(removedFromHub)) return {
15491
15952
  ...EMPTY_REPORT,
15492
15953
  ran: true,
15493
- skillsRoot,
15954
+ roots,
15494
15955
  conflicts: [{
15956
+ kind,
15495
15957
  name,
15496
15958
  reason: "edited-both-sides"
15497
15959
  }]
15498
15960
  };
15499
- yield* writeSkillManifest(manifestPath, withEntriesFor(manifest, skillsRoot, managed.filter((candidate) => candidate.name !== name)));
15961
+ yield* writeSkillManifest(manifestPath, withEntriesFor(manifest, root, managed.filter((candidate) => candidate.name !== name)));
15500
15962
  return {
15501
15963
  ...EMPTY_REPORT,
15502
15964
  ran: true,
15503
- skillsRoot,
15504
- removed: [name]
15965
+ roots,
15966
+ removed: [{
15967
+ kind,
15968
+ name
15969
+ }]
15970
+ };
15971
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset unpublish failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15972
+ const read = ({ kind, name }) => client.get({
15973
+ kind,
15974
+ name
15975
+ }).pipe(Effect.map((asset) => Option.match(asset, {
15976
+ onNone: () => ({
15977
+ found: false,
15978
+ kind,
15979
+ name,
15980
+ files: [],
15981
+ version: null,
15982
+ updatedAt: null,
15983
+ unavailable: null
15984
+ }),
15985
+ onSome: (found) => ({
15986
+ found: true,
15987
+ kind,
15988
+ name,
15989
+ files: found.files,
15990
+ version: found.version,
15991
+ updatedAt: found.updatedAt,
15992
+ unavailable: null
15993
+ })
15994
+ })), Effect.catchCause((cause) => Effect.logWarning("asset read failed", { cause }).pipe(Effect.as({
15995
+ found: false,
15996
+ kind,
15997
+ name,
15998
+ files: [],
15999
+ version: null,
16000
+ updatedAt: null,
16001
+ unavailable: "hub is unreachable"
16002
+ }))));
16003
+ const save = ({ kind, name, files, expectedVersion }) => Effect.gen(function* () {
16004
+ if (!(yield* isConfigured)) return EMPTY_REPORT;
16005
+ const written = yield* client.write({
16006
+ kind,
16007
+ name,
16008
+ files,
16009
+ expectedVersion
16010
+ }).pipe(Effect.option);
16011
+ if (Option.isNone(written)) return {
16012
+ ...EMPTY_REPORT,
16013
+ ran: true,
16014
+ roots: yield* rootsOf(),
16015
+ conflicts: [{
16016
+ kind,
16017
+ name,
16018
+ reason: "edited-both-sides"
16019
+ }]
16020
+ };
16021
+ return yield* run;
16022
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset save failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
16023
+ const deleteFromHub = ({ kind, name, expectedVersion }) => Effect.gen(function* () {
16024
+ if (!(yield* isConfigured)) return EMPTY_REPORT;
16025
+ const removed = yield* client.remove({
16026
+ kind,
16027
+ name,
16028
+ ...expectedVersion === void 0 ? {} : { expectedVersion }
16029
+ }).pipe(Effect.option);
16030
+ if (Option.isNone(removed)) return {
16031
+ ...EMPTY_REPORT,
16032
+ ran: true,
16033
+ roots: yield* rootsOf(),
16034
+ conflicts: [{
16035
+ kind,
16036
+ name,
16037
+ reason: "edited-both-sides"
16038
+ }]
15505
16039
  };
15506
- }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("skill unpublish failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
16040
+ return yield* run;
16041
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset delete failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15507
16042
  return {
15508
16043
  run,
15509
- status: skillStatus,
16044
+ status: assetStatus,
15510
16045
  publish,
15511
16046
  publishAll,
15512
- unpublish
16047
+ unpublish,
16048
+ read,
16049
+ save,
16050
+ deleteFromHub
15513
16051
  };
15514
16052
  });
15515
- const layer$52 = Layer.effect(SkillSync, make$57);
16053
+ const layer$53 = Layer.effect(AssetSync, make$58);
15516
16054
  //#endregion
15517
- //#region src/cli/skills.ts
15518
- const SyncServicesLive = layer$52.pipe(Layer.provideMerge(layer$53), Layer.provideMerge(layer$54), Layer.provideMerge(layer$55.pipe(Layer.provide(layer$62))), Layer.provideMerge(layer$62), Layer.provideMerge(FetchHttpClient.layer));
16055
+ //#region src/cli/assets.ts
16056
+ const SyncServicesLive = layer$53.pipe(Layer.provideMerge(layer$54), Layer.provideMerge(layer$55), Layer.provideMerge(layer$57.pipe(Layer.provide(layer$63))), Layer.provideMerge(layer$63), Layer.provideMerge(FetchHttpClient.layer));
15519
16057
  const runWithSync = (flags, run) => Effect.gen(function* () {
15520
16058
  const config = yield* resolveCliAuthConfig(flags, yield* GlobalFlag.LogLevel);
15521
16059
  return yield* Effect.gen(function* () {
15522
16060
  return yield* run({
15523
- sync: yield* SkillSync,
16061
+ sync: yield* AssetSync,
15524
16062
  link: yield* HubLink
15525
16063
  });
15526
- }).pipe(Effect.provide(SyncServicesLive.pipe(Layer.provide(layer$65(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, "Error")))));
16064
+ }).pipe(Effect.provide(SyncServicesLive.pipe(Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, "Error")))));
15527
16065
  });
15528
16066
  const STATE_LABEL = {
15529
16067
  synced: "synced",
15530
16068
  pending: "will update",
15531
16069
  "local-edit": "will publish",
15532
16070
  conflict: "CONFLICT",
15533
- unmanaged: "not shared"
16071
+ unmanaged: "not shared",
16072
+ "hub-only": "on the hub only"
16073
+ };
16074
+ const SKILL_WORDS = {
16075
+ kind: "skill",
16076
+ command: "skills",
16077
+ singular: "skill",
16078
+ plural: "skills",
16079
+ rootLabel: "Skills",
16080
+ nameDescription: "Name of the skill directory."
16081
+ };
16082
+ const MEMORY_WORDS = {
16083
+ kind: "memory",
16084
+ command: "memory",
16085
+ singular: "memory file",
16086
+ plural: "memory files",
16087
+ rootLabel: "Memory",
16088
+ nameDescription: "Memory filename, for example CLAUDE.md."
15534
16089
  };
16090
+ const MCP_WORDS = {
16091
+ kind: "mcp",
16092
+ command: "mcp",
16093
+ singular: "MCP server",
16094
+ plural: "MCP servers",
16095
+ rootLabel: "MCP",
16096
+ nameDescription: "Registration filename, for example linear.json."
16097
+ };
16098
+ const rootFor = (status, kind) => status.roots.find((root) => root.kind === kind)?.path ?? null;
15535
16099
  /**
15536
16100
  * Exported for tests. These two are the only place a wrong report becomes a
15537
16101
  * confident sentence, so they are worth pinning: "Already up to date" and
15538
16102
  * "nothing changed" must never be printed for a run that failed.
15539
16103
  */
15540
- const formatStatus = (status) => {
16104
+ const formatStatus = (status, words = SKILL_WORDS) => {
15541
16105
  const lines = [];
15542
- if (!status.configured) lines.push("No hub is configured. Connect one with: p4 hub connect --url <url> --token <token>");
16106
+ if (!status.configured) lines.push("No hub is configured. Connect one with: p4c hub connect --url <url> --token <token>");
15543
16107
  else {
15544
16108
  lines.push(`Hub: ${status.hubUrl ?? "unknown"}${status.source === "environment" ? " (from the environment)" : ""}`);
15545
16109
  lines.push(`Sync: ${status.syncMode}`);
15546
- lines.push(`Share: ${status.shareMode === "all" ? "all skills (new ones share automatically)" : "only the ones you choose"}`);
16110
+ lines.push(`Share: ${status.shareMode === "all" ? `all ${words.plural} (new ones share automatically)` : "only the ones you choose"}`);
15547
16111
  }
15548
- if (status.skillsRoot !== null) lines.push(`Skills: ${status.skillsRoot}`);
16112
+ const root = rootFor(status, words.kind);
16113
+ if (root !== null) lines.push(`${words.rootLabel}: ${root}`);
15549
16114
  if (status.unavailable !== null) lines.push(`Warning: ${status.unavailable}`);
15550
16115
  lines.push("");
15551
- if (status.entries.length === 0) lines.push("No skills found.");
15552
- else for (const entry of status.entries) {
16116
+ const entries = status.entries.filter((entry) => entry.kind === words.kind);
16117
+ if (entries.length === 0) lines.push(`No ${words.plural} found.`);
16118
+ else for (const entry of entries) {
15553
16119
  const version = entry.version === null ? "" : ` v${entry.version}`;
15554
16120
  const detail = entry.detail === null ? "" : ` (${entry.detail})`;
15555
16121
  lines.push(` ${entry.name.padEnd(28)} ${STATE_LABEL[entry.state] ?? entry.state}${version}${detail}`);
15556
16122
  }
15557
- for (const refusal of status.refused) lines.push(` ${refusal.name.padEnd(28)} cannot sync (${refusal.reason})`);
16123
+ for (const refusal of status.refused.filter((entry) => entry.kind === words.kind)) lines.push(` ${refusal.name.padEnd(28)} cannot sync (${refusal.reason})`);
15558
16124
  return lines.join("\n");
15559
16125
  };
16126
+ /**
16127
+ * A report covers every kind at once, so names are qualified as soon as more
16128
+ * than one kind appears in it. `published CLAUDE.md` is ambiguous in a way
16129
+ * `published memory CLAUDE.md` is not — a skill directory may legitimately be
16130
+ * called `CLAUDE.md`, since the name schema permits dots.
16131
+ */
16132
+ const describeRefs = (refs) => {
16133
+ const kinds = new Set(refs.map((ref) => ref.kind));
16134
+ return refs.map((ref) => kinds.size > 1 ? `${ref.kind} ${ref.name}` : ref.name).join(", ");
16135
+ };
15560
16136
  const formatReport = (report) => {
15561
16137
  if (!report.ran) return "No hub is configured; nothing to do.";
15562
16138
  if (report.unavailable !== null) return `Hub is unreachable (${report.unavailable}); nothing changed.`;
15563
16139
  const parts = [];
15564
- if (report.materialized.length > 0) parts.push(`updated ${report.materialized.join(", ")}`);
15565
- if (report.published.length > 0) parts.push(`published ${report.published.join(", ")}`);
15566
- if (report.removed.length > 0) parts.push(`removed ${report.removed.join(", ")}`);
16140
+ if (report.materialized.length > 0) parts.push(`updated ${describeRefs(report.materialized)}`);
16141
+ if (report.published.length > 0) parts.push(`published ${describeRefs(report.published)}`);
16142
+ if (report.removed.length > 0) parts.push(`removed ${describeRefs(report.removed)}`);
15567
16143
  for (const conflict of report.conflicts) parts.push(`CONFLICT on ${conflict.name} (${conflict.reason})`);
15568
16144
  for (const refusal of report.refused) parts.push(`cannot sync ${refusal.name} (${refusal.reason})`);
15569
16145
  const summary = parts.length === 0 ? "Already up to date." : parts.join("\n");
15570
16146
  if (report.unmanaged.length === 0) return summary;
15571
16147
  const count = report.unmanaged.length;
15572
- return `${summary}\n\n${count} local skill${count === 1 ? " is" : "s are"} not shared. Share ${count === 1 ? "it" : "them"} with: p4 skills publish --all`;
16148
+ return `${summary}\n\n${count} local item${count === 1 ? " is" : "s are"} not shared. Share ${count === 1 ? "it" : "them"} with: p4c skills publish --all`;
15573
16149
  };
15574
16150
  const urlFlag = Flag.string("url").pipe(Flag.withDescription("Base URL of the hub, for example https://hub.example.com."));
15575
- const tokenFlag = Flag.string("token").pipe(Flag.withDescription("Bearer token for this server, from `p4 hub token add`."));
15576
- const skillNameArgument = Argument.string("name").pipe(Argument.withDescription("Name of the skill directory."));
16151
+ const tokenFlag = Flag.string("token").pipe(Flag.withDescription("Bearer token for this server, from `p4c hub token add`."));
15577
16152
  const hubConnectCommand = Command.make("connect", {
15578
16153
  ...authLocationFlags,
15579
16154
  url: urlFlag,
@@ -15589,32 +16164,74 @@ const hubDisconnectCommand = Command.make("disconnect", { ...authLocationFlags }
15589
16164
  yield* link.disconnect;
15590
16165
  yield* Console.log(formatStatus(yield* sync.status));
15591
16166
  }))));
15592
- const statusCommand = Command.make("status", { ...authLocationFlags }).pipe(Command.withDescription("Show which skills are shared, changed, or in conflict."), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => sync.status.pipe(Effect.flatMap((s) => Console.log(formatStatus(s)))))));
15593
- const syncCommand = Command.make("sync", { ...authLocationFlags }).pipe(Command.withDescription("Reconcile this machine's shared skills with the hub now."), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => sync.run.pipe(Effect.flatMap((r) => Console.log(formatReport(r)))))));
15594
- const allFlag = Flag.boolean("all").pipe(Flag.withDescription("Share every local skill this machine does not already share."));
15595
- const publishCommand = Command.make("publish", {
15596
- ...authLocationFlags,
15597
- name: skillNameArgument.pipe(Argument.optional),
15598
- all: allFlag
15599
- }).pipe(Command.withDescription("Share local skills through the hub, so every machine gets them."), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => Effect.gen(function* () {
15600
- if (flags.all) return yield* sync.publishAll.pipe(Effect.flatMap((report) => Console.log(formatReport(report))));
15601
- const name = Option.getOrUndefined(flags.name);
15602
- if (name === void 0) return yield* Console.error("Name a skill, or pass --all to share every unshared one.");
15603
- return yield* sync.publish(name).pipe(Effect.flatMap((report) => Console.log(formatReport(report))));
15604
- }))));
15605
- const unpublishCommand = Command.make("unpublish", {
15606
- ...authLocationFlags,
15607
- name: skillNameArgument
15608
- }).pipe(Command.withDescription("Stop sharing a skill. It stays on this machine and leaves the others."), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => sync.unpublish(flags.name).pipe(Effect.flatMap((report) => Console.log(formatReport(report)))))));
15609
- const shareAllCommand = Command.make("share-all", { ...authLocationFlags }).pipe(Command.withDescription("Share every skill, including new ones, automatically. The default."), Command.withHandler((flags) => runWithSync(flags, ({ sync, link }) => Effect.gen(function* () {
16167
+ const makeAssetCommands = (words) => {
16168
+ const nameArgument = Argument.string("name").pipe(Argument.withDescription(words.nameDescription));
16169
+ return {
16170
+ statusCommand: Command.make("status", { ...authLocationFlags }).pipe(Command.withDescription(`Show which ${words.plural} are shared, changed, or in conflict.`), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => sync.status.pipe(Effect.flatMap((status) => Console.log(formatStatus(status, words))))))),
16171
+ syncCommand: Command.make("sync", { ...authLocationFlags }).pipe(Command.withDescription("Reconcile everything this machine shares with the hub now."), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => sync.run.pipe(Effect.flatMap((report) => Console.log(formatReport(report))))))),
16172
+ publishCommand: Command.make("publish", {
16173
+ ...authLocationFlags,
16174
+ name: nameArgument.pipe(Argument.optional),
16175
+ all: Flag.boolean("all").pipe(Flag.withDescription("Share everything local this machine does not already share."))
16176
+ }).pipe(Command.withDescription(`Share local ${words.plural} through the hub, so every machine gets them.`), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => Effect.gen(function* () {
16177
+ if (flags.all) return yield* sync.publishAll.pipe(Effect.flatMap((report) => Console.log(formatReport(report))));
16178
+ const name = Option.getOrUndefined(flags.name);
16179
+ if (name === void 0) return yield* Console.error(`Name a ${words.singular}, or pass --all to share every unshared one.`);
16180
+ return yield* sync.publish({
16181
+ kind: words.kind,
16182
+ name
16183
+ }).pipe(Effect.flatMap((report) => Console.log(formatReport(report))));
16184
+ })))),
16185
+ unpublishCommand: Command.make("unpublish", {
16186
+ ...authLocationFlags,
16187
+ name: nameArgument
16188
+ }).pipe(Command.withDescription(`Stop sharing a ${words.singular}. It stays on this machine and leaves the others.`), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => sync.unpublish({
16189
+ kind: words.kind,
16190
+ name: flags.name
16191
+ }).pipe(Effect.flatMap((report) => Console.log(formatReport(report))))))),
16192
+ showCommand: Command.make("show", {
16193
+ ...authLocationFlags,
16194
+ name: nameArgument
16195
+ }).pipe(Command.withDescription(`Print the hub's copy of a ${words.singular}, and where it lives here.`), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => Effect.gen(function* () {
16196
+ const name = flags.name;
16197
+ const content = yield* sync.read({
16198
+ kind: words.kind,
16199
+ name
16200
+ });
16201
+ if (content.unavailable !== null) return yield* Console.error(`Hub is unreachable (${content.unavailable}).`);
16202
+ if (!content.found) return yield* Console.error(`The hub has no ${words.singular} named ${name}.`);
16203
+ const lines = [`${name} v${content.version ?? "?"} updated ${content.updatedAt ?? "?"}`];
16204
+ for (const file of content.files) lines.push("", `--- ${file.path} ---`, file.content.trimEnd());
16205
+ const root = rootFor(yield* sync.status, words.kind);
16206
+ if (root !== null) lines.push("", `Edit it here and the next sync publishes it: ${root}/${name}`);
16207
+ return yield* Console.log(lines.join("\n"));
16208
+ })))),
16209
+ removeCommand: Command.make("rm", {
16210
+ ...authLocationFlags,
16211
+ name: nameArgument,
16212
+ yes: Flag.boolean("yes").pipe(Flag.withDescription("Confirm: this removes it from the hub and from every machine."))
16213
+ }).pipe(Command.withDescription(`Delete a ${words.singular} from the hub and from every machine.`), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => Effect.gen(function* () {
16214
+ if (!flags.yes) return yield* Console.error(`This deletes ${flags.name} from the hub and from every machine that syncs it.\nRe-run with --yes if that is what you want.\nTo stop sharing it from here only: p4 ${words.command} unpublish ${flags.name}`);
16215
+ const report = yield* sync.deleteFromHub({
16216
+ kind: words.kind,
16217
+ name: flags.name
16218
+ });
16219
+ return yield* Console.log(formatReport(report));
16220
+ }))))
16221
+ };
16222
+ };
16223
+ const skillCommands = makeAssetCommands(SKILL_WORDS);
16224
+ const memoryCommands = makeAssetCommands(MEMORY_WORDS);
16225
+ const mcpCommands = makeAssetCommands(MCP_WORDS);
16226
+ const shareAllCommand = Command.make("share-all", { ...authLocationFlags }).pipe(Command.withDescription("Share everything, including new items, automatically. The default."), Command.withHandler((flags) => runWithSync(flags, ({ sync, link }) => Effect.gen(function* () {
15610
16227
  yield* link.setShareMode("all");
15611
16228
  yield* Console.log(formatStatus(yield* sync.status));
15612
16229
  }))));
15613
- const shareSelectedCommand = Command.make("share-selected", { ...authLocationFlags }).pipe(Command.withDescription("Share only the skills you name with `publish`. Nothing shares on its own."), Command.withHandler((flags) => runWithSync(flags, ({ sync, link }) => Effect.gen(function* () {
16230
+ const shareSelectedCommand = Command.make("share-selected", { ...authLocationFlags }).pipe(Command.withDescription("Share only what you name with `publish`. Nothing shares on its own."), Command.withHandler((flags) => runWithSync(flags, ({ sync, link }) => Effect.gen(function* () {
15614
16231
  yield* link.setShareMode("selected");
15615
16232
  yield* Console.log(formatStatus(yield* sync.status));
15616
16233
  }))));
15617
- const pauseCommand = Command.make("pause", { ...authLocationFlags }).pipe(Command.withDescription("Stop syncing automatically. `p4 skills sync` still works."), Command.withHandler((flags) => runWithSync(flags, ({ sync, link }) => Effect.gen(function* () {
16234
+ const pauseCommand = Command.make("pause", { ...authLocationFlags }).pipe(Command.withDescription("Stop syncing automatically. `p4c skills sync` still works."), Command.withHandler((flags) => runWithSync(flags, ({ sync, link }) => Effect.gen(function* () {
15618
16235
  yield* link.setSyncMode("paused");
15619
16236
  yield* Console.log(formatStatus(yield* sync.status));
15620
16237
  }))));
@@ -15623,15 +16240,41 @@ const resumeCommand = Command.make("resume", { ...authLocationFlags }).pipe(Comm
15623
16240
  yield* Console.log(formatStatus(yield* sync.status));
15624
16241
  }))));
15625
16242
  const skillsCommand = Command.make("skills", {}).pipe(Command.withDescription("Share agent skills across your machines through the hub."), Command.withSubcommands([
15626
- statusCommand,
15627
- syncCommand,
15628
- publishCommand,
15629
- unpublishCommand,
16243
+ skillCommands.statusCommand,
16244
+ skillCommands.syncCommand,
16245
+ skillCommands.publishCommand,
16246
+ skillCommands.unpublishCommand,
16247
+ skillCommands.showCommand,
16248
+ skillCommands.removeCommand,
15630
16249
  pauseCommand,
15631
16250
  resumeCommand,
15632
16251
  shareAllCommand,
15633
16252
  shareSelectedCommand
15634
16253
  ]));
16254
+ const memoryCommand = Command.make("memory", {}).pipe(Command.withDescription("Share user memory — CLAUDE.md, AGENTS.md — across your machines."), Command.withSubcommands([
16255
+ memoryCommands.statusCommand,
16256
+ memoryCommands.syncCommand,
16257
+ memoryCommands.publishCommand,
16258
+ memoryCommands.unpublishCommand,
16259
+ memoryCommands.showCommand,
16260
+ memoryCommands.removeCommand
16261
+ ]));
16262
+ /**
16263
+ * The sharing half of `p4c mcp`.
16264
+ *
16265
+ * Registering a server — its command, its URL, its tokens — belongs in the
16266
+ * panel, where a form can ask for a secret and put it somewhere that does not
16267
+ * sync. These subcommands are the same six every kind gets, and what they move
16268
+ * is the definition only.
16269
+ */
16270
+ const mcpCommand = Command.make("mcp", {}).pipe(Command.withDescription("Share MCP server registrations across your machines."), Command.withSubcommands([
16271
+ mcpCommands.statusCommand,
16272
+ mcpCommands.syncCommand,
16273
+ mcpCommands.publishCommand,
16274
+ mcpCommands.unpublishCommand,
16275
+ mcpCommands.showCommand,
16276
+ mcpCommands.removeCommand
16277
+ ]));
15635
16278
  /** Exported for the hub command, which owns `connect`/`disconnect`. */
15636
16279
  const hubLinkCommands = [hubConnectCommand, hubDisconnectCommand];
15637
16280
  //#endregion
@@ -15729,7 +16372,7 @@ const serveHubCommand = Command.make("serve", {
15729
16372
  fileTokenHashes: hubTokenHashes(tokenFile)
15730
16373
  });
15731
16374
  if (rejectedTokenHashes.length > 0) yield* Console.error(`Ignoring ${rejectedTokenHashes.length} malformed entr${rejectedTokenHashes.length === 1 ? "y" : "ies"} in P4CODE_HUB_TOKEN_HASHES; each must be a 64-character SHA-256 hex digest.`);
15732
- if (config.tokenHashes.size === 0) yield* Console.error("No server tokens are registered, so every request will be rejected.\nMint one with: p4 hub token add --label studio");
16375
+ if (config.tokenHashes.size === 0) yield* Console.error("No server tokens are registered, so every request will be rejected.\nMint one with: p4c hub token add --label studio");
15733
16376
  return yield* runHub.pipe(Effect.provideService(HubConfig, HubConfig.of({
15734
16377
  dbPath: config.dbPath,
15735
16378
  host: config.host,
@@ -20307,6 +20950,273 @@ function mergeGitStatusParts(local, remote) {
20307
20950
  };
20308
20951
  }
20309
20952
  //#endregion
20953
+ //#region src/stream/collectUint8StreamText.ts
20954
+ const collectUint8StreamText = (input) => {
20955
+ const maxBytes = input.maxBytes ?? Number.POSITIVE_INFINITY;
20956
+ const truncatedMarker = input.truncatedMarker ?? "";
20957
+ return input.stream.pipe(Stream.runFold(() => ({
20958
+ chunks: [],
20959
+ bytes: 0,
20960
+ truncated: false
20961
+ }), (state, chunk) => {
20962
+ if (state.truncated) return state;
20963
+ const remainingBytes = maxBytes - state.bytes;
20964
+ if (remainingBytes <= 0) return {
20965
+ ...state,
20966
+ truncated: true
20967
+ };
20968
+ const nextChunk = chunk.byteLength > remainingBytes ? chunk.slice(0, remainingBytes) : chunk;
20969
+ state.chunks.push(nextChunk);
20970
+ const bytes = state.bytes + nextChunk.byteLength;
20971
+ const truncated = chunk.byteLength > remainingBytes;
20972
+ return {
20973
+ chunks: state.chunks,
20974
+ bytes,
20975
+ truncated
20976
+ };
20977
+ }), Effect.map((state) => {
20978
+ const text = Buffer.concat(state.chunks, state.bytes).toString("utf8");
20979
+ return {
20980
+ text: state.truncated && truncatedMarker.length > 0 ? `${text}${truncatedMarker}` : text,
20981
+ bytes: state.bytes,
20982
+ truncated: state.truncated
20983
+ };
20984
+ }));
20985
+ };
20986
+ //#endregion
20987
+ //#region src/processRunner.ts
20988
+ const ProcessInvocationFields = {
20989
+ command: Schema$1.String,
20990
+ argumentCount: Schema$1.Number,
20991
+ cwd: Schema$1.optional(Schema$1.String),
20992
+ spawnCwd: Schema$1.optional(Schema$1.String)
20993
+ };
20994
+ const formatProcessInvocation = (input) => {
20995
+ const executionCwd = input.spawnCwd ?? input.cwd;
20996
+ return executionCwd === void 0 ? `'${input.command}'` : `'${input.command}' in '${executionCwd}'`;
20997
+ };
20998
+ var ProcessSpawnError = class extends Schema$1.TaggedErrorClass()("ProcessSpawnError", {
20999
+ ...ProcessInvocationFields,
21000
+ resolvedCommand: Schema$1.optional(Schema$1.String),
21001
+ resolvedArgumentCount: Schema$1.optional(Schema$1.Number),
21002
+ shell: Schema$1.optional(Schema$1.Boolean),
21003
+ cause: Schema$1.Defect()
21004
+ }) {
21005
+ get message() {
21006
+ return `Failed to spawn process ${formatProcessInvocation(this)}`;
21007
+ }
21008
+ };
21009
+ var ProcessStdinError = class extends Schema$1.TaggedErrorClass()("ProcessStdinError", {
21010
+ ...ProcessInvocationFields,
21011
+ stdinBytes: Schema$1.Number,
21012
+ cause: Schema$1.Defect()
21013
+ }) {
21014
+ get message() {
21015
+ return `Failed to write stdin for process ${formatProcessInvocation(this)}`;
21016
+ }
21017
+ };
21018
+ var ProcessOutputLimitError = class extends Schema$1.TaggedErrorClass()("ProcessOutputLimitError", {
21019
+ ...ProcessInvocationFields,
21020
+ stream: Schema$1.Literals(["stdout", "stderr"]),
21021
+ maxBytes: Schema$1.Number,
21022
+ observedBytes: Schema$1.Number
21023
+ }) {
21024
+ get message() {
21025
+ return `Process ${formatProcessInvocation(this)} ${this.stream} produced ${this.observedBytes} bytes, exceeding the ${this.maxBytes} byte limit`;
21026
+ }
21027
+ };
21028
+ var ProcessReadError = class extends Schema$1.TaggedErrorClass()("ProcessReadError", {
21029
+ ...ProcessInvocationFields,
21030
+ stream: Schema$1.Literals([
21031
+ "stdout",
21032
+ "stderr",
21033
+ "exitCode"
21034
+ ]),
21035
+ cause: Schema$1.Defect()
21036
+ }) {
21037
+ get message() {
21038
+ return `Failed to read ${this.stream} for process ${formatProcessInvocation(this)}`;
21039
+ }
21040
+ };
21041
+ var ProcessTimeoutError = class extends Schema$1.TaggedErrorClass()("ProcessTimeoutError", {
21042
+ ...ProcessInvocationFields,
21043
+ timeoutMs: Schema$1.Number
21044
+ }) {
21045
+ get message() {
21046
+ return `Process ${formatProcessInvocation(this)} timed out after ${this.timeoutMs}ms`;
21047
+ }
21048
+ };
21049
+ Schema$1.Union([
21050
+ ProcessSpawnError,
21051
+ ProcessStdinError,
21052
+ ProcessOutputLimitError,
21053
+ ProcessReadError,
21054
+ ProcessTimeoutError
21055
+ ]);
21056
+ var ProcessRunner = class extends Context.Service()("@p4code/cli/processRunner") {};
21057
+ const DEFAULT_TIMEOUT = "60 seconds";
21058
+ const DEFAULT_MAX_OUTPUT_BYTES$2 = 8 * 1024 * 1024;
21059
+ const WINDOWS_COMMAND_NOT_FOUND_PATTERNS = [
21060
+ /is not recognized as an internal or external command/i,
21061
+ /n.o . reconhecido como um comando interno/i,
21062
+ /non . riconosciuto come comando interno o esterno/i,
21063
+ /n.est pas reconnu en tant que commande interne/i,
21064
+ /no se reconoce como un comando interno o externo/i,
21065
+ /wird nicht als interner oder externer befehl/i
21066
+ ];
21067
+ function hasWindowsCommandNotFoundMessage(output) {
21068
+ return WINDOWS_COMMAND_NOT_FOUND_PATTERNS.some((pattern) => pattern.test(output));
21069
+ }
21070
+ const isWindowsCommandNotFound = Effect.fn("processRunner.isWindowsCommandNotFound")(function* (code, stderr) {
21071
+ if ((yield* HostProcessPlatform) !== "win32") return false;
21072
+ if (code === 9009) return true;
21073
+ return hasWindowsCommandNotFoundMessage(stderr);
21074
+ });
21075
+ const collectText = Effect.fn("processRunner.collectText")(function* (input) {
21076
+ const stream = input.stream.pipe(Stream.mapError((cause) => new ProcessReadError({
21077
+ command: input.command,
21078
+ argumentCount: input.args.length,
21079
+ cwd: input.cwd,
21080
+ spawnCwd: input.spawnCwd,
21081
+ stream: input.streamName,
21082
+ cause
21083
+ })));
21084
+ if (input.outputMode === "truncate") return yield* collectUint8StreamText({
21085
+ stream,
21086
+ maxBytes: input.maxOutputBytes,
21087
+ truncatedMarker: input.truncatedMarker
21088
+ });
21089
+ return yield* stream.pipe(Stream.runFoldEffect(() => ({
21090
+ chunks: [],
21091
+ bytes: 0
21092
+ }), (state, chunk) => {
21093
+ const remainingBytes = input.maxOutputBytes - state.bytes;
21094
+ if (chunk.byteLength > remainingBytes) return Effect.fail(new ProcessOutputLimitError({
21095
+ command: input.command,
21096
+ argumentCount: input.args.length,
21097
+ cwd: input.cwd,
21098
+ spawnCwd: input.spawnCwd,
21099
+ stream: input.streamName,
21100
+ maxBytes: input.maxOutputBytes,
21101
+ observedBytes: state.bytes + chunk.byteLength
21102
+ }));
21103
+ state.chunks.push(chunk);
21104
+ return Effect.succeed({
21105
+ chunks: state.chunks,
21106
+ bytes: state.bytes + chunk.byteLength
21107
+ });
21108
+ }), Effect.map((state) => ({
21109
+ text: Buffer.concat(state.chunks, state.bytes).toString("utf8"),
21110
+ bytes: state.bytes,
21111
+ truncated: false
21112
+ })));
21113
+ });
21114
+ function finalizeRunProcess(effect, input) {
21115
+ const timeout = Duration.fromInputUnsafe(input.timeout ?? DEFAULT_TIMEOUT);
21116
+ const timeoutBehavior = input.timeoutBehavior ?? "error";
21117
+ return effect.pipe(Effect.scoped, Effect.timeoutOption(timeout), Effect.flatMap((result) => {
21118
+ if (Option.isSome(result)) return Effect.succeed(result.value);
21119
+ if (timeoutBehavior === "timedOutResult") return Effect.succeed({
21120
+ stdout: "",
21121
+ stderr: "",
21122
+ code: null,
21123
+ timedOut: true,
21124
+ stdoutTruncated: false,
21125
+ stderrTruncated: false
21126
+ });
21127
+ return Effect.fail(new ProcessTimeoutError({
21128
+ command: input.command,
21129
+ argumentCount: input.args.length,
21130
+ cwd: input.cwd,
21131
+ spawnCwd: input.spawnCwd,
21132
+ timeoutMs: Duration.toMillis(timeout)
21133
+ }));
21134
+ }));
21135
+ }
21136
+ const runProcessCore = Effect.fn("processRunner.runProcessCore")(function* (spawner, input) {
21137
+ const maxOutputBytes = input.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES$2;
21138
+ const outputMode = input.outputMode ?? "error";
21139
+ const truncatedMarker = input.truncatedMarker ?? "";
21140
+ const extendEnv = input.env !== void 0;
21141
+ const spawnCommand = yield* resolveSpawnCommand(input.command, input.args, input.env === void 0 ? {} : {
21142
+ env: input.env,
21143
+ extendEnv
21144
+ });
21145
+ const child = yield* spawner.spawn(ChildProcess$1.make(spawnCommand.command, spawnCommand.args, {
21146
+ ...input.spawnCwd ?? input.cwd ? { cwd: input.spawnCwd ?? input.cwd } : {},
21147
+ ...input.env !== void 0 ? {
21148
+ env: input.env,
21149
+ extendEnv
21150
+ } : {},
21151
+ shell: spawnCommand.shell
21152
+ })).pipe(Effect.mapError((cause) => new ProcessSpawnError({
21153
+ command: input.command,
21154
+ argumentCount: input.args.length,
21155
+ cwd: input.cwd,
21156
+ spawnCwd: input.spawnCwd,
21157
+ resolvedCommand: spawnCommand.command,
21158
+ resolvedArgumentCount: spawnCommand.args.length,
21159
+ shell: spawnCommand.shell,
21160
+ cause
21161
+ })));
21162
+ const stdin = input.stdin;
21163
+ const writeStdin = stdin === void 0 ? Effect.void : Stream.run(Stream.encodeText(Stream.make(stdin)), child.stdin).pipe(Effect.mapError((cause) => new ProcessStdinError({
21164
+ command: input.command,
21165
+ argumentCount: input.args.length,
21166
+ cwd: input.cwd,
21167
+ spawnCwd: input.spawnCwd,
21168
+ stdinBytes: Buffer.byteLength(stdin),
21169
+ cause
21170
+ })));
21171
+ const [stdout, stderr] = yield* Effect.all([
21172
+ collectText({
21173
+ command: input.command,
21174
+ args: input.args,
21175
+ cwd: input.cwd,
21176
+ spawnCwd: input.spawnCwd,
21177
+ streamName: "stdout",
21178
+ stream: child.stdout,
21179
+ maxOutputBytes,
21180
+ outputMode,
21181
+ truncatedMarker
21182
+ }),
21183
+ collectText({
21184
+ command: input.command,
21185
+ args: input.args,
21186
+ cwd: input.cwd,
21187
+ spawnCwd: input.spawnCwd,
21188
+ streamName: "stderr",
21189
+ stream: child.stderr,
21190
+ maxOutputBytes,
21191
+ outputMode,
21192
+ truncatedMarker
21193
+ }),
21194
+ writeStdin
21195
+ ], { concurrency: "unbounded" });
21196
+ const exitCode = yield* child.exitCode.pipe(Effect.mapError((cause) => new ProcessReadError({
21197
+ command: input.command,
21198
+ argumentCount: input.args.length,
21199
+ cwd: input.cwd,
21200
+ spawnCwd: input.spawnCwd,
21201
+ stream: "exitCode",
21202
+ cause
21203
+ })));
21204
+ return {
21205
+ stdout: stdout.text,
21206
+ stderr: stderr.text,
21207
+ code: exitCode,
21208
+ timedOut: false,
21209
+ stdoutTruncated: stdout.truncated,
21210
+ stderrTruncated: stderr.truncated
21211
+ };
21212
+ });
21213
+ const make$57 = Effect.fn("ProcessRunner.make")(function* () {
21214
+ const spawner = yield* ChildProcessSpawner$1.ChildProcessSpawner;
21215
+ const run = (input) => finalizeRunProcess(runProcessCore(spawner, input), input);
21216
+ return ProcessRunner.of({ run });
21217
+ });
21218
+ const layer$52 = Layer.effect(ProcessRunner, make$57());
21219
+ //#endregion
20310
21220
  //#region src/project/RepositoryIdentityResolver.ts
20311
21221
  const DEFAULT_REPOSITORY_IDENTITY_CACHE_CAPACITY = 512;
20312
21222
  const DEFAULT_POSITIVE_CACHE_TTL = Duration.minutes(1);
@@ -20411,7 +21321,7 @@ const make$56 = Effect.fn("RepositoryIdentityResolver.make")(function* (options
20411
21321
  });
20412
21322
  return RepositoryIdentityResolver.of({ resolve });
20413
21323
  });
20414
- const layer$51 = Layer.effect(RepositoryIdentityResolver, make$56()).pipe(Layer.provide(layer$57));
21324
+ const layer$51 = Layer.effect(RepositoryIdentityResolver, make$56()).pipe(Layer.provide(layer$52));
20415
21325
  //#endregion
20416
21326
  //#region src/orchestration/Layers/ProjectionSnapshotQuery.ts
20417
21327
  const decodeReadModel = Schema$1.decodeUnknownEffect(OrchestrationReadModel);
@@ -23429,7 +24339,7 @@ const layer$46 = (input) => Layer.effect(BootService, make$51(input));
23429
24339
  //#endregion
23430
24340
  //#region src/service/selfUpdate.ts
23431
24341
  /**
23432
- * Lets a connected client replace this server with another published `p4`
24342
+ * Lets a connected client replace this server with another published `p4c`
23433
24343
  * version over RPC — the only update path that works when the user is not at
23434
24344
  * the machine (phone against a home server, relay-managed box). The target
23435
24345
  * version is npm-installed into the pinned runtime and verified before
@@ -23648,7 +24558,7 @@ const make$50 = Effect.fn("cloud.server_self_update.make")(function* (options) {
23648
24558
  });
23649
24559
  return ServerSelfUpdate.of({ update });
23650
24560
  });
23651
- const layer$45 = Layer.effect(ServerSelfUpdate, make$50()).pipe(Layer.provide(layer$57));
24561
+ const layer$45 = Layer.effect(ServerSelfUpdate, make$50()).pipe(Layer.provide(layer$52));
23652
24562
  //#endregion
23653
24563
  //#region src/environment/ServerEnvironmentLabel.ts
23654
24564
  const ServerEnvironmentLabelCommandProbe = Schema$1.Literals(["macos-computer-name", "linux-pretty-hostname"]);
@@ -23843,7 +24753,7 @@ const make$49 = Effect.gen(function* () {
23843
24753
  * state. It intentionally has no fallback Layer.succeed value: callers must
23844
24754
  * provide the external platform services and a ServerConfig.
23845
24755
  */
23846
- const layer$44 = Layer.effect(ServerEnvironment, make$49).pipe(Layer.provide(layer$57));
24756
+ const layer$44 = Layer.effect(ServerEnvironment, make$49).pipe(Layer.provide(layer$52));
23847
24757
  //#endregion
23848
24758
  //#region src/provider/Services/ProviderSessionReaper.ts
23849
24759
  var ProviderSessionReaper = class extends Context.Service()("@p4code/cli/provider/Services/ProviderSessionReaper") {};
@@ -24927,7 +25837,7 @@ const make$48 = Effect.gen(function* () {
24927
25837
  const lifecycleEvents = yield* ServerLifecycleEvents;
24928
25838
  const serverSettings = yield* ServerSettingsService;
24929
25839
  const serverEnvironment = yield* ServerEnvironment;
24930
- const skillSync = yield* SkillSync;
25840
+ const assetSync = yield* AssetSync;
24931
25841
  const hubLink = yield* HubLink;
24932
25842
  const crypto = yield* Crypto.Crypto;
24933
25843
  const commandGate = yield* makeCommandGate;
@@ -24949,13 +25859,13 @@ const make$48 = Effect.gen(function* () {
24949
25859
  environmentVariable: error.environmentVariable,
24950
25860
  cause: error.cause
24951
25861
  })), Effect.forkScoped));
24952
- yield* Effect.logDebug("startup phase: syncing user-scope skills");
25862
+ yield* Effect.logDebug("startup phase: syncing user-scope agent assets");
24953
25863
  yield* runStartupPhase("skills.sync", Effect.gen(function* () {
24954
- if ((yield* hubLink.current).syncMode !== "auto") return yield* Effect.logDebug("skill sync is paused; skipping the automatic pass");
24955
- const report = yield* skillSync.run;
25864
+ if ((yield* hubLink.current).syncMode !== "auto") return yield* Effect.logDebug("asset sync is paused; skipping the automatic pass");
25865
+ const report = yield* assetSync.run;
24956
25866
  if (!report.ran) return;
24957
- yield* Effect.log("skill sync", {
24958
- skillsRoot: report.skillsRoot,
25867
+ yield* Effect.log("asset sync", {
25868
+ roots: report.roots,
24959
25869
  materialized: report.materialized,
24960
25870
  published: report.published,
24961
25871
  removed: report.removed,
@@ -25375,7 +26285,7 @@ const ProjectCliRuntimeLive = Layer.mergeAll(layer$42, OrchestrationLayerLive.pi
25375
26285
  const PROJECT_CLI_LIVE_SERVER_TIMEOUT = Duration.seconds(1);
25376
26286
  const withProjectCliSessionToken = (environmentAuth, run) => Effect.acquireUseRelease(environmentAuth.issueSession({
25377
26287
  scopes: AuthAdministrativeScopes,
25378
- label: "p4 project cli"
26288
+ label: "p4c project cli"
25379
26289
  }), (issued) => run(issued.token), (issued) => environmentAuth.revokeSession(issued.sessionId).pipe(Effect.ignore({ log: true })));
25380
26290
  const withProjectCliLiveServerTimeout = (effect) => effect.pipe(Effect.timeout(PROJECT_CLI_LIVE_SERVER_TIMEOUT));
25381
26291
  const makeLiveServerClient = (origin) => HttpApiClient.make(EnvironmentHttpApi, { baseUrl: origin });
@@ -25462,7 +26372,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (flags, run
25462
26372
  });
25463
26373
  yield* Console.log(output);
25464
26374
  }));
25465
- const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(Layer.provide(layer$65(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)));
26375
+ const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)));
25466
26376
  return yield* Effect.gen(function* () {
25467
26377
  const snapshot = yield* getOfflineSnapshot();
25468
26378
  const orchestrationEngine = yield* OrchestrationEngineService;
@@ -25473,7 +26383,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (flags, run
25473
26383
  });
25474
26384
  yield* Console.log(output);
25475
26385
  }).pipe(Effect.provide(offlineRuntimeLayer));
25476
- }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer, layer$42).pipe(Layer.provideMerge(FetchHttpClient.layer), Layer.provide(layer$65(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
26386
+ }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer, layer$42).pipe(Layer.provideMerge(FetchHttpClient.layer), Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
25477
26387
  });
25478
26388
  const projectAddCommand = Command.make("add", {
25479
26389
  ...projectLocationFlags,
@@ -28768,7 +29678,7 @@ const make$43 = Effect.gen(function* () {
28768
29678
  });
28769
29679
  return VcsProcess.of({ run });
28770
29680
  });
28771
- const layer$38 = Layer.effect(VcsProcess, make$43).pipe(Layer.provide(layer$57));
29681
+ const layer$38 = Layer.effect(VcsProcess, make$43).pipe(Layer.provide(layer$52));
28772
29682
  //#endregion
28773
29683
  //#region src/vcs/VcsDriver.ts
28774
29684
  var VcsDriver = class extends Context.Service()("@p4code/cli/vcs/VcsDriver") {};
@@ -32549,7 +33459,7 @@ const makeWithOptions$1 = Effect.fn("TerminalManager.makeWithOptions")(function*
32549
33459
  subscribeMetadata
32550
33460
  });
32551
33461
  });
32552
- const layer$30 = Layer.effect(TerminalManager, make$35()).pipe(Layer.provide(layer$57));
33462
+ const layer$30 = Layer.effect(TerminalManager, make$35()).pipe(Layer.provide(layer$52));
32553
33463
  //#endregion
32554
33464
  //#region src/mcp/McpInvocationContext.ts
32555
33465
  var McpInvocationContext = class extends Context.Service()("@p4code/cli/mcp/McpInvocationContext") {};
@@ -39500,7 +40410,7 @@ const make$9 = Effect.gen(function* () {
39500
40410
  const layer$4 = Layer.effect(SourceControlRepositoryService, make$9);
39501
40411
  //#endregion
39502
40412
  //#region src/ws.ts
39503
- /** Matches `p4 hub token add`, so a token minted here and one minted there are the same thing. */
40413
+ /** Matches `p4c hub token add`, so a token minted here and one minted there are the same thing. */
39504
40414
  const HUB_TOKEN_BYTES = 32;
39505
40415
  const isOrchestrationDispatchCommandError = Schema$1.is(OrchestrationDispatchCommandError);
39506
40416
  const nowIso$5 = Effect.map(DateTime.now, DateTime.formatIso);
@@ -39632,6 +40542,13 @@ const RPC_REQUIRED_SCOPE = /* @__PURE__ */ new Map([
39632
40542
  [WS_METHODS.skillsPublish, AuthOrchestrationOperateScope],
39633
40543
  [WS_METHODS.skillsPublishAll, AuthOrchestrationOperateScope],
39634
40544
  [WS_METHODS.skillsUnpublish, AuthOrchestrationOperateScope],
40545
+ [WS_METHODS.assetsRead, AuthOrchestrationReadScope],
40546
+ [WS_METHODS.assetsSave, AuthOrchestrationOperateScope],
40547
+ [WS_METHODS.assetsDelete, AuthOrchestrationOperateScope],
40548
+ [WS_METHODS.mcpList, AuthOrchestrationReadScope],
40549
+ [WS_METHODS.mcpSave, AuthOrchestrationOperateScope],
40550
+ [WS_METHODS.mcpRemove, AuthOrchestrationOperateScope],
40551
+ [WS_METHODS.mcpSetSecret, AuthOrchestrationOperateScope],
39635
40552
  [WS_METHODS.serverGetSettings, AuthOrchestrationReadScope],
39636
40553
  [WS_METHODS.serverUpdateSettings, AuthOrchestrationOperateScope],
39637
40554
  [WS_METHODS.serverDiscoverSourceControl, AuthOrchestrationReadScope],
@@ -39742,7 +40659,8 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
39742
40659
  const lifecycleEvents = yield* ServerLifecycleEvents;
39743
40660
  const serverSettings = yield* ServerSettingsService;
39744
40661
  const hubLink = yield* HubLink;
39745
- const skillSync = yield* SkillSync;
40662
+ const assetSync = yield* AssetSync;
40663
+ const mcpRegistry = yield* McpRegistry;
39746
40664
  const startup = yield* ServerRuntimeStartup;
39747
40665
  const workspaceEntries = yield* WorkspaceEntries;
39748
40666
  const workspaceFileSystem = yield* WorkspaceFileSystem;
@@ -40204,11 +41122,11 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
40204
41122
  }), { "rpc.aggregate": "tasks" }),
40205
41123
  [WS_METHODS.tasksDelete]: ({ taskId }) => observeRpcEffect$1(WS_METHODS.tasksDelete, taskRepository.deleteById({ taskId }).pipe(Effect.catch(toTaskStoreError("tasks.delete")), Effect.as({})), { "rpc.aggregate": "tasks" }),
40206
41124
  [WS_METHODS.subscribeTasks]: (_input) => observeRpcStreamEffect$1(WS_METHODS.subscribeTasks, Effect.succeed(taskRepository.streamWithSnapshot.pipe(Stream.catch((cause) => Stream.fromEffect(toTaskStoreError("tasks.subscribe")(cause))))), { "rpc.aggregate": "tasks" }),
40207
- [WS_METHODS.hubGetSyncStatus]: (_input) => observeRpcEffect$1(WS_METHODS.hubGetSyncStatus, skillSync.status, { "rpc.aggregate": "hub" }),
40208
- [WS_METHODS.hubConnect]: (input) => observeRpcEffect$1(WS_METHODS.hubConnect, hubLink.connect(input).pipe(Effect.mapError((cause) => new HubLinkError({ detail: cause.message })), Effect.andThen(skillSync.status)), { "rpc.aggregate": "hub" }),
40209
- [WS_METHODS.hubDisconnect]: (_input) => observeRpcEffect$1(WS_METHODS.hubDisconnect, hubLink.disconnect.pipe(Effect.mapError((cause) => new HubLinkError({ detail: cause.message })), Effect.andThen(skillSync.status)), { "rpc.aggregate": "hub" }),
40210
- [WS_METHODS.hubSetSyncMode]: ({ mode }) => observeRpcEffect$1(WS_METHODS.hubSetSyncMode, hubLink.setSyncMode(mode).pipe(Effect.mapError((cause) => new HubLinkError({ detail: cause.message })), Effect.andThen(skillSync.status)), { "rpc.aggregate": "hub" }),
40211
- [WS_METHODS.hubSetShareMode]: ({ mode }) => observeRpcEffect$1(WS_METHODS.hubSetShareMode, hubLink.setShareMode(mode).pipe(Effect.mapError((cause) => new HubLinkError({ detail: cause.message })), Effect.andThen(skillSync.status)), { "rpc.aggregate": "hub" }),
41125
+ [WS_METHODS.hubGetSyncStatus]: (_input) => observeRpcEffect$1(WS_METHODS.hubGetSyncStatus, assetSync.status, { "rpc.aggregate": "hub" }),
41126
+ [WS_METHODS.hubConnect]: (input) => observeRpcEffect$1(WS_METHODS.hubConnect, hubLink.connect(input).pipe(Effect.mapError((cause) => new HubLinkError({ detail: cause.message })), Effect.andThen(assetSync.status)), { "rpc.aggregate": "hub" }),
41127
+ [WS_METHODS.hubDisconnect]: (_input) => observeRpcEffect$1(WS_METHODS.hubDisconnect, hubLink.disconnect.pipe(Effect.mapError((cause) => new HubLinkError({ detail: cause.message })), Effect.andThen(assetSync.status)), { "rpc.aggregate": "hub" }),
41128
+ [WS_METHODS.hubSetSyncMode]: ({ mode }) => observeRpcEffect$1(WS_METHODS.hubSetSyncMode, hubLink.setSyncMode(mode).pipe(Effect.mapError((cause) => new HubLinkError({ detail: cause.message })), Effect.andThen(assetSync.status)), { "rpc.aggregate": "hub" }),
41129
+ [WS_METHODS.hubSetShareMode]: ({ mode }) => observeRpcEffect$1(WS_METHODS.hubSetShareMode, hubLink.setShareMode(mode).pipe(Effect.mapError((cause) => new HubLinkError({ detail: cause.message })), Effect.andThen(assetSync.status)), { "rpc.aggregate": "hub" }),
40212
41130
  [WS_METHODS.hubMintToken]: (_input) => observeRpcEffect$1(WS_METHODS.hubMintToken, Effect.gen(function* () {
40213
41131
  const token = yield* crypto.randomBytes(HUB_TOKEN_BYTES).pipe(Effect.map((bytes) => Buffer.from(bytes).toString("base64url")), Effect.orDie);
40214
41132
  return {
@@ -40218,28 +41136,49 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
40218
41136
  }), { "rpc.aggregate": "hub" }),
40219
41137
  [WS_METHODS.skillsSync]: (_input) => observeRpcEffect$1(WS_METHODS.skillsSync, Effect.gen(function* () {
40220
41138
  return {
40221
- report: yield* skillSync.run,
40222
- status: yield* skillSync.status
41139
+ report: yield* assetSync.run,
41140
+ status: yield* assetSync.status
40223
41141
  };
40224
41142
  }), { "rpc.aggregate": "skills" }),
40225
- [WS_METHODS.skillsPublish]: ({ name }) => observeRpcEffect$1(WS_METHODS.skillsPublish, Effect.gen(function* () {
41143
+ [WS_METHODS.skillsPublish]: (ref) => observeRpcEffect$1(WS_METHODS.skillsPublish, Effect.gen(function* () {
40226
41144
  return {
40227
- report: yield* skillSync.publish(name),
40228
- status: yield* skillSync.status
41145
+ report: yield* assetSync.publish(ref),
41146
+ status: yield* assetSync.status
40229
41147
  };
40230
41148
  }), { "rpc.aggregate": "skills" }),
40231
41149
  [WS_METHODS.skillsPublishAll]: (_input) => observeRpcEffect$1(WS_METHODS.skillsPublishAll, Effect.gen(function* () {
40232
41150
  return {
40233
- report: yield* skillSync.publishAll,
40234
- status: yield* skillSync.status
41151
+ report: yield* assetSync.publishAll,
41152
+ status: yield* assetSync.status
40235
41153
  };
40236
41154
  }), { "rpc.aggregate": "skills" }),
40237
- [WS_METHODS.skillsUnpublish]: ({ name }) => observeRpcEffect$1(WS_METHODS.skillsUnpublish, Effect.gen(function* () {
41155
+ [WS_METHODS.skillsUnpublish]: (ref) => observeRpcEffect$1(WS_METHODS.skillsUnpublish, Effect.gen(function* () {
40238
41156
  return {
40239
- report: yield* skillSync.unpublish(name),
40240
- status: yield* skillSync.status
41157
+ report: yield* assetSync.unpublish(ref),
41158
+ status: yield* assetSync.status
40241
41159
  };
40242
41160
  }), { "rpc.aggregate": "skills" }),
41161
+ [WS_METHODS.assetsRead]: (ref) => observeRpcEffect$1(WS_METHODS.assetsRead, assetSync.read(ref), { "rpc.aggregate": "skills" }),
41162
+ [WS_METHODS.assetsSave]: (input) => observeRpcEffect$1(WS_METHODS.assetsSave, Effect.gen(function* () {
41163
+ return {
41164
+ report: yield* assetSync.save(input),
41165
+ status: yield* assetSync.status
41166
+ };
41167
+ }), { "rpc.aggregate": "skills" }),
41168
+ [WS_METHODS.assetsDelete]: (input) => observeRpcEffect$1(WS_METHODS.assetsDelete, Effect.gen(function* () {
41169
+ return {
41170
+ report: yield* assetSync.deleteFromHub(input),
41171
+ status: yield* assetSync.status
41172
+ };
41173
+ }), { "rpc.aggregate": "skills" }),
41174
+ [WS_METHODS.mcpList]: (_input) => observeRpcEffect$1(WS_METHODS.mcpList, mcpRegistry.list.pipe(Effect.map((servers) => ({ servers }))), { "rpc.aggregate": "mcp" }),
41175
+ [WS_METHODS.mcpSave]: ({ registration }) => observeRpcEffect$1(WS_METHODS.mcpSave, mcpRegistry.save(registration).pipe(Effect.andThen(mcpRegistry.list.pipe(Effect.map((servers) => ({ servers }))))), { "rpc.aggregate": "mcp" }),
41176
+ [WS_METHODS.mcpRemove]: ({ name }) => observeRpcEffect$1(WS_METHODS.mcpRemove, mcpRegistry.remove(name).pipe(Effect.andThen(mcpRegistry.list.pipe(Effect.map((servers) => ({ servers }))))), { "rpc.aggregate": "mcp" }),
41177
+ [WS_METHODS.mcpSetSecret]: ({ name, slot, value }) => observeRpcEffect$1(WS_METHODS.mcpSetSecret, mcpRegistry.setSecret({
41178
+ name,
41179
+ slot,
41180
+ value
41181
+ }).pipe(Effect.andThen(mcpRegistry.list.pipe(Effect.map((servers) => ({ servers }))))), { "rpc.aggregate": "mcp" }),
40243
41182
  [WS_METHODS.serverGetSettings]: (_input) => observeRpcEffect$1(WS_METHODS.serverGetSettings, serverSettings.getSettings.pipe(Effect.map(redactServerSettingsForClient)), { "rpc.aggregate": "server" }),
40244
41183
  [WS_METHODS.serverUpdateSettings]: ({ patch }) => observeRpcEffect$1(WS_METHODS.serverUpdateSettings, serverSettings.updateSettings(patch).pipe(Effect.map(redactServerSettingsForClient)), { "rpc.aggregate": "server" }),
40245
41184
  [WS_METHODS.serverDiscoverSourceControl]: (_input) => observeRpcEffect$1(WS_METHODS.serverDiscoverSourceControl, sourceControlDiscovery.discover, { "rpc.aggregate": "server" }),
@@ -42440,7 +43379,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
42440
43379
  };
42441
43380
  });
42442
43381
  var OpenCodeRuntime = class extends Context.Service()("@p4code/cli/provider/opencodeRuntime") {};
42443
- const OpenCodeRuntimeLive = Layer.effect(OpenCodeRuntime, makeOpenCodeRuntime).pipe(Layer.provide(layer$66));
43382
+ const OpenCodeRuntimeLive = Layer.effect(OpenCodeRuntime, makeOpenCodeRuntime).pipe(Layer.provide(layer$67));
42444
43383
  //#endregion
42445
43384
  //#region src/textGeneration/TextGenerationUtils.ts
42446
43385
  const isTextGenerationError$2 = Schema$1.is(TextGenerationError);
@@ -45861,6 +46800,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
45861
46800
  ...ultracode ? { ultracode: true } : {}
45862
46801
  };
45863
46802
  const mcpSession = readMcpProviderSession(input.threadId);
46803
+ const externalMcpServers = options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers;
45864
46804
  const queryOptions = {
45865
46805
  ...input.cwd ? { cwd: input.cwd } : {},
45866
46806
  ...apiModelId ? { model: apiModelId } : {},
@@ -45881,11 +46821,14 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
45881
46821
  env: claudeEnvironment,
45882
46822
  ...input.cwd ? { additionalDirectories: [input.cwd] } : {},
45883
46823
  ...Object.keys(extraArgs).length > 0 ? { extraArgs } : {},
45884
- ...mcpSession ? { mcpServers: { "p4-code": {
45885
- type: "http",
45886
- url: mcpSession.endpoint,
45887
- headers: { Authorization: mcpSession.authorizationHeader }
45888
- } } } : {}
46824
+ ...mcpSession || Object.keys(externalMcpServers).length > 0 ? { mcpServers: {
46825
+ ...externalMcpServers,
46826
+ ...mcpSession ? { "p4-code": {
46827
+ type: "http",
46828
+ url: mcpSession.endpoint,
46829
+ headers: { Authorization: mcpSession.authorizationHeader }
46830
+ } } : {}
46831
+ } } : {}
45889
46832
  };
45890
46833
  yield* Effect.annotateCurrentSpan({
45891
46834
  "provider.kind": PROVIDER$5,
@@ -46345,6 +47288,7 @@ const ClaudeDriver = {
46345
47288
  const adapter = yield* makeClaudeAdapter(effectiveConfig, {
46346
47289
  instanceId,
46347
47290
  environment: processEnv,
47291
+ resolveMcpServers: (yield* McpRegistry).resolveForSession,
46348
47292
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}
46349
47293
  });
46350
47294
  const textGeneration = yield* makeClaudeTextGeneration(effectiveConfig, processEnv);
@@ -81518,16 +82462,16 @@ const SourceControlRepositoryServiceLayerLive = layer$4.pipe(Layer.provideMerge(
81518
82462
  const ReviewLayerLive = layer$9.pipe(Layer.provideMerge(layer$37), Layer.provideMerge(VcsDriverRegistryLayerLive));
81519
82463
  const VcsLayerLive = Layer.empty.pipe(Layer.provideMerge(layer$36), Layer.provideMerge(VcsDriverRegistryLayerLive), Layer.provideMerge(layer$10.pipe(Layer.provide(VcsDriverRegistryLayerLive))), Layer.provideMerge(GitWorkflowLayerLive), Layer.provideMerge(ReviewLayerLive), Layer.provideMerge(SourceControlRepositoryServiceLayerLive), Layer.provideMerge(layer$11.pipe(Layer.provide(GitWorkflowLayerLive))));
81520
82464
  const CheckpointingLayerLive = Layer.empty.pipe(Layer.provideMerge(layer$33), Layer.provideMerge(layer$34.pipe(Layer.provide(VcsDriverRegistryLayerLive))));
81521
- const PortScannerLayerLive = layer$31.pipe(Layer.provide(layer$57));
82465
+ const PortScannerLayerLive = layer$31.pipe(Layer.provide(layer$52));
81522
82466
  const TerminalLayerLive = layer$30.pipe(Layer.provide(PtyAdapterLive), Layer.provide(PortScannerLayerLive));
81523
82467
  const PreviewLayerLive = Layer.empty.pipe(Layer.provideMerge(layer$28), Layer.provideMerge(PortScannerLayerLive));
81524
82468
  const WorkspaceEntriesLayerLive = layer$26.pipe(Layer.provide(layer$42));
81525
82469
  const WorkspaceFileSystemLayerLive = layer$25.pipe(Layer.provide(layer$42), Layer.provide(WorkspaceEntriesLayerLive));
81526
82470
  const WorkspaceLayerLive = Layer.mergeAll(layer$42, WorkspaceEntriesLayerLive, WorkspaceFileSystemLayerLive);
81527
82471
  const ProjectFaviconResolverLayerLive = layer$40.pipe(Layer.provide(layer$42), Layer.provide(layer$41));
81528
- const AuthLayerLive = layer$58.pipe(Layer.provideMerge(PersistenceLayerLive), Layer.provide(layer$62));
82472
+ const AuthLayerLive = layer$59.pipe(Layer.provideMerge(PersistenceLayerLive), Layer.provide(layer$63));
81529
82473
  const ProviderRuntimeLayerLive = ProviderSessionReaperLive.pipe(Layer.provideMerge(ProviderLayerLive), Layer.provideMerge(OrchestrationLayerLive));
81530
- const RuntimeDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(CheckpointingLayerLive), Layer.provideMerge(SourceControlProviderRegistryLayerLive), Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)), Layer.provideMerge(PersistenceLayerLive), Layer.provideMerge(layer$50), Layer.provideMerge(ProviderRegistryLive), Layer.provideMerge(ProviderInstanceRegistryHydrationLive), Layer.provideMerge(ProviderEventLoggersLive), Layer.provideMerge(OpenCodeRuntimeLive), Layer.provideMerge(layer$55.pipe(Layer.provide(layer$62))), Layer.provideMerge(WorkspaceLayerLive), Layer.provideMerge(ProjectFaviconResolverLayerLive), Layer.provideMerge(layer$51), Layer.provideMerge(layer$44), Layer.provideMerge(AuthLayerLive), Layer.provideMerge(layer$62)).pipe(Layer.provideMerge(layer$8), Layer.provideMerge(layer$7), Layer.provideMerge(layer$6), Layer.provideMerge(layer$47), Layer.provideMerge(layer$49), Layer.provideMerge(layer$48), Layer.provide(layer$66));
82474
+ const RuntimeDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(CheckpointingLayerLive), Layer.provideMerge(SourceControlProviderRegistryLayerLive), Layer.provideMerge(GitLayerLive), Layer.provideMerge(VcsLayerLive), Layer.provideMerge(ProviderRuntimeLayerLive), Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)), Layer.provideMerge(PersistenceLayerLive), Layer.provideMerge(layer$50), Layer.provideMerge(ProviderRegistryLive), Layer.provideMerge(ProviderInstanceRegistryHydrationLive), Layer.provideMerge(ProviderEventLoggersLive), Layer.provideMerge(OpenCodeRuntimeLive), Layer.provideMerge(layer$57.pipe(Layer.provide(layer$63))), Layer.provideMerge(WorkspaceLayerLive), Layer.provideMerge(ProjectFaviconResolverLayerLive), Layer.provideMerge(layer$51), Layer.provideMerge(layer$44), Layer.provideMerge(AuthLayerLive), Layer.provideMerge(layer$56), Layer.provideMerge(layer$63)).pipe(Layer.provideMerge(layer$8), Layer.provideMerge(layer$7), Layer.provideMerge(layer$6), Layer.provideMerge(layer$47), Layer.provideMerge(layer$49), Layer.provideMerge(layer$48), Layer.provide(layer$67));
81531
82475
  /**
81532
82476
  * Hub asset sync.
81533
82477
  *
@@ -81535,8 +82479,8 @@ const RuntimeDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(Checkpo
81535
82479
  * not-configured shape, and every call reports that rather than failing — so
81536
82480
  * this costs a server with no hub nothing but a layer.
81537
82481
  */
81538
- const SkillSyncLive = layer$52.pipe(Layer.provideMerge(layer$53), Layer.provideMerge(layer$54));
81539
- const RuntimeServicesLive = layer$43.pipe(Layer.provideMerge(SkillSyncLive), Layer.provideMerge(RuntimeDependenciesLive));
82482
+ const AssetSyncLive = layer$53.pipe(Layer.provideMerge(layer$54), Layer.provideMerge(layer$55));
82483
+ const RuntimeServicesLive = layer$43.pipe(Layer.provideMerge(AssetSyncLive), Layer.provideMerge(RuntimeDependenciesLive));
81540
82484
  const makeRoutesLayer = Layer.mergeAll(Layer.mergeAll(HttpApiBuilder.layer(EnvironmentHttpApi).pipe(Layer.provide(authHttpApiLayer), Layer.provide(orchestrationHttpApiLayer), Layer.provide(serverEnvironmentHttpApiLayer), Layer.provide(environmentAuthenticatedAuthLayer)), otlpTracesProxyRouteLayer, assetRouteLayer, staticAndDevRouteLayer, websocketRpcRouteLayer), layer.pipe(Layer.provide(layer$2))).pipe(Layer.provide(TaskRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(ProjectionProjectRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(layer$29), Layer.provide(layer$45), Layer.provide(browserApiCorsLayer), Layer.provide(httpCompressionLayer));
81541
82485
  const makeServerLayer = Layer.unwrap(Effect.gen(function* () {
81542
82486
  const config = yield* ServerConfig$1;
@@ -81601,7 +82545,7 @@ const bootServiceLayer = (config) => layer$46({
81601
82545
  baseDir: config.baseDir,
81602
82546
  logsDir: config.logsDir,
81603
82547
  cliVersion: version
81604
- }).pipe(Layer.provide(layer$57));
82548
+ }).pipe(Layer.provide(layer$52));
81605
82549
  /** Install, update, or repair the service using the CLI version running this command. */
81606
82550
  const reconcileService = Effect.fn("cli.service.reconcile")(function* () {
81607
82551
  const service = yield* BootService;
@@ -81633,37 +82577,26 @@ function formatReconcileSuccess(input) {
81633
82577
  }
81634
82578
  function formatServiceStatus(status, cliVersion) {
81635
82579
  if (!status.supported) return "P4Code service\n Status: unavailable on this machine\n Supported on: Linux with systemd, macOS with launchd";
81636
- if (!status.installed) return "P4Code service\n Status: not installed\n Next: Run `p4 service install`.";
82580
+ if (!status.installed) return "P4Code service\n Status: not installed\n Next: Run `p4c service install`.";
81637
82581
  return [
81638
82582
  "P4Code service",
81639
82583
  ` Status: ${status.current ? `installed · @p4code/cli@${cliVersion}` : "needs an update or repair"}`,
81640
82584
  ` Unit: ${status.unitPath}`,
81641
82585
  ` Data: ${status.baseDir}`,
81642
82586
  ` Logs: ${status.logPath}`,
81643
- ...status.current ? [] : [" Next: Run `p4 service update`."]
82587
+ ...status.current ? [] : [" Next: Run `p4c service update`."]
81644
82588
  ].join("\n");
81645
82589
  }
81646
82590
  const runServiceCommand = Effect.fn("cli.service.run")(function* (flags, run) {
81647
82591
  const config = yield* resolveCliAuthConfig(flags, yield* GlobalFlag.LogLevel);
81648
82592
  const baseDirNotice = yield* describeNonDefaultBaseDir(config.baseDir);
81649
82593
  if (baseDirNotice !== null) yield* Console.error(baseDirNotice);
81650
- return yield* run.pipe(Effect.tapError((error) => isBootServiceCommandError(error) ? BootService.pipe(Effect.flatMap((service) => Console.error(`Details: ${service.logPath}`))) : Effect.void), Effect.provide(Layer.mergeAll(bootServiceLayer(config), layer$57)));
81651
- });
81652
- /**
81653
- * Runs on every `service install` and `service update`, including the
81654
- * already-current paths. Gating it on "the unit changed" meant a host whose
81655
- * service was already installed — the common case after the first setup — never
81656
- * got completions at all.
81657
- */
81658
- const reportZshCompletionInstall = Effect.gen(function* () {
81659
- const completion = yield* installZshCompletion().pipe(Effect.orElseSucceed(() => null));
81660
- if (completion !== null) yield* Console.log(formatZshCompletionInstall(completion));
82594
+ return yield* run.pipe(Effect.tapError((error) => isBootServiceCommandError(error) ? BootService.pipe(Effect.flatMap((service) => Console.error(`Details: ${service.logPath}`))) : Effect.void), Effect.provide(Layer.mergeAll(bootServiceLayer(config), layer$52)));
81661
82595
  });
81662
82596
  const serviceInstallCommand = Command.make("install", projectLocationFlags).pipe(Command.withDescription("Install P4Code as a background service for this user."), Command.withHandler((flags) => runServiceCommand(flags, Effect.gen(function* () {
81663
82597
  const result = yield* reconcileService();
81664
82598
  if (!result.changed) {
81665
82599
  yield* Console.log(`P4Code service is already installed with @p4code/cli@${version}.`);
81666
- yield* reportZshCompletionInstall;
81667
82600
  return;
81668
82601
  }
81669
82602
  yield* Console.log(formatReconcileSuccess({
@@ -81672,14 +82605,12 @@ const serviceInstallCommand = Command.make("install", projectLocationFlags).pipe
81672
82605
  cliVersion: version,
81673
82606
  platform: yield* HostProcessPlatform
81674
82607
  }));
81675
- yield* reportZshCompletionInstall;
81676
82608
  }))));
81677
82609
  const serviceUpdateCommand = Command.make("update", projectLocationFlags).pipe(Command.withDescription("Update or repair the background service so it runs this CLI build, then restart it."), Command.withHandler((flags) => runServiceCommand(flags, Effect.gen(function* () {
81678
82610
  const result = yield* reconcileService();
81679
82611
  if (!result.changed) {
81680
82612
  yield* (yield* BootService).restart;
81681
82613
  yield* Console.log(`Restarted the P4Code service on @p4code/cli@${version}.`);
81682
- yield* reportZshCompletionInstall;
81683
82614
  return;
81684
82615
  }
81685
82616
  yield* Console.log(formatReconcileSuccess({
@@ -81688,7 +82619,6 @@ const serviceUpdateCommand = Command.make("update", projectLocationFlags).pipe(C
81688
82619
  cliVersion: version,
81689
82620
  platform: yield* HostProcessPlatform
81690
82621
  }));
81691
- yield* reportZshCompletionInstall;
81692
82622
  }))));
81693
82623
  const serviceUninstallCommand = Command.make("uninstall", projectLocationFlags).pipe(Command.withDescription("Stop and remove the P4Code background service."), Command.withHandler((flags) => runServiceCommand(flags, Effect.gen(function* () {
81694
82624
  const removed = yield* (yield* BootService).uninstall;
@@ -81721,7 +82651,7 @@ const serviceCommand = Command.make("service").pipe(Command.withDescription("Man
81721
82651
  ]));
81722
82652
  //#endregion
81723
82653
  //#region src/bin.ts
81724
- const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, layer$66, FetchHttpClient.layer);
82654
+ const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, layer$67, FetchHttpClient.layer);
81725
82655
  /**
81726
82656
  * Prints an update reminder before dispatching, at most once a day.
81727
82657
  *
@@ -81737,15 +82667,24 @@ const reportUpdateAvailable = Effect.gen(function* () {
81737
82667
  stateDir: paths.stateDir
81738
82668
  });
81739
82669
  if (reminder !== null) yield* Console.error(reminder);
81740
- const completionsHint = yield* resolveMissingCompletionsHint({ stateDir: paths.stateDir });
81741
- if (completionsHint !== null) yield* Console.error(completionsHint);
81742
82670
  }).pipe(Effect.ignore);
81743
- const makeCli = () => Command.make("p4", { ...sharedServerCommandFlags }).pipe(Command.withDescription("Run the P4Code server."), Command.withHandler((flags) => runServerCommand(flags)), Command.withSubcommands([
82671
+ /**
82672
+ * `p4c`, not `p4`.
82673
+ *
82674
+ * Perforce owns `p4`, and zsh ships `_perforce` claiming it with
82675
+ * `#compdef p4 p4d`. Fighting for the name cost a completion installer, a
82676
+ * per-run "your completions are missing" hint, and an argument about which
82677
+ * fpath directory `compinit` would prefer — all of it maintenance for a
82678
+ * conflict we chose. Taking a name nothing else claims deletes the problem
82679
+ * rather than managing it.
82680
+ */
82681
+ const makeCli = () => Command.make("p4c", { ...sharedServerCommandFlags }).pipe(Command.withDescription("Run the P4Code server."), Command.withHandler((flags) => runServerCommand(flags)), Command.withSubcommands([
81744
82682
  startCommand,
81745
82683
  serveCommand,
81746
82684
  authCommand,
81747
- completionsCommand,
81748
82685
  hubCommand,
82686
+ mcpCommand,
82687
+ memoryCommand,
81749
82688
  projectCommand,
81750
82689
  serviceCommand,
81751
82690
  skillsCommand