@p4code/cli 0.0.43 → 0.0.45

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 +317 -177
  2. package/dist/client/assets/DiffPanel-PPZKQWCF.js +98 -0
  3. package/dist/client/assets/FilePreviewPanel-DdLy8b8b.js +1778 -0
  4. package/dist/client/assets/PreviewPanel-BKsstY2H.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-BBX2usb_.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-CGyzLTcp.css +1 -0
  35. package/dist/client/assets/index-DnkEkLTH.js +636 -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
@@ -233,7 +233,7 @@ const make$71 = () => {
233
233
  const layer$67 = Layer.sync(NetService, make$71);
234
234
  //#endregion
235
235
  //#region package.json
236
- var version = "0.0.43";
236
+ var version = "0.0.45";
237
237
  //#endregion
238
238
  //#region src/config.ts
239
239
  /**
@@ -13925,9 +13925,10 @@ const HubHttpServerLive = Layer.unwrap(Effect.gen(function* () {
13925
13925
  /**
13926
13926
  * Says on startup exactly what the hub is exposing and to whom.
13927
13927
  *
13928
- * A hub bound past loopback is reachable by every device the tailnet ACL lets
13929
- * through, and the bearer token is then the only remaining control. That is a
13930
- * 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.
13931
13932
  */
13932
13933
  const announce = Layer.effectDiscard(Effect.gen(function* () {
13933
13934
  const config = yield* HubConfig;
@@ -13939,7 +13940,7 @@ const announce = Layer.effectDiscard(Effect.gen(function* () {
13939
13940
  dbPath: config.dbPath,
13940
13941
  authorizedServers: config.tokenHashes.size
13941
13942
  });
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 });
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 });
13943
13944
  }));
13944
13945
  const makeHubLayer = HttpRouter.serve(layer$58).pipe(Layer.provideMerge(announce), Layer.provideMerge(HubHttpServerLive), Layer.provideMerge(TaskRepositoryLive), Layer.provideMerge(AgentAssetRepositoryLive), Layer.provideMerge(HubPersistenceLive));
13945
13946
  const runHub = Layer.launch(makeHubLayer);
@@ -14906,15 +14907,188 @@ const removeMcpRegistration = Effect.fn("mcpRegistrationFiles.remove")(function*
14906
14907
  yield* fileSystem.remove(path.join(input.registrationsRoot, input.name), { force: true });
14907
14908
  });
14908
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
14909
15082
  //#region src/sync/memoryFile.ts
14910
15083
  /**
14911
15084
  * Reading and writing user-scope memory files.
14912
15085
  *
14913
15086
  * Memory is the instruction file an agent reads before anything else, so a
14914
15087
  * 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.
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.
14918
15092
  *
14919
15093
  * That last part is the entire design constraint. `<configDir>` also holds
14920
15094
  * `settings.json`, `.claude.json` and `.credentials.json`. "Write the file this
@@ -14951,9 +15125,38 @@ const MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md"];
14951
15125
  * it on a case-sensitive one would make the same hub row materialize on one
14952
15126
  * machine and refuse on another.
14953
15127
  */
14954
- 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);
14955
15158
  /** The same test the skill reader uses: a NUL is what breaks a JSON text column. */
14956
- const looksBinary$1 = (content) => content.includes("\0");
15159
+ const looksBinary = (content) => content.includes("\0");
14957
15160
  /**
14958
15161
  * Read whichever memory files exist in this machine's config directory.
14959
15162
  *
@@ -14977,7 +15180,7 @@ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memor
14977
15180
  });
14978
15181
  continue;
14979
15182
  }
14980
- if (looksBinary$1(content)) {
15183
+ if (looksBinary(content)) {
14981
15184
  refused.push({
14982
15185
  name,
14983
15186
  reason: "binary-file"
@@ -15007,6 +15210,23 @@ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memor
15007
15210
  files
15008
15211
  });
15009
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
+ }
15010
15230
  return {
15011
15231
  skills,
15012
15232
  refused
@@ -15020,7 +15240,10 @@ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memor
15020
15240
  * carrying a second file called `settings.json` would otherwise write it, which
15021
15241
  * is the same escape by a longer route.
15022
15242
  */
15023
- 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
+ };
15024
15247
  /**
15025
15248
  * Write a memory file, or refuse.
15026
15249
  *
@@ -15034,6 +15257,11 @@ const writeMemoryFile = Effect.fn("memoryFile.writeMemoryFile")(function* (input
15034
15257
  if (!isWritableMemoryAsset(input)) return "refused";
15035
15258
  const fileSystem = yield* FileSystem.FileSystem;
15036
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"));
15037
15265
  const contents = input.files[0]?.content ?? "";
15038
15266
  yield* fileSystem.makeDirectory(input.memoryRoot, { recursive: true }).pipe(Effect.ignore);
15039
15267
  return yield* writeFileStringAtomically({
@@ -15043,6 +15271,13 @@ const writeMemoryFile = Effect.fn("memoryFile.writeMemoryFile")(function* (input
15043
15271
  });
15044
15272
  const removeMemoryFile = Effect.fn("memoryFile.removeMemoryFile")(function* (input) {
15045
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
+ }
15046
15281
  const fileSystem = yield* FileSystem.FileSystem;
15047
15282
  const path = yield* Path.Path;
15048
15283
  yield* fileSystem.remove(path.join(input.memoryRoot, input.name), { force: true });
@@ -15108,169 +15343,6 @@ const writeSkillManifest = Effect.fn("skillManifest.write")(function* (manifestP
15108
15343
  });
15109
15344
  });
15110
15345
  //#endregion
15111
- //#region src/sync/skillDirectory.ts
15112
- /**
15113
- * Reading and writing skill directories on disk.
15114
- *
15115
- * Kept apart from the sync itself so the filesystem shape has its own tests,
15116
- * and so the rules about what is refusable — binaries, oversized files, deep
15117
- * trees — live in one place rather than being rediscovered at each call site.
15118
- *
15119
- * @module sync/skillDirectory
15120
- */
15121
- /** Bounded so a stray symlink or a vendored `node_modules` cannot walk forever. */
15122
- const MAX_SKILL_DEPTH = 4;
15123
- /**
15124
- * Entries that are never part of a skill, skipped rather than refused.
15125
- *
15126
- * The distinction matters. Refusing an asset because it cannot be represented
15127
- * faithfully is right — a half-materialized skill loads and misbehaves. But a
15128
- * `.DS_Store` is not part of the skill at all, and macOS drops one into any
15129
- * directory that has been opened in Finder, so refusing on its account rejects
15130
- * a perfectly good skill for a reason its author cannot see.
15131
- *
15132
- * These could not sync in any case: `AgentAssetFilePath` rejects a leading dot,
15133
- * so a dotfile has no representable path even if it were text.
15134
- */
15135
- const IGNORED_ENTRIES = /* @__PURE__ */ new Set([
15136
- ".DS_Store",
15137
- "Thumbs.db",
15138
- ".git",
15139
- ".svn",
15140
- ".hg",
15141
- "node_modules",
15142
- "__pycache__",
15143
- ".venv",
15144
- ".pytest_cache",
15145
- ".ruff_cache",
15146
- ".mypy_cache"
15147
- ]);
15148
- /**
15149
- * Anything else with a leading dot is skipped too — an editor's `.idea`, a
15150
- * `.gitignore` that belongs to the checkout rather than the skill. The rule is
15151
- * the same one the path schema already enforces, applied earlier so it reads as
15152
- * "not part of the skill" instead of "this skill is broken".
15153
- */
15154
- const isIgnoredEntry = (entry) => IGNORED_ENTRIES.has(entry) || entry.startsWith(".");
15155
- /**
15156
- * A file is binary if decoding it yields a NUL.
15157
- *
15158
- * Cheap, and right for what is being asked: the question is not "is this valid
15159
- * UTF-8" but "can this round-trip through a JSON text column", and a NUL is
15160
- * what actually breaks that on the way back out of SQLite.
15161
- */
15162
- const looksBinary = (content) => content.includes("\0");
15163
- const readSkillFiles = Effect.fn("skillDirectory.readSkillFiles")(function* (root) {
15164
- const fileSystem = yield* FileSystem.FileSystem;
15165
- const path = yield* Path.Path;
15166
- const files = [];
15167
- const walk = Effect.fn("skillDirectory.walk")(function* (directory, prefix, depth) {
15168
- if (depth > MAX_SKILL_DEPTH) return "too-deep";
15169
- const entries = yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []));
15170
- for (const entry of [...entries].sort()) {
15171
- if (isIgnoredEntry(entry)) continue;
15172
- const absolute = path.join(directory, entry);
15173
- const relative = prefix === "" ? entry : `${prefix}/${entry}`;
15174
- const info = yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0));
15175
- if (info === void 0) return "unreadable";
15176
- if (info.type === "Directory") {
15177
- const nested = yield* walk(absolute, relative, depth + 1);
15178
- if (nested !== null) return nested;
15179
- continue;
15180
- }
15181
- if (info.type !== "File") continue;
15182
- if (files.length >= 512) return "too-many-files";
15183
- const content = yield* fileSystem.readFileString(absolute).pipe(Effect.orElseSucceed(() => void 0));
15184
- if (content === void 0) return "unreadable";
15185
- if (looksBinary(content)) return "binary-file";
15186
- if (content.length > 524288) return "file-too-large";
15187
- files.push({
15188
- path: relative,
15189
- content
15190
- });
15191
- }
15192
- return null;
15193
- });
15194
- const refusal = yield* walk(root, "", 1);
15195
- if (refusal !== null) return { refusal };
15196
- if (agentAssetTotalBytes(files) > 4194304) return { refusal: "asset-too-large" };
15197
- return { files };
15198
- });
15199
- /**
15200
- * Read every skill directory under a user-scope `skills/` root.
15201
- *
15202
- * A directory with no `SKILL.md` is not a skill and is skipped silently —
15203
- * Claude Code would ignore it too, so reporting it would be noise about
15204
- * something that was never a skill in the first place.
15205
- */
15206
- const readSkillDirectory = Effect.fn("skillDirectory.readSkillDirectory")(function* (skillsRoot) {
15207
- const fileSystem = yield* FileSystem.FileSystem;
15208
- const path = yield* Path.Path;
15209
- const entries = yield* fileSystem.readDirectory(skillsRoot).pipe(Effect.orElseSucceed(() => []));
15210
- const skills = [];
15211
- const refused = [];
15212
- for (const entry of [...entries].sort()) {
15213
- if (isIgnoredEntry(entry)) continue;
15214
- const directory = path.join(skillsRoot, entry);
15215
- if ((yield* fileSystem.stat(directory).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "Directory") continue;
15216
- if (!(yield* fileSystem.exists(path.join(directory, "SKILL.md")).pipe(Effect.orElseSucceed(() => false)))) continue;
15217
- const read = yield* readSkillFiles(directory);
15218
- if ("refusal" in read) {
15219
- refused.push({
15220
- name: entry,
15221
- reason: read.refusal
15222
- });
15223
- continue;
15224
- }
15225
- skills.push({
15226
- name: entry,
15227
- files: read.files
15228
- });
15229
- }
15230
- return {
15231
- skills,
15232
- refused
15233
- };
15234
- });
15235
- /**
15236
- * Replace a skill directory with exactly the asset's files.
15237
- *
15238
- * Written to a sibling and then swapped, so a crash mid-write leaves the old
15239
- * skill intact rather than half of the new one. A skill is loaded as a unit and
15240
- * a partially written one is instructions with the middle missing, which an
15241
- * agent will happily follow.
15242
- */
15243
- const writeSkillDirectory = Effect.fn("skillDirectory.writeSkillDirectory")(function* (input) {
15244
- const fileSystem = yield* FileSystem.FileSystem;
15245
- const path = yield* Path.Path;
15246
- const target = path.join(input.skillsRoot, input.name);
15247
- const staging = path.join(input.skillsRoot, `.${input.name}.p4code-staging`);
15248
- yield* fileSystem.makeDirectory(input.skillsRoot, { recursive: true });
15249
- yield* fileSystem.remove(staging, {
15250
- recursive: true,
15251
- force: true
15252
- });
15253
- yield* fileSystem.makeDirectory(staging, { recursive: true });
15254
- for (const file of input.files) {
15255
- const destination = path.join(staging, file.path);
15256
- yield* fileSystem.makeDirectory(path.dirname(destination), { recursive: true });
15257
- yield* fileSystem.writeFileString(destination, file.content);
15258
- }
15259
- yield* fileSystem.remove(target, {
15260
- recursive: true,
15261
- force: true
15262
- });
15263
- yield* fileSystem.rename(staging, target);
15264
- });
15265
- const removeSkillDirectory = Effect.fn("skillDirectory.removeSkillDirectory")(function* (input) {
15266
- const fileSystem = yield* FileSystem.FileSystem;
15267
- const path = yield* Path.Path;
15268
- yield* fileSystem.remove(path.join(input.skillsRoot, input.name), {
15269
- recursive: true,
15270
- force: true
15271
- });
15272
- });
15273
- //#endregion
15274
15346
  //#region src/sync/skillSyncPlan.ts
15275
15347
  /**
15276
15348
  * Deciding what a skill sync should do, given three views of the world.
@@ -16148,6 +16220,72 @@ const formatReport = (report) => {
16148
16220
  const count = report.unmanaged.length;
16149
16221
  return `${summary}\n\n${count} local item${count === 1 ? " is" : "s are"} not shared. Share ${count === 1 ? "it" : "them"} with: p4c skills publish --all`;
16150
16222
  };
16223
+ /**
16224
+ * Every kind, in the order a person thinks about them.
16225
+ *
16226
+ * The list, rather than the three constants written out again at each call
16227
+ * site: a fourth kind should appear in the universal commands by being added
16228
+ * here, not by being remembered in three places.
16229
+ */
16230
+ const ALL_WORDS = [
16231
+ SKILL_WORDS,
16232
+ MEMORY_WORDS,
16233
+ MCP_WORDS
16234
+ ];
16235
+ /**
16236
+ * The whole picture, in one page.
16237
+ *
16238
+ * `formatStatus` above answers "what about my skills", which is the right
16239
+ * question when you typed `p4c skills status`. This answers "what is shared
16240
+ * from this machine", which is the question people actually arrive with — and
16241
+ * the one the per-noun commands could not answer without being run three times
16242
+ * and read together.
16243
+ */
16244
+ const formatStatusAll = (status) => {
16245
+ const lines = [];
16246
+ if (!status.configured) lines.push("No hub is configured. Connect one with: p4c hub connect --url <url> --token <token>");
16247
+ else {
16248
+ lines.push(`Hub: ${status.hubUrl ?? "unknown"}${status.source === "environment" ? " (from the environment)" : ""}`);
16249
+ lines.push(`Sync: ${status.syncMode}`);
16250
+ lines.push(`Share: ${status.shareMode === "all" ? "everything (new items share automatically)" : "only the ones you choose"}`);
16251
+ }
16252
+ if (status.unavailable !== null) lines.push(`Warning: ${status.unavailable}`);
16253
+ for (const words of ALL_WORDS) {
16254
+ const entries = status.entries.filter((entry) => entry.kind === words.kind);
16255
+ const refused = status.refused.filter((entry) => entry.kind === words.kind);
16256
+ const root = rootFor(status, words.kind);
16257
+ lines.push("");
16258
+ lines.push(`${words.rootLabel}${root === null ? "" : `: ${root}`}`);
16259
+ if (entries.length === 0 && refused.length === 0) {
16260
+ lines.push(` No ${words.plural} found.`);
16261
+ continue;
16262
+ }
16263
+ for (const entry of entries) {
16264
+ const version = entry.version === null ? "" : ` v${entry.version}`;
16265
+ const detail = entry.detail === null ? "" : ` (${entry.detail})`;
16266
+ lines.push(` ${entry.name.padEnd(28)} ${STATE_LABEL[entry.state] ?? entry.state}${version}${detail}`);
16267
+ }
16268
+ for (const refusal of refused) lines.push(` ${refusal.name.padEnd(28)} cannot sync (${refusal.reason})`);
16269
+ }
16270
+ return lines.join("\n");
16271
+ };
16272
+ /**
16273
+ * `p4c sync` and `p4c status` — the whole config, without picking a noun.
16274
+ *
16275
+ * These are not new behaviour. `sync.run` and `sync.status` were always
16276
+ * kind-agnostic, so `p4c skills sync` has been reconciling memory and MCP
16277
+ * registrations along with skills all along — the noun in front of it named
16278
+ * where you happened to start, not what it touched. Somebody reasoning from
16279
+ * the command they typed would conclude they had synced their skills and
16280
+ * nothing else, which was wrong in the direction that matters: it reads as
16281
+ * "my memory did not sync" when it did.
16282
+ *
16283
+ * The per-noun `sync` and `status` stay. They are the right commands when the
16284
+ * question really is about one kind, and removing them would break the muscle
16285
+ * memory of anyone who has been typing them.
16286
+ */
16287
+ const syncAllCommand = Command.make("sync", { ...authLocationFlags }).pipe(Command.withDescription("Reconcile everything this machine shares — skills, memory and MCP."), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => sync.run.pipe(Effect.flatMap((report) => Console.log(formatReport(report)))))));
16288
+ const statusAllCommand = Command.make("status", { ...authLocationFlags }).pipe(Command.withDescription("Show everything this machine shares with the hub, by kind."), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => sync.status.pipe(Effect.flatMap((status) => Console.log(formatStatusAll(status)))))));
16151
16289
  const urlFlag = Flag.string("url").pipe(Flag.withDescription("Base URL of the hub, for example https://hub.example.com."));
16152
16290
  const tokenFlag = Flag.string("token").pipe(Flag.withDescription("Bearer token for this server, from `p4c hub token add`."));
16153
16291
  const hubConnectCommand = Command.make("connect", {
@@ -16212,7 +16350,7 @@ const makeAssetCommands = (words) => {
16212
16350
  name: nameArgument,
16213
16351
  yes: Flag.boolean("yes").pipe(Flag.withDescription("Confirm: this removes it from the hub and from every machine."))
16214
16352
  }).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}`);
16353
+ 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}`);
16216
16354
  const report = yield* sync.deleteFromHub({
16217
16355
  kind: words.kind,
16218
16356
  name: flags.name
@@ -82692,7 +82830,9 @@ const makeCli = () => Command.make("p4c", { ...sharedServerCommandFlags }).pipe(
82692
82830
  memoryCommand,
82693
82831
  projectCommand,
82694
82832
  serviceCommand,
82695
- skillsCommand
82833
+ skillsCommand,
82834
+ statusAllCommand,
82835
+ syncAllCommand
82696
82836
  ]));
82697
82837
  const cli = makeCli();
82698
82838
  /**