@p4code/cli 0.0.43 → 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.
- package/dist/bin.mjs +248 -176
- package/dist/client/assets/DiffPanel-DD3E29O7.js +98 -0
- package/dist/client/assets/FilePreviewPanel-fBOIzVAm.js +1778 -0
- package/dist/client/assets/PreviewPanel-BXyhScZZ.js +2 -0
- package/dist/client/assets/{angular-html-D2LtifxG.js → angular-html-BwUZQlj7.js} +2 -2
- package/dist/client/assets/{angular-ts-oVDpKJb2.js → angular-ts-8EX8ZNeQ.js} +2 -2
- package/dist/client/assets/{apl-2vnm7rdp.js → apl-DYm7AarA.js} +2 -2
- package/dist/client/assets/arrow-right-BNeeftk2.js +2 -0
- package/dist/client/assets/{astro-CDbEfI_f.js → astro-DFaFMvm9.js} +2 -2
- package/dist/client/assets/{blade-B1MwvSO0.js → blade-D9aigeeZ.js} +2 -2
- package/dist/client/assets/{c-Bi5eHJiI.js → c-BvAsa11Y.js} +2 -2
- package/dist/client/assets/{cobol-BxESWi7D.js → cobol-BZwY3--F.js} +2 -2
- package/dist/client/assets/{coffee-s151wUsp.js → coffee-vNqcHwuy.js} +2 -2
- package/dist/client/assets/{cpp-B51xjAkw.js → cpp-D3Xet0_6.js} +2 -2
- package/dist/client/assets/{crystal-BW0dq2Rw.js → crystal-D-UW0VhN.js} +2 -2
- package/dist/client/assets/{css-Czn5ycv4.js → css-B1CDNsy0.js} +2 -2
- package/dist/client/assets/{edge-DPtVbjt1.js → edge-B_geUcoi.js} +2 -2
- package/dist/client/assets/{elixir-381Dy51E.js → elixir-BzK0Ks1B.js} +2 -2
- package/dist/client/assets/{elm-Bpu_ckb9.js → elm-BvlMiaGz.js} +2 -2
- package/dist/client/assets/{erb-u1vpMXOw.js → erb-OFCHFHP8.js} +2 -2
- package/dist/client/assets/fileCommentAnnotations-DI_Ec6DZ.js +2 -0
- package/dist/client/assets/{git-rebase-ShzdQ6mA.js → git-rebase-Xc_wijNW.js} +2 -2
- package/dist/client/assets/{glimmer-js-DI4_LeGA.js → glimmer-js-D-GpFszK.js} +2 -2
- package/dist/client/assets/{glimmer-ts-BrA1R4ia.js → glimmer-ts-_MmK7qB5.js} +2 -2
- package/dist/client/assets/{glsl--BAUZuR7.js → glsl-Cm_ctoze.js} +2 -2
- package/dist/client/assets/{graphql-XmtNe4tX.js → graphql-De_-LrEt.js} +2 -2
- package/dist/client/assets/{hack-ZPcuqt6I.js → hack-qUzK29KK.js} +2 -2
- package/dist/client/assets/{haml-BLmOYSAV.js → haml-mqyUpOBj.js} +2 -2
- package/dist/client/assets/{handlebars-BxR19vQc.js → handlebars-DS3jjsXS.js} +2 -2
- package/dist/client/assets/{html-derivative-CLJquIOq.js → html-derivative-BPRW_sHn.js} +2 -2
- package/dist/client/assets/{html-Do3TWS07.js → html-ippoHerj.js} +2 -2
- package/dist/client/assets/{http-C6M7KGrt.js → http-xrFfvFRg.js} +2 -2
- package/dist/client/assets/{hurl-AFYDoSjV.js → hurl-DSL-wjHh.js} +2 -2
- package/dist/client/assets/index-BAsvpSiM.js +636 -0
- package/dist/client/assets/index-CGyzLTcp.css +1 -0
- package/dist/client/assets/{java-DoRNyNZ_.js → java-B8xLHoVB.js} +2 -2
- package/dist/client/assets/{javascript-Dd-zEQJs.js → javascript-DkxetjtW.js} +2 -2
- package/dist/client/assets/{jinja-TJRtEusk.js → jinja-DrlmZXOI.js} +2 -2
- package/dist/client/assets/{jison-bG9mcEJe.js → jison-CBYMCWiZ.js} +2 -2
- package/dist/client/assets/{json-DY6EaQUk.js → json-aobk_LSd.js} +2 -2
- package/dist/client/assets/{jsx-BuL0_m-Z.js → jsx-BTcIKPoF.js} +2 -2
- package/dist/client/assets/{julia-4bcuyKPV.js → julia-BwUvFb1K.js} +2 -2
- package/dist/client/assets/{just-F3UNctxi.js → just-2GL8m28O.js} +2 -2
- package/dist/client/assets/{latex-DCJY-Aag.js → latex-BwWZNh8B.js} +2 -2
- package/dist/client/assets/{liquid-M906bGYJ.js → liquid-j8UWR5fn.js} +2 -2
- package/dist/client/assets/{lua-CeupCtSb.js → lua-BiEJazUk.js} +2 -2
- package/dist/client/assets/{marko-D_3SsXZn.js → marko-BrkuG9X9.js} +2 -2
- package/dist/client/assets/{mdc-DRUaGPVE.js → mdc-CbE9Gm1n.js} +2 -2
- package/dist/client/assets/{nginx-B_DJSFb0.js → nginx-C-gcDf_H.js} +2 -2
- package/dist/client/assets/{nim-BxoRFkNd.js → nim-9X20IMJZ.js} +2 -2
- package/dist/client/assets/{perl-CjLiNsGL.js → perl-Dqj-SUzY.js} +2 -2
- package/dist/client/assets/{php-DcklQON0.js → php-CQymwCam.js} +2 -2
- package/dist/client/assets/{pug-DHYHUkbl.js → pug-DIOYJ7Tj.js} +2 -2
- package/dist/client/assets/{qml-Cggn-sFR.js → qml-BpdlPbMh.js} +2 -2
- package/dist/client/assets/{r-BhkOaKoM.js → r-BdXpML7C.js} +2 -2
- package/dist/client/assets/{razor-DksRt091.js → razor-CmGIbQUz.js} +2 -2
- package/dist/client/assets/{regexp-CVeVJKpO.js → regexp-udR9FXf8.js} +2 -2
- package/dist/client/assets/{rst-B8gnyB7p.js → rst-C81pNvVo.js} +2 -2
- package/dist/client/assets/{ruby-B7aYI7EY.js → ruby-D5xBAIVx.js} +2 -2
- package/dist/client/assets/{sas-DmDsVHsr.js → sas-pegDgwhF.js} +2 -2
- package/dist/client/assets/{scss-Cf0hFbmP.js → scss-Dx_Aq9w8.js} +2 -2
- package/dist/client/assets/{shellscript-CJ2l7Ljo.js → shellscript-Bl-HTaUh.js} +2 -2
- package/dist/client/assets/{shellsession-CE-6mg0T.js → shellsession-CezqakSW.js} +2 -2
- package/dist/client/assets/{soy-eTwUOhLL.js → soy-DclELXc7.js} +2 -2
- package/dist/client/assets/{sql-DaTGJ1E1.js → sql-DAsY-zzx.js} +2 -2
- package/dist/client/assets/{stata-DCqqmU-V.js → stata-DjQ16uuy.js} +2 -2
- package/dist/client/assets/{surrealql-DJuyQLb6.js → surrealql-BLzgjzgE.js} +2 -2
- package/dist/client/assets/{svelte-DZfElFr5.js → svelte-DFGwC3eP.js} +2 -2
- package/dist/client/assets/{templ-BeuT7j1S.js → templ-BiFbdCJD.js} +2 -2
- package/dist/client/assets/{tex-CUjxQ3iG.js → tex-DQCF2PEO.js} +2 -2
- package/dist/client/assets/textarea-lBMORu4g.js +46 -0
- package/dist/client/assets/{ts-tags-C00W--Ve.js → ts-tags-CkQC6ekt.js} +2 -2
- package/dist/client/assets/{tsx-h-kXP2gA.js → tsx-m7HWHXfL.js} +2 -2
- package/dist/client/assets/{twig-DhMoAQY-.js → twig-B5P5jotH.js} +2 -2
- package/dist/client/assets/{typescript-DwJdYtiC.js → typescript-DmGwuo7e.js} +2 -2
- package/dist/client/assets/{vue-CvLnzHbi.js → vue-Rn_gU7Px.js} +2 -2
- package/dist/client/assets/{vue-html-fj2AgeR4.js → vue-html-DbKQF2OX.js} +2 -2
- package/dist/client/assets/{vue-vine-D1-Lx9ZR.js → vue-vine-C13tIrLE.js} +2 -2
- package/dist/client/assets/{xml-CDkXSKu_.js → xml-D7fQm-eG.js} +2 -2
- package/dist/client/assets/{xsl-BkotjMT0.js → xsl-Bv743my_.js} +2 -2
- package/dist/client/assets/{yaml-BssI7mmn.js → yaml--h7pPfkH.js} +2 -2
- package/dist/client/index.html +3 -3
- package/package.json +1 -1
- package/dist/client/assets/DiffPanel-D8nVXZ0Y.js +0 -98
- package/dist/client/assets/FilePreviewPanel-CFKCFyJ3.js +0 -1778
- package/dist/client/assets/PreviewPanel-VQey9906.js +0 -2
- package/dist/client/assets/arrow-right-DmJqM7J1.js +0 -2
- package/dist/client/assets/fileCommentAnnotations-BwnrRpfq.js +0 -2
- package/dist/client/assets/index-XxxL_T8f.js +0 -635
- package/dist/client/assets/index-mqpX0B5G.css +0 -1
- 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.
|
|
236
|
+
var version = "0.0.44";
|
|
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
|
|
13929
|
-
*
|
|
13930
|
-
* deliberate configuration, but it should
|
|
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
|
|
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.
|
|
14916
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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.
|
|
@@ -16212,7 +16284,7 @@ const makeAssetCommands = (words) => {
|
|
|
16212
16284
|
name: nameArgument,
|
|
16213
16285
|
yes: Flag.boolean("yes").pipe(Flag.withDescription("Confirm: this removes it from the hub and from every machine."))
|
|
16214
16286
|
}).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:
|
|
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}`);
|
|
16216
16288
|
const report = yield* sync.deleteFromHub({
|
|
16217
16289
|
kind: words.kind,
|
|
16218
16290
|
name: flags.name
|