@p4code/cli 0.0.41 → 0.0.43

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
@@ -7,6 +7,7 @@ import * as Console from "effect/Console";
7
7
  import * as Effect from "effect/Effect";
8
8
  import * as Layer from "effect/Layer";
9
9
  import { Argument, Command, Flag, GlobalFlag, Prompt } from "effect/unstable/cli";
10
+ import * as CliError from "effect/unstable/cli/CliError";
10
11
  import { FetchHttpClient, Headers, HttpBody, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse, HttpRouter, HttpServer, HttpServerRequest, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http";
11
12
  import * as Context from "effect/Context";
12
13
  import * as NodeOS from "node:os";
@@ -54,13 +55,11 @@ import * as Exit from "effect/Exit";
54
55
  import * as SchemaGetter from "effect/SchemaGetter";
55
56
  import * as Config from "effect/Config";
56
57
  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
58
  import * as Cache from "effect/Cache";
61
59
  import * as Deferred from "effect/Deferred";
62
60
  import * as Equal from "effect/Equal";
63
61
  import * as Semaphore from "effect/Semaphore";
62
+ import * as Scope from "effect/Scope";
64
63
  import { parse } from "yaml";
65
64
  import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
66
65
  import * as Clock from "effect/Clock";
@@ -69,6 +68,8 @@ import * as Queue from "effect/Queue";
69
68
  import { cast, dual } from "effect/Function";
70
69
  import Mime from "@effect/platform-node/Mime";
71
70
  import * as Arr from "effect/Array";
71
+ import * as ChildProcess$1 from "effect/unstable/process/ChildProcess";
72
+ import * as ChildProcessSpawner$1 from "effect/unstable/process/ChildProcessSpawner";
72
73
  import * as Schedule from "effect/Schedule";
73
74
  import * as HttpClient$1 from "effect/unstable/http/HttpClient";
74
75
  import * as HttpClientRequest$1 from "effect/unstable/http/HttpClientRequest";
@@ -120,7 +121,7 @@ const closeServer = (server) => {
120
121
  * NetService - Service tag for startup networking helpers.
121
122
  */
122
123
  var NetService = class extends Context.Service()("@p4code/shared/Net/NetService") {};
123
- const make$70 = () => {
124
+ const make$71 = () => {
124
125
  /**
125
126
  * Returns true when a TCP server can bind to {host, port}.
126
127
  * `EADDRNOTAVAIL` is treated as available so IPv6-absent hosts don't fail
@@ -229,10 +230,10 @@ const make$70 = () => {
229
230
  })
230
231
  };
231
232
  };
232
- const layer$66 = Layer.sync(NetService, make$70);
233
+ const layer$67 = Layer.sync(NetService, make$71);
233
234
  //#endregion
234
235
  //#region package.json
235
- var version = "0.0.40";
236
+ var version = "0.0.43";
236
237
  //#endregion
237
238
  //#region src/config.ts
238
239
  /**
@@ -253,8 +254,8 @@ var ServerConfig$1 = class extends Context.Service()("@p4code/cli/config/ServerC
253
254
  /** @deprecated Import and use `layerTest` from this module. */
254
255
  static layerTest = (cwd, baseDirOrPrefix) => layerTest$2(cwd, baseDirOrPrefix);
255
256
  };
256
- const make$69 = (config) => ServerConfig$1.of(config);
257
- const layer$65 = (config) => Layer.succeed(ServerConfig$1, make$69(config));
257
+ const make$70 = (config) => ServerConfig$1.of(config);
258
+ const layer$66 = (config) => Layer.succeed(ServerConfig$1, make$70(config));
258
259
  const deriveServerPaths = Effect.fn(function* (baseDir, devUrl, options = {}) {
259
260
  const { join } = yield* Path.Path;
260
261
  const stateDir = join(baseDir, devUrl !== void 0 && !options.baseDirIsExplicit ? "dev" : "userdata");
@@ -922,7 +923,7 @@ const resolveLatestVersion = Effect.fn("cli.updateCheck.resolve")(function* (sta
922
923
  * `service update` restarts it onto the new one.
923
924
  */
924
925
  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");
926
+ return [`Update available: ${PACKAGE_NAME}@${input.latestVersion} (running ${input.currentVersion}).`, ` npm i -g ${PACKAGE_NAME}@latest && p4c service update`].join("\n");
926
927
  }
927
928
  /**
928
929
  * The reminder line, or null when there is nothing to say. Callers print it;
@@ -6177,7 +6178,7 @@ var ServerProviderUpdateError = class extends Schema$1.TaggedErrorClass()("Serve
6177
6178
  }
6178
6179
  };
6179
6180
  const ServerSelfUpdateInput = Schema$1.Struct({
6180
- /** Exact npm version of the `p4` package to install (never a dist-tag, so
6181
+ /** Exact npm version of the `p4c` package to install (never a dist-tag, so
6181
6182
  the server and the acknowledging client agree on what was requested). */
6182
6183
  targetVersion: TrimmedNonEmptyString });
6183
6184
  /** Acknowledgement that the update artifact is installed and the server is
@@ -7312,10 +7313,26 @@ const AGENT_ASSET_TOTAL_MAX_BYTES = 4 * 1024 * 1024;
7312
7313
  * What kind of thing this is.
7313
7314
  *
7314
7315
  * 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.
7316
+ * asset lands on disk, and an unknown kind has no answer to that. Each kind
7317
+ * needs a materializer, which is what stops this from being a place to add a
7318
+ * string.
7319
+ *
7320
+ * - `skill` — a directory under `<configDir>/skills`, named by the asset.
7321
+ * - `memory` — a single file directly in `<configDir>`, named by the asset.
7322
+ * The name is the filename, and only the allowlisted ones exist: that
7323
+ * directory also holds `settings.json` and `.credentials.json`, so "write the
7324
+ * file this asset names" has to be bounded by something narrower than the
7325
+ * path schema.
7326
+ * - `mcp` — an external MCP server registration, in p4code's own state
7327
+ * directory rather than the provider's. The definition travels; the bearer
7328
+ * tokens and API keys it references do not, and stay in each machine's own
7329
+ * secret store.
7317
7330
  */
7318
- const AgentAssetKind = Schema$1.Literals(["skill"]);
7331
+ const AgentAssetKind = Schema$1.Literals([
7332
+ "skill",
7333
+ "memory",
7334
+ "mcp"
7335
+ ]);
7319
7336
  /**
7320
7337
  * An asset's name, which for a skill is also its directory name.
7321
7338
  *
@@ -7331,6 +7348,18 @@ const AgentAssetName = TrimmedNonEmptyString.check(Schema$1.isMaxLength(AGENT_AS
7331
7348
  description: "Letters, digits, dot, dash and underscore; must not start with a separator."
7332
7349
  }));
7333
7350
  /**
7351
+ * Which asset, across every kind.
7352
+ *
7353
+ * Kind and name together, because name alone stopped being unique the moment a
7354
+ * second kind existed: `AgentAssetName` permits dots, so a skill directory may
7355
+ * legitimately be called `CLAUDE.md`. A report saying only "published
7356
+ * CLAUDE.md" would be ambiguous about what had just been sent where.
7357
+ */
7358
+ const AgentAssetRef = Schema$1.Struct({
7359
+ kind: AgentAssetKind,
7360
+ name: AgentAssetName
7361
+ });
7362
+ /**
7334
7363
  * A path inside the asset, relative to the asset's own directory.
7335
7364
  *
7336
7365
  * Same reasoning as the name and the same threat, in a more tempting form:
@@ -7428,56 +7457,77 @@ const HubLinkSource = Schema$1.Literals([
7428
7457
  "none"
7429
7458
  ]);
7430
7459
  /**
7431
- * What a skill's three views add up to.
7460
+ * What an asset's three views add up to.
7432
7461
  *
7433
7462
  * `pending` means the next sync will act; `local-edit` means it will publish
7434
7463
  * this machine's copy; `conflict` means it will do nothing and wants a person.
7435
7464
  * `unmanaged` is the important one — p4code did not write it, so it is neither
7436
7465
  * uploaded nor written over.
7466
+ *
7467
+ * `hub-only` is the one that has no local half: the hub holds it and this
7468
+ * machine does not materialize it. That is normal for a memory file belonging
7469
+ * to a provider this machine does not run, and it is the state that makes
7470
+ * hub-side editing worth having — otherwise the only way to reach such an asset
7471
+ * would be to install the provider first.
7437
7472
  */
7438
- const SkillSyncEntryState = Schema$1.Literals([
7473
+ const AgentSyncEntryState = Schema$1.Literals([
7439
7474
  "synced",
7440
7475
  "pending",
7441
7476
  "local-edit",
7442
7477
  "conflict",
7443
- "unmanaged"
7478
+ "unmanaged",
7479
+ "hub-only"
7444
7480
  ]);
7445
- const SkillSyncEntry = Schema$1.Struct({
7481
+ const AgentSyncEntry = Schema$1.Struct({
7482
+ kind: AgentAssetKind,
7446
7483
  name: Schema$1.String,
7447
- state: SkillSyncEntryState,
7484
+ state: AgentSyncEntryState,
7448
7485
  version: Schema$1.NullOr(Schema$1.Int),
7449
7486
  /** Conflict reason or pending action, for a caller that wants to explain. */
7450
7487
  detail: Schema$1.NullOr(Schema$1.String)
7451
7488
  });
7452
- const SkillSyncRefusal = Schema$1.Struct({
7489
+ const AgentSyncRefusal = Schema$1.Struct({
7490
+ kind: AgentAssetKind,
7453
7491
  name: Schema$1.String,
7454
7492
  reason: Schema$1.String
7455
7493
  });
7456
- const SkillSyncConflictReport = Schema$1.Struct({
7494
+ const AgentSyncConflictReport = Schema$1.Struct({
7495
+ kind: AgentAssetKind,
7457
7496
  name: Schema$1.String,
7458
7497
  reason: Schema$1.String
7459
7498
  });
7460
- const SkillSyncStatus = Schema$1.Struct({
7499
+ /**
7500
+ * Where one kind materializes on this machine.
7501
+ *
7502
+ * A list rather than a field per kind, so adding the third kind is data rather
7503
+ * than another optional string every reader has to learn about. Null when the
7504
+ * root cannot be resolved — a provider that is not configured here.
7505
+ */
7506
+ const AgentSyncRoot = Schema$1.Struct({
7507
+ kind: AgentAssetKind,
7508
+ path: Schema$1.NullOr(Schema$1.String)
7509
+ });
7510
+ const AgentSyncStatus = Schema$1.Struct({
7461
7511
  configured: Schema$1.Boolean,
7462
7512
  hubUrl: Schema$1.NullOr(Schema$1.String),
7463
7513
  syncMode: HubSyncMode,
7464
7514
  shareMode: HubShareMode,
7465
7515
  source: HubLinkSource,
7466
- skillsRoot: Schema$1.NullOr(Schema$1.String),
7467
- entries: Schema$1.Array(SkillSyncEntry),
7468
- refused: Schema$1.Array(SkillSyncRefusal),
7516
+ roots: Schema$1.Array(AgentSyncRoot),
7517
+ entries: Schema$1.Array(AgentSyncEntry),
7518
+ refused: Schema$1.Array(AgentSyncRefusal),
7469
7519
  /** Set when the hub could not be reached. Local state is still reported. */
7470
7520
  unavailable: Schema$1.NullOr(Schema$1.String)
7471
7521
  });
7472
- const SkillSyncReport = Schema$1.Struct({
7522
+ const AgentSyncReport = Schema$1.Struct({
7473
7523
  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),
7524
+ roots: Schema$1.Array(AgentSyncRoot),
7525
+ materialized: Schema$1.Array(AgentAssetRef),
7526
+ published: Schema$1.Array(AgentAssetRef),
7527
+ removed: Schema$1.Array(AgentAssetRef),
7528
+ conflicts: Schema$1.Array(AgentSyncConflictReport),
7529
+ unmanaged: Schema$1.Array(AgentAssetRef),
7530
+ refused: Schema$1.Array(AgentSyncRefusal),
7481
7531
  unavailable: Schema$1.NullOr(Schema$1.String)
7482
7532
  });
7483
7533
  const HubConnectInput = Schema$1.Struct({
@@ -7497,7 +7547,57 @@ const HubMintedToken = Schema$1.Struct({
7497
7547
  token: Schema$1.String,
7498
7548
  digest: Schema$1.String
7499
7549
  });
7500
- const SkillNameInput = Schema$1.Struct({ name: AgentAssetName });
7550
+ const AgentAssetRefInput = AgentAssetRef;
7551
+ /**
7552
+ * One asset as the hub holds it, for editing.
7553
+ *
7554
+ * Answered as `found: false` rather than as an error, because "the hub does not
7555
+ * have this" is a normal answer to a panel that is showing a row another
7556
+ * machine may have deleted a moment ago. `version` is carried back out so the
7557
+ * save that follows can be a compare-and-set rather than a blind overwrite.
7558
+ */
7559
+ const AgentAssetContent = Schema$1.Struct({
7560
+ found: Schema$1.Boolean,
7561
+ kind: AgentAssetKind,
7562
+ name: AgentAssetName,
7563
+ files: Schema$1.Array(AgentAssetFile),
7564
+ version: Schema$1.NullOr(Schema$1.Int),
7565
+ updatedAt: Schema$1.NullOr(IsoDateTime),
7566
+ /** Set when the hub could not be reached, so a caller can say why it is blank. */
7567
+ unavailable: Schema$1.NullOr(Schema$1.String)
7568
+ });
7569
+ /**
7570
+ * Edit an asset in the hub, rather than on disk and upward.
7571
+ *
7572
+ * The hub is the shared copy, so this is the edit that reaches every machine
7573
+ * including ones that are asleep — and the only way to reach an asset this
7574
+ * machine does not materialize. `expectedVersion` is required, not optional:
7575
+ * an unconditional overwrite of shared state is exactly the operation that
7576
+ * loses another machine's edit without telling anyone.
7577
+ */
7578
+ const AgentAssetSaveInput = Schema$1.Struct({
7579
+ kind: AgentAssetKind,
7580
+ name: AgentAssetName,
7581
+ files: AgentAssetFiles,
7582
+ expectedVersion: PositiveInt
7583
+ });
7584
+ /**
7585
+ * Delete an asset from the hub and so from every machine that syncs it.
7586
+ *
7587
+ * Distinct from unpublishing, which only ends *this* machine's share and keeps
7588
+ * the local copy. This one is the destructive one, and it does not require the
7589
+ * asset to be managed here — that restriction is what made a stale row from a
7590
+ * decommissioned machine impossible to clear.
7591
+ *
7592
+ * `expectedVersion` is optional here alone: someone deleting a row they can see
7593
+ * in a list has usually never held a version for it, and refusing on that basis
7594
+ * would make the operation unreachable in exactly the case it exists for.
7595
+ */
7596
+ const AgentAssetDeleteInput = Schema$1.Struct({
7597
+ kind: AgentAssetKind,
7598
+ name: AgentAssetName,
7599
+ expectedVersion: Schema$1.optional(PositiveInt)
7600
+ });
7501
7601
  /**
7502
7602
  * Changing the hub link failed.
7503
7603
  *
@@ -7522,6 +7622,143 @@ const encoder$3 = new TextEncoder();
7522
7622
  */
7523
7623
  const agentAssetTotalBytes = (files) => files.reduce((total, file) => total + encoder$3.encode(file.content).length + encoder$3.encode(file.path).length, 0);
7524
7624
  //#endregion
7625
+ //#region ../../packages/contracts/src/mcpServer.ts
7626
+ /**
7627
+ * External MCP server registrations.
7628
+ *
7629
+ * The servers a person adds themselves — a ticket tracker, a search index, a
7630
+ * build system — as opposed to p4code's own toolkit, which the adapters inject
7631
+ * on every session and which is not registered anywhere.
7632
+ *
7633
+ * **These are p4code's own data, not the provider's.** The obvious place to put
7634
+ * them is `~/.claude.json` under `mcpServers`, which is where the Claude CLI
7635
+ * keeps its own. p4code does not write that file: it also holds `oauthAccount`,
7636
+ * `machineID` and a `projects` map of per-path trust decisions, so owning one
7637
+ * key inside it means round-tripping all of that. The SDK takes `mcpServers` as
7638
+ * a session option and the adapter already passes it, so a registration p4code
7639
+ * stores itself reaches the agent by the same door its own toolkit uses —
7640
+ * without touching the provider's file at all.
7641
+ *
7642
+ * **Secret values are not in here, and that is the point.** A registration
7643
+ * routinely needs a bearer token in a header or an API key in the environment.
7644
+ * Those are named here and stored per machine in `ServerSecretStore`; the
7645
+ * registration itself carries only the name of the slot. That is what lets the
7646
+ * whole registration sync through the hub — which stores asset files verbatim —
7647
+ * without a working credential ever leaving the machine that holds it.
7648
+ *
7649
+ * @module contracts/mcpServer
7650
+ */
7651
+ /**
7652
+ * A server's name, reused from asset names rather than defined again.
7653
+ *
7654
+ * It is the key in the SDK's `mcpServers` record *and* a path segment in
7655
+ * p4code's own store, so it needs exactly the constraint an asset name already
7656
+ * has. Two definitions of the same rule is how one of them ends up laxer.
7657
+ */
7658
+ const McpServerName = AgentAssetName;
7659
+ /**
7660
+ * Where a secret goes when it is resolved.
7661
+ *
7662
+ * The slot, not the value. `header` fills one entry of an http server's
7663
+ * `headers`; `env` fills one entry of a stdio server's environment.
7664
+ */
7665
+ const McpSecretTarget = Schema$1.Literals(["header", "env"]);
7666
+ /**
7667
+ * One secret this server needs, named but not carried.
7668
+ *
7669
+ * There is no `secretName` field: the storage name is derived from the server,
7670
+ * the target and the key. A caller-supplied name would be one more thing to get
7671
+ * wrong, and two servers could name the same slot differently or — worse — the
7672
+ * same name for different slots.
7673
+ */
7674
+ const McpSecretSlot = Schema$1.Struct({
7675
+ target: McpSecretTarget,
7676
+ key: TrimmedNonEmptyString
7677
+ });
7678
+ /**
7679
+ * The storage name for one slot's value.
7680
+ *
7681
+ * Derived in the contract so both ends agree without either importing the
7682
+ * other's helper: the server resolving a session and the panel writing a value
7683
+ * have to compute the same string or the value is written where nothing looks.
7684
+ */
7685
+ const mcpSecretStorageName = (input) => `mcp.${input.server}.${input.target}.${input.key}`;
7686
+ const commonFields = {
7687
+ name: McpServerName,
7688
+ /**
7689
+ * Off means registered and not injected.
7690
+ *
7691
+ * Kept rather than deleted because turning a server off to test something is
7692
+ * the common case, and re-entering a command line and four headers to turn it
7693
+ * back on is how people stop turning it off.
7694
+ */
7695
+ enabled: Schema$1.Boolean,
7696
+ secrets: Schema$1.optional(Schema$1.Array(McpSecretSlot))
7697
+ };
7698
+ /**
7699
+ * A server p4code launches as a child process.
7700
+ *
7701
+ * `env` holds only values that are not secret. Anything that is goes in
7702
+ * `secrets` and is resolved from this machine's own store at session start.
7703
+ */
7704
+ const McpStdioServer = Schema$1.Struct({
7705
+ ...commonFields,
7706
+ transport: Schema$1.Literal("stdio"),
7707
+ command: TrimmedNonEmptyString,
7708
+ args: Schema$1.optional(Schema$1.Array(Schema$1.String)),
7709
+ env: Schema$1.optional(Schema$1.Record(Schema$1.String, Schema$1.String))
7710
+ });
7711
+ /** A server reached over HTTP. `headers` holds only non-secret values. */
7712
+ const McpRemoteServer = Schema$1.Struct({
7713
+ ...commonFields,
7714
+ transport: Schema$1.Literals(["http", "sse"]),
7715
+ url: TrimmedNonEmptyString,
7716
+ headers: Schema$1.optional(Schema$1.Record(Schema$1.String, Schema$1.String))
7717
+ });
7718
+ /**
7719
+ * A registration, as a union rather than one struct of optional fields.
7720
+ *
7721
+ * A stdio server with a `url` and no `command` is not a thing, and a struct
7722
+ * with both optional would let one be stored and then fail at session start —
7723
+ * on a machine that may not be the one where it was typed.
7724
+ */
7725
+ const McpServerRegistration = Schema$1.Union([McpStdioServer, McpRemoteServer]);
7726
+ /**
7727
+ * What a panel needs to show, including what this machine cannot run.
7728
+ *
7729
+ * `missingSecrets` is the field that earns this type. A registration syncs
7730
+ * everywhere; its secrets do not. So a server can be perfectly valid and still
7731
+ * unusable *here*, and saying which slot is empty is the difference between a
7732
+ * fixable state and a mystery.
7733
+ */
7734
+ const McpServerStatus = Schema$1.Struct({
7735
+ registration: McpServerRegistration,
7736
+ missingSecrets: Schema$1.Array(McpSecretSlot),
7737
+ /** True when this machine holds everything needed to start it. */
7738
+ ready: Schema$1.Boolean
7739
+ });
7740
+ const McpServerList = Schema$1.Struct({ servers: Schema$1.Array(McpServerStatus) });
7741
+ const McpServerSaveInput = Schema$1.Struct({ registration: McpServerRegistration });
7742
+ const McpServerNameInput = Schema$1.Struct({ name: McpServerName });
7743
+ /**
7744
+ * Setting one secret value, on this machine only.
7745
+ *
7746
+ * Separate from saving the registration because they go to different places and
7747
+ * only one of them travels. Bundling them would make the panel's save button
7748
+ * the thing that decides whether a credential syncs, which is not a decision to
7749
+ * leave to a button.
7750
+ */
7751
+ const McpServerSecretInput = Schema$1.Struct({
7752
+ name: McpServerName,
7753
+ slot: McpSecretSlot,
7754
+ value: Schema$1.String
7755
+ });
7756
+ var McpRegistryError = class extends Schema$1.TaggedErrorClass()("McpRegistryError", { detail: Schema$1.String }) {
7757
+ get message() {
7758
+ return this.detail;
7759
+ }
7760
+ };
7761
+ //#endregion
7525
7762
  //#region ../../packages/contracts/src/filesystem.ts
7526
7763
  const FILESYSTEM_PATH_MAX_LENGTH = 512;
7527
7764
  const FilesystemBrowseInput = Schema$1.Struct({
@@ -7777,6 +8014,13 @@ const WS_METHODS = {
7777
8014
  skillsPublish: "skills.publish",
7778
8015
  skillsPublishAll: "skills.publishAll",
7779
8016
  skillsUnpublish: "skills.unpublish",
8017
+ assetsRead: "assets.read",
8018
+ assetsSave: "assets.save",
8019
+ assetsDelete: "assets.delete",
8020
+ mcpList: "mcp.list",
8021
+ mcpSave: "mcp.save",
8022
+ mcpRemove: "mcp.remove",
8023
+ mcpSetSecret: "mcp.setSecret",
7780
8024
  sourceControlLookupRepository: "sourceControl.lookupRepository",
7781
8025
  sourceControlCloneRepository: "sourceControl.cloneRepository",
7782
8026
  sourceControlPublishRepository: "sourceControl.publishRepository",
@@ -7846,36 +8090,36 @@ const WsTasksDeleteRpc = Rpc.make(WS_METHODS.tasksDelete, {
7846
8090
  const HubRpcError = Schema$1.Union([HubLinkError, EnvironmentAuthorizationError]);
7847
8091
  const WsHubGetSyncStatusRpc = Rpc.make(WS_METHODS.hubGetSyncStatus, {
7848
8092
  payload: Schema$1.Struct({}),
7849
- success: SkillSyncStatus,
8093
+ success: AgentSyncStatus,
7850
8094
  error: Schema$1.Union([EnvironmentAuthorizationError])
7851
8095
  });
7852
8096
  const WsHubConnectRpc = Rpc.make(WS_METHODS.hubConnect, {
7853
8097
  payload: HubConnectInput,
7854
- success: SkillSyncStatus,
8098
+ success: AgentSyncStatus,
7855
8099
  error: HubRpcError
7856
8100
  });
7857
8101
  const WsHubDisconnectRpc = Rpc.make(WS_METHODS.hubDisconnect, {
7858
8102
  payload: Schema$1.Struct({}),
7859
- success: SkillSyncStatus,
8103
+ success: AgentSyncStatus,
7860
8104
  error: HubRpcError
7861
8105
  });
7862
8106
  const WsHubSetSyncModeRpc = Rpc.make(WS_METHODS.hubSetSyncMode, {
7863
8107
  payload: HubSyncModeInput,
7864
- success: SkillSyncStatus,
8108
+ success: AgentSyncStatus,
7865
8109
  error: HubRpcError
7866
8110
  });
7867
8111
  /**
7868
8112
  * Mint a bearer token for a hub, without contacting one.
7869
8113
  *
7870
8114
  * 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
8115
+ * generation, the same thing `p4c hub token add` does. It exists as an RPC
7872
8116
  * rather than in the browser because a client reached over plain HTTP on a LAN
7873
8117
  * has no `crypto.subtle`, and a token generator that silently degrades is not
7874
8118
  * a thing to ship.
7875
8119
  */
7876
8120
  const WsHubSetShareModeRpc = Rpc.make(WS_METHODS.hubSetShareMode, {
7877
8121
  payload: HubShareModeInput,
7878
- success: SkillSyncStatus,
8122
+ success: AgentSyncStatus,
7879
8123
  error: HubRpcError
7880
8124
  });
7881
8125
  const WsHubMintTokenRpc = Rpc.make(WS_METHODS.hubMintToken, {
@@ -7886,36 +8130,99 @@ const WsHubMintTokenRpc = Rpc.make(WS_METHODS.hubMintToken, {
7886
8130
  const WsSkillsSyncRpc = Rpc.make(WS_METHODS.skillsSync, {
7887
8131
  payload: Schema$1.Struct({}),
7888
8132
  success: Schema$1.Struct({
7889
- report: SkillSyncReport,
7890
- status: SkillSyncStatus
8133
+ report: AgentSyncReport,
8134
+ status: AgentSyncStatus
7891
8135
  }),
7892
8136
  error: Schema$1.Union([EnvironmentAuthorizationError])
7893
8137
  });
7894
8138
  const WsSkillsPublishRpc = Rpc.make(WS_METHODS.skillsPublish, {
7895
- payload: SkillNameInput,
8139
+ payload: AgentAssetRefInput,
7896
8140
  success: Schema$1.Struct({
7897
- report: SkillSyncReport,
7898
- status: SkillSyncStatus
8141
+ report: AgentSyncReport,
8142
+ status: AgentSyncStatus
7899
8143
  }),
7900
8144
  error: Schema$1.Union([EnvironmentAuthorizationError])
7901
8145
  });
7902
8146
  const WsSkillsPublishAllRpc = Rpc.make(WS_METHODS.skillsPublishAll, {
7903
8147
  payload: Schema$1.Struct({}),
7904
8148
  success: Schema$1.Struct({
7905
- report: SkillSyncReport,
7906
- status: SkillSyncStatus
8149
+ report: AgentSyncReport,
8150
+ status: AgentSyncStatus
7907
8151
  }),
7908
8152
  error: Schema$1.Union([EnvironmentAuthorizationError])
7909
8153
  });
7910
8154
  const WsSkillsUnpublishRpc = Rpc.make(WS_METHODS.skillsUnpublish, {
7911
- payload: SkillNameInput,
8155
+ payload: AgentAssetRefInput,
7912
8156
  success: Schema$1.Struct({
7913
- report: SkillSyncReport,
7914
- status: SkillSyncStatus
8157
+ report: AgentSyncReport,
8158
+ status: AgentSyncStatus
7915
8159
  }),
7916
8160
  error: Schema$1.Union([EnvironmentAuthorizationError])
7917
8161
  });
7918
8162
  /**
8163
+ * Reading, editing and deleting an asset in the hub itself.
8164
+ *
8165
+ * The hub is the shared copy, so these reach every machine — including ones
8166
+ * that are asleep, and including assets this machine does not materialize at
8167
+ * all. Without them the only way to change a shared asset was to edit the local
8168
+ * file and wait for it to propagate, which is no way at all when the asset
8169
+ * belongs to a provider this machine does not run or to a machine you no longer
8170
+ * have.
8171
+ */
8172
+ const WsAssetsReadRpc = Rpc.make(WS_METHODS.assetsRead, {
8173
+ payload: AgentAssetRefInput,
8174
+ success: AgentAssetContent,
8175
+ error: Schema$1.Union([EnvironmentAuthorizationError])
8176
+ });
8177
+ const WsAssetsSaveRpc = Rpc.make(WS_METHODS.assetsSave, {
8178
+ payload: AgentAssetSaveInput,
8179
+ success: Schema$1.Struct({
8180
+ report: AgentSyncReport,
8181
+ status: AgentSyncStatus
8182
+ }),
8183
+ error: Schema$1.Union([EnvironmentAuthorizationError])
8184
+ });
8185
+ const WsAssetsDeleteRpc = Rpc.make(WS_METHODS.assetsDelete, {
8186
+ payload: AgentAssetDeleteInput,
8187
+ success: Schema$1.Struct({
8188
+ report: AgentSyncReport,
8189
+ status: AgentSyncStatus
8190
+ }),
8191
+ error: Schema$1.Union([EnvironmentAuthorizationError])
8192
+ });
8193
+ /**
8194
+ * Registering the user's own MCP servers.
8195
+ *
8196
+ * Every one of these answers with the whole list, for the same reason the sync
8197
+ * calls answer with the whole status: a panel that had to re-read after each
8198
+ * write would show a stale row in between.
8199
+ *
8200
+ * `setSecret` is deliberately its own call rather than a field on `save`. The
8201
+ * two go to different places — one to a file that syncs between machines, one
8202
+ * to a store that never leaves this one — and folding them together would make
8203
+ * a save button the thing that decides whether a credential travels.
8204
+ */
8205
+ const WsMcpListRpc = Rpc.make(WS_METHODS.mcpList, {
8206
+ payload: Schema$1.Struct({}),
8207
+ success: McpServerList,
8208
+ error: Schema$1.Union([EnvironmentAuthorizationError])
8209
+ });
8210
+ const WsMcpSaveRpc = Rpc.make(WS_METHODS.mcpSave, {
8211
+ payload: McpServerSaveInput,
8212
+ success: McpServerList,
8213
+ error: Schema$1.Union([McpRegistryError, EnvironmentAuthorizationError])
8214
+ });
8215
+ const WsMcpRemoveRpc = Rpc.make(WS_METHODS.mcpRemove, {
8216
+ payload: McpServerNameInput,
8217
+ success: McpServerList,
8218
+ error: Schema$1.Union([McpRegistryError, EnvironmentAuthorizationError])
8219
+ });
8220
+ const WsMcpSetSecretRpc = Rpc.make(WS_METHODS.mcpSetSecret, {
8221
+ payload: McpServerSecretInput,
8222
+ success: McpServerList,
8223
+ error: Schema$1.Union([McpRegistryError, EnvironmentAuthorizationError])
8224
+ });
8225
+ /**
7919
8226
  * The board, then every change to it, for as long as the client stays
7920
8227
  * subscribed. The leading snapshot is what makes this sufficient on its own —
7921
8228
  * a client does not list first, so there is no window between the two in which
@@ -8283,7 +8590,7 @@ const WsSubscribeAuthAccessRpc = Rpc.make(WS_METHODS.subscribeAuthAccess, {
8283
8590
  error: Schema$1.Union([AuthAccessStreamError, EnvironmentAuthorizationError]),
8284
8591
  stream: true
8285
8592
  });
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);
8593
+ 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
8594
  //#endregion
8288
8595
  //#region ../../packages/shared/src/oauthScope.ts
8289
8596
  const OAUTH_SCOPE_TOKEN = /^[\u0021\u0023-\u005b\u005d-\u007e]+$/u;
@@ -8429,7 +8736,7 @@ function deriveAuthClientMetadata(input) {
8429
8736
  //#endregion
8430
8737
  //#region src/auth/EnvironmentAuthPolicy.ts
8431
8738
  var EnvironmentAuthPolicy = class extends Context.Service()("@p4code/cli/auth/EnvironmentAuthPolicy") {};
8432
- const make$68 = Effect.gen(function* () {
8739
+ const make$69 = Effect.gen(function* () {
8433
8740
  const config = yield* ServerConfig$1;
8434
8741
  const isRemoteReachable = isRemoteReachableHost(config.host);
8435
8742
  const policy = config.mode === "desktop" ? isRemoteReachable ? "remote-reachable" : "desktop-managed-local" : isRemoteReachable ? "remote-reachable" : "loopback-browser";
@@ -8447,7 +8754,7 @@ const make$68 = Effect.gen(function* () {
8447
8754
  };
8448
8755
  return EnvironmentAuthPolicy.of({ getDescriptor: () => Effect.succeed(descriptor).pipe(Effect.withSpan("EnvironmentAuthPolicy.getDescriptor")) });
8449
8756
  });
8450
- const layer$64 = Layer.effect(EnvironmentAuthPolicy, make$68);
8757
+ const layer$65 = Layer.effect(EnvironmentAuthPolicy, make$69);
8451
8758
  //#endregion
8452
8759
  //#region src/persistence/Errors.ts
8453
8760
  function summarizeSchemaIssue(issue) {
@@ -8628,7 +8935,7 @@ function toPersistenceSqlOrDecodeError$6(sqlOperation, decodeOperation, correlat
8628
8935
  cause
8629
8936
  });
8630
8937
  }
8631
- const make$67 = Effect.gen(function* () {
8938
+ const make$68 = Effect.gen(function* () {
8632
8939
  const sql = yield* SqlClient.SqlClient;
8633
8940
  const createSessionRow = SqlSchema.void({
8634
8941
  Request: CreateAuthSessionInput,
@@ -8762,7 +9069,7 @@ const make$67 = Effect.gen(function* () {
8762
9069
  setLastConnectedAt
8763
9070
  };
8764
9071
  });
8765
- const layer$63 = Layer.effect(AuthSessionRepository, make$67);
9072
+ const layer$64 = Layer.effect(AuthSessionRepository, make$68);
8766
9073
  //#endregion
8767
9074
  //#region src/auth/ServerSecretStore.ts
8768
9075
  const secretStoreErrorContext = {
@@ -8829,7 +9136,7 @@ const isSecretStoreError = Schema$1.is(SecretStoreError);
8829
9136
  const isPlatformError = (value) => Predicate.isTagged(value, "PlatformError");
8830
9137
  const isSecretAlreadyExistsError = (error) => "cause" in error && isPlatformError(error.cause) && error.cause.reason._tag === "AlreadyExists";
8831
9138
  var ServerSecretStore = class extends Context.Service()("@p4code/cli/auth/ServerSecretStore") {};
8832
- const make$66 = Effect.gen(function* () {
9139
+ const make$67 = Effect.gen(function* () {
8833
9140
  const crypto = yield* Crypto.Crypto;
8834
9141
  const fileSystem = yield* FileSystem.FileSystem;
8835
9142
  const path = yield* Path.Path;
@@ -8899,7 +9206,7 @@ const make$66 = Effect.gen(function* () {
8899
9206
  remove
8900
9207
  });
8901
9208
  });
8902
- const layer$62 = Layer.effect(ServerSecretStore, make$66);
9209
+ const layer$63 = Layer.effect(ServerSecretStore, make$67);
8903
9210
  //#endregion
8904
9211
  //#region src/auth/SessionStore.ts
8905
9212
  var MalformedSessionTokenError = class extends Schema$1.TaggedErrorClass()("MalformedSessionTokenError", {}) {
@@ -9137,7 +9444,7 @@ function toAuthClientSession(input) {
9137
9444
  current: false
9138
9445
  };
9139
9446
  }
9140
- const make$65 = Effect.gen(function* () {
9447
+ const make$66 = Effect.gen(function* () {
9141
9448
  const crypto = yield* Crypto.Crypto;
9142
9449
  const serverConfig = yield* ServerConfig$1;
9143
9450
  const secretStore = yield* ServerSecretStore;
@@ -9451,7 +9758,7 @@ const make$65 = Effect.gen(function* () {
9451
9758
  markDisconnected
9452
9759
  });
9453
9760
  });
9454
- const layer$61 = Layer.effect(SessionStore, make$65).pipe(Layer.provideMerge(layer$63));
9761
+ const layer$62 = Layer.effect(SessionStore, make$66).pipe(Layer.provideMerge(layer$64));
9455
9762
  //#endregion
9456
9763
  //#region src/persistence/AuthPairingLinks.ts
9457
9764
  const AuthPairingLinkRecord = Schema$1.Struct({
@@ -9512,7 +9819,7 @@ function toPersistenceSqlOrDecodeError$5(sqlOperation, decodeOperation, correlat
9512
9819
  cause
9513
9820
  });
9514
9821
  }
9515
- const make$64 = Effect.gen(function* () {
9822
+ const make$65 = Effect.gen(function* () {
9516
9823
  const sql = yield* SqlClient.SqlClient;
9517
9824
  const createPairingLinkRow = SqlSchema.void({
9518
9825
  Request: CreateAuthPairingLinkInput,
@@ -9647,7 +9954,7 @@ const make$64 = Effect.gen(function* () {
9647
9954
  getByCredential
9648
9955
  };
9649
9956
  });
9650
- const layer$60 = Layer.effect(AuthPairingLinkRepository, make$64);
9957
+ const layer$61 = Layer.effect(AuthPairingLinkRepository, make$65);
9651
9958
  //#endregion
9652
9959
  //#region src/auth/PairingGrantStore.ts
9653
9960
  var UnknownBootstrapCredentialError = class extends Schema$1.TaggedErrorClass()("UnknownBootstrapCredentialError", {}) {
@@ -9742,7 +10049,7 @@ const DEV_STARTUP_TTL_HOURS = Duration.hours(24);
9742
10049
  const PAIRING_TOKEN_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
9743
10050
  const PAIRING_TOKEN_LENGTH = 12;
9744
10051
  const PAIRING_TOKEN_REJECTION_LIMIT = Math.floor(256 / 32) * 32;
9745
- const make$63 = Effect.gen(function* () {
10052
+ const make$64 = Effect.gen(function* () {
9746
10053
  const crypto = yield* Crypto.Crypto;
9747
10054
  const config = yield* ServerConfig$1;
9748
10055
  const pairingLinks = yield* AuthPairingLinkRepository;
@@ -9940,7 +10247,7 @@ const make$63 = Effect.gen(function* () {
9940
10247
  consume
9941
10248
  });
9942
10249
  });
9943
- const layer$59 = Layer.effect(PairingGrantStore, make$63).pipe(Layer.provideMerge(layer$60));
10250
+ const layer$60 = Layer.effect(PairingGrantStore, make$64).pipe(Layer.provideMerge(layer$61));
9944
10251
  //#endregion
9945
10252
  //#region src/persistence/Migrations/001_OrchestrationEvents.ts
9946
10253
  var _001_OrchestrationEvents_default = Effect.gen(function* () {
@@ -11555,7 +11862,7 @@ Layer.effectDiscard(runMigrations());
11555
11862
  //#region src/persistence/Layers/Sqlite.ts
11556
11863
  const defaultSqliteClientLoaders = {
11557
11864
  bun: () => import("@effect/sql-sqlite-bun/SqliteClient"),
11558
- node: () => import("./NodeSqliteClient-DGAB9NWW.mjs")
11865
+ node: () => import("./NodeSqliteClient-CRS7rg24.mjs")
11559
11866
  };
11560
11867
  const makeRuntimeSqliteLayer = Effect.fn("makeRuntimeSqliteLayer")(function* (config) {
11561
11868
  const runtime = process.versions.bun !== void 0 ? "bun" : "node";
@@ -11713,7 +12020,7 @@ function parseBearerToken(request) {
11713
12020
  const token = header.slice(7).trim();
11714
12021
  return token.length > 0 ? token : null;
11715
12022
  }
11716
- const make$62 = Effect.gen(function* () {
12023
+ const make$63 = Effect.gen(function* () {
11717
12024
  const policy = yield* EnvironmentAuthPolicy;
11718
12025
  const bootstrapCredentials = yield* PairingGrantStore;
11719
12026
  const sessions = yield* SessionStore;
@@ -11908,9 +12215,9 @@ const make$62 = Effect.gen(function* () {
11908
12215
  issueStartupPairingUrl
11909
12216
  });
11910
12217
  });
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));
12218
+ const layer$59 = Layer.effect(EnvironmentAuth, make$63).pipe(Layer.provideMerge(layer$60), Layer.provideMerge(layer$62), Layer.provideMerge(layer$65));
12219
+ const storageLayer = Layer.mergeAll(layer$63, layerConfig);
12220
+ const runtimeLayer = layer$59.pipe(Layer.provideMerge(storageLayer));
11914
12221
  //#endregion
11915
12222
  //#region src/cliAuthFormat.ts
11916
12223
  const newline = "\n";
@@ -12665,7 +12972,7 @@ const runWithEnvironmentAuth = (flags, run, options) => Effect.gen(function* ()
12665
12972
  }
12666
12973
  return yield* Effect.gen(function* () {
12667
12974
  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)))));
12975
+ }).pipe(Effect.provide(Layer.mergeAll(runtimeLayer).pipe(Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)))));
12669
12976
  });
12670
12977
  const ttlFlag = Flag.string("ttl").pipe(Flag.withSchema(DurationFromString), Flag.withDescription("TTL, for example `5m`, `1h`, `30d`, or `15 minutes`."), Flag.optional);
12671
12978
  const jsonFlag = Flag.boolean("json").pipe(Flag.withDescription("Emit JSON instead of human-readable output."), Flag.withDefault(false));
@@ -12750,422 +13057,6 @@ const sessionCommand = Command.make("session").pipe(Command.withDescription("Man
12750
13057
  ]));
12751
13058
  const authCommand = Command.make("auth").pipe(Command.withDescription("Manage the local auth control plane for headless deployments."), Command.withSubcommands([pairingCommand, sessionCommand]));
12752
13059
  //#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
13060
  //#region src/hub/HubConfig.ts
13170
13061
  /**
13171
13062
  * Configuration for the hub process.
@@ -13427,7 +13318,7 @@ const makeAgentAssetRepository = Effect.gen(function* () {
13427
13318
  };
13428
13319
  const existing = yield* findByKey(key);
13429
13320
  if (Option.isNone(existing)) return { _tag: "deleted" };
13430
- if (existing.value.version !== request.expectedVersion) return {
13321
+ if (request.expectedVersion !== void 0 && existing.value.version !== request.expectedVersion) return {
13431
13322
  _tag: "conflict",
13432
13323
  current: Option.some(existing.value)
13433
13324
  };
@@ -13809,29 +13700,34 @@ const putAssetRoute = HttpRouter.add("PUT", "/assets", respondToHubFailures(Effe
13809
13700
  return HttpServerResponse.jsonUnsafe(outcome.asset, { status: outcome.created ? 201 : 200 });
13810
13701
  })));
13811
13702
  /**
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.
13703
+ * Deletion is conditional when `expectedVersion` is supplied, which the sync
13704
+ * always does. Two machines can both hold a skill and both decide to remove it,
13705
+ * and one may be acting on a copy that predates the other's edit — deleting on
13706
+ * that basis discards the edit with nothing to notice it by.
13707
+ *
13708
+ * Omitting it deletes unconditionally, and that is deliberate: a person
13709
+ * clearing a row they can only see in a listing has never held a version for
13710
+ * it. Requiring one would leave an asset published by a decommissioned machine
13711
+ * permanently stuck, which is a worse failure than the race it would prevent.
13816
13712
  */
13817
13713
  const deleteAssetRoute = HttpRouter.add("DELETE", "/assets/:kind/:name", respondToHubFailures(Effect.gen(function* () {
13818
13714
  yield* authenticateHubRequest();
13819
13715
  const params = yield* HttpRouter.params;
13820
13716
  const request = yield* HttpServerRequest.HttpServerRequest;
13821
- const url = new URL(request.url, "http://hub.invalid");
13717
+ const rawExpectedVersion = new URL(request.url, "http://hub.invalid").searchParams.get("expectedVersion");
13822
13718
  const requested = yield* decodeOrInvalid(Schema$1.Struct({
13823
13719
  kind: AgentAssetKind,
13824
13720
  name: AgentAssetName,
13825
- expectedVersion: Schema$1.FiniteFromString.check(Schema$1.isGreaterThanOrEqualTo(1))
13721
+ expectedVersion: Schema$1.optional(Schema$1.FiniteFromString.check(Schema$1.isGreaterThanOrEqualTo(1)))
13826
13722
  }), "Path or expectedVersion query parameter is not valid.")({
13827
13723
  kind: params.kind,
13828
13724
  name: params.name,
13829
- expectedVersion: url.searchParams.get("expectedVersion")
13725
+ ...rawExpectedVersion === null ? {} : { expectedVersion: rawExpectedVersion }
13830
13726
  });
13831
13727
  const outcome = yield* (yield* AgentAssetRepository).deleteByName(requested);
13832
13728
  return outcome._tag === "conflict" ? assetConflict(Option.getOrNull(outcome.current)) : HttpServerResponse.empty({ status: 204 });
13833
13729
  })));
13834
- const layer$56 = Layer.mergeAll(healthRoute, listTasksRoute, getTaskRoute, createTaskRoute, updateTaskRoute, deleteTaskRoute, listAssetsRoute, getAssetRoute, putAssetRoute, deleteAssetRoute);
13730
+ const layer$58 = Layer.mergeAll(healthRoute, listTasksRoute, getTaskRoute, createTaskRoute, updateTaskRoute, deleteTaskRoute, listAssetsRoute, getAssetRoute, putAssetRoute, deleteAssetRoute);
13835
13731
  //#endregion
13836
13732
  //#region src/hub/Migrations/001_Tasks.ts
13837
13733
  /**
@@ -13978,7 +13874,7 @@ const runHubMigrations = Effect.fn("runHubMigrations")(function* () {
13978
13874
  //#region src/hub/Sqlite.ts
13979
13875
  const sqliteClientLoaders = {
13980
13876
  bun: () => import("@effect/sql-sqlite-bun/SqliteClient"),
13981
- node: () => import("./NodeSqliteClient-DGAB9NWW.mjs")
13877
+ node: () => import("./NodeSqliteClient-CRS7rg24.mjs")
13982
13878
  };
13983
13879
  const makeHubSqliteLayer = Effect.fn("makeHubSqliteLayer")(function* (config) {
13984
13880
  const runtime = process.versions.bun !== void 0 ? "bun" : "node";
@@ -14045,7 +13941,7 @@ const announce = Layer.effectDiscard(Effect.gen(function* () {
14045
13941
  });
14046
13942
  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
13943
  }));
14048
- const makeHubLayer = HttpRouter.serve(layer$56).pipe(Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
13944
+ const makeHubLayer = HttpRouter.serve(layer$58).pipe(Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
14049
13945
  const runHub = Layer.launch(makeHubLayer);
14050
13946
  //#endregion
14051
13947
  //#region src/atomicWrite.ts
@@ -14169,7 +14065,7 @@ function stripDefaultServerSettings(current, defaults) {
14169
14065
  }
14170
14066
  return Object.is(current, defaults) ? void 0 : current;
14171
14067
  }
14172
- const make$60 = Effect.gen(function* () {
14068
+ const make$62 = Effect.gen(function* () {
14173
14069
  const { settingsPath } = yield* ServerConfig$1;
14174
14070
  const fs = yield* FileSystem.FileSystem;
14175
14071
  const pathService = yield* Path.Path;
@@ -14390,7 +14286,252 @@ const make$60 = Effect.gen(function* () {
14390
14286
  }
14391
14287
  };
14392
14288
  });
14393
- const layer$55 = Layer.effect(ServerSettingsService, make$60);
14289
+ const layer$57 = Layer.effect(ServerSettingsService, make$62);
14290
+ //#endregion
14291
+ //#region src/pathExpansion.ts
14292
+ /**
14293
+ * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the
14294
+ * current user's home directory. Spawned processes don't get shell
14295
+ * expansion, so env vars like `CODEX_HOME=~/.codex-work` would be passed
14296
+ * verbatim and treated as relative paths by the receiver.
14297
+ *
14298
+ * Matches the behavior of the other `expandHomePath` helpers in the
14299
+ * workspace layers and CLI bootstrap: `~` alone and both `~/` and `~\`
14300
+ * separators are handled. Returns the input unchanged if it doesn't
14301
+ * start with `~` or is empty. Does not handle `~user` (other-user)
14302
+ * expansion.
14303
+ */
14304
+ function expandHomePath$3(value) {
14305
+ if (!value) return value;
14306
+ if (value === "~") return NodeOS.homedir();
14307
+ if (value.startsWith("~/") || value.startsWith("~\\")) return NodePath.join(NodeOS.homedir(), value.slice(2));
14308
+ return value;
14309
+ }
14310
+ //#endregion
14311
+ //#region src/provider/Drivers/ClaudeSkills.ts
14312
+ /**
14313
+ * ClaudeSkills — filesystem discovery of Claude Code skills for the `$` picker.
14314
+ *
14315
+ * Claude Code loads skills from `<config dir>/skills` (user scope) and
14316
+ * `<cwd>/.claude/skills` (project scope), one directory per skill with a
14317
+ * `SKILL.md` carrying YAML frontmatter. The Agent SDK init handshake surfaces
14318
+ * skills only as slash commands without their filesystem paths, so the
14319
+ * provider snapshot scans the same locations directly, mirroring how the
14320
+ * Codex app-server reports its skills.
14321
+ *
14322
+ * @module provider/Drivers/ClaudeSkills
14323
+ */
14324
+ const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
14325
+ function parseSkillFrontmatter(contents) {
14326
+ const match = FRONTMATTER_PATTERN.exec(contents);
14327
+ if (!match) return { kind: "missing" };
14328
+ let parsed;
14329
+ try {
14330
+ parsed = parse(match[1] ?? "");
14331
+ } catch {
14332
+ return { kind: "malformed" };
14333
+ }
14334
+ if (typeof parsed !== "object" || parsed === null) return { kind: "malformed" };
14335
+ const record = parsed;
14336
+ const name = typeof record.name === "string" ? record.name.trim() : "";
14337
+ const description = typeof record.description === "string" ? record.description.trim() : "";
14338
+ return {
14339
+ kind: "parsed",
14340
+ ...name ? { name } : {},
14341
+ ...description ? { description } : {}
14342
+ };
14343
+ }
14344
+ /**
14345
+ * Resolve the Claude config directory the CLI would use, matching the
14346
+ * precedence the spawned CLI sees: the instance's `homePath` (exported as
14347
+ * `CLAUDE_CONFIG_DIR` by `makeClaudeEnvironment`), then a `CLAUDE_CONFIG_DIR`
14348
+ * already present in the process environment, then `~/.claude`.
14349
+ *
14350
+ * Exported for the memory sync, which materializes `CLAUDE.md` directly into
14351
+ * this directory. Same reasoning as the skills root below: resolving it a
14352
+ * second time somewhere else is how the sync and the runtime come to disagree
14353
+ * about which config directory they are talking about.
14354
+ */
14355
+ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(function* (config, environment, cwd) {
14356
+ const path = yield* Path.Path;
14357
+ const homePath = config.homePath.trim();
14358
+ if (homePath.length > 0) return path.resolve(expandHomePath$3(homePath));
14359
+ const environmentConfigDir = environment.CLAUDE_CONFIG_DIR?.trim() ?? "";
14360
+ if (environmentConfigDir.length > 0) return cwd ? path.resolve(cwd, environmentConfigDir) : path.resolve(environmentConfigDir);
14361
+ return path.join(NodeOS.homedir(), ".claude");
14362
+ });
14363
+ /**
14364
+ * The user-scope skills directory the CLI would read.
14365
+ *
14366
+ * Exported for the hub sync, which materializes into exactly this directory —
14367
+ * the same resolution the discovery below uses, so a synced skill is one the
14368
+ * `$` picker can see. Deriving it a second time is how the two would drift.
14369
+ */
14370
+ const resolveClaudeUserSkillsDir = Effect.fn("resolveClaudeUserSkillsDir")(function* (config, environment, cwd) {
14371
+ const path = yield* Path.Path;
14372
+ const configDirPath = yield* resolveClaudeConfigDirPath(config, environment ?? process.env, cwd);
14373
+ return path.join(configDirPath, "skills");
14374
+ });
14375
+ /**
14376
+ * Enumerate Claude Code skills from the user config dir and the workspace.
14377
+ * Discovery is best-effort: unreadable roots and malformed skill entries are
14378
+ * skipped so a broken skill never degrades the provider snapshot. On name
14379
+ * collisions the project-scoped skill wins, matching Claude Code's
14380
+ * most-specific-wins resolution.
14381
+ */
14382
+ const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* (config, cwd, environment) {
14383
+ const fileSystem = yield* FileSystem.FileSystem;
14384
+ const path = yield* Path.Path;
14385
+ const configDirPath = yield* resolveClaudeConfigDirPath(config, environment ?? process.env, cwd);
14386
+ const roots = [{
14387
+ directory: path.join(configDirPath, "skills"),
14388
+ scope: "user"
14389
+ }, ...cwd ? [{
14390
+ directory: path.join(cwd, ".claude", "skills"),
14391
+ scope: "project"
14392
+ }] : []];
14393
+ const skillsByName = /* @__PURE__ */ new Map();
14394
+ for (const root of roots) {
14395
+ const entries = yield* fileSystem.readDirectory(root.directory).pipe(Effect.orElseSucceed(() => []));
14396
+ for (const entry of [...entries].sort()) {
14397
+ const skillPath = path.join(root.directory, entry, "SKILL.md");
14398
+ const contents = yield* fileSystem.readFileString(skillPath).pipe(Effect.orElseSucceed(() => void 0));
14399
+ if (contents === void 0) continue;
14400
+ const frontmatter = parseSkillFrontmatter(contents);
14401
+ if (frontmatter.kind === "malformed") continue;
14402
+ const name = (frontmatter.kind === "parsed" ? frontmatter.name : void 0) ?? entry.trim();
14403
+ if (!name) continue;
14404
+ skillsByName.set(name, {
14405
+ name,
14406
+ path: skillPath,
14407
+ enabled: true,
14408
+ scope: root.scope,
14409
+ ...frontmatter.kind === "parsed" && frontmatter.description ? { description: frontmatter.description } : {}
14410
+ });
14411
+ }
14412
+ }
14413
+ return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name));
14414
+ });
14415
+ /** Extension for a registration file, so the sync's asset name is `<server>.json`. */
14416
+ const MCP_REGISTRATION_SUFFIX = ".json";
14417
+ const RegistrationFromJson$1 = Schema$1.fromJsonString(McpServerRegistration);
14418
+ const decodeRegistration$1 = Schema$1.decodeUnknownExit(RegistrationFromJson$1);
14419
+ const encodeRegistration = Schema$1.encodeSync(RegistrationFromJson$1);
14420
+ var McpRegistry = class extends Context.Service()("@p4code/cli/mcp/McpRegistry") {};
14421
+ const slotsOf = (registration) => registration.secrets ?? [];
14422
+ const make$61 = Effect.gen(function* () {
14423
+ const config = yield* ServerConfig$1;
14424
+ const secrets = yield* ServerSecretStore;
14425
+ const fileSystem = yield* FileSystem.FileSystem;
14426
+ const path = yield* Path.Path;
14427
+ const services = yield* Effect.context();
14428
+ const root = path.join(config.stateDir, "mcp");
14429
+ const fileFor = (name) => path.join(root, `${name}${MCP_REGISTRATION_SUFFIX}`);
14430
+ const readSecret = Effect.fn("McpRegistry.readSecret")(function* (server, slot) {
14431
+ const stored = yield* secrets.get(mcpSecretStorageName({
14432
+ server,
14433
+ target: slot.target,
14434
+ key: slot.key
14435
+ })).pipe(Effect.orElseSucceed(() => Option.none()));
14436
+ return Option.map(stored, (bytes) => new TextDecoder().decode(bytes));
14437
+ });
14438
+ /**
14439
+ * Every readable registration.
14440
+ *
14441
+ * A file that will not parse is skipped rather than failing the read. One
14442
+ * corrupt registration should cost that one server, not every server and the
14443
+ * panel that lists them.
14444
+ */
14445
+ const readAll = Effect.fn("McpRegistry.readAll")(function* () {
14446
+ const entries = yield* fileSystem.readDirectory(root).pipe(Effect.orElseSucceed(() => []));
14447
+ const found = [];
14448
+ for (const entry of [...entries].sort()) {
14449
+ if (!entry.endsWith(".json")) continue;
14450
+ const raw = yield* fileSystem.readFileString(path.join(root, entry)).pipe(Effect.orElseSucceed(() => void 0));
14451
+ if (raw === void 0) continue;
14452
+ const decoded = decodeRegistration$1(raw);
14453
+ if (decoded._tag !== "Success") continue;
14454
+ found.push(decoded.value);
14455
+ }
14456
+ return found;
14457
+ });
14458
+ const list = Effect.gen(function* () {
14459
+ const registrations = yield* readAll();
14460
+ const statuses = [];
14461
+ for (const registration of registrations) {
14462
+ const missingSecrets = [];
14463
+ for (const slot of slotsOf(registration)) {
14464
+ const value = yield* readSecret(registration.name, slot);
14465
+ if (Option.isNone(value)) missingSecrets.push(slot);
14466
+ }
14467
+ statuses.push({
14468
+ registration,
14469
+ missingSecrets,
14470
+ ready: registration.enabled && missingSecrets.length === 0
14471
+ });
14472
+ }
14473
+ return statuses;
14474
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("mcp registry list failed", { cause }).pipe(Effect.as([]))));
14475
+ const save = (registration) => Effect.gen(function* () {
14476
+ yield* fileSystem.makeDirectory(root, { recursive: true }).pipe(Effect.ignore);
14477
+ yield* writeFileStringAtomically({
14478
+ filePath: fileFor(registration.name),
14479
+ contents: `${encodeRegistration(registration)}\n`
14480
+ });
14481
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("mcp registry save failed", { cause }).pipe(Effect.asVoid)));
14482
+ const remove = (name) => Effect.gen(function* () {
14483
+ const removed = (yield* readAll()).find((entry) => entry.name === name);
14484
+ yield* fileSystem.remove(fileFor(name), { force: true });
14485
+ for (const slot of removed === void 0 ? [] : slotsOf(removed)) yield* secrets.remove(mcpSecretStorageName({
14486
+ server: name,
14487
+ target: slot.target,
14488
+ key: slot.key
14489
+ })).pipe(Effect.ignore);
14490
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("mcp registry remove failed", { cause }).pipe(Effect.asVoid)));
14491
+ const setSecret = ({ name, slot, value }) => secrets.set(mcpSecretStorageName({
14492
+ server: name,
14493
+ target: slot.target,
14494
+ key: slot.key
14495
+ }), new TextEncoder().encode(value)).pipe(Effect.catchCause((cause) => Effect.logWarning("mcp secret write failed", { cause }).pipe(Effect.asVoid)));
14496
+ return {
14497
+ list,
14498
+ save,
14499
+ remove,
14500
+ setSecret,
14501
+ resolveForSession: Effect.gen(function* () {
14502
+ const registrations = yield* readAll();
14503
+ const resolved = {};
14504
+ for (const registration of registrations) {
14505
+ if (!registration.enabled) continue;
14506
+ const headers = { ...registration.transport === "stdio" ? {} : registration.headers ?? {} };
14507
+ const env = { ...registration.transport === "stdio" ? registration.env ?? {} : {} };
14508
+ let complete = true;
14509
+ for (const slot of slotsOf(registration)) {
14510
+ const value = yield* readSecret(registration.name, slot);
14511
+ if (Option.isNone(value)) {
14512
+ complete = false;
14513
+ break;
14514
+ }
14515
+ if (slot.target === "header") headers[slot.key] = value.value;
14516
+ else env[slot.key] = value.value;
14517
+ }
14518
+ if (!complete) continue;
14519
+ resolved[registration.name] = registration.transport === "stdio" ? {
14520
+ type: "stdio",
14521
+ command: registration.command,
14522
+ args: registration.args ?? [],
14523
+ env
14524
+ } : {
14525
+ type: registration.transport,
14526
+ url: registration.url,
14527
+ headers
14528
+ };
14529
+ }
14530
+ return resolved;
14531
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("mcp registry resolve failed", { cause }).pipe(Effect.as({}))))
14532
+ };
14533
+ });
14534
+ const layer$56 = Layer.effect(McpRegistry, make$61);
14394
14535
  //#endregion
14395
14536
  //#region src/sync/HubLink.ts
14396
14537
  /**
@@ -14487,7 +14628,7 @@ const formatHubLink = (input) => encodeStoredHubLink({
14487
14628
  shareMode: input.shareMode
14488
14629
  });
14489
14630
  const fromEnvironment = (environment) => validateHubLink(environment.P4CODE_HUB_URL ?? "", environment.P4CODE_HUB_TOKEN ?? "");
14490
- const make$59 = Effect.fn("HubLink.make")(function* (environment) {
14631
+ const make$60 = Effect.fn("HubLink.make")(function* (environment) {
14491
14632
  const secrets = yield* ServerSecretStore;
14492
14633
  const env = environment ?? process.env;
14493
14634
  const fromEnv = fromEnvironment(env);
@@ -14553,7 +14694,7 @@ const make$59 = Effect.fn("HubLink.make")(function* (environment) {
14553
14694
  })
14554
14695
  };
14555
14696
  });
14556
- const layer$54 = Layer.effect(HubLink, make$59());
14697
+ const layer$55 = Layer.effect(HubLink, make$60());
14557
14698
  //#endregion
14558
14699
  //#region src/sync/HubAssetClient.ts
14559
14700
  /**
@@ -14586,7 +14727,7 @@ const decodeAssetListPage = Schema$1.decodeUnknownEffect(AssetListPage);
14586
14727
  const decodeConflictBody = Schema$1.decodeUnknownEffect(ConflictBody);
14587
14728
  const decodeAsset = Schema$1.decodeUnknownEffect(AgentAsset);
14588
14729
  var HubAssetClient = class extends Context.Service()("@p4code/cli/sync/HubAssetClient") {};
14589
- const make$58 = Effect.gen(function* () {
14730
+ const make$59 = Effect.gen(function* () {
14590
14731
  const http = yield* HttpClient.HttpClient;
14591
14732
  const link = yield* HubLink;
14592
14733
  const requireSettings = Effect.gen(function* () {
@@ -14623,13 +14764,24 @@ const make$58 = Effect.gen(function* () {
14623
14764
  });
14624
14765
  const list = (kind) => Effect.gen(function* () {
14625
14766
  const settings = yield* requireSettings;
14626
- const response = yield* send("list", settings, HttpClientRequest.get(`${settings.baseUrl}/assets?kind=${encodeURIComponent(kind)}`));
14767
+ const query = kind === void 0 ? "" : `?kind=${encodeURIComponent(kind)}`;
14768
+ const response = yield* send("list", settings, HttpClientRequest.get(`${settings.baseUrl}/assets${query}`));
14627
14769
  if (response.status !== 200) return yield* new HubUnavailable({
14628
14770
  operation: "list",
14629
14771
  detail: `status ${response.status}`
14630
14772
  });
14631
14773
  return (yield* decodeBody("list", decodeAssetListPage, response)).assets;
14632
14774
  });
14775
+ const get = ({ kind, name }) => Effect.gen(function* () {
14776
+ const settings = yield* requireSettings;
14777
+ const response = yield* send("get", settings, HttpClientRequest.get(`${settings.baseUrl}/assets/${encodeURIComponent(kind)}/${encodeURIComponent(name)}`));
14778
+ if (response.status === 404) return Option.none();
14779
+ if (response.status !== 200) return yield* new HubUnavailable({
14780
+ operation: "get",
14781
+ detail: `status ${response.status}`
14782
+ });
14783
+ return Option.some(yield* decodeBody("get", decodeAsset, response));
14784
+ });
14633
14785
  const write = (input) => Effect.gen(function* () {
14634
14786
  const settings = yield* requireSettings;
14635
14787
  const response = yield* send("write", settings, HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.put(`${settings.baseUrl}/assets`), input));
@@ -14642,7 +14794,8 @@ const make$58 = Effect.gen(function* () {
14642
14794
  });
14643
14795
  const remove = ({ kind, name, expectedVersion }) => Effect.gen(function* () {
14644
14796
  const settings = yield* requireSettings;
14645
- const response = yield* send("remove", settings, HttpClientRequest.delete(`${settings.baseUrl}/assets/${encodeURIComponent(kind)}/${encodeURIComponent(name)}?expectedVersion=${expectedVersion}`));
14797
+ const query = expectedVersion === void 0 ? "" : `?expectedVersion=${expectedVersion}`;
14798
+ const response = yield* send("remove", settings, HttpClientRequest.delete(`${settings.baseUrl}/assets/${encodeURIComponent(kind)}/${encodeURIComponent(name)}${query}`));
14646
14799
  if (response.status === 409) return yield* yield* conflictFrom(name, response);
14647
14800
  if (response.status !== 204 && response.status !== 404) return yield* new HubUnavailable({
14648
14801
  operation: "remove",
@@ -14651,130 +14804,248 @@ const make$58 = Effect.gen(function* () {
14651
14804
  });
14652
14805
  return {
14653
14806
  list,
14807
+ get,
14654
14808
  write,
14655
14809
  remove
14656
14810
  };
14657
14811
  });
14658
- const layer$53 = Layer.effect(HubAssetClient, make$58);
14812
+ const layer$54 = Layer.effect(HubAssetClient, make$59);
14659
14813
  //#endregion
14660
- //#region src/pathExpansion.ts
14814
+ //#region src/sync/mcpRegistrationFiles.ts
14661
14815
  /**
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.
14816
+ * Reading and writing MCP registration files for the sync.
14666
14817
  *
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.
14818
+ * The third asset driver's filesystem half. Unlike memory, the directory is
14819
+ * p4code's own nothing else lives in `<stateDir>/mcp` so there is no
14820
+ * allowlist to police and the rules are about shape rather than about which
14821
+ * neighbours must not be touched.
14822
+ *
14823
+ * **What is not here is the point.** A registration references its bearer
14824
+ * tokens and API keys by slot name; the values live in `ServerSecretStore` and
14825
+ * are never written into these files. So a registration can travel through a
14826
+ * hub that stores asset files verbatim without a working credential leaving the
14827
+ * machine that holds it. This module enforces the file shape; `McpRegistry`
14828
+ * enforces the split.
14829
+ *
14830
+ * @module sync/mcpRegistrationFiles
14672
14831
  */
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
14832
+ const RegistrationFromJson = Schema$1.fromJsonString(McpServerRegistration);
14833
+ const decodeRegistration = Schema$1.decodeUnknownExit(RegistrationFromJson);
14681
14834
  /**
14682
- * ClaudeSkills filesystem discovery of Claude Code skills for the `$` picker.
14835
+ * Every registration in the directory, as syncable assets.
14683
14836
  *
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.
14837
+ * The asset name is the filename `linear.json` for the same reason memory's
14838
+ * is: one string is both the hub key and the path, so there is no mapping to
14839
+ * get wrong in one direction and not the other.
14690
14840
  *
14691
- * @module provider/Drivers/ClaudeSkills
14841
+ * A file that will not parse is refused rather than skipped. Unlike a stray
14842
+ * `.DS_Store`, a malformed `.json` in this directory is something p4code wrote
14843
+ * or something the sync brought in, so it is a fault worth reporting rather
14844
+ * than noise to ignore.
14692
14845
  */
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" };
14846
+ const readMcpRegistrations = Effect.fn("mcpRegistrationFiles.read")(function* (registrationsRoot) {
14847
+ const fileSystem = yield* FileSystem.FileSystem;
14848
+ const path = yield* Path.Path;
14849
+ const entries = yield* fileSystem.readDirectory(registrationsRoot).pipe(Effect.orElseSucceed(() => []));
14850
+ const skills = [];
14851
+ const refused = [];
14852
+ for (const entry of [...entries].sort()) {
14853
+ if (!entry.endsWith(".json") || entry.startsWith(".")) continue;
14854
+ const content = yield* fileSystem.readFileString(path.join(registrationsRoot, entry)).pipe(Effect.orElseSucceed(() => void 0));
14855
+ if (content === void 0) {
14856
+ refused.push({
14857
+ name: entry,
14858
+ reason: "unreadable"
14859
+ });
14860
+ continue;
14861
+ }
14862
+ if (decodeRegistration(content)._tag !== "Success") {
14863
+ refused.push({
14864
+ name: entry,
14865
+ reason: "unreadable"
14866
+ });
14867
+ continue;
14868
+ }
14869
+ skills.push({
14870
+ name: entry,
14871
+ files: [{
14872
+ path: entry,
14873
+ content
14874
+ }]
14875
+ });
14702
14876
  }
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
14877
  return {
14708
- kind: "parsed",
14709
- ...name ? { name } : {},
14710
- ...description ? { description } : {}
14878
+ skills,
14879
+ refused
14711
14880
  };
14712
- }
14881
+ });
14713
14882
  /**
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`.
14883
+ * Write a registration that arrived from the hub.
14884
+ *
14885
+ * Refused unless it is exactly one file whose path is the asset's own name and
14886
+ * whose content decodes as a registration. The last check is what stops another
14887
+ * machine — or a hub row edited by hand — from installing something this server
14888
+ * would then feed to an agent as a tool source.
14718
14889
  */
14719
- const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(function* (config, environment, cwd) {
14890
+ const writeMcpRegistration = Effect.fn("mcpRegistrationFiles.write")(function* (input) {
14891
+ const fileSystem = yield* FileSystem.FileSystem;
14720
14892
  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");
14893
+ const file = input.files[0];
14894
+ if (!input.name.endsWith(".json") || input.files.length !== 1 || file === void 0 || file.path !== input.name) return "refused";
14895
+ if (decodeRegistration(file.content)._tag !== "Success") return "refused";
14896
+ yield* fileSystem.makeDirectory(input.registrationsRoot, { recursive: true }).pipe(Effect.ignore);
14897
+ return yield* writeFileStringAtomically({
14898
+ filePath: path.join(input.registrationsRoot, input.name),
14899
+ contents: file.content
14900
+ }).pipe(Effect.as("written"), Effect.orElseSucceed(() => "failed"));
14901
+ });
14902
+ const removeMcpRegistration = Effect.fn("mcpRegistrationFiles.remove")(function* (input) {
14903
+ if (!input.name.endsWith(".json")) return;
14904
+ const fileSystem = yield* FileSystem.FileSystem;
14905
+ const path = yield* Path.Path;
14906
+ yield* fileSystem.remove(path.join(input.registrationsRoot, input.name), { force: true });
14726
14907
  });
14908
+ //#endregion
14909
+ //#region src/sync/memoryFile.ts
14727
14910
  /**
14728
- * The user-scope skills directory the CLI would read.
14911
+ * Reading and writing user-scope memory files.
14729
14912
  *
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.
14913
+ * Memory is the instruction file an agent reads before anything else, so a
14914
+ * machine whose `CLAUDE.md` differs is a machine whose agent behaves
14915
+ * differently which is the whole reason the hub exists. Unlike a skill it is
14916
+ * a single file, and it sits *directly* in the provider's config directory
14917
+ * rather than in a subdirectory the sync could own outright.
14918
+ *
14919
+ * That last part is the entire design constraint. `<configDir>` also holds
14920
+ * `settings.json`, `.claude.json` and `.credentials.json`. "Write the file this
14921
+ * asset names" is therefore not safe on its own: an authorized machine — or a
14922
+ * hub row someone wrote by hand — could name `settings.json` and have every
14923
+ * other machine overwrite its own configuration on the next sync. The path
14924
+ * schema does not save us here, because `settings.json` is a perfectly valid
14925
+ * asset path.
14926
+ *
14927
+ * So the allowlist below is the boundary, and it is checked on the way in as
14928
+ * well as on the way out: a file this module will not write is also one it will
14929
+ * not read, so nothing outside it can reach the hub in the first place.
14930
+ *
14931
+ * @module sync/memoryFile
14733
14932
  */
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
14933
  /**
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.
14934
+ * The only files in the provider config directory this sync will ever touch.
14935
+ *
14936
+ * Both names, not one: Claude Code reads `CLAUDE.md`, Codex reads `AGENTS.md`,
14937
+ * and a machine that syncs both is the ordinary case rather than a special one.
14938
+ * Nothing else in that directory is memory, and everything else in it is either
14939
+ * a credential or a per-machine setting.
14940
+ *
14941
+ * The asset's name *is* the filename. That keeps the hub key and the on-disk
14942
+ * path the same string, so there is no mapping table to get wrong, and it makes
14943
+ * the allowlist checkable at the boundary rather than after a join.
14745
14944
  */
14746
- const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* (config, cwd, environment) {
14945
+ const MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md"];
14946
+ /**
14947
+ * Whether this name is one of the memory files.
14948
+ *
14949
+ * Exact match, deliberately. Not a suffix test and not case-insensitive: a
14950
+ * check that accepted `claude.md` on a case-insensitive filesystem and rejected
14951
+ * it on a case-sensitive one would make the same hub row materialize on one
14952
+ * machine and refuse on another.
14953
+ */
14954
+ const isMemoryAssetName = (name) => MEMORY_FILE_NAMES.includes(name);
14955
+ /** The same test the skill reader uses: a NUL is what breaks a JSON text column. */
14956
+ const looksBinary$1 = (content) => content.includes("\0");
14957
+ /**
14958
+ * Read whichever memory files exist in this machine's config directory.
14959
+ *
14960
+ * Only the allowlisted names are even stat'd. A missing file is not a refusal:
14961
+ * most machines have a `CLAUDE.md` and no `AGENTS.md`, and reporting the second
14962
+ * as a problem would be reporting the normal case.
14963
+ */
14964
+ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memoryRoot) {
14747
14965
  const fileSystem = yield* FileSystem.FileSystem;
14748
14966
  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, {
14967
+ const skills = [];
14968
+ const refused = [];
14969
+ for (const name of MEMORY_FILE_NAMES) {
14970
+ const absolute = path.join(memoryRoot, name);
14971
+ if ((yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "File") continue;
14972
+ const content = yield* fileSystem.readFileString(absolute).pipe(Effect.orElseSucceed(() => void 0));
14973
+ if (content === void 0) {
14974
+ refused.push({
14769
14975
  name,
14770
- path: skillPath,
14771
- enabled: true,
14772
- scope: root.scope,
14773
- ...frontmatter.kind === "parsed" && frontmatter.description ? { description: frontmatter.description } : {}
14976
+ reason: "unreadable"
14977
+ });
14978
+ continue;
14979
+ }
14980
+ if (looksBinary$1(content)) {
14981
+ refused.push({
14982
+ name,
14983
+ reason: "binary-file"
14984
+ });
14985
+ continue;
14986
+ }
14987
+ if (content.length > 524288) {
14988
+ refused.push({
14989
+ name,
14990
+ reason: "file-too-large"
14991
+ });
14992
+ continue;
14993
+ }
14994
+ const files = [{
14995
+ path: name,
14996
+ content
14997
+ }];
14998
+ if (agentAssetTotalBytes(files) > 4194304) {
14999
+ refused.push({
15000
+ name,
15001
+ reason: "asset-too-large"
14774
15002
  });
15003
+ continue;
14775
15004
  }
15005
+ skills.push({
15006
+ name,
15007
+ files
15008
+ });
14776
15009
  }
14777
- return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name));
15010
+ return {
15011
+ skills,
15012
+ refused
15013
+ };
15014
+ });
15015
+ /**
15016
+ * Whether an asset from the hub is a memory file this machine will write.
15017
+ *
15018
+ * Both halves are checked. The name has to be allowlisted, and the files have
15019
+ * to be exactly the one file that name refers to — an asset named `CLAUDE.md`
15020
+ * carrying a second file called `settings.json` would otherwise write it, which
15021
+ * is the same escape by a longer route.
15022
+ */
15023
+ const isWritableMemoryAsset = (input) => isMemoryAssetName(input.name) && input.files.length === 1 && input.files[0]?.path === input.name;
15024
+ /**
15025
+ * Write a memory file, or refuse.
15026
+ *
15027
+ * Answers rather than failing, because the caller's response to "this asset is
15028
+ * not writable here" is to report it alongside the other refusals, not to
15029
+ * abandon the sync. Written atomically: memory is read at the start of every
15030
+ * agent turn, and a half-written instruction file is instructions with the end
15031
+ * missing, which an agent will follow without noticing.
15032
+ */
15033
+ const writeMemoryFile = Effect.fn("memoryFile.writeMemoryFile")(function* (input) {
15034
+ if (!isWritableMemoryAsset(input)) return "refused";
15035
+ const fileSystem = yield* FileSystem.FileSystem;
15036
+ const path = yield* Path.Path;
15037
+ const contents = input.files[0]?.content ?? "";
15038
+ yield* fileSystem.makeDirectory(input.memoryRoot, { recursive: true }).pipe(Effect.ignore);
15039
+ return yield* writeFileStringAtomically({
15040
+ filePath: path.join(input.memoryRoot, input.name),
15041
+ contents
15042
+ }).pipe(Effect.as("written"), Effect.orElseSucceed(() => "failed"));
15043
+ });
15044
+ const removeMemoryFile = Effect.fn("memoryFile.removeMemoryFile")(function* (input) {
15045
+ if (!isMemoryAssetName(input.name)) return;
15046
+ const fileSystem = yield* FileSystem.FileSystem;
15047
+ const path = yield* Path.Path;
15048
+ yield* fileSystem.remove(path.join(input.memoryRoot, input.name), { force: true });
14778
15049
  });
14779
15050
  //#endregion
14780
15051
  //#region src/sync/skillManifest.ts
@@ -15151,10 +15422,10 @@ function planSkillSync(input) {
15151
15422
  };
15152
15423
  }
15153
15424
  //#endregion
15154
- //#region src/sync/SkillSync.ts
15425
+ //#region src/sync/AssetSync.ts
15155
15426
  const EMPTY_REPORT = {
15156
15427
  ran: false,
15157
- skillsRoot: null,
15428
+ roots: [],
15158
15429
  materialized: [],
15159
15430
  published: [],
15160
15431
  removed: [],
@@ -15163,13 +15434,12 @@ const EMPTY_REPORT = {
15163
15434
  refused: [],
15164
15435
  unavailable: null
15165
15436
  };
15166
- var SkillSync = class extends Context.Service()("@p4code/cli/sync/SkillSync") {};
15167
- const make$57 = Effect.gen(function* () {
15437
+ var AssetSync = class extends Context.Service()("@p4code/cli/sync/AssetSync") {};
15438
+ const make$58 = Effect.gen(function* () {
15168
15439
  const client = yield* HubAssetClient;
15169
15440
  const link = yield* HubLink;
15170
15441
  const settingsStore = yield* ServerSettingsService;
15171
15442
  const config = yield* ServerConfig$1;
15172
- const fileSystem = yield* FileSystem.FileSystem;
15173
15443
  const path = yield* Path.Path;
15174
15444
  /**
15175
15445
  * Captured here rather than left in each method's requirements, so the
@@ -15178,267 +15448,446 @@ const make$57 = Effect.gen(function* () {
15178
15448
  * neither should have to carry a filesystem to ask for a sync.
15179
15449
  */
15180
15450
  const services = yield* Effect.context();
15451
+ /**
15452
+ * Still `skill-sync-manifest.json`.
15453
+ *
15454
+ * Renaming it would be tidier and would also declare every skill this machine
15455
+ * manages to be unmanaged, which on the next sync means re-publishing all of
15456
+ * them into collisions. The manifest is keyed by root directory, and the two
15457
+ * kinds have different roots, so one file holds both without ambiguity.
15458
+ */
15181
15459
  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 ?? "" });
15460
+ const claudeHome = Effect.fn("AssetSync.claudeHome")(function* () {
15461
+ return { homePath: (yield* settingsStore.getSettings.pipe(Effect.orElseSucceed(() => void 0)))?.providers.claudeAgent.homePath ?? "" };
15184
15462
  });
15463
+ const drivers = [
15464
+ {
15465
+ kind: "skill",
15466
+ resolveRoot: Effect.gen(function* () {
15467
+ return yield* resolveClaudeUserSkillsDir(yield* claudeHome());
15468
+ }),
15469
+ read: (root) => readSkillDirectory(root),
15470
+ write: ({ root, name, files }) => writeSkillDirectory({
15471
+ skillsRoot: root,
15472
+ name,
15473
+ files
15474
+ }).pipe(Effect.as("written"), Effect.orElseSucceed(() => "failed")),
15475
+ remove: ({ root, name }) => removeSkillDirectory({
15476
+ skillsRoot: root,
15477
+ name
15478
+ }).pipe(Effect.ignore)
15479
+ },
15480
+ {
15481
+ kind: "memory",
15482
+ resolveRoot: Effect.gen(function* () {
15483
+ return yield* resolveClaudeConfigDirPath(yield* claudeHome(), process.env);
15484
+ }),
15485
+ read: (root) => readMemoryFiles(root),
15486
+ write: ({ root, name, files }) => writeMemoryFile({
15487
+ memoryRoot: root,
15488
+ name,
15489
+ files
15490
+ }).pipe(Effect.map((outcome) => outcome)),
15491
+ remove: ({ root, name }) => removeMemoryFile({
15492
+ memoryRoot: root,
15493
+ name
15494
+ }).pipe(Effect.ignore)
15495
+ },
15496
+ {
15497
+ kind: "mcp",
15498
+ resolveRoot: Effect.succeed(path.join(config.stateDir, "mcp")),
15499
+ read: (root) => readMcpRegistrations(root),
15500
+ write: ({ root, name, files }) => writeMcpRegistration({
15501
+ registrationsRoot: root,
15502
+ name,
15503
+ files
15504
+ }),
15505
+ remove: ({ root, name }) => removeMcpRegistration({
15506
+ registrationsRoot: root,
15507
+ name
15508
+ }).pipe(Effect.ignore)
15509
+ }
15510
+ ];
15511
+ const driverFor = (kind) => drivers.find((driver) => driver.kind === kind);
15185
15512
  const isConfigured = link.current.pipe(Effect.map((state) => Option.isSome(state.settings)));
15513
+ /**
15514
+ * The three views for one kind, gathered once.
15515
+ *
15516
+ * `status` and `run` both need exactly this and disagreeing about it is how a
15517
+ * panel comes to show something the next sync does not do.
15518
+ */
15519
+ const viewsFor = Effect.fn("AssetSync.viewsFor")(function* (driver, hubAssets) {
15520
+ const root = yield* driver.resolveRoot;
15521
+ const hub = hubAssets.filter((asset) => asset.kind === driver.kind);
15522
+ if (root === null) return {
15523
+ root,
15524
+ hub,
15525
+ local: [],
15526
+ refused: [],
15527
+ managed: []
15528
+ };
15529
+ const local = yield* driver.read(root);
15530
+ const manifest = yield* readSkillManifest(manifestPath);
15531
+ return {
15532
+ root,
15533
+ hub,
15534
+ local: local.skills,
15535
+ refused: local.refused,
15536
+ managed: entriesFor(manifest, root)
15537
+ };
15538
+ });
15539
+ const rootsOf = Effect.fn("AssetSync.rootsOf")(function* () {
15540
+ const roots = [];
15541
+ for (const driver of drivers) roots.push({
15542
+ kind: driver.kind,
15543
+ path: yield* driver.resolveRoot
15544
+ });
15545
+ return roots;
15546
+ });
15186
15547
  const run = Effect.gen(function* () {
15187
15548
  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 {
15549
+ const roots = yield* rootsOf();
15550
+ const hubAssets = yield* client.list().pipe(Effect.option);
15551
+ if (Option.isNone(hubAssets)) return {
15191
15552
  ...EMPTY_REPORT,
15192
15553
  ran: true,
15193
- skillsRoot,
15554
+ roots,
15194
15555
  unavailable: "hub is unreachable"
15195
15556
  };
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
15557
  const materialized = [];
15205
15558
  const published = [];
15206
15559
  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({
15560
+ const unmanaged = [];
15561
+ const conflicts = [];
15562
+ const refused = [];
15563
+ const shareMode = (yield* link.current).shareMode;
15564
+ for (const driver of drivers) {
15565
+ const views = yield* viewsFor(driver, hubAssets.value);
15566
+ const kind = driver.kind;
15567
+ for (const entry of views.refused) refused.push({
15568
+ kind,
15569
+ name: entry.name,
15570
+ reason: entry.reason
15571
+ });
15572
+ if (views.root === null) continue;
15573
+ const root = views.root;
15574
+ const plan = planSkillSync({
15575
+ hub: views.hub,
15576
+ local: views.local,
15577
+ manifest: views.managed
15578
+ });
15579
+ const entries = new Map(views.managed.map((entry) => [entry.name, entry]));
15580
+ /** One manifest for the whole run, re-read per kind so roots do not clobber. */
15581
+ const commit = Effect.fn("AssetSync.commit")(function* () {
15582
+ const manifest = yield* readSkillManifest(manifestPath);
15583
+ yield* writeSkillManifest(manifestPath, withEntriesFor(manifest, root, [...entries.values()]));
15584
+ });
15585
+ for (const action of plan.actions) switch (action._tag) {
15586
+ case "materialize": {
15587
+ const outcome = yield* driver.write({
15588
+ root,
15589
+ name: action.asset.name,
15590
+ files: action.asset.files
15591
+ });
15592
+ if (outcome !== "written") {
15593
+ refused.push({
15594
+ kind,
15595
+ name: action.asset.name,
15596
+ reason: outcome === "refused" ? "not-writable-here" : "write-failed"
15597
+ });
15598
+ break;
15599
+ }
15600
+ entries.set(action.asset.name, {
15601
+ name: action.asset.name,
15602
+ version: action.asset.version,
15603
+ digest: digestSkillFiles(action.asset.files)
15604
+ });
15605
+ materialized.push({
15606
+ kind,
15607
+ name: action.asset.name
15608
+ });
15609
+ break;
15610
+ }
15611
+ case "publish": {
15612
+ const written = yield* client.write({
15613
+ kind,
15614
+ name: action.name,
15615
+ files: action.files,
15616
+ expectedVersion: action.expectedVersion
15617
+ }).pipe(Effect.option);
15618
+ if (Option.isNone(written)) {
15619
+ conflicts.push({
15620
+ kind,
15621
+ name: action.name,
15622
+ reason: "edited-both-sides"
15623
+ });
15624
+ break;
15625
+ }
15626
+ entries.set(action.name, {
15231
15627
  name: action.name,
15232
- reason: "edited-both-sides"
15628
+ version: written.value.version,
15629
+ digest: digestSkillFiles(written.value.files)
15630
+ });
15631
+ published.push({
15632
+ kind,
15633
+ name: action.name
15233
15634
  });
15234
15635
  break;
15235
15636
  }
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;
15637
+ case "removeLocal":
15638
+ yield* driver.remove({
15639
+ root,
15640
+ name: action.name
15641
+ });
15642
+ entries.delete(action.name);
15643
+ removed.push({
15644
+ kind,
15645
+ name: action.name
15646
+ });
15647
+ break;
15648
+ case "forget":
15649
+ entries.delete(action.name);
15650
+ break;
15243
15651
  }
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({
15652
+ for (const conflict of plan.conflicts) conflicts.push({
15653
+ kind,
15654
+ name: conflict.name,
15655
+ reason: conflict.reason
15656
+ });
15657
+ yield* commit();
15658
+ const autoShared = /* @__PURE__ */ new Set();
15659
+ if (shareMode === "all") {
15660
+ for (const name of plan.unmanaged) {
15661
+ const local = views.local.find((entry) => entry.name === name);
15662
+ if (local === void 0) continue;
15663
+ const written = yield* client.write({
15664
+ kind,
15270
15665
  name,
15271
- reason: "unmanaged-local-collision"
15666
+ files: local.files
15667
+ }).pipe(Effect.option);
15668
+ if (Option.isNone(written)) {
15669
+ conflicts.push({
15670
+ kind,
15671
+ name,
15672
+ reason: "unmanaged-local-collision"
15673
+ });
15674
+ continue;
15675
+ }
15676
+ entries.set(name, {
15677
+ name,
15678
+ version: written.value.version,
15679
+ digest: digestSkillFiles(written.value.files)
15272
15680
  });
15273
- continue;
15681
+ published.push({
15682
+ kind,
15683
+ name
15684
+ });
15685
+ autoShared.add(name);
15274
15686
  }
15275
- entries.set(name, {
15276
- name,
15277
- version: written.value.version,
15278
- digest: digestSkillFiles(written.value.files)
15279
- });
15280
- autoShared.push(name);
15687
+ yield* commit();
15281
15688
  }
15282
- yield* writeSkillManifest(manifestPath, withEntriesFor(manifest, skillsRoot, [...entries.values()]));
15689
+ for (const name of plan.unmanaged) if (!autoShared.has(name)) unmanaged.push({
15690
+ kind,
15691
+ name
15692
+ });
15283
15693
  }
15284
15694
  return {
15285
15695
  ran: true,
15286
- skillsRoot,
15696
+ roots,
15287
15697
  materialized,
15288
- published: [...published, ...autoShared],
15698
+ published,
15289
15699
  removed,
15290
15700
  conflicts,
15291
- unmanaged: plan.unmanaged.filter((name) => !autoShared.includes(name)),
15292
- refused: local.refused,
15701
+ unmanaged,
15702
+ refused,
15293
15703
  unavailable: null
15294
15704
  };
15295
- }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("skill sync failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15705
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset sync failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15296
15706
  /**
15297
- * The same three views the sync reads, rendered rather than acted on.
15707
+ * The same views the sync reads, rendered rather than acted on.
15298
15708
  *
15299
15709
  * Deliberately its own pass instead of a dry-run flag on `run`: a dry run
15300
15710
  * sharing the executing code path is one `if` away from writing during a
15301
15711
  * preview, and this is the call a panel makes every time it opens.
15302
15712
  */
15303
- const skillStatus = Effect.gen(function* () {
15713
+ const assetStatus = Effect.gen(function* () {
15304
15714
  const state = yield* link.current;
15305
- const skillsRoot = yield* resolveSkillsRoot();
15306
- const local = yield* readSkillDirectory(skillsRoot);
15307
- const managed = entriesFor(yield* readSkillManifest(manifestPath), skillsRoot);
15715
+ const roots = yield* rootsOf();
15308
15716
  const base = {
15309
15717
  syncMode: state.syncMode,
15310
15718
  shareMode: state.shareMode,
15311
15719
  source: state.source,
15312
- skillsRoot,
15313
- refused: local.refused
15720
+ roots
15314
15721
  };
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) => ({
15722
+ const hubAssets = Option.isNone(state.settings) ? Option.none() : yield* client.list().pipe(Effect.option);
15723
+ const entries = [];
15724
+ const refused = [];
15725
+ for (const driver of drivers) {
15726
+ const kind = driver.kind;
15727
+ const views = yield* viewsFor(driver, Option.getOrElse(hubAssets, () => []));
15728
+ for (const entry of views.refused) refused.push({
15729
+ kind,
15334
15730
  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
- });
15731
+ reason: entry.reason
15732
+ });
15733
+ if (Option.isNone(hubAssets)) {
15734
+ for (const entry of views.managed) entries.push({
15735
+ kind,
15736
+ name: entry.name,
15737
+ state: "synced",
15738
+ version: entry.version,
15739
+ detail: null
15740
+ });
15741
+ for (const local of views.local) {
15742
+ if (views.managed.some((entry) => entry.name === local.name)) continue;
15743
+ entries.push({
15744
+ kind,
15745
+ name: local.name,
15746
+ state: "unmanaged",
15747
+ version: null,
15748
+ detail: null
15749
+ });
15750
+ }
15751
+ continue;
15752
+ }
15753
+ const plan = planSkillSync({
15754
+ hub: views.hub,
15755
+ local: views.local,
15756
+ manifest: views.managed
15757
+ });
15758
+ const conflictByName = new Map(plan.conflicts.map((entry) => [entry.name, entry.reason]));
15759
+ const pendingByName = new Map(plan.actions.map((action) => [action._tag === "materialize" ? action.asset.name : action.name, action._tag]));
15760
+ const managedByName = new Map(views.managed.map((entry) => [entry.name, entry]));
15761
+ const hubByName = new Map(views.hub.map((asset) => [asset.name, asset]));
15762
+ const localNames = new Set(views.local.map((entry) => entry.name));
15763
+ const names = [.../* @__PURE__ */ new Set([
15764
+ ...hubByName.keys(),
15765
+ ...localNames,
15766
+ ...managedByName.keys()
15767
+ ])].sort();
15768
+ for (const name of names) {
15769
+ const version = hubByName.get(name)?.version ?? managedByName.get(name)?.version ?? null;
15770
+ const conflict = conflictByName.get(name);
15771
+ if (conflict !== void 0) {
15772
+ entries.push({
15773
+ kind,
15774
+ name,
15775
+ state: "conflict",
15776
+ version,
15777
+ detail: conflict
15778
+ });
15779
+ continue;
15780
+ }
15781
+ if (views.root === null) {
15782
+ entries.push({
15783
+ kind,
15784
+ name,
15785
+ state: "hub-only",
15786
+ version,
15787
+ detail: null
15788
+ });
15789
+ continue;
15790
+ }
15791
+ const action = pendingByName.get(name);
15792
+ if (action === "publish") {
15793
+ entries.push({
15794
+ kind,
15795
+ name,
15796
+ state: "local-edit",
15797
+ version,
15798
+ detail: null
15799
+ });
15800
+ continue;
15801
+ }
15802
+ if (action === "materialize" || action === "removeLocal") {
15803
+ entries.push({
15804
+ kind,
15805
+ name,
15806
+ state: "pending",
15807
+ version,
15808
+ detail: action
15809
+ });
15810
+ continue;
15811
+ }
15812
+ const isManaged = managedByName.has(name);
15813
+ entries.push({
15814
+ kind,
15815
+ name,
15816
+ state: isManaged ? "synced" : "unmanaged",
15817
+ version: isManaged ? version : null,
15818
+ detail: null
15819
+ });
15820
+ }
15821
+ }
15384
15822
  return {
15385
15823
  ...base,
15386
- configured: true,
15387
- hubUrl,
15824
+ configured: Option.isSome(state.settings),
15825
+ hubUrl: Option.match(state.settings, {
15826
+ onNone: () => null,
15827
+ onSome: (settings) => settings.baseUrl
15828
+ }),
15388
15829
  entries,
15389
- unavailable: null
15830
+ refused,
15831
+ unavailable: Option.isSome(state.settings) && Option.isNone(hubAssets) ? "hub is unreachable" : null
15390
15832
  };
15391
- }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("skill sync status failed", { cause }).pipe(Effect.as({
15833
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset sync status failed", { cause }).pipe(Effect.as({
15392
15834
  configured: false,
15393
15835
  hubUrl: null,
15394
15836
  syncMode: "auto",
15395
15837
  shareMode: "all",
15396
15838
  source: "none",
15397
- skillsRoot: null,
15839
+ roots: [],
15398
15840
  entries: [],
15399
15841
  refused: [],
15400
- unavailable: "could not read local skills"
15842
+ unavailable: "could not read local assets"
15401
15843
  }))));
15402
- const publish = (name) => Effect.gen(function* () {
15844
+ const publish = ({ kind, name }) => Effect.gen(function* () {
15403
15845
  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 {
15846
+ const driver = driverFor(kind);
15847
+ if (driver === void 0) return EMPTY_REPORT;
15848
+ const root = yield* driver.resolveRoot;
15849
+ const roots = yield* rootsOf();
15850
+ if (root === null) return {
15407
15851
  ...EMPTY_REPORT,
15408
15852
  ran: true,
15409
- skillsRoot
15853
+ roots
15410
15854
  };
15411
- const local = yield* readSkillDirectory(skillsRoot);
15412
- const refused = local.refused.filter((entry) => entry.name === name);
15855
+ const local = yield* driver.read(root);
15856
+ const refused = local.refused.filter((entry) => entry.name === name).map((entry) => ({
15857
+ kind,
15858
+ name: entry.name,
15859
+ reason: entry.reason
15860
+ }));
15413
15861
  if (refused.length > 0) return {
15414
15862
  ...EMPTY_REPORT,
15415
15863
  ran: true,
15416
- skillsRoot,
15864
+ roots,
15417
15865
  refused
15418
15866
  };
15419
- const skill = local.skills.find((entry) => entry.name === name);
15420
- if (skill === void 0) return {
15867
+ const asset = local.skills.find((entry) => entry.name === name);
15868
+ if (asset === void 0) return {
15421
15869
  ...EMPTY_REPORT,
15422
15870
  ran: true,
15423
- skillsRoot
15871
+ roots
15424
15872
  };
15425
15873
  const written = yield* client.write({
15426
- kind: "skill",
15874
+ kind,
15427
15875
  name,
15428
- files: skill.files
15876
+ files: asset.files
15429
15877
  }).pipe(Effect.option);
15430
15878
  if (Option.isNone(written)) return {
15431
15879
  ...EMPTY_REPORT,
15432
15880
  ran: true,
15433
- skillsRoot,
15881
+ roots,
15434
15882
  conflicts: [{
15883
+ kind,
15435
15884
  name,
15436
15885
  reason: "unmanaged-local-collision"
15437
15886
  }]
15438
15887
  };
15439
15888
  const manifest = yield* readSkillManifest(manifestPath);
15440
- const entries = entriesFor(manifest, skillsRoot).filter((entry) => entry.name !== name);
15441
- yield* writeSkillManifest(manifestPath, withEntriesFor(manifest, skillsRoot, [...entries, {
15889
+ const entries = entriesFor(manifest, root).filter((entry) => entry.name !== name);
15890
+ yield* writeSkillManifest(manifestPath, withEntriesFor(manifest, root, [...entries, {
15442
15891
  name,
15443
15892
  version: written.value.version,
15444
15893
  digest: digestSkillFiles(written.value.files)
@@ -15446,134 +15895,261 @@ const make$57 = Effect.gen(function* () {
15446
15895
  return {
15447
15896
  ...EMPTY_REPORT,
15448
15897
  ran: true,
15449
- skillsRoot,
15450
- published: [name]
15898
+ roots,
15899
+ published: [{
15900
+ kind,
15901
+ name
15902
+ }]
15451
15903
  };
15452
- }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("skill publish failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15904
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset publish failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15453
15905
  const publishAll = Effect.gen(function* () {
15454
15906
  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);
15907
+ const status = yield* assetStatus;
15908
+ const candidates = status.entries.filter((entry) => entry.state === "unmanaged");
15458
15909
  const published = [];
15459
15910
  const conflicts = [];
15460
- for (const name of candidates) {
15461
- const report = yield* publish(name);
15911
+ for (const candidate of candidates) {
15912
+ const report = yield* publish({
15913
+ kind: candidate.kind,
15914
+ name: candidate.name
15915
+ });
15462
15916
  published.push(...report.published);
15463
15917
  conflicts.push(...report.conflicts);
15464
15918
  }
15465
15919
  return {
15466
15920
  ...EMPTY_REPORT,
15467
15921
  ran: true,
15468
- skillsRoot,
15922
+ roots: status.roots,
15469
15923
  published,
15470
15924
  conflicts,
15471
15925
  refused: status.refused
15472
15926
  };
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* () {
15927
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset publish-all failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15928
+ const unpublish = ({ kind, name }) => Effect.gen(function* () {
15475
15929
  if (!(yield* isConfigured)) return EMPTY_REPORT;
15476
- const skillsRoot = yield* resolveSkillsRoot();
15930
+ const driver = driverFor(kind);
15931
+ if (driver === void 0) return EMPTY_REPORT;
15932
+ const roots = yield* rootsOf();
15933
+ const root = yield* driver.resolveRoot;
15934
+ if (root === null) return {
15935
+ ...EMPTY_REPORT,
15936
+ ran: true,
15937
+ roots
15938
+ };
15477
15939
  const manifest = yield* readSkillManifest(manifestPath);
15478
- const managed = entriesFor(manifest, skillsRoot);
15940
+ const managed = entriesFor(manifest, root);
15479
15941
  const entry = managed.find((candidate) => candidate.name === name);
15480
15942
  if (entry === void 0) return {
15481
15943
  ...EMPTY_REPORT,
15482
15944
  ran: true,
15483
- skillsRoot
15945
+ roots
15484
15946
  };
15485
15947
  const removedFromHub = yield* client.remove({
15486
- kind: "skill",
15948
+ kind,
15487
15949
  name,
15488
15950
  expectedVersion: entry.version
15489
15951
  }).pipe(Effect.option);
15490
15952
  if (Option.isNone(removedFromHub)) return {
15491
15953
  ...EMPTY_REPORT,
15492
15954
  ran: true,
15493
- skillsRoot,
15955
+ roots,
15494
15956
  conflicts: [{
15957
+ kind,
15495
15958
  name,
15496
15959
  reason: "edited-both-sides"
15497
15960
  }]
15498
15961
  };
15499
- yield* writeSkillManifest(manifestPath, withEntriesFor(manifest, skillsRoot, managed.filter((candidate) => candidate.name !== name)));
15962
+ yield* writeSkillManifest(manifestPath, withEntriesFor(manifest, root, managed.filter((candidate) => candidate.name !== name)));
15500
15963
  return {
15501
15964
  ...EMPTY_REPORT,
15502
15965
  ran: true,
15503
- skillsRoot,
15504
- removed: [name]
15966
+ roots,
15967
+ removed: [{
15968
+ kind,
15969
+ name
15970
+ }]
15971
+ };
15972
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset unpublish failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15973
+ const read = ({ kind, name }) => client.get({
15974
+ kind,
15975
+ name
15976
+ }).pipe(Effect.map((asset) => Option.match(asset, {
15977
+ onNone: () => ({
15978
+ found: false,
15979
+ kind,
15980
+ name,
15981
+ files: [],
15982
+ version: null,
15983
+ updatedAt: null,
15984
+ unavailable: null
15985
+ }),
15986
+ onSome: (found) => ({
15987
+ found: true,
15988
+ kind,
15989
+ name,
15990
+ files: found.files,
15991
+ version: found.version,
15992
+ updatedAt: found.updatedAt,
15993
+ unavailable: null
15994
+ })
15995
+ })), Effect.catchCause((cause) => Effect.logWarning("asset read failed", { cause }).pipe(Effect.as({
15996
+ found: false,
15997
+ kind,
15998
+ name,
15999
+ files: [],
16000
+ version: null,
16001
+ updatedAt: null,
16002
+ unavailable: "hub is unreachable"
16003
+ }))));
16004
+ const save = ({ kind, name, files, expectedVersion }) => Effect.gen(function* () {
16005
+ if (!(yield* isConfigured)) return EMPTY_REPORT;
16006
+ const written = yield* client.write({
16007
+ kind,
16008
+ name,
16009
+ files,
16010
+ expectedVersion
16011
+ }).pipe(Effect.option);
16012
+ if (Option.isNone(written)) return {
16013
+ ...EMPTY_REPORT,
16014
+ ran: true,
16015
+ roots: yield* rootsOf(),
16016
+ conflicts: [{
16017
+ kind,
16018
+ name,
16019
+ reason: "edited-both-sides"
16020
+ }]
16021
+ };
16022
+ return yield* run;
16023
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset save failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
16024
+ const deleteFromHub = ({ kind, name, expectedVersion }) => Effect.gen(function* () {
16025
+ if (!(yield* isConfigured)) return EMPTY_REPORT;
16026
+ const removed = yield* client.remove({
16027
+ kind,
16028
+ name,
16029
+ ...expectedVersion === void 0 ? {} : { expectedVersion }
16030
+ }).pipe(Effect.option);
16031
+ if (Option.isNone(removed)) return {
16032
+ ...EMPTY_REPORT,
16033
+ ran: true,
16034
+ roots: yield* rootsOf(),
16035
+ conflicts: [{
16036
+ kind,
16037
+ name,
16038
+ reason: "edited-both-sides"
16039
+ }]
15505
16040
  };
15506
- }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("skill unpublish failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
16041
+ return yield* run;
16042
+ }).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset delete failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
15507
16043
  return {
15508
16044
  run,
15509
- status: skillStatus,
16045
+ status: assetStatus,
15510
16046
  publish,
15511
16047
  publishAll,
15512
- unpublish
16048
+ unpublish,
16049
+ read,
16050
+ save,
16051
+ deleteFromHub
15513
16052
  };
15514
16053
  });
15515
- const layer$52 = Layer.effect(SkillSync, make$57);
16054
+ const layer$53 = Layer.effect(AssetSync, make$58);
15516
16055
  //#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));
16056
+ //#region src/cli/assets.ts
16057
+ 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
16058
  const runWithSync = (flags, run) => Effect.gen(function* () {
15520
16059
  const config = yield* resolveCliAuthConfig(flags, yield* GlobalFlag.LogLevel);
15521
16060
  return yield* Effect.gen(function* () {
15522
16061
  return yield* run({
15523
- sync: yield* SkillSync,
16062
+ sync: yield* AssetSync,
15524
16063
  link: yield* HubLink
15525
16064
  });
15526
- }).pipe(Effect.provide(SyncServicesLive.pipe(Layer.provide(layer$65(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, "Error")))));
16065
+ }).pipe(Effect.provide(SyncServicesLive.pipe(Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, "Error")))));
15527
16066
  });
15528
16067
  const STATE_LABEL = {
15529
16068
  synced: "synced",
15530
16069
  pending: "will update",
15531
16070
  "local-edit": "will publish",
15532
16071
  conflict: "CONFLICT",
15533
- unmanaged: "not shared"
16072
+ unmanaged: "not shared",
16073
+ "hub-only": "on the hub only"
16074
+ };
16075
+ const SKILL_WORDS = {
16076
+ kind: "skill",
16077
+ command: "skills",
16078
+ singular: "skill",
16079
+ plural: "skills",
16080
+ rootLabel: "Skills",
16081
+ nameDescription: "Name of the skill directory."
16082
+ };
16083
+ const MEMORY_WORDS = {
16084
+ kind: "memory",
16085
+ command: "memory",
16086
+ singular: "memory file",
16087
+ plural: "memory files",
16088
+ rootLabel: "Memory",
16089
+ nameDescription: "Memory filename, for example CLAUDE.md."
15534
16090
  };
16091
+ const MCP_WORDS = {
16092
+ kind: "mcp",
16093
+ command: "mcp",
16094
+ singular: "MCP server",
16095
+ plural: "MCP servers",
16096
+ rootLabel: "MCP",
16097
+ nameDescription: "Registration filename, for example linear.json."
16098
+ };
16099
+ const rootFor = (status, kind) => status.roots.find((root) => root.kind === kind)?.path ?? null;
15535
16100
  /**
15536
16101
  * Exported for tests. These two are the only place a wrong report becomes a
15537
16102
  * confident sentence, so they are worth pinning: "Already up to date" and
15538
16103
  * "nothing changed" must never be printed for a run that failed.
15539
16104
  */
15540
- const formatStatus = (status) => {
16105
+ const formatStatus = (status, words = SKILL_WORDS) => {
15541
16106
  const lines = [];
15542
- if (!status.configured) lines.push("No hub is configured. Connect one with: p4 hub connect --url <url> --token <token>");
16107
+ if (!status.configured) lines.push("No hub is configured. Connect one with: p4c hub connect --url <url> --token <token>");
15543
16108
  else {
15544
16109
  lines.push(`Hub: ${status.hubUrl ?? "unknown"}${status.source === "environment" ? " (from the environment)" : ""}`);
15545
16110
  lines.push(`Sync: ${status.syncMode}`);
15546
- lines.push(`Share: ${status.shareMode === "all" ? "all skills (new ones share automatically)" : "only the ones you choose"}`);
16111
+ lines.push(`Share: ${status.shareMode === "all" ? `all ${words.plural} (new ones share automatically)` : "only the ones you choose"}`);
15547
16112
  }
15548
- if (status.skillsRoot !== null) lines.push(`Skills: ${status.skillsRoot}`);
16113
+ const root = rootFor(status, words.kind);
16114
+ if (root !== null) lines.push(`${words.rootLabel}: ${root}`);
15549
16115
  if (status.unavailable !== null) lines.push(`Warning: ${status.unavailable}`);
15550
16116
  lines.push("");
15551
- if (status.entries.length === 0) lines.push("No skills found.");
15552
- else for (const entry of status.entries) {
16117
+ const entries = status.entries.filter((entry) => entry.kind === words.kind);
16118
+ if (entries.length === 0) lines.push(`No ${words.plural} found.`);
16119
+ else for (const entry of entries) {
15553
16120
  const version = entry.version === null ? "" : ` v${entry.version}`;
15554
16121
  const detail = entry.detail === null ? "" : ` (${entry.detail})`;
15555
16122
  lines.push(` ${entry.name.padEnd(28)} ${STATE_LABEL[entry.state] ?? entry.state}${version}${detail}`);
15556
16123
  }
15557
- for (const refusal of status.refused) lines.push(` ${refusal.name.padEnd(28)} cannot sync (${refusal.reason})`);
16124
+ for (const refusal of status.refused.filter((entry) => entry.kind === words.kind)) lines.push(` ${refusal.name.padEnd(28)} cannot sync (${refusal.reason})`);
15558
16125
  return lines.join("\n");
15559
16126
  };
16127
+ /**
16128
+ * A report covers every kind at once, so names are qualified as soon as more
16129
+ * than one kind appears in it. `published CLAUDE.md` is ambiguous in a way
16130
+ * `published memory CLAUDE.md` is not — a skill directory may legitimately be
16131
+ * called `CLAUDE.md`, since the name schema permits dots.
16132
+ */
16133
+ const describeRefs = (refs) => {
16134
+ const kinds = new Set(refs.map((ref) => ref.kind));
16135
+ return refs.map((ref) => kinds.size > 1 ? `${ref.kind} ${ref.name}` : ref.name).join(", ");
16136
+ };
15560
16137
  const formatReport = (report) => {
15561
16138
  if (!report.ran) return "No hub is configured; nothing to do.";
15562
16139
  if (report.unavailable !== null) return `Hub is unreachable (${report.unavailable}); nothing changed.`;
15563
16140
  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(", ")}`);
16141
+ if (report.materialized.length > 0) parts.push(`updated ${describeRefs(report.materialized)}`);
16142
+ if (report.published.length > 0) parts.push(`published ${describeRefs(report.published)}`);
16143
+ if (report.removed.length > 0) parts.push(`removed ${describeRefs(report.removed)}`);
15567
16144
  for (const conflict of report.conflicts) parts.push(`CONFLICT on ${conflict.name} (${conflict.reason})`);
15568
16145
  for (const refusal of report.refused) parts.push(`cannot sync ${refusal.name} (${refusal.reason})`);
15569
16146
  const summary = parts.length === 0 ? "Already up to date." : parts.join("\n");
15570
16147
  if (report.unmanaged.length === 0) return summary;
15571
16148
  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`;
16149
+ 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
16150
  };
15574
16151
  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."));
16152
+ const tokenFlag = Flag.string("token").pipe(Flag.withDescription("Bearer token for this server, from `p4c hub token add`."));
15577
16153
  const hubConnectCommand = Command.make("connect", {
15578
16154
  ...authLocationFlags,
15579
16155
  url: urlFlag,
@@ -15589,32 +16165,74 @@ const hubDisconnectCommand = Command.make("disconnect", { ...authLocationFlags }
15589
16165
  yield* link.disconnect;
15590
16166
  yield* Console.log(formatStatus(yield* sync.status));
15591
16167
  }))));
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* () {
16168
+ const makeAssetCommands = (words) => {
16169
+ const nameArgument = Argument.string("name").pipe(Argument.withDescription(words.nameDescription));
16170
+ return {
16171
+ 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))))))),
16172
+ 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))))))),
16173
+ publishCommand: Command.make("publish", {
16174
+ ...authLocationFlags,
16175
+ name: nameArgument.pipe(Argument.optional),
16176
+ all: Flag.boolean("all").pipe(Flag.withDescription("Share everything local this machine does not already share."))
16177
+ }).pipe(Command.withDescription(`Share local ${words.plural} through the hub, so every machine gets them.`), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => Effect.gen(function* () {
16178
+ if (flags.all) return yield* sync.publishAll.pipe(Effect.flatMap((report) => Console.log(formatReport(report))));
16179
+ const name = Option.getOrUndefined(flags.name);
16180
+ if (name === void 0) return yield* Console.error(`Name a ${words.singular}, or pass --all to share every unshared one.`);
16181
+ return yield* sync.publish({
16182
+ kind: words.kind,
16183
+ name
16184
+ }).pipe(Effect.flatMap((report) => Console.log(formatReport(report))));
16185
+ })))),
16186
+ unpublishCommand: Command.make("unpublish", {
16187
+ ...authLocationFlags,
16188
+ name: nameArgument
16189
+ }).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({
16190
+ kind: words.kind,
16191
+ name: flags.name
16192
+ }).pipe(Effect.flatMap((report) => Console.log(formatReport(report))))))),
16193
+ showCommand: Command.make("show", {
16194
+ ...authLocationFlags,
16195
+ name: nameArgument
16196
+ }).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* () {
16197
+ const name = flags.name;
16198
+ const content = yield* sync.read({
16199
+ kind: words.kind,
16200
+ name
16201
+ });
16202
+ if (content.unavailable !== null) return yield* Console.error(`Hub is unreachable (${content.unavailable}).`);
16203
+ if (!content.found) return yield* Console.error(`The hub has no ${words.singular} named ${name}.`);
16204
+ const lines = [`${name} v${content.version ?? "?"} updated ${content.updatedAt ?? "?"}`];
16205
+ for (const file of content.files) lines.push("", `--- ${file.path} ---`, file.content.trimEnd());
16206
+ const root = rootFor(yield* sync.status, words.kind);
16207
+ if (root !== null) lines.push("", `Edit it here and the next sync publishes it: ${root}/${name}`);
16208
+ return yield* Console.log(lines.join("\n"));
16209
+ })))),
16210
+ removeCommand: Command.make("rm", {
16211
+ ...authLocationFlags,
16212
+ name: nameArgument,
16213
+ yes: Flag.boolean("yes").pipe(Flag.withDescription("Confirm: this removes it from the hub and from every machine."))
16214
+ }).pipe(Command.withDescription(`Delete a ${words.singular} from the hub and from every machine.`), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => Effect.gen(function* () {
16215
+ 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}`);
16216
+ const report = yield* sync.deleteFromHub({
16217
+ kind: words.kind,
16218
+ name: flags.name
16219
+ });
16220
+ return yield* Console.log(formatReport(report));
16221
+ }))))
16222
+ };
16223
+ };
16224
+ const skillCommands = makeAssetCommands(SKILL_WORDS);
16225
+ const memoryCommands = makeAssetCommands(MEMORY_WORDS);
16226
+ const mcpCommands = makeAssetCommands(MCP_WORDS);
16227
+ 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
16228
  yield* link.setShareMode("all");
15611
16229
  yield* Console.log(formatStatus(yield* sync.status));
15612
16230
  }))));
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* () {
16231
+ 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
16232
  yield* link.setShareMode("selected");
15615
16233
  yield* Console.log(formatStatus(yield* sync.status));
15616
16234
  }))));
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* () {
16235
+ 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
16236
  yield* link.setSyncMode("paused");
15619
16237
  yield* Console.log(formatStatus(yield* sync.status));
15620
16238
  }))));
@@ -15623,15 +16241,41 @@ const resumeCommand = Command.make("resume", { ...authLocationFlags }).pipe(Comm
15623
16241
  yield* Console.log(formatStatus(yield* sync.status));
15624
16242
  }))));
15625
16243
  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,
16244
+ skillCommands.statusCommand,
16245
+ skillCommands.syncCommand,
16246
+ skillCommands.publishCommand,
16247
+ skillCommands.unpublishCommand,
16248
+ skillCommands.showCommand,
16249
+ skillCommands.removeCommand,
15630
16250
  pauseCommand,
15631
16251
  resumeCommand,
15632
16252
  shareAllCommand,
15633
16253
  shareSelectedCommand
15634
16254
  ]));
16255
+ const memoryCommand = Command.make("memory", {}).pipe(Command.withDescription("Share user memory — CLAUDE.md, AGENTS.md — across your machines."), Command.withSubcommands([
16256
+ memoryCommands.statusCommand,
16257
+ memoryCommands.syncCommand,
16258
+ memoryCommands.publishCommand,
16259
+ memoryCommands.unpublishCommand,
16260
+ memoryCommands.showCommand,
16261
+ memoryCommands.removeCommand
16262
+ ]));
16263
+ /**
16264
+ * The sharing half of `p4c mcp`.
16265
+ *
16266
+ * Registering a server — its command, its URL, its tokens — belongs in the
16267
+ * panel, where a form can ask for a secret and put it somewhere that does not
16268
+ * sync. These subcommands are the same six every kind gets, and what they move
16269
+ * is the definition only.
16270
+ */
16271
+ const mcpCommand = Command.make("mcp", {}).pipe(Command.withDescription("Share MCP server registrations across your machines."), Command.withSubcommands([
16272
+ mcpCommands.statusCommand,
16273
+ mcpCommands.syncCommand,
16274
+ mcpCommands.publishCommand,
16275
+ mcpCommands.unpublishCommand,
16276
+ mcpCommands.showCommand,
16277
+ mcpCommands.removeCommand
16278
+ ]));
15635
16279
  /** Exported for the hub command, which owns `connect`/`disconnect`. */
15636
16280
  const hubLinkCommands = [hubConnectCommand, hubDisconnectCommand];
15637
16281
  //#endregion
@@ -15729,7 +16373,7 @@ const serveHubCommand = Command.make("serve", {
15729
16373
  fileTokenHashes: hubTokenHashes(tokenFile)
15730
16374
  });
15731
16375
  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");
16376
+ 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
16377
  return yield* runHub.pipe(Effect.provideService(HubConfig, HubConfig.of({
15734
16378
  dbPath: config.dbPath,
15735
16379
  host: config.host,
@@ -20307,6 +20951,273 @@ function mergeGitStatusParts(local, remote) {
20307
20951
  };
20308
20952
  }
20309
20953
  //#endregion
20954
+ //#region src/stream/collectUint8StreamText.ts
20955
+ const collectUint8StreamText = (input) => {
20956
+ const maxBytes = input.maxBytes ?? Number.POSITIVE_INFINITY;
20957
+ const truncatedMarker = input.truncatedMarker ?? "";
20958
+ return input.stream.pipe(Stream.runFold(() => ({
20959
+ chunks: [],
20960
+ bytes: 0,
20961
+ truncated: false
20962
+ }), (state, chunk) => {
20963
+ if (state.truncated) return state;
20964
+ const remainingBytes = maxBytes - state.bytes;
20965
+ if (remainingBytes <= 0) return {
20966
+ ...state,
20967
+ truncated: true
20968
+ };
20969
+ const nextChunk = chunk.byteLength > remainingBytes ? chunk.slice(0, remainingBytes) : chunk;
20970
+ state.chunks.push(nextChunk);
20971
+ const bytes = state.bytes + nextChunk.byteLength;
20972
+ const truncated = chunk.byteLength > remainingBytes;
20973
+ return {
20974
+ chunks: state.chunks,
20975
+ bytes,
20976
+ truncated
20977
+ };
20978
+ }), Effect.map((state) => {
20979
+ const text = Buffer.concat(state.chunks, state.bytes).toString("utf8");
20980
+ return {
20981
+ text: state.truncated && truncatedMarker.length > 0 ? `${text}${truncatedMarker}` : text,
20982
+ bytes: state.bytes,
20983
+ truncated: state.truncated
20984
+ };
20985
+ }));
20986
+ };
20987
+ //#endregion
20988
+ //#region src/processRunner.ts
20989
+ const ProcessInvocationFields = {
20990
+ command: Schema$1.String,
20991
+ argumentCount: Schema$1.Number,
20992
+ cwd: Schema$1.optional(Schema$1.String),
20993
+ spawnCwd: Schema$1.optional(Schema$1.String)
20994
+ };
20995
+ const formatProcessInvocation = (input) => {
20996
+ const executionCwd = input.spawnCwd ?? input.cwd;
20997
+ return executionCwd === void 0 ? `'${input.command}'` : `'${input.command}' in '${executionCwd}'`;
20998
+ };
20999
+ var ProcessSpawnError = class extends Schema$1.TaggedErrorClass()("ProcessSpawnError", {
21000
+ ...ProcessInvocationFields,
21001
+ resolvedCommand: Schema$1.optional(Schema$1.String),
21002
+ resolvedArgumentCount: Schema$1.optional(Schema$1.Number),
21003
+ shell: Schema$1.optional(Schema$1.Boolean),
21004
+ cause: Schema$1.Defect()
21005
+ }) {
21006
+ get message() {
21007
+ return `Failed to spawn process ${formatProcessInvocation(this)}`;
21008
+ }
21009
+ };
21010
+ var ProcessStdinError = class extends Schema$1.TaggedErrorClass()("ProcessStdinError", {
21011
+ ...ProcessInvocationFields,
21012
+ stdinBytes: Schema$1.Number,
21013
+ cause: Schema$1.Defect()
21014
+ }) {
21015
+ get message() {
21016
+ return `Failed to write stdin for process ${formatProcessInvocation(this)}`;
21017
+ }
21018
+ };
21019
+ var ProcessOutputLimitError = class extends Schema$1.TaggedErrorClass()("ProcessOutputLimitError", {
21020
+ ...ProcessInvocationFields,
21021
+ stream: Schema$1.Literals(["stdout", "stderr"]),
21022
+ maxBytes: Schema$1.Number,
21023
+ observedBytes: Schema$1.Number
21024
+ }) {
21025
+ get message() {
21026
+ return `Process ${formatProcessInvocation(this)} ${this.stream} produced ${this.observedBytes} bytes, exceeding the ${this.maxBytes} byte limit`;
21027
+ }
21028
+ };
21029
+ var ProcessReadError = class extends Schema$1.TaggedErrorClass()("ProcessReadError", {
21030
+ ...ProcessInvocationFields,
21031
+ stream: Schema$1.Literals([
21032
+ "stdout",
21033
+ "stderr",
21034
+ "exitCode"
21035
+ ]),
21036
+ cause: Schema$1.Defect()
21037
+ }) {
21038
+ get message() {
21039
+ return `Failed to read ${this.stream} for process ${formatProcessInvocation(this)}`;
21040
+ }
21041
+ };
21042
+ var ProcessTimeoutError = class extends Schema$1.TaggedErrorClass()("ProcessTimeoutError", {
21043
+ ...ProcessInvocationFields,
21044
+ timeoutMs: Schema$1.Number
21045
+ }) {
21046
+ get message() {
21047
+ return `Process ${formatProcessInvocation(this)} timed out after ${this.timeoutMs}ms`;
21048
+ }
21049
+ };
21050
+ Schema$1.Union([
21051
+ ProcessSpawnError,
21052
+ ProcessStdinError,
21053
+ ProcessOutputLimitError,
21054
+ ProcessReadError,
21055
+ ProcessTimeoutError
21056
+ ]);
21057
+ var ProcessRunner = class extends Context.Service()("@p4code/cli/processRunner") {};
21058
+ const DEFAULT_TIMEOUT = "60 seconds";
21059
+ const DEFAULT_MAX_OUTPUT_BYTES$2 = 8 * 1024 * 1024;
21060
+ const WINDOWS_COMMAND_NOT_FOUND_PATTERNS = [
21061
+ /is not recognized as an internal or external command/i,
21062
+ /n.o . reconhecido como um comando interno/i,
21063
+ /non . riconosciuto come comando interno o esterno/i,
21064
+ /n.est pas reconnu en tant que commande interne/i,
21065
+ /no se reconoce como un comando interno o externo/i,
21066
+ /wird nicht als interner oder externer befehl/i
21067
+ ];
21068
+ function hasWindowsCommandNotFoundMessage(output) {
21069
+ return WINDOWS_COMMAND_NOT_FOUND_PATTERNS.some((pattern) => pattern.test(output));
21070
+ }
21071
+ const isWindowsCommandNotFound = Effect.fn("processRunner.isWindowsCommandNotFound")(function* (code, stderr) {
21072
+ if ((yield* HostProcessPlatform) !== "win32") return false;
21073
+ if (code === 9009) return true;
21074
+ return hasWindowsCommandNotFoundMessage(stderr);
21075
+ });
21076
+ const collectText = Effect.fn("processRunner.collectText")(function* (input) {
21077
+ const stream = input.stream.pipe(Stream.mapError((cause) => new ProcessReadError({
21078
+ command: input.command,
21079
+ argumentCount: input.args.length,
21080
+ cwd: input.cwd,
21081
+ spawnCwd: input.spawnCwd,
21082
+ stream: input.streamName,
21083
+ cause
21084
+ })));
21085
+ if (input.outputMode === "truncate") return yield* collectUint8StreamText({
21086
+ stream,
21087
+ maxBytes: input.maxOutputBytes,
21088
+ truncatedMarker: input.truncatedMarker
21089
+ });
21090
+ return yield* stream.pipe(Stream.runFoldEffect(() => ({
21091
+ chunks: [],
21092
+ bytes: 0
21093
+ }), (state, chunk) => {
21094
+ const remainingBytes = input.maxOutputBytes - state.bytes;
21095
+ if (chunk.byteLength > remainingBytes) return Effect.fail(new ProcessOutputLimitError({
21096
+ command: input.command,
21097
+ argumentCount: input.args.length,
21098
+ cwd: input.cwd,
21099
+ spawnCwd: input.spawnCwd,
21100
+ stream: input.streamName,
21101
+ maxBytes: input.maxOutputBytes,
21102
+ observedBytes: state.bytes + chunk.byteLength
21103
+ }));
21104
+ state.chunks.push(chunk);
21105
+ return Effect.succeed({
21106
+ chunks: state.chunks,
21107
+ bytes: state.bytes + chunk.byteLength
21108
+ });
21109
+ }), Effect.map((state) => ({
21110
+ text: Buffer.concat(state.chunks, state.bytes).toString("utf8"),
21111
+ bytes: state.bytes,
21112
+ truncated: false
21113
+ })));
21114
+ });
21115
+ function finalizeRunProcess(effect, input) {
21116
+ const timeout = Duration.fromInputUnsafe(input.timeout ?? DEFAULT_TIMEOUT);
21117
+ const timeoutBehavior = input.timeoutBehavior ?? "error";
21118
+ return effect.pipe(Effect.scoped, Effect.timeoutOption(timeout), Effect.flatMap((result) => {
21119
+ if (Option.isSome(result)) return Effect.succeed(result.value);
21120
+ if (timeoutBehavior === "timedOutResult") return Effect.succeed({
21121
+ stdout: "",
21122
+ stderr: "",
21123
+ code: null,
21124
+ timedOut: true,
21125
+ stdoutTruncated: false,
21126
+ stderrTruncated: false
21127
+ });
21128
+ return Effect.fail(new ProcessTimeoutError({
21129
+ command: input.command,
21130
+ argumentCount: input.args.length,
21131
+ cwd: input.cwd,
21132
+ spawnCwd: input.spawnCwd,
21133
+ timeoutMs: Duration.toMillis(timeout)
21134
+ }));
21135
+ }));
21136
+ }
21137
+ const runProcessCore = Effect.fn("processRunner.runProcessCore")(function* (spawner, input) {
21138
+ const maxOutputBytes = input.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES$2;
21139
+ const outputMode = input.outputMode ?? "error";
21140
+ const truncatedMarker = input.truncatedMarker ?? "";
21141
+ const extendEnv = input.env !== void 0;
21142
+ const spawnCommand = yield* resolveSpawnCommand(input.command, input.args, input.env === void 0 ? {} : {
21143
+ env: input.env,
21144
+ extendEnv
21145
+ });
21146
+ const child = yield* spawner.spawn(ChildProcess$1.make(spawnCommand.command, spawnCommand.args, {
21147
+ ...input.spawnCwd ?? input.cwd ? { cwd: input.spawnCwd ?? input.cwd } : {},
21148
+ ...input.env !== void 0 ? {
21149
+ env: input.env,
21150
+ extendEnv
21151
+ } : {},
21152
+ shell: spawnCommand.shell
21153
+ })).pipe(Effect.mapError((cause) => new ProcessSpawnError({
21154
+ command: input.command,
21155
+ argumentCount: input.args.length,
21156
+ cwd: input.cwd,
21157
+ spawnCwd: input.spawnCwd,
21158
+ resolvedCommand: spawnCommand.command,
21159
+ resolvedArgumentCount: spawnCommand.args.length,
21160
+ shell: spawnCommand.shell,
21161
+ cause
21162
+ })));
21163
+ const stdin = input.stdin;
21164
+ const writeStdin = stdin === void 0 ? Effect.void : Stream.run(Stream.encodeText(Stream.make(stdin)), child.stdin).pipe(Effect.mapError((cause) => new ProcessStdinError({
21165
+ command: input.command,
21166
+ argumentCount: input.args.length,
21167
+ cwd: input.cwd,
21168
+ spawnCwd: input.spawnCwd,
21169
+ stdinBytes: Buffer.byteLength(stdin),
21170
+ cause
21171
+ })));
21172
+ const [stdout, stderr] = yield* Effect.all([
21173
+ collectText({
21174
+ command: input.command,
21175
+ args: input.args,
21176
+ cwd: input.cwd,
21177
+ spawnCwd: input.spawnCwd,
21178
+ streamName: "stdout",
21179
+ stream: child.stdout,
21180
+ maxOutputBytes,
21181
+ outputMode,
21182
+ truncatedMarker
21183
+ }),
21184
+ collectText({
21185
+ command: input.command,
21186
+ args: input.args,
21187
+ cwd: input.cwd,
21188
+ spawnCwd: input.spawnCwd,
21189
+ streamName: "stderr",
21190
+ stream: child.stderr,
21191
+ maxOutputBytes,
21192
+ outputMode,
21193
+ truncatedMarker
21194
+ }),
21195
+ writeStdin
21196
+ ], { concurrency: "unbounded" });
21197
+ const exitCode = yield* child.exitCode.pipe(Effect.mapError((cause) => new ProcessReadError({
21198
+ command: input.command,
21199
+ argumentCount: input.args.length,
21200
+ cwd: input.cwd,
21201
+ spawnCwd: input.spawnCwd,
21202
+ stream: "exitCode",
21203
+ cause
21204
+ })));
21205
+ return {
21206
+ stdout: stdout.text,
21207
+ stderr: stderr.text,
21208
+ code: exitCode,
21209
+ timedOut: false,
21210
+ stdoutTruncated: stdout.truncated,
21211
+ stderrTruncated: stderr.truncated
21212
+ };
21213
+ });
21214
+ const make$57 = Effect.fn("ProcessRunner.make")(function* () {
21215
+ const spawner = yield* ChildProcessSpawner$1.ChildProcessSpawner;
21216
+ const run = (input) => finalizeRunProcess(runProcessCore(spawner, input), input);
21217
+ return ProcessRunner.of({ run });
21218
+ });
21219
+ const layer$52 = Layer.effect(ProcessRunner, make$57());
21220
+ //#endregion
20310
21221
  //#region src/project/RepositoryIdentityResolver.ts
20311
21222
  const DEFAULT_REPOSITORY_IDENTITY_CACHE_CAPACITY = 512;
20312
21223
  const DEFAULT_POSITIVE_CACHE_TTL = Duration.minutes(1);
@@ -20411,7 +21322,7 @@ const make$56 = Effect.fn("RepositoryIdentityResolver.make")(function* (options
20411
21322
  });
20412
21323
  return RepositoryIdentityResolver.of({ resolve });
20413
21324
  });
20414
- const layer$51 = Layer.effect(RepositoryIdentityResolver, make$56()).pipe(Layer.provide(layer$57));
21325
+ const layer$51 = Layer.effect(RepositoryIdentityResolver, make$56()).pipe(Layer.provide(layer$52));
20415
21326
  //#endregion
20416
21327
  //#region src/orchestration/Layers/ProjectionSnapshotQuery.ts
20417
21328
  const decodeReadModel = Schema$1.decodeUnknownEffect(OrchestrationReadModel);
@@ -23316,6 +24227,7 @@ const make$51 = Effect.fn("cloud.boot_service.make")(function* (input) {
23316
24227
  });
23317
24228
  const activateLaunchAgent = Effect.gen(function* () {
23318
24229
  yield* runStep("unloading the previous service", "launchctl", ["bootout", serviceTarget]).pipe(Effect.ignore);
24230
+ yield* runStep("enabling the service", "launchctl", ["enable", serviceTarget]).pipe(Effect.ignore);
23319
24231
  yield* runStep("loading the service", "launchctl", [
23320
24232
  "bootstrap",
23321
24233
  guiDomain,
@@ -23429,7 +24341,7 @@ const layer$46 = (input) => Layer.effect(BootService, make$51(input));
23429
24341
  //#endregion
23430
24342
  //#region src/service/selfUpdate.ts
23431
24343
  /**
23432
- * Lets a connected client replace this server with another published `p4`
24344
+ * Lets a connected client replace this server with another published `p4c`
23433
24345
  * version over RPC — the only update path that works when the user is not at
23434
24346
  * the machine (phone against a home server, relay-managed box). The target
23435
24347
  * version is npm-installed into the pinned runtime and verified before
@@ -23648,7 +24560,7 @@ const make$50 = Effect.fn("cloud.server_self_update.make")(function* (options) {
23648
24560
  });
23649
24561
  return ServerSelfUpdate.of({ update });
23650
24562
  });
23651
- const layer$45 = Layer.effect(ServerSelfUpdate, make$50()).pipe(Layer.provide(layer$57));
24563
+ const layer$45 = Layer.effect(ServerSelfUpdate, make$50()).pipe(Layer.provide(layer$52));
23652
24564
  //#endregion
23653
24565
  //#region src/environment/ServerEnvironmentLabel.ts
23654
24566
  const ServerEnvironmentLabelCommandProbe = Schema$1.Literals(["macos-computer-name", "linux-pretty-hostname"]);
@@ -23843,7 +24755,7 @@ const make$49 = Effect.gen(function* () {
23843
24755
  * state. It intentionally has no fallback Layer.succeed value: callers must
23844
24756
  * provide the external platform services and a ServerConfig.
23845
24757
  */
23846
- const layer$44 = Layer.effect(ServerEnvironment, make$49).pipe(Layer.provide(layer$57));
24758
+ const layer$44 = Layer.effect(ServerEnvironment, make$49).pipe(Layer.provide(layer$52));
23847
24759
  //#endregion
23848
24760
  //#region src/provider/Services/ProviderSessionReaper.ts
23849
24761
  var ProviderSessionReaper = class extends Context.Service()("@p4code/cli/provider/Services/ProviderSessionReaper") {};
@@ -24927,7 +25839,7 @@ const make$48 = Effect.gen(function* () {
24927
25839
  const lifecycleEvents = yield* ServerLifecycleEvents;
24928
25840
  const serverSettings = yield* ServerSettingsService;
24929
25841
  const serverEnvironment = yield* ServerEnvironment;
24930
- const skillSync = yield* SkillSync;
25842
+ const assetSync = yield* AssetSync;
24931
25843
  const hubLink = yield* HubLink;
24932
25844
  const crypto = yield* Crypto.Crypto;
24933
25845
  const commandGate = yield* makeCommandGate;
@@ -24949,13 +25861,13 @@ const make$48 = Effect.gen(function* () {
24949
25861
  environmentVariable: error.environmentVariable,
24950
25862
  cause: error.cause
24951
25863
  })), Effect.forkScoped));
24952
- yield* Effect.logDebug("startup phase: syncing user-scope skills");
25864
+ yield* Effect.logDebug("startup phase: syncing user-scope agent assets");
24953
25865
  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;
25866
+ if ((yield* hubLink.current).syncMode !== "auto") return yield* Effect.logDebug("asset sync is paused; skipping the automatic pass");
25867
+ const report = yield* assetSync.run;
24956
25868
  if (!report.ran) return;
24957
- yield* Effect.log("skill sync", {
24958
- skillsRoot: report.skillsRoot,
25869
+ yield* Effect.log("asset sync", {
25870
+ roots: report.roots,
24959
25871
  materialized: report.materialized,
24960
25872
  published: report.published,
24961
25873
  removed: report.removed,
@@ -25375,7 +26287,7 @@ const ProjectCliRuntimeLive = Layer.mergeAll(layer$42, OrchestrationLayerLive.pi
25375
26287
  const PROJECT_CLI_LIVE_SERVER_TIMEOUT = Duration.seconds(1);
25376
26288
  const withProjectCliSessionToken = (environmentAuth, run) => Effect.acquireUseRelease(environmentAuth.issueSession({
25377
26289
  scopes: AuthAdministrativeScopes,
25378
- label: "p4 project cli"
26290
+ label: "p4c project cli"
25379
26291
  }), (issued) => run(issued.token), (issued) => environmentAuth.revokeSession(issued.sessionId).pipe(Effect.ignore({ log: true })));
25380
26292
  const withProjectCliLiveServerTimeout = (effect) => effect.pipe(Effect.timeout(PROJECT_CLI_LIVE_SERVER_TIMEOUT));
25381
26293
  const makeLiveServerClient = (origin) => HttpApiClient.make(EnvironmentHttpApi, { baseUrl: origin });
@@ -25462,7 +26374,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (flags, run
25462
26374
  });
25463
26375
  yield* Console.log(output);
25464
26376
  }));
25465
- const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(Layer.provide(layer$65(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)));
26377
+ const offlineRuntimeLayer = ProjectCliRuntimeLive.pipe(Layer.provide(layer$66(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)));
25466
26378
  return yield* Effect.gen(function* () {
25467
26379
  const snapshot = yield* getOfflineSnapshot();
25468
26380
  const orchestrationEngine = yield* OrchestrationEngineService;
@@ -25473,7 +26385,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (flags, run
25473
26385
  });
25474
26386
  yield* Console.log(output);
25475
26387
  }).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)))));
26388
+ }).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
26389
  });
25478
26390
  const projectAddCommand = Command.make("add", {
25479
26391
  ...projectLocationFlags,
@@ -28768,7 +29680,7 @@ const make$43 = Effect.gen(function* () {
28768
29680
  });
28769
29681
  return VcsProcess.of({ run });
28770
29682
  });
28771
- const layer$38 = Layer.effect(VcsProcess, make$43).pipe(Layer.provide(layer$57));
29683
+ const layer$38 = Layer.effect(VcsProcess, make$43).pipe(Layer.provide(layer$52));
28772
29684
  //#endregion
28773
29685
  //#region src/vcs/VcsDriver.ts
28774
29686
  var VcsDriver = class extends Context.Service()("@p4code/cli/vcs/VcsDriver") {};
@@ -32549,7 +33461,7 @@ const makeWithOptions$1 = Effect.fn("TerminalManager.makeWithOptions")(function*
32549
33461
  subscribeMetadata
32550
33462
  });
32551
33463
  });
32552
- const layer$30 = Layer.effect(TerminalManager, make$35()).pipe(Layer.provide(layer$57));
33464
+ const layer$30 = Layer.effect(TerminalManager, make$35()).pipe(Layer.provide(layer$52));
32553
33465
  //#endregion
32554
33466
  //#region src/mcp/McpInvocationContext.ts
32555
33467
  var McpInvocationContext = class extends Context.Service()("@p4code/cli/mcp/McpInvocationContext") {};
@@ -39500,7 +40412,7 @@ const make$9 = Effect.gen(function* () {
39500
40412
  const layer$4 = Layer.effect(SourceControlRepositoryService, make$9);
39501
40413
  //#endregion
39502
40414
  //#region src/ws.ts
39503
- /** Matches `p4 hub token add`, so a token minted here and one minted there are the same thing. */
40415
+ /** Matches `p4c hub token add`, so a token minted here and one minted there are the same thing. */
39504
40416
  const HUB_TOKEN_BYTES = 32;
39505
40417
  const isOrchestrationDispatchCommandError = Schema$1.is(OrchestrationDispatchCommandError);
39506
40418
  const nowIso$5 = Effect.map(DateTime.now, DateTime.formatIso);
@@ -39632,6 +40544,13 @@ const RPC_REQUIRED_SCOPE = /* @__PURE__ */ new Map([
39632
40544
  [WS_METHODS.skillsPublish, AuthOrchestrationOperateScope],
39633
40545
  [WS_METHODS.skillsPublishAll, AuthOrchestrationOperateScope],
39634
40546
  [WS_METHODS.skillsUnpublish, AuthOrchestrationOperateScope],
40547
+ [WS_METHODS.assetsRead, AuthOrchestrationReadScope],
40548
+ [WS_METHODS.assetsSave, AuthOrchestrationOperateScope],
40549
+ [WS_METHODS.assetsDelete, AuthOrchestrationOperateScope],
40550
+ [WS_METHODS.mcpList, AuthOrchestrationReadScope],
40551
+ [WS_METHODS.mcpSave, AuthOrchestrationOperateScope],
40552
+ [WS_METHODS.mcpRemove, AuthOrchestrationOperateScope],
40553
+ [WS_METHODS.mcpSetSecret, AuthOrchestrationOperateScope],
39635
40554
  [WS_METHODS.serverGetSettings, AuthOrchestrationReadScope],
39636
40555
  [WS_METHODS.serverUpdateSettings, AuthOrchestrationOperateScope],
39637
40556
  [WS_METHODS.serverDiscoverSourceControl, AuthOrchestrationReadScope],
@@ -39742,7 +40661,8 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
39742
40661
  const lifecycleEvents = yield* ServerLifecycleEvents;
39743
40662
  const serverSettings = yield* ServerSettingsService;
39744
40663
  const hubLink = yield* HubLink;
39745
- const skillSync = yield* SkillSync;
40664
+ const assetSync = yield* AssetSync;
40665
+ const mcpRegistry = yield* McpRegistry;
39746
40666
  const startup = yield* ServerRuntimeStartup;
39747
40667
  const workspaceEntries = yield* WorkspaceEntries;
39748
40668
  const workspaceFileSystem = yield* WorkspaceFileSystem;
@@ -40204,11 +41124,11 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
40204
41124
  }), { "rpc.aggregate": "tasks" }),
40205
41125
  [WS_METHODS.tasksDelete]: ({ taskId }) => observeRpcEffect$1(WS_METHODS.tasksDelete, taskRepository.deleteById({ taskId }).pipe(Effect.catch(toTaskStoreError("tasks.delete")), Effect.as({})), { "rpc.aggregate": "tasks" }),
40206
41126
  [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" }),
41127
+ [WS_METHODS.hubGetSyncStatus]: (_input) => observeRpcEffect$1(WS_METHODS.hubGetSyncStatus, assetSync.status, { "rpc.aggregate": "hub" }),
41128
+ [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" }),
41129
+ [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" }),
41130
+ [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" }),
41131
+ [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
41132
  [WS_METHODS.hubMintToken]: (_input) => observeRpcEffect$1(WS_METHODS.hubMintToken, Effect.gen(function* () {
40213
41133
  const token = yield* crypto.randomBytes(HUB_TOKEN_BYTES).pipe(Effect.map((bytes) => Buffer.from(bytes).toString("base64url")), Effect.orDie);
40214
41134
  return {
@@ -40218,28 +41138,49 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
40218
41138
  }), { "rpc.aggregate": "hub" }),
40219
41139
  [WS_METHODS.skillsSync]: (_input) => observeRpcEffect$1(WS_METHODS.skillsSync, Effect.gen(function* () {
40220
41140
  return {
40221
- report: yield* skillSync.run,
40222
- status: yield* skillSync.status
41141
+ report: yield* assetSync.run,
41142
+ status: yield* assetSync.status
40223
41143
  };
40224
41144
  }), { "rpc.aggregate": "skills" }),
40225
- [WS_METHODS.skillsPublish]: ({ name }) => observeRpcEffect$1(WS_METHODS.skillsPublish, Effect.gen(function* () {
41145
+ [WS_METHODS.skillsPublish]: (ref) => observeRpcEffect$1(WS_METHODS.skillsPublish, Effect.gen(function* () {
40226
41146
  return {
40227
- report: yield* skillSync.publish(name),
40228
- status: yield* skillSync.status
41147
+ report: yield* assetSync.publish(ref),
41148
+ status: yield* assetSync.status
40229
41149
  };
40230
41150
  }), { "rpc.aggregate": "skills" }),
40231
41151
  [WS_METHODS.skillsPublishAll]: (_input) => observeRpcEffect$1(WS_METHODS.skillsPublishAll, Effect.gen(function* () {
40232
41152
  return {
40233
- report: yield* skillSync.publishAll,
40234
- status: yield* skillSync.status
41153
+ report: yield* assetSync.publishAll,
41154
+ status: yield* assetSync.status
40235
41155
  };
40236
41156
  }), { "rpc.aggregate": "skills" }),
40237
- [WS_METHODS.skillsUnpublish]: ({ name }) => observeRpcEffect$1(WS_METHODS.skillsUnpublish, Effect.gen(function* () {
41157
+ [WS_METHODS.skillsUnpublish]: (ref) => observeRpcEffect$1(WS_METHODS.skillsUnpublish, Effect.gen(function* () {
40238
41158
  return {
40239
- report: yield* skillSync.unpublish(name),
40240
- status: yield* skillSync.status
41159
+ report: yield* assetSync.unpublish(ref),
41160
+ status: yield* assetSync.status
40241
41161
  };
40242
41162
  }), { "rpc.aggregate": "skills" }),
41163
+ [WS_METHODS.assetsRead]: (ref) => observeRpcEffect$1(WS_METHODS.assetsRead, assetSync.read(ref), { "rpc.aggregate": "skills" }),
41164
+ [WS_METHODS.assetsSave]: (input) => observeRpcEffect$1(WS_METHODS.assetsSave, Effect.gen(function* () {
41165
+ return {
41166
+ report: yield* assetSync.save(input),
41167
+ status: yield* assetSync.status
41168
+ };
41169
+ }), { "rpc.aggregate": "skills" }),
41170
+ [WS_METHODS.assetsDelete]: (input) => observeRpcEffect$1(WS_METHODS.assetsDelete, Effect.gen(function* () {
41171
+ return {
41172
+ report: yield* assetSync.deleteFromHub(input),
41173
+ status: yield* assetSync.status
41174
+ };
41175
+ }), { "rpc.aggregate": "skills" }),
41176
+ [WS_METHODS.mcpList]: (_input) => observeRpcEffect$1(WS_METHODS.mcpList, mcpRegistry.list.pipe(Effect.map((servers) => ({ servers }))), { "rpc.aggregate": "mcp" }),
41177
+ [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" }),
41178
+ [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" }),
41179
+ [WS_METHODS.mcpSetSecret]: ({ name, slot, value }) => observeRpcEffect$1(WS_METHODS.mcpSetSecret, mcpRegistry.setSecret({
41180
+ name,
41181
+ slot,
41182
+ value
41183
+ }).pipe(Effect.andThen(mcpRegistry.list.pipe(Effect.map((servers) => ({ servers }))))), { "rpc.aggregate": "mcp" }),
40243
41184
  [WS_METHODS.serverGetSettings]: (_input) => observeRpcEffect$1(WS_METHODS.serverGetSettings, serverSettings.getSettings.pipe(Effect.map(redactServerSettingsForClient)), { "rpc.aggregate": "server" }),
40244
41185
  [WS_METHODS.serverUpdateSettings]: ({ patch }) => observeRpcEffect$1(WS_METHODS.serverUpdateSettings, serverSettings.updateSettings(patch).pipe(Effect.map(redactServerSettingsForClient)), { "rpc.aggregate": "server" }),
40245
41186
  [WS_METHODS.serverDiscoverSourceControl]: (_input) => observeRpcEffect$1(WS_METHODS.serverDiscoverSourceControl, sourceControlDiscovery.discover, { "rpc.aggregate": "server" }),
@@ -42440,7 +43381,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
42440
43381
  };
42441
43382
  });
42442
43383
  var OpenCodeRuntime = class extends Context.Service()("@p4code/cli/provider/opencodeRuntime") {};
42443
- const OpenCodeRuntimeLive = Layer.effect(OpenCodeRuntime, makeOpenCodeRuntime).pipe(Layer.provide(layer$66));
43384
+ const OpenCodeRuntimeLive = Layer.effect(OpenCodeRuntime, makeOpenCodeRuntime).pipe(Layer.provide(layer$67));
42444
43385
  //#endregion
42445
43386
  //#region src/textGeneration/TextGenerationUtils.ts
42446
43387
  const isTextGenerationError$2 = Schema$1.is(TextGenerationError);
@@ -43432,14 +44373,15 @@ const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(functio
43432
44373
  const versionProbe = yield* runClaudeCommand(claudeSettings, ["--version"], resolvedEnvironment).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result);
43433
44374
  if (Result.isFailure(versionProbe)) {
43434
44375
  const error = versionProbe.failure;
43435
- yield* Effect.logWarning("Claude Agent CLI health check failed.", { errorTag: error._tag });
44376
+ const missing = isCommandMissingCause(error);
44377
+ yield* (missing ? Effect.logDebug : Effect.logWarning)("Claude Agent CLI health check failed.", { errorTag: error._tag });
43436
44378
  return buildServerProvider({
43437
44379
  presentation: CLAUDE_PRESENTATION,
43438
44380
  enabled: claudeSettings.enabled,
43439
44381
  checkedAt,
43440
44382
  models: allModels,
43441
44383
  probe: {
43442
- installed: !isCommandMissingCause(error),
44384
+ installed: !missing,
43443
44385
  version: null,
43444
44386
  status: "error",
43445
44387
  auth: { status: "unknown" },
@@ -45861,6 +46803,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
45861
46803
  ...ultracode ? { ultracode: true } : {}
45862
46804
  };
45863
46805
  const mcpSession = readMcpProviderSession(input.threadId);
46806
+ const externalMcpServers = options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers;
45864
46807
  const queryOptions = {
45865
46808
  ...input.cwd ? { cwd: input.cwd } : {},
45866
46809
  ...apiModelId ? { model: apiModelId } : {},
@@ -45881,11 +46824,14 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
45881
46824
  env: claudeEnvironment,
45882
46825
  ...input.cwd ? { additionalDirectories: [input.cwd] } : {},
45883
46826
  ...Object.keys(extraArgs).length > 0 ? { extraArgs } : {},
45884
- ...mcpSession ? { mcpServers: { "p4-code": {
45885
- type: "http",
45886
- url: mcpSession.endpoint,
45887
- headers: { Authorization: mcpSession.authorizationHeader }
45888
- } } } : {}
46827
+ ...mcpSession || Object.keys(externalMcpServers).length > 0 ? { mcpServers: {
46828
+ ...externalMcpServers,
46829
+ ...mcpSession ? { "p4-code": {
46830
+ type: "http",
46831
+ url: mcpSession.endpoint,
46832
+ headers: { Authorization: mcpSession.authorizationHeader }
46833
+ } } : {}
46834
+ } } : {}
45889
46835
  };
45890
46836
  yield* Effect.annotateCurrentSpan({
45891
46837
  "provider.kind": PROVIDER$5,
@@ -46345,6 +47291,7 @@ const ClaudeDriver = {
46345
47291
  const adapter = yield* makeClaudeAdapter(effectiveConfig, {
46346
47292
  instanceId,
46347
47293
  environment: processEnv,
47294
+ resolveMcpServers: (yield* McpRegistry).resolveForSession,
46348
47295
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}
46349
47296
  });
46350
47297
  const textGeneration = yield* makeClaudeTextGeneration(effectiveConfig, processEnv);
@@ -72617,14 +73564,15 @@ const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(functio
72617
73564
  const aboutProbe = yield* runCursorAboutCommand(cursorSettings, environment).pipe(Effect.timeoutOption(ABOUT_TIMEOUT_MS), Effect.result);
72618
73565
  if (Result.isFailure(aboutProbe)) {
72619
73566
  const error = aboutProbe.failure;
72620
- yield* Effect.logWarning("Cursor Agent CLI health check failed.", { errorTag: error._tag });
73567
+ const missing = isCommandMissingCause(error);
73568
+ yield* (missing ? Effect.logDebug : Effect.logWarning)("Cursor Agent CLI health check failed.", { errorTag: error._tag });
72621
73569
  return buildServerProvider({
72622
73570
  presentation: CURSOR_PRESENTATION,
72623
73571
  enabled: cursorSettings.enabled,
72624
73572
  checkedAt,
72625
73573
  models: fallbackModels,
72626
73574
  probe: {
72627
- installed: !isCommandMissingCause(error),
73575
+ installed: !missing,
72628
73576
  version: null,
72629
73577
  status: "error",
72630
73578
  auth: { status: "unknown" },
@@ -75261,14 +76209,15 @@ const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(function* (
75261
76209
  const versionResult = yield* runGrokVersionCommand(grokSettings, environment).pipe(Effect.timeoutOption(VERSION_PROBE_TIMEOUT_MS), Effect.result);
75262
76210
  if (Result.isFailure(versionResult)) {
75263
76211
  const error = versionResult.failure;
75264
- yield* Effect.logWarning("Grok CLI health check failed.", { errorTag: error._tag });
76212
+ const missing = isCommandMissingCause(error);
76213
+ yield* (missing ? Effect.logDebug : Effect.logWarning)("Grok CLI health check failed.", { errorTag: error._tag });
75265
76214
  return buildServerProvider({
75266
76215
  presentation: GROK_PRESENTATION,
75267
76216
  enabled: grokSettings.enabled,
75268
76217
  checkedAt,
75269
76218
  models: fallbackModels,
75270
76219
  probe: {
75271
- installed: !isCommandMissingCause(error),
76220
+ installed: !missing,
75272
76221
  version: null,
75273
76222
  status: "error",
75274
76223
  auth: { status: "unknown" },
@@ -81518,16 +82467,16 @@ const SourceControlRepositoryServiceLayerLive = layer$4.pipe(Layer.provideMerge(
81518
82467
  const ReviewLayerLive = layer$9.pipe(Layer.provideMerge(layer$37), Layer.provideMerge(VcsDriverRegistryLayerLive));
81519
82468
  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
82469
  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));
82470
+ const PortScannerLayerLive = layer$31.pipe(Layer.provide(layer$52));
81522
82471
  const TerminalLayerLive = layer$30.pipe(Layer.provide(PtyAdapterLive), Layer.provide(PortScannerLayerLive));
81523
82472
  const PreviewLayerLive = Layer.empty.pipe(Layer.provideMerge(layer$28), Layer.provideMerge(PortScannerLayerLive));
81524
82473
  const WorkspaceEntriesLayerLive = layer$26.pipe(Layer.provide(layer$42));
81525
82474
  const WorkspaceFileSystemLayerLive = layer$25.pipe(Layer.provide(layer$42), Layer.provide(WorkspaceEntriesLayerLive));
81526
82475
  const WorkspaceLayerLive = Layer.mergeAll(layer$42, WorkspaceEntriesLayerLive, WorkspaceFileSystemLayerLive);
81527
82476
  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));
82477
+ const AuthLayerLive = layer$59.pipe(Layer.provideMerge(PersistenceLayerLive), Layer.provide(layer$63));
81529
82478
  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));
82479
+ 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
82480
  /**
81532
82481
  * Hub asset sync.
81533
82482
  *
@@ -81535,8 +82484,8 @@ const RuntimeDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(Checkpo
81535
82484
  * not-configured shape, and every call reports that rather than failing — so
81536
82485
  * this costs a server with no hub nothing but a layer.
81537
82486
  */
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));
82487
+ const AssetSyncLive = layer$53.pipe(Layer.provideMerge(layer$54), Layer.provideMerge(layer$55));
82488
+ const RuntimeServicesLive = layer$43.pipe(Layer.provideMerge(AssetSyncLive), Layer.provideMerge(RuntimeDependenciesLive));
81540
82489
  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
82490
  const makeServerLayer = Layer.unwrap(Effect.gen(function* () {
81542
82491
  const config = yield* ServerConfig$1;
@@ -81601,7 +82550,7 @@ const bootServiceLayer = (config) => layer$46({
81601
82550
  baseDir: config.baseDir,
81602
82551
  logsDir: config.logsDir,
81603
82552
  cliVersion: version
81604
- }).pipe(Layer.provide(layer$57));
82553
+ }).pipe(Layer.provide(layer$52));
81605
82554
  /** Install, update, or repair the service using the CLI version running this command. */
81606
82555
  const reconcileService = Effect.fn("cli.service.reconcile")(function* () {
81607
82556
  const service = yield* BootService;
@@ -81633,37 +82582,26 @@ function formatReconcileSuccess(input) {
81633
82582
  }
81634
82583
  function formatServiceStatus(status, cliVersion) {
81635
82584
  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`.";
82585
+ if (!status.installed) return "P4Code service\n Status: not installed\n Next: Run `p4c service install`.";
81637
82586
  return [
81638
82587
  "P4Code service",
81639
82588
  ` Status: ${status.current ? `installed · @p4code/cli@${cliVersion}` : "needs an update or repair"}`,
81640
82589
  ` Unit: ${status.unitPath}`,
81641
82590
  ` Data: ${status.baseDir}`,
81642
82591
  ` Logs: ${status.logPath}`,
81643
- ...status.current ? [] : [" Next: Run `p4 service update`."]
82592
+ ...status.current ? [] : [" Next: Run `p4c service update`."]
81644
82593
  ].join("\n");
81645
82594
  }
81646
82595
  const runServiceCommand = Effect.fn("cli.service.run")(function* (flags, run) {
81647
82596
  const config = yield* resolveCliAuthConfig(flags, yield* GlobalFlag.LogLevel);
81648
82597
  const baseDirNotice = yield* describeNonDefaultBaseDir(config.baseDir);
81649
82598
  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));
82599
+ 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
82600
  });
81662
82601
  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
82602
  const result = yield* reconcileService();
81664
82603
  if (!result.changed) {
81665
82604
  yield* Console.log(`P4Code service is already installed with @p4code/cli@${version}.`);
81666
- yield* reportZshCompletionInstall;
81667
82605
  return;
81668
82606
  }
81669
82607
  yield* Console.log(formatReconcileSuccess({
@@ -81672,14 +82610,12 @@ const serviceInstallCommand = Command.make("install", projectLocationFlags).pipe
81672
82610
  cliVersion: version,
81673
82611
  platform: yield* HostProcessPlatform
81674
82612
  }));
81675
- yield* reportZshCompletionInstall;
81676
82613
  }))));
81677
82614
  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
82615
  const result = yield* reconcileService();
81679
82616
  if (!result.changed) {
81680
82617
  yield* (yield* BootService).restart;
81681
82618
  yield* Console.log(`Restarted the P4Code service on @p4code/cli@${version}.`);
81682
- yield* reportZshCompletionInstall;
81683
82619
  return;
81684
82620
  }
81685
82621
  yield* Console.log(formatReconcileSuccess({
@@ -81688,7 +82624,6 @@ const serviceUpdateCommand = Command.make("update", projectLocationFlags).pipe(C
81688
82624
  cliVersion: version,
81689
82625
  platform: yield* HostProcessPlatform
81690
82626
  }));
81691
- yield* reportZshCompletionInstall;
81692
82627
  }))));
81693
82628
  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
82629
  const removed = yield* (yield* BootService).uninstall;
@@ -81721,7 +82656,7 @@ const serviceCommand = Command.make("service").pipe(Command.withDescription("Man
81721
82656
  ]));
81722
82657
  //#endregion
81723
82658
  //#region src/bin.ts
81724
- const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, layer$66, FetchHttpClient.layer);
82659
+ const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, layer$67, FetchHttpClient.layer);
81725
82660
  /**
81726
82661
  * Prints an update reminder before dispatching, at most once a day.
81727
82662
  *
@@ -81737,21 +82672,60 @@ const reportUpdateAvailable = Effect.gen(function* () {
81737
82672
  stateDir: paths.stateDir
81738
82673
  });
81739
82674
  if (reminder !== null) yield* Console.error(reminder);
81740
- const completionsHint = yield* resolveMissingCompletionsHint({ stateDir: paths.stateDir });
81741
- if (completionsHint !== null) yield* Console.error(completionsHint);
81742
82675
  }).pipe(Effect.ignore);
81743
- const makeCli = () => Command.make("p4", { ...sharedServerCommandFlags }).pipe(Command.withDescription("Run the P4Code server."), Command.withHandler((flags) => runServerCommand(flags)), Command.withSubcommands([
82676
+ /**
82677
+ * `p4c`, not `p4`.
82678
+ *
82679
+ * Perforce owns `p4`, and zsh ships `_perforce` claiming it with
82680
+ * `#compdef p4 p4d`. Fighting for the name cost a completion installer, a
82681
+ * per-run "your completions are missing" hint, and an argument about which
82682
+ * fpath directory `compinit` would prefer — all of it maintenance for a
82683
+ * conflict we chose. Taking a name nothing else claims deletes the problem
82684
+ * rather than managing it.
82685
+ */
82686
+ const makeCli = () => Command.make("p4c", { ...sharedServerCommandFlags }).pipe(Command.withDescription("Run the P4Code server."), Command.withHandler((flags) => runServerCommand(flags)), Command.withSubcommands([
81744
82687
  startCommand,
81745
82688
  serveCommand,
81746
82689
  authCommand,
81747
- completionsCommand,
81748
82690
  hubCommand,
82691
+ mcpCommand,
82692
+ memoryCommand,
81749
82693
  projectCommand,
81750
82694
  serviceCommand,
81751
82695
  skillsCommand
81752
82696
  ]));
81753
82697
  const cli = makeCli();
81754
- if (import.meta.main) reportUpdateAvailable.pipe(Effect.andThen(Command.run(cli, { version })), Effect.scoped, Effect.provide(CliRuntimeLayer), NodeRuntime.runMain);
82698
+ /**
82699
+ * A one-line message for an expected failure, without the stack.
82700
+ *
82701
+ * Every domain error here is a `Schema.TaggedErrorClass` with a `message`
82702
+ * getter written for a person — "Background setup failed while loading the
82703
+ * service (exit code 5)" — and the surrounding command has usually already
82704
+ * printed the log path that holds the detail. Appending twenty frames of
82705
+ * bundled `dist/bin.mjs` to that adds nothing anybody can act on.
82706
+ */
82707
+ const describeExpectedFailure = (error) => {
82708
+ const message = error?.message;
82709
+ if (typeof message === "string" && message.trim().length > 0) return message;
82710
+ return String(error);
82711
+ };
82712
+ /**
82713
+ * Expected failures print their message; defects keep their stack.
82714
+ *
82715
+ * The distinction is Effect's own, and it is the right one: a typed failure is
82716
+ * a condition the code anticipated and described, so a trace through a
82717
+ * bundled file is noise. A defect is a bug, and there the trace is the only
82718
+ * thing worth having.
82719
+ *
82720
+ * `CliError` is re-raised rather than handled, because `runMain` already
82721
+ * renders usage and validation failures better than a bare message would —
82722
+ * catching them here would replace "unknown flag --foo, did you mean --force"
82723
+ * with one flat line.
82724
+ */
82725
+ const reportFailuresWithoutStacks = (effect) => effect.pipe(Effect.catchIf((error) => !CliError.isCliError(error), (error) => Console.error(describeExpectedFailure(error)).pipe(Effect.andThen(Effect.sync(() => {
82726
+ process.exitCode = 1;
82727
+ })))));
82728
+ if (import.meta.main) reportUpdateAvailable.pipe(Effect.andThen(Command.run(cli, { version })), reportFailuresWithoutStacks, Effect.scoped, Effect.provide(CliRuntimeLayer), NodeRuntime.runMain);
81755
82729
  //#endregion
81756
82730
  export { cli, makeCli };
81757
82731