@p4code/cli 0.0.42 → 0.0.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/bin.mjs +290 -183
  2. package/dist/client/assets/DiffPanel-DD3E29O7.js +98 -0
  3. package/dist/client/assets/FilePreviewPanel-fBOIzVAm.js +1778 -0
  4. package/dist/client/assets/PreviewPanel-BXyhScZZ.js +2 -0
  5. package/dist/client/assets/{angular-html-D2LtifxG.js → angular-html-BwUZQlj7.js} +2 -2
  6. package/dist/client/assets/{angular-ts-oVDpKJb2.js → angular-ts-8EX8ZNeQ.js} +2 -2
  7. package/dist/client/assets/{apl-2vnm7rdp.js → apl-DYm7AarA.js} +2 -2
  8. package/dist/client/assets/arrow-right-BNeeftk2.js +2 -0
  9. package/dist/client/assets/{astro-CDbEfI_f.js → astro-DFaFMvm9.js} +2 -2
  10. package/dist/client/assets/{blade-B1MwvSO0.js → blade-D9aigeeZ.js} +2 -2
  11. package/dist/client/assets/{c-Bi5eHJiI.js → c-BvAsa11Y.js} +2 -2
  12. package/dist/client/assets/{cobol-BxESWi7D.js → cobol-BZwY3--F.js} +2 -2
  13. package/dist/client/assets/{coffee-s151wUsp.js → coffee-vNqcHwuy.js} +2 -2
  14. package/dist/client/assets/{cpp-B51xjAkw.js → cpp-D3Xet0_6.js} +2 -2
  15. package/dist/client/assets/{crystal-BW0dq2Rw.js → crystal-D-UW0VhN.js} +2 -2
  16. package/dist/client/assets/{css-Czn5ycv4.js → css-B1CDNsy0.js} +2 -2
  17. package/dist/client/assets/{edge-DPtVbjt1.js → edge-B_geUcoi.js} +2 -2
  18. package/dist/client/assets/{elixir-381Dy51E.js → elixir-BzK0Ks1B.js} +2 -2
  19. package/dist/client/assets/{elm-Bpu_ckb9.js → elm-BvlMiaGz.js} +2 -2
  20. package/dist/client/assets/{erb-u1vpMXOw.js → erb-OFCHFHP8.js} +2 -2
  21. package/dist/client/assets/fileCommentAnnotations-DI_Ec6DZ.js +2 -0
  22. package/dist/client/assets/{git-rebase-ShzdQ6mA.js → git-rebase-Xc_wijNW.js} +2 -2
  23. package/dist/client/assets/{glimmer-js-DI4_LeGA.js → glimmer-js-D-GpFszK.js} +2 -2
  24. package/dist/client/assets/{glimmer-ts-BrA1R4ia.js → glimmer-ts-_MmK7qB5.js} +2 -2
  25. package/dist/client/assets/{glsl--BAUZuR7.js → glsl-Cm_ctoze.js} +2 -2
  26. package/dist/client/assets/{graphql-XmtNe4tX.js → graphql-De_-LrEt.js} +2 -2
  27. package/dist/client/assets/{hack-ZPcuqt6I.js → hack-qUzK29KK.js} +2 -2
  28. package/dist/client/assets/{haml-BLmOYSAV.js → haml-mqyUpOBj.js} +2 -2
  29. package/dist/client/assets/{handlebars-BxR19vQc.js → handlebars-DS3jjsXS.js} +2 -2
  30. package/dist/client/assets/{html-derivative-CLJquIOq.js → html-derivative-BPRW_sHn.js} +2 -2
  31. package/dist/client/assets/{html-Do3TWS07.js → html-ippoHerj.js} +2 -2
  32. package/dist/client/assets/{http-C6M7KGrt.js → http-xrFfvFRg.js} +2 -2
  33. package/dist/client/assets/{hurl-AFYDoSjV.js → hurl-DSL-wjHh.js} +2 -2
  34. package/dist/client/assets/index-BAsvpSiM.js +636 -0
  35. package/dist/client/assets/index-CGyzLTcp.css +1 -0
  36. package/dist/client/assets/{java-DoRNyNZ_.js → java-B8xLHoVB.js} +2 -2
  37. package/dist/client/assets/{javascript-Dd-zEQJs.js → javascript-DkxetjtW.js} +2 -2
  38. package/dist/client/assets/{jinja-TJRtEusk.js → jinja-DrlmZXOI.js} +2 -2
  39. package/dist/client/assets/{jison-bG9mcEJe.js → jison-CBYMCWiZ.js} +2 -2
  40. package/dist/client/assets/{json-DY6EaQUk.js → json-aobk_LSd.js} +2 -2
  41. package/dist/client/assets/{jsx-BuL0_m-Z.js → jsx-BTcIKPoF.js} +2 -2
  42. package/dist/client/assets/{julia-4bcuyKPV.js → julia-BwUvFb1K.js} +2 -2
  43. package/dist/client/assets/{just-F3UNctxi.js → just-2GL8m28O.js} +2 -2
  44. package/dist/client/assets/{latex-DCJY-Aag.js → latex-BwWZNh8B.js} +2 -2
  45. package/dist/client/assets/{liquid-M906bGYJ.js → liquid-j8UWR5fn.js} +2 -2
  46. package/dist/client/assets/{lua-CeupCtSb.js → lua-BiEJazUk.js} +2 -2
  47. package/dist/client/assets/{marko-D_3SsXZn.js → marko-BrkuG9X9.js} +2 -2
  48. package/dist/client/assets/{mdc-DRUaGPVE.js → mdc-CbE9Gm1n.js} +2 -2
  49. package/dist/client/assets/{nginx-B_DJSFb0.js → nginx-C-gcDf_H.js} +2 -2
  50. package/dist/client/assets/{nim-BxoRFkNd.js → nim-9X20IMJZ.js} +2 -2
  51. package/dist/client/assets/{perl-CjLiNsGL.js → perl-Dqj-SUzY.js} +2 -2
  52. package/dist/client/assets/{php-DcklQON0.js → php-CQymwCam.js} +2 -2
  53. package/dist/client/assets/{pug-DHYHUkbl.js → pug-DIOYJ7Tj.js} +2 -2
  54. package/dist/client/assets/{qml-Cggn-sFR.js → qml-BpdlPbMh.js} +2 -2
  55. package/dist/client/assets/{r-BhkOaKoM.js → r-BdXpML7C.js} +2 -2
  56. package/dist/client/assets/{razor-DksRt091.js → razor-CmGIbQUz.js} +2 -2
  57. package/dist/client/assets/{regexp-CVeVJKpO.js → regexp-udR9FXf8.js} +2 -2
  58. package/dist/client/assets/{rst-B8gnyB7p.js → rst-C81pNvVo.js} +2 -2
  59. package/dist/client/assets/{ruby-B7aYI7EY.js → ruby-D5xBAIVx.js} +2 -2
  60. package/dist/client/assets/{sas-DmDsVHsr.js → sas-pegDgwhF.js} +2 -2
  61. package/dist/client/assets/{scss-Cf0hFbmP.js → scss-Dx_Aq9w8.js} +2 -2
  62. package/dist/client/assets/{shellscript-CJ2l7Ljo.js → shellscript-Bl-HTaUh.js} +2 -2
  63. package/dist/client/assets/{shellsession-CE-6mg0T.js → shellsession-CezqakSW.js} +2 -2
  64. package/dist/client/assets/{soy-eTwUOhLL.js → soy-DclELXc7.js} +2 -2
  65. package/dist/client/assets/{sql-DaTGJ1E1.js → sql-DAsY-zzx.js} +2 -2
  66. package/dist/client/assets/{stata-DCqqmU-V.js → stata-DjQ16uuy.js} +2 -2
  67. package/dist/client/assets/{surrealql-DJuyQLb6.js → surrealql-BLzgjzgE.js} +2 -2
  68. package/dist/client/assets/{svelte-DZfElFr5.js → svelte-DFGwC3eP.js} +2 -2
  69. package/dist/client/assets/{templ-BeuT7j1S.js → templ-BiFbdCJD.js} +2 -2
  70. package/dist/client/assets/{tex-CUjxQ3iG.js → tex-DQCF2PEO.js} +2 -2
  71. package/dist/client/assets/textarea-lBMORu4g.js +46 -0
  72. package/dist/client/assets/{ts-tags-C00W--Ve.js → ts-tags-CkQC6ekt.js} +2 -2
  73. package/dist/client/assets/{tsx-h-kXP2gA.js → tsx-m7HWHXfL.js} +2 -2
  74. package/dist/client/assets/{twig-DhMoAQY-.js → twig-B5P5jotH.js} +2 -2
  75. package/dist/client/assets/{typescript-DwJdYtiC.js → typescript-DmGwuo7e.js} +2 -2
  76. package/dist/client/assets/{vue-CvLnzHbi.js → vue-Rn_gU7Px.js} +2 -2
  77. package/dist/client/assets/{vue-html-fj2AgeR4.js → vue-html-DbKQF2OX.js} +2 -2
  78. package/dist/client/assets/{vue-vine-D1-Lx9ZR.js → vue-vine-C13tIrLE.js} +2 -2
  79. package/dist/client/assets/{xml-CDkXSKu_.js → xml-D7fQm-eG.js} +2 -2
  80. package/dist/client/assets/{xsl-BkotjMT0.js → xsl-Bv743my_.js} +2 -2
  81. package/dist/client/assets/{yaml-BssI7mmn.js → yaml--h7pPfkH.js} +2 -2
  82. package/dist/client/index.html +3 -3
  83. package/package.json +1 -1
  84. package/dist/client/assets/DiffPanel-D8nVXZ0Y.js +0 -98
  85. package/dist/client/assets/FilePreviewPanel-CFKCFyJ3.js +0 -1778
  86. package/dist/client/assets/PreviewPanel-VQey9906.js +0 -2
  87. package/dist/client/assets/arrow-right-DmJqM7J1.js +0 -2
  88. package/dist/client/assets/fileCommentAnnotations-BwnrRpfq.js +0 -2
  89. package/dist/client/assets/index-XxxL_T8f.js +0 -635
  90. package/dist/client/assets/index-mqpX0B5G.css +0 -1
  91. package/dist/client/assets/textarea-BM2z9WNB.js +0 -46
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";
@@ -232,7 +233,7 @@ const make$71 = () => {
232
233
  const layer$67 = Layer.sync(NetService, make$71);
233
234
  //#endregion
234
235
  //#region package.json
235
- var version = "0.0.42";
236
+ var version = "0.0.44";
236
237
  //#endregion
237
238
  //#region src/config.ts
238
239
  /**
@@ -13924,9 +13925,10 @@ const HubHttpServerLive = Layer.unwrap(Effect.gen(function* () {
13924
13925
  /**
13925
13926
  * Says on startup exactly what the hub is exposing and to whom.
13926
13927
  *
13927
- * A hub bound past loopback is reachable by every device the tailnet ACL lets
13928
- * through, and the bearer token is then the only remaining control. That is a
13929
- * deliberate configuration, but it should never be a silent one.
13928
+ * A hub bound past loopback is reachable by whatever can route to it — on the
13929
+ * hosted deployment, the public internet — and the bearer token is then the
13930
+ * only remaining control. That is a deliberate configuration, but it should
13931
+ * never be a silent one.
13930
13932
  */
13931
13933
  const announce = Layer.effectDiscard(Effect.gen(function* () {
13932
13934
  const config = yield* HubConfig;
@@ -13938,7 +13940,7 @@ const announce = Layer.effectDiscard(Effect.gen(function* () {
13938
13940
  dbPath: config.dbPath,
13939
13941
  authorizedServers: config.tokenHashes.size
13940
13942
  });
13941
- if (isRemoteReachableHost(config.host)) yield* Effect.logWarning("Hub is bound beyond loopback; the bearer token and your tailnet ACL are the only controls.", { host: config.host });
13943
+ if (isRemoteReachableHost(config.host)) yield* Effect.logWarning("Hub is reachable beyond loopback. The bearer token is the only control — repeated auth failures are not rate limited.", { host: config.host });
13942
13944
  }));
13943
13945
  const makeHubLayer = HttpRouter.serve(layer$58).pipe(Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
13944
13946
  const runHub = Layer.launch(makeHubLayer);
@@ -14905,15 +14907,188 @@ const removeMcpRegistration = Effect.fn("mcpRegistrationFiles.remove")(function*
14905
14907
  yield* fileSystem.remove(path.join(input.registrationsRoot, input.name), { force: true });
14906
14908
  });
14907
14909
  //#endregion
14910
+ //#region src/sync/skillDirectory.ts
14911
+ /**
14912
+ * Reading and writing skill directories on disk.
14913
+ *
14914
+ * Kept apart from the sync itself so the filesystem shape has its own tests,
14915
+ * and so the rules about what is refusable — binaries, oversized files, deep
14916
+ * trees — live in one place rather than being rediscovered at each call site.
14917
+ *
14918
+ * @module sync/skillDirectory
14919
+ */
14920
+ /** Bounded so a stray symlink or a vendored `node_modules` cannot walk forever. */
14921
+ const MAX_SKILL_DEPTH = 4;
14922
+ /**
14923
+ * Entries that are never part of a skill, skipped rather than refused.
14924
+ *
14925
+ * The distinction matters. Refusing an asset because it cannot be represented
14926
+ * faithfully is right — a half-materialized skill loads and misbehaves. But a
14927
+ * `.DS_Store` is not part of the skill at all, and macOS drops one into any
14928
+ * directory that has been opened in Finder, so refusing on its account rejects
14929
+ * a perfectly good skill for a reason its author cannot see.
14930
+ *
14931
+ * These could not sync in any case: `AgentAssetFilePath` rejects a leading dot,
14932
+ * so a dotfile has no representable path even if it were text.
14933
+ */
14934
+ const IGNORED_ENTRIES = /* @__PURE__ */ new Set([
14935
+ ".DS_Store",
14936
+ "Thumbs.db",
14937
+ ".git",
14938
+ ".svn",
14939
+ ".hg",
14940
+ "node_modules",
14941
+ "__pycache__",
14942
+ ".venv",
14943
+ ".pytest_cache",
14944
+ ".ruff_cache",
14945
+ ".mypy_cache"
14946
+ ]);
14947
+ /**
14948
+ * Anything else with a leading dot is skipped too — an editor's `.idea`, a
14949
+ * `.gitignore` that belongs to the checkout rather than the skill. The rule is
14950
+ * the same one the path schema already enforces, applied earlier so it reads as
14951
+ * "not part of the skill" instead of "this skill is broken".
14952
+ */
14953
+ const isIgnoredEntry = (entry) => IGNORED_ENTRIES.has(entry) || entry.startsWith(".");
14954
+ /**
14955
+ * A file is binary if decoding it yields a NUL.
14956
+ *
14957
+ * Cheap, and right for what is being asked: the question is not "is this valid
14958
+ * UTF-8" but "can this round-trip through a JSON text column", and a NUL is
14959
+ * what actually breaks that on the way back out of SQLite.
14960
+ */
14961
+ const looksBinary$1 = (content) => content.includes("\0");
14962
+ /**
14963
+ * Read one directory as an asset's files, or say why it cannot be one.
14964
+ *
14965
+ * Exported because memory's `global-memory/` is the same shape by a different
14966
+ * name: a directory whose whole contents are the asset. Sharing this keeps the
14967
+ * refusal rules — depth, file count, binaries, size — identical for both,
14968
+ * rather than letting a second copy drift into accepting what the first
14969
+ * rejects.
14970
+ */
14971
+ const readSkillFiles = Effect.fn("skillDirectory.readSkillFiles")(function* (root) {
14972
+ const fileSystem = yield* FileSystem.FileSystem;
14973
+ const path = yield* Path.Path;
14974
+ const files = [];
14975
+ const walk = Effect.fn("skillDirectory.walk")(function* (directory, prefix, depth) {
14976
+ if (depth > MAX_SKILL_DEPTH) return "too-deep";
14977
+ const entries = yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []));
14978
+ for (const entry of [...entries].sort()) {
14979
+ if (isIgnoredEntry(entry)) continue;
14980
+ const absolute = path.join(directory, entry);
14981
+ const relative = prefix === "" ? entry : `${prefix}/${entry}`;
14982
+ const info = yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0));
14983
+ if (info === void 0) return "unreadable";
14984
+ if (info.type === "Directory") {
14985
+ const nested = yield* walk(absolute, relative, depth + 1);
14986
+ if (nested !== null) return nested;
14987
+ continue;
14988
+ }
14989
+ if (info.type !== "File") continue;
14990
+ if (files.length >= 512) return "too-many-files";
14991
+ const content = yield* fileSystem.readFileString(absolute).pipe(Effect.orElseSucceed(() => void 0));
14992
+ if (content === void 0) return "unreadable";
14993
+ if (looksBinary$1(content)) return "binary-file";
14994
+ if (content.length > 524288) return "file-too-large";
14995
+ files.push({
14996
+ path: relative,
14997
+ content
14998
+ });
14999
+ }
15000
+ return null;
15001
+ });
15002
+ const refusal = yield* walk(root, "", 1);
15003
+ if (refusal !== null) return { refusal };
15004
+ if (agentAssetTotalBytes(files) > 4194304) return { refusal: "asset-too-large" };
15005
+ return { files };
15006
+ });
15007
+ /**
15008
+ * Read every skill directory under a user-scope `skills/` root.
15009
+ *
15010
+ * A directory with no `SKILL.md` is not a skill and is skipped silently —
15011
+ * Claude Code would ignore it too, so reporting it would be noise about
15012
+ * something that was never a skill in the first place.
15013
+ */
15014
+ const readSkillDirectory = Effect.fn("skillDirectory.readSkillDirectory")(function* (skillsRoot) {
15015
+ const fileSystem = yield* FileSystem.FileSystem;
15016
+ const path = yield* Path.Path;
15017
+ const entries = yield* fileSystem.readDirectory(skillsRoot).pipe(Effect.orElseSucceed(() => []));
15018
+ const skills = [];
15019
+ const refused = [];
15020
+ for (const entry of [...entries].sort()) {
15021
+ if (isIgnoredEntry(entry)) continue;
15022
+ const directory = path.join(skillsRoot, entry);
15023
+ if ((yield* fileSystem.stat(directory).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "Directory") continue;
15024
+ if (!(yield* fileSystem.exists(path.join(directory, "SKILL.md")).pipe(Effect.orElseSucceed(() => false)))) continue;
15025
+ const read = yield* readSkillFiles(directory);
15026
+ if ("refusal" in read) {
15027
+ refused.push({
15028
+ name: entry,
15029
+ reason: read.refusal
15030
+ });
15031
+ continue;
15032
+ }
15033
+ skills.push({
15034
+ name: entry,
15035
+ files: read.files
15036
+ });
15037
+ }
15038
+ return {
15039
+ skills,
15040
+ refused
15041
+ };
15042
+ });
15043
+ /**
15044
+ * Replace a skill directory with exactly the asset's files.
15045
+ *
15046
+ * Written to a sibling and then swapped, so a crash mid-write leaves the old
15047
+ * skill intact rather than half of the new one. A skill is loaded as a unit and
15048
+ * a partially written one is instructions with the middle missing, which an
15049
+ * agent will happily follow.
15050
+ */
15051
+ const writeSkillDirectory = Effect.fn("skillDirectory.writeSkillDirectory")(function* (input) {
15052
+ const fileSystem = yield* FileSystem.FileSystem;
15053
+ const path = yield* Path.Path;
15054
+ const target = path.join(input.skillsRoot, input.name);
15055
+ const staging = path.join(input.skillsRoot, `.${input.name}.p4code-staging`);
15056
+ yield* fileSystem.makeDirectory(input.skillsRoot, { recursive: true });
15057
+ yield* fileSystem.remove(staging, {
15058
+ recursive: true,
15059
+ force: true
15060
+ });
15061
+ yield* fileSystem.makeDirectory(staging, { recursive: true });
15062
+ for (const file of input.files) {
15063
+ const destination = path.join(staging, file.path);
15064
+ yield* fileSystem.makeDirectory(path.dirname(destination), { recursive: true });
15065
+ yield* fileSystem.writeFileString(destination, file.content);
15066
+ }
15067
+ yield* fileSystem.remove(target, {
15068
+ recursive: true,
15069
+ force: true
15070
+ });
15071
+ yield* fileSystem.rename(staging, target);
15072
+ });
15073
+ const removeSkillDirectory = Effect.fn("skillDirectory.removeSkillDirectory")(function* (input) {
15074
+ const fileSystem = yield* FileSystem.FileSystem;
15075
+ const path = yield* Path.Path;
15076
+ yield* fileSystem.remove(path.join(input.skillsRoot, input.name), {
15077
+ recursive: true,
15078
+ force: true
15079
+ });
15080
+ });
15081
+ //#endregion
14908
15082
  //#region src/sync/memoryFile.ts
14909
15083
  /**
14910
15084
  * Reading and writing user-scope memory files.
14911
15085
  *
14912
15086
  * Memory is the instruction file an agent reads before anything else, so a
14913
15087
  * machine whose `CLAUDE.md` differs is a machine whose agent behaves
14914
- * differently — which is the whole reason the hub exists. Unlike a skill it is
14915
- * a single file, and it sits *directly* in the provider's config directory
14916
- * rather than in a subdirectory the sync could own outright.
15088
+ * differently — which is the whole reason the hub exists. It comes in two
15089
+ * shapes: the entry-point files, which sit *directly* in the provider's config
15090
+ * directory rather than in a subdirectory the sync could own outright, and the
15091
+ * directory of rules those files include by reference.
14917
15092
  *
14918
15093
  * That last part is the entire design constraint. `<configDir>` also holds
14919
15094
  * `settings.json`, `.claude.json` and `.credentials.json`. "Write the file this
@@ -14950,9 +15125,38 @@ const MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md"];
14950
15125
  * it on a case-sensitive one would make the same hub row materialize on one
14951
15126
  * machine and refuse on another.
14952
15127
  */
14953
- const isMemoryAssetName = (name) => MEMORY_FILE_NAMES.includes(name);
15128
+ const isMemoryFileName = (name) => MEMORY_FILE_NAMES.includes(name);
15129
+ /**
15130
+ * The directories in the config directory this sync will carry whole.
15131
+ *
15132
+ * A memory file is rarely the whole of the memory. `CLAUDE.md` is written to be
15133
+ * an index — a dozen `@~/.claude/global-memory/...` lines and little else — so
15134
+ * syncing the file alone hands the other machine a list of pointers to files
15135
+ * that are not there, and an agent reading it behaves as if the rules it names
15136
+ * do not exist. That failure is silent: the includes resolve to nothing rather
15137
+ * than to an error.
15138
+ *
15139
+ * So this is an allowlist too, and for the same reason the file one exists. The
15140
+ * escape it prevents is different, though, and weaker: `AgentAssetFilePath`
15141
+ * already forbids `..`, leading slashes and backslashes, so a directory asset
15142
+ * cannot write outside its own directory whatever it names its files. What the
15143
+ * allowlist adds is that the *directory* cannot be named freely either — an
15144
+ * asset is not permitted to claim `.claude/projects` or any other directory
15145
+ * that already means something to the provider.
15146
+ */
15147
+ const MEMORY_DIRECTORY_NAMES = ["global-memory"];
15148
+ /** Whether this name is one of the memory directories. Exact match, as above. */
15149
+ const isMemoryDirectoryName = (name) => MEMORY_DIRECTORY_NAMES.includes(name);
15150
+ /**
15151
+ * Whether this name is memory of either shape.
15152
+ *
15153
+ * The two lists must stay disjoint, and are: an asset that is a file in one and
15154
+ * a directory in the other would have the writer delete a file to put a
15155
+ * directory in its place, or the reverse, depending on which machine ran last.
15156
+ */
15157
+ const isMemoryAssetName = (name) => isMemoryFileName(name) || isMemoryDirectoryName(name);
14954
15158
  /** The same test the skill reader uses: a NUL is what breaks a JSON text column. */
14955
- const looksBinary$1 = (content) => content.includes("\0");
15159
+ const looksBinary = (content) => content.includes("\0");
14956
15160
  /**
14957
15161
  * Read whichever memory files exist in this machine's config directory.
14958
15162
  *
@@ -14976,7 +15180,7 @@ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memor
14976
15180
  });
14977
15181
  continue;
14978
15182
  }
14979
- if (looksBinary$1(content)) {
15183
+ if (looksBinary(content)) {
14980
15184
  refused.push({
14981
15185
  name,
14982
15186
  reason: "binary-file"
@@ -15006,6 +15210,23 @@ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memor
15006
15210
  files
15007
15211
  });
15008
15212
  }
15213
+ for (const name of MEMORY_DIRECTORY_NAMES) {
15214
+ const absolute = path.join(memoryRoot, name);
15215
+ if ((yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "Directory") continue;
15216
+ const read = yield* readSkillFiles(absolute);
15217
+ if ("refusal" in read) {
15218
+ refused.push({
15219
+ name,
15220
+ reason: read.refusal
15221
+ });
15222
+ continue;
15223
+ }
15224
+ if (read.files.length === 0) continue;
15225
+ skills.push({
15226
+ name,
15227
+ files: read.files
15228
+ });
15229
+ }
15009
15230
  return {
15010
15231
  skills,
15011
15232
  refused
@@ -15019,7 +15240,10 @@ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memor
15019
15240
  * carrying a second file called `settings.json` would otherwise write it, which
15020
15241
  * is the same escape by a longer route.
15021
15242
  */
15022
- const isWritableMemoryAsset = (input) => isMemoryAssetName(input.name) && input.files.length === 1 && input.files[0]?.path === input.name;
15243
+ const isWritableMemoryAsset = (input) => {
15244
+ if (isMemoryDirectoryName(input.name)) return true;
15245
+ return isMemoryFileName(input.name) && input.files.length === 1 && input.files[0]?.path === input.name;
15246
+ };
15023
15247
  /**
15024
15248
  * Write a memory file, or refuse.
15025
15249
  *
@@ -15033,6 +15257,11 @@ const writeMemoryFile = Effect.fn("memoryFile.writeMemoryFile")(function* (input
15033
15257
  if (!isWritableMemoryAsset(input)) return "refused";
15034
15258
  const fileSystem = yield* FileSystem.FileSystem;
15035
15259
  const path = yield* Path.Path;
15260
+ if (isMemoryDirectoryName(input.name)) return yield* writeSkillDirectory({
15261
+ skillsRoot: input.memoryRoot,
15262
+ name: input.name,
15263
+ files: input.files
15264
+ }).pipe(Effect.as("written"), Effect.orElseSucceed(() => "failed"));
15036
15265
  const contents = input.files[0]?.content ?? "";
15037
15266
  yield* fileSystem.makeDirectory(input.memoryRoot, { recursive: true }).pipe(Effect.ignore);
15038
15267
  return yield* writeFileStringAtomically({
@@ -15042,6 +15271,13 @@ const writeMemoryFile = Effect.fn("memoryFile.writeMemoryFile")(function* (input
15042
15271
  });
15043
15272
  const removeMemoryFile = Effect.fn("memoryFile.removeMemoryFile")(function* (input) {
15044
15273
  if (!isMemoryAssetName(input.name)) return;
15274
+ if (isMemoryDirectoryName(input.name)) {
15275
+ yield* removeSkillDirectory({
15276
+ skillsRoot: input.memoryRoot,
15277
+ name: input.name
15278
+ });
15279
+ return;
15280
+ }
15045
15281
  const fileSystem = yield* FileSystem.FileSystem;
15046
15282
  const path = yield* Path.Path;
15047
15283
  yield* fileSystem.remove(path.join(input.memoryRoot, input.name), { force: true });
@@ -15107,169 +15343,6 @@ const writeSkillManifest = Effect.fn("skillManifest.write")(function* (manifestP
15107
15343
  });
15108
15344
  });
15109
15345
  //#endregion
15110
- //#region src/sync/skillDirectory.ts
15111
- /**
15112
- * Reading and writing skill directories on disk.
15113
- *
15114
- * Kept apart from the sync itself so the filesystem shape has its own tests,
15115
- * and so the rules about what is refusable — binaries, oversized files, deep
15116
- * trees — live in one place rather than being rediscovered at each call site.
15117
- *
15118
- * @module sync/skillDirectory
15119
- */
15120
- /** Bounded so a stray symlink or a vendored `node_modules` cannot walk forever. */
15121
- const MAX_SKILL_DEPTH = 4;
15122
- /**
15123
- * Entries that are never part of a skill, skipped rather than refused.
15124
- *
15125
- * The distinction matters. Refusing an asset because it cannot be represented
15126
- * faithfully is right — a half-materialized skill loads and misbehaves. But a
15127
- * `.DS_Store` is not part of the skill at all, and macOS drops one into any
15128
- * directory that has been opened in Finder, so refusing on its account rejects
15129
- * a perfectly good skill for a reason its author cannot see.
15130
- *
15131
- * These could not sync in any case: `AgentAssetFilePath` rejects a leading dot,
15132
- * so a dotfile has no representable path even if it were text.
15133
- */
15134
- const IGNORED_ENTRIES = /* @__PURE__ */ new Set([
15135
- ".DS_Store",
15136
- "Thumbs.db",
15137
- ".git",
15138
- ".svn",
15139
- ".hg",
15140
- "node_modules",
15141
- "__pycache__",
15142
- ".venv",
15143
- ".pytest_cache",
15144
- ".ruff_cache",
15145
- ".mypy_cache"
15146
- ]);
15147
- /**
15148
- * Anything else with a leading dot is skipped too — an editor's `.idea`, a
15149
- * `.gitignore` that belongs to the checkout rather than the skill. The rule is
15150
- * the same one the path schema already enforces, applied earlier so it reads as
15151
- * "not part of the skill" instead of "this skill is broken".
15152
- */
15153
- const isIgnoredEntry = (entry) => IGNORED_ENTRIES.has(entry) || entry.startsWith(".");
15154
- /**
15155
- * A file is binary if decoding it yields a NUL.
15156
- *
15157
- * Cheap, and right for what is being asked: the question is not "is this valid
15158
- * UTF-8" but "can this round-trip through a JSON text column", and a NUL is
15159
- * what actually breaks that on the way back out of SQLite.
15160
- */
15161
- const looksBinary = (content) => content.includes("\0");
15162
- const readSkillFiles = Effect.fn("skillDirectory.readSkillFiles")(function* (root) {
15163
- const fileSystem = yield* FileSystem.FileSystem;
15164
- const path = yield* Path.Path;
15165
- const files = [];
15166
- const walk = Effect.fn("skillDirectory.walk")(function* (directory, prefix, depth) {
15167
- if (depth > MAX_SKILL_DEPTH) return "too-deep";
15168
- const entries = yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []));
15169
- for (const entry of [...entries].sort()) {
15170
- if (isIgnoredEntry(entry)) continue;
15171
- const absolute = path.join(directory, entry);
15172
- const relative = prefix === "" ? entry : `${prefix}/${entry}`;
15173
- const info = yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0));
15174
- if (info === void 0) return "unreadable";
15175
- if (info.type === "Directory") {
15176
- const nested = yield* walk(absolute, relative, depth + 1);
15177
- if (nested !== null) return nested;
15178
- continue;
15179
- }
15180
- if (info.type !== "File") continue;
15181
- if (files.length >= 512) return "too-many-files";
15182
- const content = yield* fileSystem.readFileString(absolute).pipe(Effect.orElseSucceed(() => void 0));
15183
- if (content === void 0) return "unreadable";
15184
- if (looksBinary(content)) return "binary-file";
15185
- if (content.length > 524288) return "file-too-large";
15186
- files.push({
15187
- path: relative,
15188
- content
15189
- });
15190
- }
15191
- return null;
15192
- });
15193
- const refusal = yield* walk(root, "", 1);
15194
- if (refusal !== null) return { refusal };
15195
- if (agentAssetTotalBytes(files) > 4194304) return { refusal: "asset-too-large" };
15196
- return { files };
15197
- });
15198
- /**
15199
- * Read every skill directory under a user-scope `skills/` root.
15200
- *
15201
- * A directory with no `SKILL.md` is not a skill and is skipped silently —
15202
- * Claude Code would ignore it too, so reporting it would be noise about
15203
- * something that was never a skill in the first place.
15204
- */
15205
- const readSkillDirectory = Effect.fn("skillDirectory.readSkillDirectory")(function* (skillsRoot) {
15206
- const fileSystem = yield* FileSystem.FileSystem;
15207
- const path = yield* Path.Path;
15208
- const entries = yield* fileSystem.readDirectory(skillsRoot).pipe(Effect.orElseSucceed(() => []));
15209
- const skills = [];
15210
- const refused = [];
15211
- for (const entry of [...entries].sort()) {
15212
- if (isIgnoredEntry(entry)) continue;
15213
- const directory = path.join(skillsRoot, entry);
15214
- if ((yield* fileSystem.stat(directory).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "Directory") continue;
15215
- if (!(yield* fileSystem.exists(path.join(directory, "SKILL.md")).pipe(Effect.orElseSucceed(() => false)))) continue;
15216
- const read = yield* readSkillFiles(directory);
15217
- if ("refusal" in read) {
15218
- refused.push({
15219
- name: entry,
15220
- reason: read.refusal
15221
- });
15222
- continue;
15223
- }
15224
- skills.push({
15225
- name: entry,
15226
- files: read.files
15227
- });
15228
- }
15229
- return {
15230
- skills,
15231
- refused
15232
- };
15233
- });
15234
- /**
15235
- * Replace a skill directory with exactly the asset's files.
15236
- *
15237
- * Written to a sibling and then swapped, so a crash mid-write leaves the old
15238
- * skill intact rather than half of the new one. A skill is loaded as a unit and
15239
- * a partially written one is instructions with the middle missing, which an
15240
- * agent will happily follow.
15241
- */
15242
- const writeSkillDirectory = Effect.fn("skillDirectory.writeSkillDirectory")(function* (input) {
15243
- const fileSystem = yield* FileSystem.FileSystem;
15244
- const path = yield* Path.Path;
15245
- const target = path.join(input.skillsRoot, input.name);
15246
- const staging = path.join(input.skillsRoot, `.${input.name}.p4code-staging`);
15247
- yield* fileSystem.makeDirectory(input.skillsRoot, { recursive: true });
15248
- yield* fileSystem.remove(staging, {
15249
- recursive: true,
15250
- force: true
15251
- });
15252
- yield* fileSystem.makeDirectory(staging, { recursive: true });
15253
- for (const file of input.files) {
15254
- const destination = path.join(staging, file.path);
15255
- yield* fileSystem.makeDirectory(path.dirname(destination), { recursive: true });
15256
- yield* fileSystem.writeFileString(destination, file.content);
15257
- }
15258
- yield* fileSystem.remove(target, {
15259
- recursive: true,
15260
- force: true
15261
- });
15262
- yield* fileSystem.rename(staging, target);
15263
- });
15264
- const removeSkillDirectory = Effect.fn("skillDirectory.removeSkillDirectory")(function* (input) {
15265
- const fileSystem = yield* FileSystem.FileSystem;
15266
- const path = yield* Path.Path;
15267
- yield* fileSystem.remove(path.join(input.skillsRoot, input.name), {
15268
- recursive: true,
15269
- force: true
15270
- });
15271
- });
15272
- //#endregion
15273
15346
  //#region src/sync/skillSyncPlan.ts
15274
15347
  /**
15275
15348
  * Deciding what a skill sync should do, given three views of the world.
@@ -16211,7 +16284,7 @@ const makeAssetCommands = (words) => {
16211
16284
  name: nameArgument,
16212
16285
  yes: Flag.boolean("yes").pipe(Flag.withDescription("Confirm: this removes it from the hub and from every machine."))
16213
16286
  }).pipe(Command.withDescription(`Delete a ${words.singular} from the hub and from every machine.`), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => Effect.gen(function* () {
16214
- if (!flags.yes) return yield* Console.error(`This deletes ${flags.name} from the hub and from every machine that syncs it.\nRe-run with --yes if that is what you want.\nTo stop sharing it from here only: p4 ${words.command} unpublish ${flags.name}`);
16287
+ 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: p4c ${words.command} unpublish ${flags.name}`);
16215
16288
  const report = yield* sync.deleteFromHub({
16216
16289
  kind: words.kind,
16217
16290
  name: flags.name
@@ -24226,6 +24299,7 @@ const make$51 = Effect.fn("cloud.boot_service.make")(function* (input) {
24226
24299
  });
24227
24300
  const activateLaunchAgent = Effect.gen(function* () {
24228
24301
  yield* runStep("unloading the previous service", "launchctl", ["bootout", serviceTarget]).pipe(Effect.ignore);
24302
+ yield* runStep("enabling the service", "launchctl", ["enable", serviceTarget]).pipe(Effect.ignore);
24229
24303
  yield* runStep("loading the service", "launchctl", [
24230
24304
  "bootstrap",
24231
24305
  guiDomain,
@@ -44371,14 +44445,15 @@ const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(functio
44371
44445
  const versionProbe = yield* runClaudeCommand(claudeSettings, ["--version"], resolvedEnvironment).pipe(Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result);
44372
44446
  if (Result.isFailure(versionProbe)) {
44373
44447
  const error = versionProbe.failure;
44374
- yield* Effect.logWarning("Claude Agent CLI health check failed.", { errorTag: error._tag });
44448
+ const missing = isCommandMissingCause(error);
44449
+ yield* (missing ? Effect.logDebug : Effect.logWarning)("Claude Agent CLI health check failed.", { errorTag: error._tag });
44375
44450
  return buildServerProvider({
44376
44451
  presentation: CLAUDE_PRESENTATION,
44377
44452
  enabled: claudeSettings.enabled,
44378
44453
  checkedAt,
44379
44454
  models: allModels,
44380
44455
  probe: {
44381
- installed: !isCommandMissingCause(error),
44456
+ installed: !missing,
44382
44457
  version: null,
44383
44458
  status: "error",
44384
44459
  auth: { status: "unknown" },
@@ -73561,14 +73636,15 @@ const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(functio
73561
73636
  const aboutProbe = yield* runCursorAboutCommand(cursorSettings, environment).pipe(Effect.timeoutOption(ABOUT_TIMEOUT_MS), Effect.result);
73562
73637
  if (Result.isFailure(aboutProbe)) {
73563
73638
  const error = aboutProbe.failure;
73564
- yield* Effect.logWarning("Cursor Agent CLI health check failed.", { errorTag: error._tag });
73639
+ const missing = isCommandMissingCause(error);
73640
+ yield* (missing ? Effect.logDebug : Effect.logWarning)("Cursor Agent CLI health check failed.", { errorTag: error._tag });
73565
73641
  return buildServerProvider({
73566
73642
  presentation: CURSOR_PRESENTATION,
73567
73643
  enabled: cursorSettings.enabled,
73568
73644
  checkedAt,
73569
73645
  models: fallbackModels,
73570
73646
  probe: {
73571
- installed: !isCommandMissingCause(error),
73647
+ installed: !missing,
73572
73648
  version: null,
73573
73649
  status: "error",
73574
73650
  auth: { status: "unknown" },
@@ -76205,14 +76281,15 @@ const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(function* (
76205
76281
  const versionResult = yield* runGrokVersionCommand(grokSettings, environment).pipe(Effect.timeoutOption(VERSION_PROBE_TIMEOUT_MS), Effect.result);
76206
76282
  if (Result.isFailure(versionResult)) {
76207
76283
  const error = versionResult.failure;
76208
- yield* Effect.logWarning("Grok CLI health check failed.", { errorTag: error._tag });
76284
+ const missing = isCommandMissingCause(error);
76285
+ yield* (missing ? Effect.logDebug : Effect.logWarning)("Grok CLI health check failed.", { errorTag: error._tag });
76209
76286
  return buildServerProvider({
76210
76287
  presentation: GROK_PRESENTATION,
76211
76288
  enabled: grokSettings.enabled,
76212
76289
  checkedAt,
76213
76290
  models: fallbackModels,
76214
76291
  probe: {
76215
- installed: !isCommandMissingCause(error),
76292
+ installed: !missing,
76216
76293
  version: null,
76217
76294
  status: "error",
76218
76295
  auth: { status: "unknown" },
@@ -82690,7 +82767,37 @@ const makeCli = () => Command.make("p4c", { ...sharedServerCommandFlags }).pipe(
82690
82767
  skillsCommand
82691
82768
  ]));
82692
82769
  const cli = makeCli();
82693
- if (import.meta.main) reportUpdateAvailable.pipe(Effect.andThen(Command.run(cli, { version })), Effect.scoped, Effect.provide(CliRuntimeLayer), NodeRuntime.runMain);
82770
+ /**
82771
+ * A one-line message for an expected failure, without the stack.
82772
+ *
82773
+ * Every domain error here is a `Schema.TaggedErrorClass` with a `message`
82774
+ * getter written for a person — "Background setup failed while loading the
82775
+ * service (exit code 5)" — and the surrounding command has usually already
82776
+ * printed the log path that holds the detail. Appending twenty frames of
82777
+ * bundled `dist/bin.mjs` to that adds nothing anybody can act on.
82778
+ */
82779
+ const describeExpectedFailure = (error) => {
82780
+ const message = error?.message;
82781
+ if (typeof message === "string" && message.trim().length > 0) return message;
82782
+ return String(error);
82783
+ };
82784
+ /**
82785
+ * Expected failures print their message; defects keep their stack.
82786
+ *
82787
+ * The distinction is Effect's own, and it is the right one: a typed failure is
82788
+ * a condition the code anticipated and described, so a trace through a
82789
+ * bundled file is noise. A defect is a bug, and there the trace is the only
82790
+ * thing worth having.
82791
+ *
82792
+ * `CliError` is re-raised rather than handled, because `runMain` already
82793
+ * renders usage and validation failures better than a bare message would —
82794
+ * catching them here would replace "unknown flag --foo, did you mean --force"
82795
+ * with one flat line.
82796
+ */
82797
+ const reportFailuresWithoutStacks = (effect) => effect.pipe(Effect.catchIf((error) => !CliError.isCliError(error), (error) => Console.error(describeExpectedFailure(error)).pipe(Effect.andThen(Effect.sync(() => {
82798
+ process.exitCode = 1;
82799
+ })))));
82800
+ if (import.meta.main) reportUpdateAvailable.pipe(Effect.andThen(Command.run(cli, { version })), reportFailuresWithoutStacks, Effect.scoped, Effect.provide(CliRuntimeLayer), NodeRuntime.runMain);
82694
82801
  //#endregion
82695
82802
  export { cli, makeCli };
82696
82803