@p4code/cli 0.1.17 → 0.1.18
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 +781 -260
- package/dist/client/assets/{DiffPanel-BrM3QqVj.js → DiffPanel-CP_cM9m0.js} +2 -2
- package/dist/client/assets/{FilePreviewPanel-DXk_4dj4.js → FilePreviewPanel-CLho0PPu.js} +2 -2
- package/dist/client/assets/{PreviewPanel-DfngnmUt.js → PreviewPanel-DvnUpMQv.js} +2 -2
- package/dist/client/assets/{angular-html-veA1EpVJ.js → angular-html-CEMakyzJ.js} +2 -2
- package/dist/client/assets/{angular-ts-CjhObYqk.js → angular-ts-YXZHq2uz.js} +2 -2
- package/dist/client/assets/{apl-BaBqmioI.js → apl-BTa85Bsd.js} +2 -2
- package/dist/client/assets/arrow-right-Bi8kADrV.js +2 -0
- package/dist/client/assets/{astro-aZep3bUH.js → astro-DIbti84c.js} +2 -2
- package/dist/client/assets/{blade-B-PS40R8.js → blade-CKAdvdwG.js} +2 -2
- package/dist/client/assets/{c-Da6G9SJL.js → c-DW17xzCK.js} +2 -2
- package/dist/client/assets/{cobol-MFD_Q4Xa.js → cobol-DXiicIcZ.js} +2 -2
- package/dist/client/assets/{coffee-DOKaGG2H.js → coffee-9Pdmmdn2.js} +2 -2
- package/dist/client/assets/{cpp-iD8OoA0X.js → cpp-bm3vFVmW.js} +2 -2
- package/dist/client/assets/{crystal-BgCh46iL.js → crystal-DbxZDZf9.js} +2 -2
- package/dist/client/assets/{css-C0AvK1Fp.js → css-BEBnbxDe.js} +2 -2
- package/dist/client/assets/{edge-DWC_9CO2.js → edge-K97wVEy_.js} +2 -2
- package/dist/client/assets/{elixir-5ahlHJpU.js → elixir-Rc2JDLLD.js} +2 -2
- package/dist/client/assets/{elm-B4b4KSk0.js → elm-QsA8aREL.js} +2 -2
- package/dist/client/assets/{erb-CNRZFKcV.js → erb-CliRJz4v.js} +2 -2
- package/dist/client/assets/{fileCommentAnnotations-V3SOLaB4.js → fileCommentAnnotations-DtIPA0Fm.js} +2 -2
- package/dist/client/assets/{git-rebase-AbfQBerd.js → git-rebase-Da_Wuhd_.js} +2 -2
- package/dist/client/assets/{glimmer-js-_dx97vsR.js → glimmer-js-XPbTw3tC.js} +2 -2
- package/dist/client/assets/{glimmer-ts-BFmDDwi5.js → glimmer-ts-BtK3NOkH.js} +2 -2
- package/dist/client/assets/{glsl-CXroyNbj.js → glsl-DqEtJptk.js} +2 -2
- package/dist/client/assets/{graphql-RU5TF49j.js → graphql-BSRTeXs0.js} +2 -2
- package/dist/client/assets/{hack-S6OyQkI3.js → hack-Dw9HaNHZ.js} +2 -2
- package/dist/client/assets/{haml-CclbQxsV.js → haml-BBNabTlb.js} +2 -2
- package/dist/client/assets/{handlebars-DFD506ku.js → handlebars-BgXzhWSj.js} +2 -2
- package/dist/client/assets/{html-derivative-oHJ5ggVN.js → html-derivative-CANLUZ-z.js} +2 -2
- package/dist/client/assets/{html-BtV0fnqE.js → html-wBdSHoow.js} +2 -2
- package/dist/client/assets/{http-C8JeRqCn.js → http-BY44QKMy.js} +2 -2
- package/dist/client/assets/{hurl-C1cew6hc.js → hurl-EMZQkkjM.js} +2 -2
- package/dist/client/assets/{index-5pO5drbg.js → index-kwzTYbVb.js} +191 -184
- package/dist/client/assets/{java-CrUEPeR7.js → java-ByjPbT42.js} +2 -2
- package/dist/client/assets/{javascript-DJT7psEs.js → javascript-C9vbUEcm.js} +2 -2
- package/dist/client/assets/{jinja-CXxzh_AI.js → jinja-DH-AbsBN.js} +2 -2
- package/dist/client/assets/{jison-BEgWlYuP.js → jison-Bef1Ntr_.js} +2 -2
- package/dist/client/assets/{json-DYMLm7oI.js → json-Gk_oM0bP.js} +2 -2
- package/dist/client/assets/{jsx-C2i-6RBR.js → jsx-Ds7YHLou.js} +2 -2
- package/dist/client/assets/{julia-268B5DSC.js → julia-Dm-6xpIK.js} +2 -2
- package/dist/client/assets/{just-DHlfSR_g.js → just-BXY93QN4.js} +2 -2
- package/dist/client/assets/{latex-hZJ97DlU.js → latex-B3-eYkFA.js} +2 -2
- package/dist/client/assets/{liquid-BXWKgP9a.js → liquid-C10OBLnm.js} +2 -2
- package/dist/client/assets/{lua-DZpMPVk1.js → lua-BUR8bsxC.js} +2 -2
- package/dist/client/assets/{marko-C4krJn5M.js → marko-COPLM_fz.js} +2 -2
- package/dist/client/assets/{mdc-DPMybKM9.js → mdc-CTAUTBsz.js} +2 -2
- package/dist/client/assets/{nginx-DS7EZxTP.js → nginx-D1BLqLRM.js} +2 -2
- package/dist/client/assets/{nim-DQ5EIgDG.js → nim-Xs8VbhdY.js} +2 -2
- package/dist/client/assets/{perl-BYRfkLcm.js → perl-D7-zmFa1.js} +2 -2
- package/dist/client/assets/{php-CRmJir-2.js → php-BiTVgoaL.js} +2 -2
- package/dist/client/assets/{pug-ChlH277L.js → pug-DphfHoFa.js} +2 -2
- package/dist/client/assets/{qml-D03lc83e.js → qml-CYcNnFOd.js} +2 -2
- package/dist/client/assets/{r-B0iWOCwM.js → r-BrJ-p0TS.js} +2 -2
- package/dist/client/assets/{razor-jqTSj5Nf.js → razor-1gio6m_g.js} +2 -2
- package/dist/client/assets/{regexp-BtdW0BtX.js → regexp-CNiuKup7.js} +2 -2
- package/dist/client/assets/{rst-ODlKAfCn.js → rst-BPOsO8_U.js} +2 -2
- package/dist/client/assets/{ruby-D3-MIdGC.js → ruby-CpfKHKDc.js} +2 -2
- package/dist/client/assets/{sas-CHM8NNaC.js → sas-CngX1we2.js} +2 -2
- package/dist/client/assets/{scss-C5u0ylVF.js → scss-B2remLQX.js} +2 -2
- package/dist/client/assets/{shellscript-e38vxsyD.js → shellscript-CQmxALRY.js} +2 -2
- package/dist/client/assets/{shellsession-C2xN505I.js → shellsession-BB-5CB_r.js} +2 -2
- package/dist/client/assets/{soy-Cm15Zvq3.js → soy-kYUnX51I.js} +2 -2
- package/dist/client/assets/{sql-BpFtjTfW.js → sql-Cq9fRkwr.js} +2 -2
- package/dist/client/assets/{stata-BlPPmFYy.js → stata-17nboilR.js} +2 -2
- package/dist/client/assets/{surrealql-BCULE51k.js → surrealql-D1hrcsSn.js} +2 -2
- package/dist/client/assets/{svelte-BNRSv2pn.js → svelte-GmGi3eK8.js} +2 -2
- package/dist/client/assets/{templ-DoGq1v8F.js → templ-DySdTLEs.js} +2 -2
- package/dist/client/assets/{tex--cpmM4NW.js → tex-CblcFqPi.js} +2 -2
- package/dist/client/assets/textarea-Cf56oU_6.js +46 -0
- package/dist/client/assets/{ts-tags-D5eCylEK.js → ts-tags-BQC5pzJI.js} +2 -2
- package/dist/client/assets/{tsx-Bu8K9CNP.js → tsx-1bNjXgiV.js} +2 -2
- package/dist/client/assets/{twig-BZHf1w2v.js → twig-CNwthYXl.js} +2 -2
- package/dist/client/assets/{typescript-CdzN72UO.js → typescript-BTKoaKmx.js} +2 -2
- package/dist/client/assets/{vue-BZJs2XXT.js → vue-B4XpWxge.js} +2 -2
- package/dist/client/assets/{vue-html-zt0y5fO9.js → vue-html-CcUSqexV.js} +2 -2
- package/dist/client/assets/{vue-vine-SsQX7Kll.js → vue-vine-JDMd26pY.js} +2 -2
- package/dist/client/assets/{xml-BKlTVdhA.js → xml-DRuNSJP8.js} +2 -2
- package/dist/client/assets/{xsl-B00O3UQ-.js → xsl-BIJJg1B0.js} +2 -2
- package/dist/client/assets/{yaml-Dr4fa503.js → yaml-BNEnOsJI.js} +2 -2
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/dist/client/assets/arrow-right-Cvlj0Cvu.js +0 -2
- package/dist/client/assets/textarea-M0r5voiR.js +0 -46
package/dist/bin.mjs
CHANGED
|
@@ -233,7 +233,7 @@ const make$75 = () => {
|
|
|
233
233
|
const layer$71 = Layer.sync(NetService, make$75);
|
|
234
234
|
//#endregion
|
|
235
235
|
//#region package.json
|
|
236
|
-
var version = "0.1.
|
|
236
|
+
var version = "0.1.18";
|
|
237
237
|
//#endregion
|
|
238
238
|
//#region src/config.ts
|
|
239
239
|
/**
|
|
@@ -4773,6 +4773,7 @@ Schema$1.Literals([
|
|
|
4773
4773
|
"config.warning",
|
|
4774
4774
|
"deprecation.notice",
|
|
4775
4775
|
"files.persisted",
|
|
4776
|
+
"code.published",
|
|
4776
4777
|
"runtime.warning",
|
|
4777
4778
|
"runtime.error"
|
|
4778
4779
|
]);
|
|
@@ -4821,6 +4822,7 @@ const ModelReroutedType = Schema$1.Literal("model.rerouted");
|
|
|
4821
4822
|
const ConfigWarningType = Schema$1.Literal("config.warning");
|
|
4822
4823
|
const DeprecationNoticeType = Schema$1.Literal("deprecation.notice");
|
|
4823
4824
|
const FilesPersistedType = Schema$1.Literal("files.persisted");
|
|
4825
|
+
const CodePublishedType = Schema$1.Literal("code.published");
|
|
4824
4826
|
const ToolDeniedType = Schema$1.Literal("tool.denied");
|
|
4825
4827
|
const RuntimeWarningType = Schema$1.Literal("runtime.warning");
|
|
4826
4828
|
const RuntimeErrorType = Schema$1.Literal("runtime.error");
|
|
@@ -5123,6 +5125,12 @@ const FilesPersistedPayload = Schema$1.Struct({
|
|
|
5123
5125
|
error: TrimmedNonEmptyStringSchema$1
|
|
5124
5126
|
})))
|
|
5125
5127
|
});
|
|
5128
|
+
const CodePublishedPayload = Schema$1.Struct({
|
|
5129
|
+
hostingProvider: TrimmedNonEmptyStringSchema$1,
|
|
5130
|
+
url: TrimmedNonEmptyStringSchema$1,
|
|
5131
|
+
repo: Schema$1.optional(TrimmedNonEmptyStringSchema$1),
|
|
5132
|
+
identifier: Schema$1.optional(TrimmedNonEmptyStringSchema$1)
|
|
5133
|
+
});
|
|
5126
5134
|
const ToolDeniedPayload = Schema$1.Struct({
|
|
5127
5135
|
toolName: TrimmedNonEmptyStringSchema$1,
|
|
5128
5136
|
toolUseId: Schema$1.optional(TrimmedNonEmptyStringSchema$1),
|
|
@@ -5363,6 +5371,11 @@ const ProviderRuntimeFilesPersistedEvent = Schema$1.Struct({
|
|
|
5363
5371
|
type: FilesPersistedType,
|
|
5364
5372
|
payload: FilesPersistedPayload
|
|
5365
5373
|
});
|
|
5374
|
+
const ProviderRuntimeCodePublishedEvent = Schema$1.Struct({
|
|
5375
|
+
...ProviderRuntimeEventBase.fields,
|
|
5376
|
+
type: CodePublishedType,
|
|
5377
|
+
payload: CodePublishedPayload
|
|
5378
|
+
});
|
|
5366
5379
|
const ProviderRuntimeToolDeniedEvent = Schema$1.Struct({
|
|
5367
5380
|
...ProviderRuntimeEventBase.fields,
|
|
5368
5381
|
type: ToolDeniedType,
|
|
@@ -5424,6 +5437,7 @@ Schema$1.Union([
|
|
|
5424
5437
|
ProviderRuntimeConfigWarningEvent,
|
|
5425
5438
|
ProviderRuntimeDeprecationNoticeEvent,
|
|
5426
5439
|
ProviderRuntimeFilesPersistedEvent,
|
|
5440
|
+
ProviderRuntimeCodePublishedEvent,
|
|
5427
5441
|
ProviderRuntimeToolDeniedEvent,
|
|
5428
5442
|
ProviderRuntimeWarningEvent,
|
|
5429
5443
|
ProviderRuntimeErrorEvent
|
|
@@ -5795,6 +5809,20 @@ const ServerSettings = Schema$1.Struct({
|
|
|
5795
5809
|
* back for sessions started by the Claude provider.
|
|
5796
5810
|
*/
|
|
5797
5811
|
enableToolCallNarration: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
|
|
5812
|
+
/**
|
|
5813
|
+
* Whether the model may spawn subagents without being asked to.
|
|
5814
|
+
*
|
|
5815
|
+
* On by default: a bounded search or review whose result is far smaller than
|
|
5816
|
+
* the reading it takes to produce is exactly the work a subagent is for, and
|
|
5817
|
+
* an agent that has to ask first will simply do it inline instead.
|
|
5818
|
+
*
|
|
5819
|
+
* Stated in the appended prompt whichever way it is set, rather than only
|
|
5820
|
+
* when off. The provider's own preset prompt carries an instruction not to
|
|
5821
|
+
* spawn subagents unless asked, and p4code requests that preset wholesale —
|
|
5822
|
+
* so there is nothing to withhold, and only a later, more specific
|
|
5823
|
+
* instruction can override an earlier general one.
|
|
5824
|
+
*/
|
|
5825
|
+
enableUnpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
|
|
5798
5826
|
automaticGitFetchInterval: Schema$1.DurationFromMillis.pipe(Schema$1.withDecodingDefault(Effect.succeed(Duration.toMillis(DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL)))),
|
|
5799
5827
|
defaultThreadEnvMode: ThreadEnvMode.pipe(Schema$1.withDecodingDefault(Effect.succeed("local"))),
|
|
5800
5828
|
newWorktreesStartFromOrigin: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
|
|
@@ -5929,6 +5957,7 @@ const ServerSettingsPatch = Schema$1.Struct({
|
|
|
5929
5957
|
enableAssistantStreaming: Schema$1.optionalKey(Schema$1.Boolean),
|
|
5930
5958
|
enableProviderUpdateChecks: Schema$1.optionalKey(Schema$1.Boolean),
|
|
5931
5959
|
enableToolCallNarration: Schema$1.optionalKey(Schema$1.Boolean),
|
|
5960
|
+
enableUnpromptedSubagents: Schema$1.optionalKey(Schema$1.Boolean),
|
|
5932
5961
|
automaticGitFetchInterval: Schema$1.optionalKey(Schema$1.DurationFromMillis),
|
|
5933
5962
|
defaultThreadEnvMode: Schema$1.optionalKey(ThreadEnvMode),
|
|
5934
5963
|
newWorktreesStartFromOrigin: Schema$1.optionalKey(Schema$1.Boolean),
|
|
@@ -7820,11 +7849,17 @@ const AGENT_ASSET_TOTAL_MAX_BYTES = 4 * 1024 * 1024;
|
|
|
7820
7849
|
* directory rather than the provider's. The definition travels; the bearer
|
|
7821
7850
|
* tokens and API keys it references do not, and stay in each machine's own
|
|
7822
7851
|
* secret store.
|
|
7852
|
+
* - `agent` — a subagent definition, a single markdown file in
|
|
7853
|
+
* `<configDir>/agents`. The name is the filename, as for `memory`. This is the
|
|
7854
|
+
* only artifact that bounds what a subagent is handed and what it hands back,
|
|
7855
|
+
* because p4code observes subagent runs rather than constructing their
|
|
7856
|
+
* prompts — so the I/O contract can only ship as a file the provider reads.
|
|
7823
7857
|
*/
|
|
7824
7858
|
const AgentAssetKind = Schema$1.Literals([
|
|
7825
7859
|
"skill",
|
|
7826
7860
|
"memory",
|
|
7827
|
-
"mcp"
|
|
7861
|
+
"mcp",
|
|
7862
|
+
"agent"
|
|
7828
7863
|
]);
|
|
7829
7864
|
/**
|
|
7830
7865
|
* An asset's name, which for a skill is also its directory name.
|
|
@@ -15532,7 +15567,15 @@ function buildSkillOverrides(disabled, discovered) {
|
|
|
15532
15567
|
* @module provider/Drivers/ClaudeSkills
|
|
15533
15568
|
*/
|
|
15534
15569
|
const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
15535
|
-
|
|
15570
|
+
/**
|
|
15571
|
+
* The `name`/`description` header a skill and a subagent definition both carry.
|
|
15572
|
+
*
|
|
15573
|
+
* Exported because agent definitions are the same header in a different file,
|
|
15574
|
+
* and the sync validates them before publishing one. A second copy of this
|
|
15575
|
+
* would be a second answer to "does this load in the provider", which is the
|
|
15576
|
+
* one question both callers are asking.
|
|
15577
|
+
*/
|
|
15578
|
+
function parseMarkdownFrontmatter(contents) {
|
|
15536
15579
|
const match = FRONTMATTER_PATTERN.exec(contents);
|
|
15537
15580
|
if (!match) return { kind: "missing" };
|
|
15538
15581
|
let parsed;
|
|
@@ -15583,6 +15626,19 @@ const resolveClaudeUserSkillsDir = Effect.fn("resolveClaudeUserSkillsDir")(funct
|
|
|
15583
15626
|
return path.join(configDirPath, "skills");
|
|
15584
15627
|
});
|
|
15585
15628
|
/**
|
|
15629
|
+
* The user-scope subagent directory the CLI would read.
|
|
15630
|
+
*
|
|
15631
|
+
* Same resolution as the skills directory beside it, for the same reason: the
|
|
15632
|
+
* sync materializes into exactly the directory the provider loads from, and
|
|
15633
|
+
* deriving it a second time is how the two come to disagree about where an
|
|
15634
|
+
* agent definition lives.
|
|
15635
|
+
*/
|
|
15636
|
+
const resolveClaudeUserAgentsDir = Effect.fn("resolveClaudeUserAgentsDir")(function* (config, environment, cwd) {
|
|
15637
|
+
const path = yield* Path.Path;
|
|
15638
|
+
const configDirPath = yield* resolveClaudeConfigDirPath(config, environment ?? process.env, cwd);
|
|
15639
|
+
return path.join(configDirPath, "agents");
|
|
15640
|
+
});
|
|
15641
|
+
/**
|
|
15586
15642
|
* Enumerate Claude Code skills from the user config dir and the workspace.
|
|
15587
15643
|
* Discovery is best-effort: unreadable roots and malformed skill entries are
|
|
15588
15644
|
* skipped so a broken skill never degrades the provider snapshot. On name
|
|
@@ -15607,7 +15663,7 @@ const discoverClaudeSkills = Effect.fn("discoverClaudeSkills")(function* (config
|
|
|
15607
15663
|
const skillPath = path.join(root.directory, entry, "SKILL.md");
|
|
15608
15664
|
const contents = yield* fileSystem.readFileString(skillPath).pipe(Effect.orElseSucceed(() => void 0));
|
|
15609
15665
|
if (contents === void 0) continue;
|
|
15610
|
-
const frontmatter =
|
|
15666
|
+
const frontmatter = parseMarkdownFrontmatter(contents);
|
|
15611
15667
|
if (frontmatter.kind === "malformed") continue;
|
|
15612
15668
|
const name = (frontmatter.kind === "parsed" ? frontmatter.name : void 0) ?? entry.trim();
|
|
15613
15669
|
if (!name) continue;
|
|
@@ -16429,6 +16485,288 @@ const make$62 = Effect.gen(function* () {
|
|
|
16429
16485
|
});
|
|
16430
16486
|
const layer$57 = Layer.effect(McpRegistry, make$62);
|
|
16431
16487
|
//#endregion
|
|
16488
|
+
//#region src/sync/skillDirectory.ts
|
|
16489
|
+
/**
|
|
16490
|
+
* Reading and writing skill directories on disk.
|
|
16491
|
+
*
|
|
16492
|
+
* Kept apart from the sync itself so the filesystem shape has its own tests,
|
|
16493
|
+
* and so the rules about what is refusable — binaries, oversized files, deep
|
|
16494
|
+
* trees — live in one place rather than being rediscovered at each call site.
|
|
16495
|
+
*
|
|
16496
|
+
* @module sync/skillDirectory
|
|
16497
|
+
*/
|
|
16498
|
+
/** Bounded so a stray symlink or a vendored `node_modules` cannot walk forever. */
|
|
16499
|
+
const MAX_SKILL_DEPTH = 4;
|
|
16500
|
+
/**
|
|
16501
|
+
* Entries that are never part of a skill, skipped rather than refused.
|
|
16502
|
+
*
|
|
16503
|
+
* The distinction matters. Refusing an asset because it cannot be represented
|
|
16504
|
+
* faithfully is right — a half-materialized skill loads and misbehaves. But a
|
|
16505
|
+
* `.DS_Store` is not part of the skill at all, and macOS drops one into any
|
|
16506
|
+
* directory that has been opened in Finder, so refusing on its account rejects
|
|
16507
|
+
* a perfectly good skill for a reason its author cannot see.
|
|
16508
|
+
*
|
|
16509
|
+
* These could not sync in any case: `AgentAssetFilePath` rejects a leading dot,
|
|
16510
|
+
* so a dotfile has no representable path even if it were text.
|
|
16511
|
+
*/
|
|
16512
|
+
const IGNORED_ENTRIES = /* @__PURE__ */ new Set([
|
|
16513
|
+
".DS_Store",
|
|
16514
|
+
"Thumbs.db",
|
|
16515
|
+
".git",
|
|
16516
|
+
".svn",
|
|
16517
|
+
".hg",
|
|
16518
|
+
"node_modules",
|
|
16519
|
+
"__pycache__",
|
|
16520
|
+
".venv",
|
|
16521
|
+
".pytest_cache",
|
|
16522
|
+
".ruff_cache",
|
|
16523
|
+
".mypy_cache"
|
|
16524
|
+
]);
|
|
16525
|
+
/**
|
|
16526
|
+
* Anything else with a leading dot is skipped too — an editor's `.idea`, a
|
|
16527
|
+
* `.gitignore` that belongs to the checkout rather than the skill. The rule is
|
|
16528
|
+
* the same one the path schema already enforces, applied earlier so it reads as
|
|
16529
|
+
* "not part of the skill" instead of "this skill is broken".
|
|
16530
|
+
*/
|
|
16531
|
+
const isIgnoredEntry = (entry) => IGNORED_ENTRIES.has(entry) || entry.startsWith(".");
|
|
16532
|
+
/**
|
|
16533
|
+
* A file is binary if decoding it yields a NUL.
|
|
16534
|
+
*
|
|
16535
|
+
* Cheap, and right for what is being asked: the question is not "is this valid
|
|
16536
|
+
* UTF-8" but "can this round-trip through a JSON text column", and a NUL is
|
|
16537
|
+
* what actually breaks that on the way back out of SQLite.
|
|
16538
|
+
*
|
|
16539
|
+
* Exported so the single-file kinds ask it the same way this one does, rather
|
|
16540
|
+
* than each keeping a copy that could drift into a different answer.
|
|
16541
|
+
*/
|
|
16542
|
+
const looksBinary$1 = (content) => content.includes("\0");
|
|
16543
|
+
/**
|
|
16544
|
+
* Read one directory as an asset's files, or say why it cannot be one.
|
|
16545
|
+
*
|
|
16546
|
+
* Exported because memory's `global-memory/` is the same shape by a different
|
|
16547
|
+
* name: a directory whose whole contents are the asset. Sharing this keeps the
|
|
16548
|
+
* refusal rules — depth, file count, binaries, size — identical for both,
|
|
16549
|
+
* rather than letting a second copy drift into accepting what the first
|
|
16550
|
+
* rejects.
|
|
16551
|
+
*/
|
|
16552
|
+
const readSkillFiles = Effect.fn("skillDirectory.readSkillFiles")(function* (root) {
|
|
16553
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
16554
|
+
const path = yield* Path.Path;
|
|
16555
|
+
const files = [];
|
|
16556
|
+
const walk = Effect.fn("skillDirectory.walk")(function* (directory, prefix, depth) {
|
|
16557
|
+
if (depth > MAX_SKILL_DEPTH) return "too-deep";
|
|
16558
|
+
const entries = yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []));
|
|
16559
|
+
for (const entry of [...entries].sort()) {
|
|
16560
|
+
if (isIgnoredEntry(entry)) continue;
|
|
16561
|
+
const absolute = path.join(directory, entry);
|
|
16562
|
+
const relative = prefix === "" ? entry : `${prefix}/${entry}`;
|
|
16563
|
+
const info = yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0));
|
|
16564
|
+
if (info === void 0) return "unreadable";
|
|
16565
|
+
if (info.type === "Directory") {
|
|
16566
|
+
const nested = yield* walk(absolute, relative, depth + 1);
|
|
16567
|
+
if (nested !== null) return nested;
|
|
16568
|
+
continue;
|
|
16569
|
+
}
|
|
16570
|
+
if (info.type !== "File") continue;
|
|
16571
|
+
if (files.length >= 512) return "too-many-files";
|
|
16572
|
+
const content = yield* fileSystem.readFileString(absolute).pipe(Effect.orElseSucceed(() => void 0));
|
|
16573
|
+
if (content === void 0) return "unreadable";
|
|
16574
|
+
if (looksBinary$1(content)) return "binary-file";
|
|
16575
|
+
if (content.length > 524288) return "file-too-large";
|
|
16576
|
+
files.push({
|
|
16577
|
+
path: relative,
|
|
16578
|
+
content
|
|
16579
|
+
});
|
|
16580
|
+
}
|
|
16581
|
+
return null;
|
|
16582
|
+
});
|
|
16583
|
+
const refusal = yield* walk(root, "", 1);
|
|
16584
|
+
if (refusal !== null) return { refusal };
|
|
16585
|
+
if (agentAssetTotalBytes(files) > 4194304) return { refusal: "asset-too-large" };
|
|
16586
|
+
return { files };
|
|
16587
|
+
});
|
|
16588
|
+
/**
|
|
16589
|
+
* Read every skill directory under a user-scope `skills/` root.
|
|
16590
|
+
*
|
|
16591
|
+
* A directory with no `SKILL.md` is not a skill and is skipped silently —
|
|
16592
|
+
* Claude Code would ignore it too, so reporting it would be noise about
|
|
16593
|
+
* something that was never a skill in the first place.
|
|
16594
|
+
*/
|
|
16595
|
+
const readSkillDirectory = Effect.fn("skillDirectory.readSkillDirectory")(function* (skillsRoot) {
|
|
16596
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
16597
|
+
const path = yield* Path.Path;
|
|
16598
|
+
const entries = yield* fileSystem.readDirectory(skillsRoot).pipe(Effect.orElseSucceed(() => []));
|
|
16599
|
+
const skills = [];
|
|
16600
|
+
const refused = [];
|
|
16601
|
+
for (const entry of [...entries].sort()) {
|
|
16602
|
+
if (isIgnoredEntry(entry)) continue;
|
|
16603
|
+
const directory = path.join(skillsRoot, entry);
|
|
16604
|
+
if ((yield* fileSystem.stat(directory).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "Directory") continue;
|
|
16605
|
+
if (!(yield* fileSystem.exists(path.join(directory, "SKILL.md")).pipe(Effect.orElseSucceed(() => false)))) continue;
|
|
16606
|
+
const read = yield* readSkillFiles(directory);
|
|
16607
|
+
if ("refusal" in read) {
|
|
16608
|
+
refused.push({
|
|
16609
|
+
name: entry,
|
|
16610
|
+
reason: read.refusal
|
|
16611
|
+
});
|
|
16612
|
+
continue;
|
|
16613
|
+
}
|
|
16614
|
+
skills.push({
|
|
16615
|
+
name: entry,
|
|
16616
|
+
files: read.files
|
|
16617
|
+
});
|
|
16618
|
+
}
|
|
16619
|
+
return {
|
|
16620
|
+
skills,
|
|
16621
|
+
refused
|
|
16622
|
+
};
|
|
16623
|
+
});
|
|
16624
|
+
/**
|
|
16625
|
+
* Replace a skill directory with exactly the asset's files.
|
|
16626
|
+
*
|
|
16627
|
+
* Written to a sibling and then swapped, so a crash mid-write leaves the old
|
|
16628
|
+
* skill intact rather than half of the new one. A skill is loaded as a unit and
|
|
16629
|
+
* a partially written one is instructions with the middle missing, which an
|
|
16630
|
+
* agent will happily follow.
|
|
16631
|
+
*/
|
|
16632
|
+
const writeSkillDirectory = Effect.fn("skillDirectory.writeSkillDirectory")(function* (input) {
|
|
16633
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
16634
|
+
const path = yield* Path.Path;
|
|
16635
|
+
const target = path.join(input.skillsRoot, input.name);
|
|
16636
|
+
const staging = path.join(input.skillsRoot, `.${input.name}.p4code-staging`);
|
|
16637
|
+
yield* fileSystem.makeDirectory(input.skillsRoot, { recursive: true });
|
|
16638
|
+
yield* fileSystem.remove(staging, {
|
|
16639
|
+
recursive: true,
|
|
16640
|
+
force: true
|
|
16641
|
+
});
|
|
16642
|
+
yield* fileSystem.makeDirectory(staging, { recursive: true });
|
|
16643
|
+
for (const file of input.files) {
|
|
16644
|
+
const destination = path.join(staging, file.path);
|
|
16645
|
+
yield* fileSystem.makeDirectory(path.dirname(destination), { recursive: true });
|
|
16646
|
+
yield* fileSystem.writeFileString(destination, file.content);
|
|
16647
|
+
}
|
|
16648
|
+
yield* fileSystem.remove(target, {
|
|
16649
|
+
recursive: true,
|
|
16650
|
+
force: true
|
|
16651
|
+
});
|
|
16652
|
+
yield* fileSystem.rename(staging, target);
|
|
16653
|
+
});
|
|
16654
|
+
const removeSkillDirectory = Effect.fn("skillDirectory.removeSkillDirectory")(function* (input) {
|
|
16655
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
16656
|
+
const path = yield* Path.Path;
|
|
16657
|
+
yield* fileSystem.remove(path.join(input.skillsRoot, input.name), {
|
|
16658
|
+
recursive: true,
|
|
16659
|
+
force: true
|
|
16660
|
+
});
|
|
16661
|
+
});
|
|
16662
|
+
/**
|
|
16663
|
+
* Whether this text is a definition the provider would actually load.
|
|
16664
|
+
*
|
|
16665
|
+
* Both fields, not just the header. `name` is how a subagent is addressed and
|
|
16666
|
+
* `description` is how the orchestrator decides to address it, so a definition
|
|
16667
|
+
* missing either is present on disk and unreachable in practice.
|
|
16668
|
+
*/
|
|
16669
|
+
const isLoadableAgentDefinition = (content) => {
|
|
16670
|
+
const frontmatter = parseMarkdownFrontmatter(content);
|
|
16671
|
+
return frontmatter.kind === "parsed" && frontmatter.name !== void 0 && frontmatter.description !== void 0;
|
|
16672
|
+
};
|
|
16673
|
+
/**
|
|
16674
|
+
* Every definition in the directory, as syncable assets.
|
|
16675
|
+
*
|
|
16676
|
+
* A file that will not load is refused rather than skipped, unlike a stray
|
|
16677
|
+
* `.DS_Store`: a `.md` in this directory is meant to be an agent, so a broken
|
|
16678
|
+
* header is a fault worth reporting instead of noise to ignore.
|
|
16679
|
+
*/
|
|
16680
|
+
const readAgentDefinitions = Effect.fn("agentDefinitionFiles.read")(function* (agentsRoot) {
|
|
16681
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
16682
|
+
const path = yield* Path.Path;
|
|
16683
|
+
const entries = yield* fileSystem.readDirectory(agentsRoot).pipe(Effect.orElseSucceed(() => []));
|
|
16684
|
+
const skills = [];
|
|
16685
|
+
const refused = [];
|
|
16686
|
+
for (const entry of [...entries].sort()) {
|
|
16687
|
+
if (!entry.endsWith(".md") || entry.startsWith(".")) continue;
|
|
16688
|
+
const absolute = path.join(agentsRoot, entry);
|
|
16689
|
+
if ((yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "File") continue;
|
|
16690
|
+
const content = yield* fileSystem.readFileString(absolute).pipe(Effect.orElseSucceed(() => void 0));
|
|
16691
|
+
if (content === void 0) {
|
|
16692
|
+
refused.push({
|
|
16693
|
+
name: entry,
|
|
16694
|
+
reason: "unreadable"
|
|
16695
|
+
});
|
|
16696
|
+
continue;
|
|
16697
|
+
}
|
|
16698
|
+
if (looksBinary$1(content)) {
|
|
16699
|
+
refused.push({
|
|
16700
|
+
name: entry,
|
|
16701
|
+
reason: "binary-file"
|
|
16702
|
+
});
|
|
16703
|
+
continue;
|
|
16704
|
+
}
|
|
16705
|
+
if (content.length > 524288) {
|
|
16706
|
+
refused.push({
|
|
16707
|
+
name: entry,
|
|
16708
|
+
reason: "file-too-large"
|
|
16709
|
+
});
|
|
16710
|
+
continue;
|
|
16711
|
+
}
|
|
16712
|
+
if (!isLoadableAgentDefinition(content)) {
|
|
16713
|
+
refused.push({
|
|
16714
|
+
name: entry,
|
|
16715
|
+
reason: "unreadable"
|
|
16716
|
+
});
|
|
16717
|
+
continue;
|
|
16718
|
+
}
|
|
16719
|
+
const files = [{
|
|
16720
|
+
path: entry,
|
|
16721
|
+
content
|
|
16722
|
+
}];
|
|
16723
|
+
if (agentAssetTotalBytes(files) > 4194304) {
|
|
16724
|
+
refused.push({
|
|
16725
|
+
name: entry,
|
|
16726
|
+
reason: "asset-too-large"
|
|
16727
|
+
});
|
|
16728
|
+
continue;
|
|
16729
|
+
}
|
|
16730
|
+
skills.push({
|
|
16731
|
+
name: entry,
|
|
16732
|
+
files
|
|
16733
|
+
});
|
|
16734
|
+
}
|
|
16735
|
+
return {
|
|
16736
|
+
skills,
|
|
16737
|
+
refused
|
|
16738
|
+
};
|
|
16739
|
+
});
|
|
16740
|
+
/**
|
|
16741
|
+
* Write a definition that arrived from the hub.
|
|
16742
|
+
*
|
|
16743
|
+
* Refused unless it is exactly one markdown file whose path is the asset's own
|
|
16744
|
+
* name and whose frontmatter would load. The last check is the one that matters
|
|
16745
|
+
* across machines: it is what stops a hub row edited by hand from installing a
|
|
16746
|
+
* file the provider quietly ignores on every machine that syncs it.
|
|
16747
|
+
*
|
|
16748
|
+
* Written atomically because a definition is read whole when a subagent starts,
|
|
16749
|
+
* and half of one is instructions with the contract missing.
|
|
16750
|
+
*/
|
|
16751
|
+
const writeAgentDefinition = Effect.fn("agentDefinitionFiles.write")(function* (input) {
|
|
16752
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
16753
|
+
const path = yield* Path.Path;
|
|
16754
|
+
const file = input.files[0];
|
|
16755
|
+
if (!input.name.endsWith(".md") || input.files.length !== 1 || file === void 0 || file.path !== input.name) return "refused";
|
|
16756
|
+
if (!isLoadableAgentDefinition(file.content)) return "refused";
|
|
16757
|
+
yield* fileSystem.makeDirectory(input.agentsRoot, { recursive: true }).pipe(Effect.ignore);
|
|
16758
|
+
return yield* writeFileStringAtomically({
|
|
16759
|
+
filePath: path.join(input.agentsRoot, input.name),
|
|
16760
|
+
contents: file.content
|
|
16761
|
+
}).pipe(Effect.as("written"), Effect.orElseSucceed(() => "failed"));
|
|
16762
|
+
});
|
|
16763
|
+
const removeAgentDefinition = Effect.fn("agentDefinitionFiles.remove")(function* (input) {
|
|
16764
|
+
if (!input.name.endsWith(".md")) return;
|
|
16765
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
16766
|
+
const path = yield* Path.Path;
|
|
16767
|
+
yield* fileSystem.remove(path.join(input.agentsRoot, input.name), { force: true });
|
|
16768
|
+
});
|
|
16769
|
+
//#endregion
|
|
16432
16770
|
//#region src/sync/HubLink.ts
|
|
16433
16771
|
/**
|
|
16434
16772
|
* Where this server's hub is, what it authenticates with, and whether it syncs
|
|
@@ -16802,178 +17140,6 @@ const removeMcpRegistration = Effect.fn("mcpRegistrationFiles.remove")(function*
|
|
|
16802
17140
|
yield* fileSystem.remove(path.join(input.registrationsRoot, input.name), { force: true });
|
|
16803
17141
|
});
|
|
16804
17142
|
//#endregion
|
|
16805
|
-
//#region src/sync/skillDirectory.ts
|
|
16806
|
-
/**
|
|
16807
|
-
* Reading and writing skill directories on disk.
|
|
16808
|
-
*
|
|
16809
|
-
* Kept apart from the sync itself so the filesystem shape has its own tests,
|
|
16810
|
-
* and so the rules about what is refusable — binaries, oversized files, deep
|
|
16811
|
-
* trees — live in one place rather than being rediscovered at each call site.
|
|
16812
|
-
*
|
|
16813
|
-
* @module sync/skillDirectory
|
|
16814
|
-
*/
|
|
16815
|
-
/** Bounded so a stray symlink or a vendored `node_modules` cannot walk forever. */
|
|
16816
|
-
const MAX_SKILL_DEPTH = 4;
|
|
16817
|
-
/**
|
|
16818
|
-
* Entries that are never part of a skill, skipped rather than refused.
|
|
16819
|
-
*
|
|
16820
|
-
* The distinction matters. Refusing an asset because it cannot be represented
|
|
16821
|
-
* faithfully is right — a half-materialized skill loads and misbehaves. But a
|
|
16822
|
-
* `.DS_Store` is not part of the skill at all, and macOS drops one into any
|
|
16823
|
-
* directory that has been opened in Finder, so refusing on its account rejects
|
|
16824
|
-
* a perfectly good skill for a reason its author cannot see.
|
|
16825
|
-
*
|
|
16826
|
-
* These could not sync in any case: `AgentAssetFilePath` rejects a leading dot,
|
|
16827
|
-
* so a dotfile has no representable path even if it were text.
|
|
16828
|
-
*/
|
|
16829
|
-
const IGNORED_ENTRIES = /* @__PURE__ */ new Set([
|
|
16830
|
-
".DS_Store",
|
|
16831
|
-
"Thumbs.db",
|
|
16832
|
-
".git",
|
|
16833
|
-
".svn",
|
|
16834
|
-
".hg",
|
|
16835
|
-
"node_modules",
|
|
16836
|
-
"__pycache__",
|
|
16837
|
-
".venv",
|
|
16838
|
-
".pytest_cache",
|
|
16839
|
-
".ruff_cache",
|
|
16840
|
-
".mypy_cache"
|
|
16841
|
-
]);
|
|
16842
|
-
/**
|
|
16843
|
-
* Anything else with a leading dot is skipped too — an editor's `.idea`, a
|
|
16844
|
-
* `.gitignore` that belongs to the checkout rather than the skill. The rule is
|
|
16845
|
-
* the same one the path schema already enforces, applied earlier so it reads as
|
|
16846
|
-
* "not part of the skill" instead of "this skill is broken".
|
|
16847
|
-
*/
|
|
16848
|
-
const isIgnoredEntry = (entry) => IGNORED_ENTRIES.has(entry) || entry.startsWith(".");
|
|
16849
|
-
/**
|
|
16850
|
-
* A file is binary if decoding it yields a NUL.
|
|
16851
|
-
*
|
|
16852
|
-
* Cheap, and right for what is being asked: the question is not "is this valid
|
|
16853
|
-
* UTF-8" but "can this round-trip through a JSON text column", and a NUL is
|
|
16854
|
-
* what actually breaks that on the way back out of SQLite.
|
|
16855
|
-
*/
|
|
16856
|
-
const looksBinary$1 = (content) => content.includes("\0");
|
|
16857
|
-
/**
|
|
16858
|
-
* Read one directory as an asset's files, or say why it cannot be one.
|
|
16859
|
-
*
|
|
16860
|
-
* Exported because memory's `global-memory/` is the same shape by a different
|
|
16861
|
-
* name: a directory whose whole contents are the asset. Sharing this keeps the
|
|
16862
|
-
* refusal rules — depth, file count, binaries, size — identical for both,
|
|
16863
|
-
* rather than letting a second copy drift into accepting what the first
|
|
16864
|
-
* rejects.
|
|
16865
|
-
*/
|
|
16866
|
-
const readSkillFiles = Effect.fn("skillDirectory.readSkillFiles")(function* (root) {
|
|
16867
|
-
const fileSystem = yield* FileSystem.FileSystem;
|
|
16868
|
-
const path = yield* Path.Path;
|
|
16869
|
-
const files = [];
|
|
16870
|
-
const walk = Effect.fn("skillDirectory.walk")(function* (directory, prefix, depth) {
|
|
16871
|
-
if (depth > MAX_SKILL_DEPTH) return "too-deep";
|
|
16872
|
-
const entries = yield* fileSystem.readDirectory(directory).pipe(Effect.orElseSucceed(() => []));
|
|
16873
|
-
for (const entry of [...entries].sort()) {
|
|
16874
|
-
if (isIgnoredEntry(entry)) continue;
|
|
16875
|
-
const absolute = path.join(directory, entry);
|
|
16876
|
-
const relative = prefix === "" ? entry : `${prefix}/${entry}`;
|
|
16877
|
-
const info = yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0));
|
|
16878
|
-
if (info === void 0) return "unreadable";
|
|
16879
|
-
if (info.type === "Directory") {
|
|
16880
|
-
const nested = yield* walk(absolute, relative, depth + 1);
|
|
16881
|
-
if (nested !== null) return nested;
|
|
16882
|
-
continue;
|
|
16883
|
-
}
|
|
16884
|
-
if (info.type !== "File") continue;
|
|
16885
|
-
if (files.length >= 512) return "too-many-files";
|
|
16886
|
-
const content = yield* fileSystem.readFileString(absolute).pipe(Effect.orElseSucceed(() => void 0));
|
|
16887
|
-
if (content === void 0) return "unreadable";
|
|
16888
|
-
if (looksBinary$1(content)) return "binary-file";
|
|
16889
|
-
if (content.length > 524288) return "file-too-large";
|
|
16890
|
-
files.push({
|
|
16891
|
-
path: relative,
|
|
16892
|
-
content
|
|
16893
|
-
});
|
|
16894
|
-
}
|
|
16895
|
-
return null;
|
|
16896
|
-
});
|
|
16897
|
-
const refusal = yield* walk(root, "", 1);
|
|
16898
|
-
if (refusal !== null) return { refusal };
|
|
16899
|
-
if (agentAssetTotalBytes(files) > 4194304) return { refusal: "asset-too-large" };
|
|
16900
|
-
return { files };
|
|
16901
|
-
});
|
|
16902
|
-
/**
|
|
16903
|
-
* Read every skill directory under a user-scope `skills/` root.
|
|
16904
|
-
*
|
|
16905
|
-
* A directory with no `SKILL.md` is not a skill and is skipped silently —
|
|
16906
|
-
* Claude Code would ignore it too, so reporting it would be noise about
|
|
16907
|
-
* something that was never a skill in the first place.
|
|
16908
|
-
*/
|
|
16909
|
-
const readSkillDirectory = Effect.fn("skillDirectory.readSkillDirectory")(function* (skillsRoot) {
|
|
16910
|
-
const fileSystem = yield* FileSystem.FileSystem;
|
|
16911
|
-
const path = yield* Path.Path;
|
|
16912
|
-
const entries = yield* fileSystem.readDirectory(skillsRoot).pipe(Effect.orElseSucceed(() => []));
|
|
16913
|
-
const skills = [];
|
|
16914
|
-
const refused = [];
|
|
16915
|
-
for (const entry of [...entries].sort()) {
|
|
16916
|
-
if (isIgnoredEntry(entry)) continue;
|
|
16917
|
-
const directory = path.join(skillsRoot, entry);
|
|
16918
|
-
if ((yield* fileSystem.stat(directory).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "Directory") continue;
|
|
16919
|
-
if (!(yield* fileSystem.exists(path.join(directory, "SKILL.md")).pipe(Effect.orElseSucceed(() => false)))) continue;
|
|
16920
|
-
const read = yield* readSkillFiles(directory);
|
|
16921
|
-
if ("refusal" in read) {
|
|
16922
|
-
refused.push({
|
|
16923
|
-
name: entry,
|
|
16924
|
-
reason: read.refusal
|
|
16925
|
-
});
|
|
16926
|
-
continue;
|
|
16927
|
-
}
|
|
16928
|
-
skills.push({
|
|
16929
|
-
name: entry,
|
|
16930
|
-
files: read.files
|
|
16931
|
-
});
|
|
16932
|
-
}
|
|
16933
|
-
return {
|
|
16934
|
-
skills,
|
|
16935
|
-
refused
|
|
16936
|
-
};
|
|
16937
|
-
});
|
|
16938
|
-
/**
|
|
16939
|
-
* Replace a skill directory with exactly the asset's files.
|
|
16940
|
-
*
|
|
16941
|
-
* Written to a sibling and then swapped, so a crash mid-write leaves the old
|
|
16942
|
-
* skill intact rather than half of the new one. A skill is loaded as a unit and
|
|
16943
|
-
* a partially written one is instructions with the middle missing, which an
|
|
16944
|
-
* agent will happily follow.
|
|
16945
|
-
*/
|
|
16946
|
-
const writeSkillDirectory = Effect.fn("skillDirectory.writeSkillDirectory")(function* (input) {
|
|
16947
|
-
const fileSystem = yield* FileSystem.FileSystem;
|
|
16948
|
-
const path = yield* Path.Path;
|
|
16949
|
-
const target = path.join(input.skillsRoot, input.name);
|
|
16950
|
-
const staging = path.join(input.skillsRoot, `.${input.name}.p4code-staging`);
|
|
16951
|
-
yield* fileSystem.makeDirectory(input.skillsRoot, { recursive: true });
|
|
16952
|
-
yield* fileSystem.remove(staging, {
|
|
16953
|
-
recursive: true,
|
|
16954
|
-
force: true
|
|
16955
|
-
});
|
|
16956
|
-
yield* fileSystem.makeDirectory(staging, { recursive: true });
|
|
16957
|
-
for (const file of input.files) {
|
|
16958
|
-
const destination = path.join(staging, file.path);
|
|
16959
|
-
yield* fileSystem.makeDirectory(path.dirname(destination), { recursive: true });
|
|
16960
|
-
yield* fileSystem.writeFileString(destination, file.content);
|
|
16961
|
-
}
|
|
16962
|
-
yield* fileSystem.remove(target, {
|
|
16963
|
-
recursive: true,
|
|
16964
|
-
force: true
|
|
16965
|
-
});
|
|
16966
|
-
yield* fileSystem.rename(staging, target);
|
|
16967
|
-
});
|
|
16968
|
-
const removeSkillDirectory = Effect.fn("skillDirectory.removeSkillDirectory")(function* (input) {
|
|
16969
|
-
const fileSystem = yield* FileSystem.FileSystem;
|
|
16970
|
-
const path = yield* Path.Path;
|
|
16971
|
-
yield* fileSystem.remove(path.join(input.skillsRoot, input.name), {
|
|
16972
|
-
recursive: true,
|
|
16973
|
-
force: true
|
|
16974
|
-
});
|
|
16975
|
-
});
|
|
16976
|
-
//#endregion
|
|
16977
17143
|
//#region src/sync/memoryFile.ts
|
|
16978
17144
|
/**
|
|
16979
17145
|
* Reading and writing user-scope memory files.
|
|
@@ -17254,12 +17420,20 @@ const PROCEED = { _tag: "proceed" };
|
|
|
17254
17420
|
* - A skill without `SKILL.md` would be invisible to discovery: the picker
|
|
17255
17421
|
* skips directories without one, so the create would look like it silently
|
|
17256
17422
|
* failed. Refused with the reason instead.
|
|
17423
|
+
* - An agent definition is one markdown file whose name is the asset's own, and
|
|
17424
|
+
* anything else is invisible in the same way — the provider reads `.md` in
|
|
17425
|
+
* that directory and nothing else. The frontmatter check is the writer's,
|
|
17426
|
+
* because it needs a YAML parse this module deliberately does without.
|
|
17257
17427
|
* - An existing local name is never overwritten. Create means create; editing
|
|
17258
17428
|
* is a different operation with a different surface.
|
|
17259
17429
|
*/
|
|
17260
17430
|
function decideCreateLocal(input) {
|
|
17261
17431
|
if (input.asset.kind === "mcp") return refuse("refused", "MCP registrations are created through mcp.save, not as files.");
|
|
17262
17432
|
if (input.asset.kind === "skill" && !input.asset.files.some((file) => file.path === "SKILL.md")) return refuse("refused", "A skill needs a SKILL.md at its root; without one, discovery skips the directory.");
|
|
17433
|
+
if (input.asset.kind === "agent") {
|
|
17434
|
+
const [file, ...rest] = input.asset.files;
|
|
17435
|
+
if (!input.asset.name.endsWith(".md") || rest.length > 0 || file?.path !== input.asset.name) return refuse("refused", "An agent definition is a single markdown file named after the asset, for example 'reviewer.md' holding 'reviewer.md'.");
|
|
17436
|
+
}
|
|
17263
17437
|
if (input.localNames.has(input.asset.name)) return refuse("exists", `An asset named '${input.asset.name}' already exists locally.`);
|
|
17264
17438
|
return PROCEED;
|
|
17265
17439
|
}
|
|
@@ -17502,6 +17676,22 @@ const make$59 = Effect.gen(function* () {
|
|
|
17502
17676
|
name
|
|
17503
17677
|
}).pipe(Effect.ignore)
|
|
17504
17678
|
},
|
|
17679
|
+
{
|
|
17680
|
+
kind: "agent",
|
|
17681
|
+
resolveRoot: Effect.gen(function* () {
|
|
17682
|
+
return yield* resolveClaudeUserAgentsDir(yield* claudeHome());
|
|
17683
|
+
}),
|
|
17684
|
+
read: (root) => readAgentDefinitions(root),
|
|
17685
|
+
write: ({ root, name, files }) => writeAgentDefinition({
|
|
17686
|
+
agentsRoot: root,
|
|
17687
|
+
name,
|
|
17688
|
+
files
|
|
17689
|
+
}),
|
|
17690
|
+
remove: ({ root, name }) => removeAgentDefinition({
|
|
17691
|
+
agentsRoot: root,
|
|
17692
|
+
name
|
|
17693
|
+
}).pipe(Effect.ignore)
|
|
17694
|
+
},
|
|
17505
17695
|
{
|
|
17506
17696
|
kind: "mcp",
|
|
17507
17697
|
resolveRoot: Effect.succeed(path.join(config.stateDir, "mcp")),
|
|
@@ -18143,6 +18333,262 @@ const make$59 = Effect.gen(function* () {
|
|
|
18143
18333
|
});
|
|
18144
18334
|
const layer$54 = Layer.effect(AssetSync, make$59);
|
|
18145
18335
|
//#endregion
|
|
18336
|
+
//#region src/provider/CompressPrompts.ts
|
|
18337
|
+
/**
|
|
18338
|
+
* Caveman-style response compression rulesets, adapted from the caveman
|
|
18339
|
+
* plugin's SKILL.md. Pre-baked per level so injection is a lookup, not a
|
|
18340
|
+
* runtime template. Applies to assistant prose only: code blocks, commits,
|
|
18341
|
+
* PRs, error strings, and safety-critical text stay uncompressed.
|
|
18342
|
+
*/
|
|
18343
|
+
const COMPRESS_SHARED_RULES = `Respond terse. All technical substance stays. Only fluff dies.
|
|
18344
|
+
|
|
18345
|
+
## Persistence
|
|
18346
|
+
|
|
18347
|
+
Active every response. No revert after many turns. No filler drift. Still active if unsure.
|
|
18348
|
+
|
|
18349
|
+
## Rules
|
|
18350
|
+
|
|
18351
|
+
Drop filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Short synonyms (big not extensive, fix not "implement a solution for"). No decorative tables/emoji, no dumping long raw error logs unless asked - quote shortest decisive line. Standard well-known tech acronyms OK (DB/API/HTTP); never invent new abbreviations (cfg/impl/req/res/fn) - tokenizers split them same as the full word: zero tokens saved, reader still decodes. No causal arrows (->) in prose. Technical terms exact. Code blocks unchanged. Errors quoted exact.
|
|
18352
|
+
|
|
18353
|
+
Never drop not/never/no/only/except: flipping the meaning costs more than any token saved. Numbers and units exact.
|
|
18354
|
+
|
|
18355
|
+
Tool calls: fire direct. No plan, no progress note between calls, never announce the next call. If the harness asks for a short lead-in before tool calls, write that one line and nothing more. Otherwise text before a call only to clarify, warn about a security or irreversible action, or resolve ambiguity.
|
|
18356
|
+
|
|
18357
|
+
Preserve the user's dominant language: reply in the language the user writes, whatever language the examples or the surrounding context use. Compress the style, not the language. Every emitted line in that language, lead-in lines included, not just the final reply. Always keep technical terms, code, API names, CLI commands, commit-type keywords (feat/fix/...), and exact error strings verbatim unless the user explicitly asks for translation.
|
|
18358
|
+
|
|
18359
|
+
Dropping articles applies to article languages only. Where small markers carry case or role (particles, postpositions), keep them: grammar, not filler. Compress politeness and filler instead.
|
|
18360
|
+
|
|
18361
|
+
No self-reference. Never name or announce the style. No compressed answer plus normal recap - compressed output only. Exception: user explicitly asks what the mode is.
|
|
18362
|
+
|
|
18363
|
+
## Auto-Clarity
|
|
18364
|
+
|
|
18365
|
+
Drop compression and write normally when:
|
|
18366
|
+
- Security warnings
|
|
18367
|
+
- Irreversible action confirmations
|
|
18368
|
+
- Multi-step sequences where fragment order or omitted conjunctions risk misread
|
|
18369
|
+
- Compression itself creates technical ambiguity (e.g. "migrate table drop column backup first" - order unclear without articles and conjunctions)
|
|
18370
|
+
- User asks to clarify or repeats a question
|
|
18371
|
+
|
|
18372
|
+
Resume compression after the clear part is done. The example below shows the format only: write the warning in the session's language, not the example's.
|
|
18373
|
+
|
|
18374
|
+
Example - destructive operation:
|
|
18375
|
+
> **Warning:** This will permanently delete all rows in the \`users\` table and cannot be undone.
|
|
18376
|
+
> \`\`\`sql
|
|
18377
|
+
> DROP TABLE users;
|
|
18378
|
+
> \`\`\`
|
|
18379
|
+
> Compression resumes. Verify a backup exists first.
|
|
18380
|
+
|
|
18381
|
+
## Boundaries
|
|
18382
|
+
|
|
18383
|
+
Anything persisted outside the chat: write normal, uncompressed prose. Covers code, code comments, commit messages, PR and issue text, docs, memory files, and messages to third parties.`;
|
|
18384
|
+
const COMPRESS_RULESETS = {
|
|
18385
|
+
lite: `## Response compression: lite
|
|
18386
|
+
|
|
18387
|
+
${COMPRESS_SHARED_RULES}
|
|
18388
|
+
|
|
18389
|
+
## Intensity: lite
|
|
18390
|
+
|
|
18391
|
+
No filler or hedging. Keep articles and full sentences. Professional but tight.
|
|
18392
|
+
|
|
18393
|
+
Example - "Why does my React component re-render?"
|
|
18394
|
+
"Your component re-renders because you create a new object reference each render. Wrap it in \`useMemo\`."
|
|
18395
|
+
|
|
18396
|
+
Example - "Explain database connection pooling."
|
|
18397
|
+
"Connection pooling reuses open connections instead of creating a new one per request. Avoids repeated handshake overhead."`,
|
|
18398
|
+
full: `## Response compression: full
|
|
18399
|
+
|
|
18400
|
+
${COMPRESS_SHARED_RULES}
|
|
18401
|
+
|
|
18402
|
+
## Intensity: full
|
|
18403
|
+
|
|
18404
|
+
Drop articles (a/an/the), fragments OK, short synonyms. Pattern: [thing] [action] [reason]. [next step].
|
|
18405
|
+
|
|
18406
|
+
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
|
|
18407
|
+
Yes: "Bug in auth middleware. Token expiry check use \`<\` not \`<=\`. Fix:"
|
|
18408
|
+
|
|
18409
|
+
Example - "Why does my React component re-render?"
|
|
18410
|
+
"New object ref each render. Inline object prop = new ref = re-render. Wrap in \`useMemo\`."
|
|
18411
|
+
|
|
18412
|
+
Example - "Explain database connection pooling."
|
|
18413
|
+
"Pool reuses open DB connections. No new connection per request. Skips handshake overhead."`,
|
|
18414
|
+
ultra: `## Response compression: ultra
|
|
18415
|
+
|
|
18416
|
+
${COMPRESS_SHARED_RULES}
|
|
18417
|
+
|
|
18418
|
+
## Intensity: ultra
|
|
18419
|
+
|
|
18420
|
+
Drop articles, fragments OK, short synonyms. Also strip conjunctions when cause-then-effect stays unambiguous. One word when one word is enough. State each fact once. Still no invented abbreviations (cfg/impl/req/res/fn/auth) and still no arrows (X -> Y): both measure as zero token saving and cost decode clarity. Code symbols, function names, API names, error strings: never touch.
|
|
18421
|
+
|
|
18422
|
+
Example - "Why does my React component re-render?"
|
|
18423
|
+
"Inline object prop, new ref, re-render. \`useMemo\`."
|
|
18424
|
+
|
|
18425
|
+
Example - "Explain database connection pooling."
|
|
18426
|
+
"Pool reuses open DB connections. No per-request handshake."`
|
|
18427
|
+
};
|
|
18428
|
+
const COMPRESS_TURN_REMINDERS = {
|
|
18429
|
+
lite: "[response style: compressed lite - no filler/hedging, full sentences; keep negations and numbers exact; code/commits/security text normal]",
|
|
18430
|
+
full: "[response style: compressed full - terse fragments, drop articles/filler; keep negations and numbers exact; code/commits/security text normal]",
|
|
18431
|
+
ultra: "[response style: compressed ultra - maximum terseness, one word when enough; keep negations and numbers exact; code/commits/security text normal]"
|
|
18432
|
+
};
|
|
18433
|
+
function compressRulesetFor(mode) {
|
|
18434
|
+
return mode === "off" ? void 0 : COMPRESS_RULESETS[mode];
|
|
18435
|
+
}
|
|
18436
|
+
function compressTurnReminderFor(mode) {
|
|
18437
|
+
return mode === "off" ? void 0 : COMPRESS_TURN_REMINDERS[mode];
|
|
18438
|
+
}
|
|
18439
|
+
//#endregion
|
|
18440
|
+
//#region src/sync/agentPresets.ts
|
|
18441
|
+
/**
|
|
18442
|
+
* The row ceiling both presets impose.
|
|
18443
|
+
*
|
|
18444
|
+
* A cap rather than "be brief": an uncapped list is how a subagent turns a
|
|
18445
|
+
* bounded answer back into a page, and a number is the only form of that
|
|
18446
|
+
* instruction a model cannot negotiate with.
|
|
18447
|
+
*/
|
|
18448
|
+
const PRESET_MAX_RESULT_ROWS = 20;
|
|
18449
|
+
/**
|
|
18450
|
+
* Named `p4-*` on purpose.
|
|
18451
|
+
*
|
|
18452
|
+
* The directory is shared with whatever the user has written by hand, and
|
|
18453
|
+
* installing a preset must never quietly replace their own `reviewer`. The
|
|
18454
|
+
* prefix also makes provenance obvious in the settings list, where every kind
|
|
18455
|
+
* of agent definition appears in one column.
|
|
18456
|
+
*/
|
|
18457
|
+
const PRESET_NAME_PREFIX = "p4-";
|
|
18458
|
+
/**
|
|
18459
|
+
* What every preset says about what it is handed.
|
|
18460
|
+
*
|
|
18461
|
+
* Shared because it is the same contract whatever the subagent does: no
|
|
18462
|
+
* inherited history, exact paths, and a refusal instead of a guess. The last
|
|
18463
|
+
* clause is the one that earns its place — a subagent that invents the missing
|
|
18464
|
+
* half of an under-specified prompt returns something confident and wrong,
|
|
18465
|
+
* which costs more than the turn that asked for it.
|
|
18466
|
+
*/
|
|
18467
|
+
const INPUT_CONTRACT = `## What you are given
|
|
18468
|
+
|
|
18469
|
+
You inherit no session history. The prompt is the whole of what you know: one
|
|
18470
|
+
request, plus whatever exact paths the orchestrator has already established.
|
|
18471
|
+
Treat those paths as authoritative and start from them rather than searching for
|
|
18472
|
+
them again.
|
|
18473
|
+
|
|
18474
|
+
What the prompt does not say, you do not assume. If it names no request, or
|
|
18475
|
+
names a path that is not there, answer with the BLOCKED line below. Guessing at
|
|
18476
|
+
the missing half of a request produces an answer that reads as confident and is
|
|
18477
|
+
not checkable, which is worse than no answer.`;
|
|
18478
|
+
/**
|
|
18479
|
+
* What every preset says about the answer.
|
|
18480
|
+
*
|
|
18481
|
+
* The two sentinels matter as much as the table. Without `NONE` a clean run
|
|
18482
|
+
* comes back as a paragraph explaining that everything looked fine; without
|
|
18483
|
+
* `BLOCKED` an under-specified request comes back as an apology. Both are the
|
|
18484
|
+
* narrative this contract exists to remove, and both are cheap to name.
|
|
18485
|
+
*/
|
|
18486
|
+
const outputContract = (row, extra) => [
|
|
18487
|
+
"## What you return",
|
|
18488
|
+
"",
|
|
18489
|
+
"Your final message is the result. It is the table and nothing else: no",
|
|
18490
|
+
"preamble, no restatement of the request, no summary underneath it.",
|
|
18491
|
+
"",
|
|
18492
|
+
`One row per finding, at most ${PRESET_MAX_RESULT_ROWS}, most important first:`,
|
|
18493
|
+
"",
|
|
18494
|
+
` ${row}`,
|
|
18495
|
+
"",
|
|
18496
|
+
"Nothing to report: the single word NONE.",
|
|
18497
|
+
"Request insufficient: BLOCKED - <what is missing>, on one line.",
|
|
18498
|
+
"",
|
|
18499
|
+
"Paths are repository-relative and carry a line number. A row without one is",
|
|
18500
|
+
"a row you have not verified: open the file and get it.",
|
|
18501
|
+
...extra
|
|
18502
|
+
].join("\n");
|
|
18503
|
+
/**
|
|
18504
|
+
* Assemble one definition.
|
|
18505
|
+
*
|
|
18506
|
+
* The compression ruleset goes last so the contract above it is read first.
|
|
18507
|
+
* Both are instructions to the same model and neither overrides the other:
|
|
18508
|
+
* the contract fixes the shape, the ruleset fixes the prose inside it.
|
|
18509
|
+
*/
|
|
18510
|
+
const buildPreset = (input) => {
|
|
18511
|
+
const name = `${PRESET_NAME_PREFIX}${input.name}`;
|
|
18512
|
+
const content = [
|
|
18513
|
+
"---",
|
|
18514
|
+
`name: ${name}`,
|
|
18515
|
+
`description: ${JSON.stringify(input.description)}`,
|
|
18516
|
+
`tools: ${input.tools}`,
|
|
18517
|
+
"---",
|
|
18518
|
+
"",
|
|
18519
|
+
input.body.trim(),
|
|
18520
|
+
"",
|
|
18521
|
+
compressRulesetFor("full") ?? "",
|
|
18522
|
+
""
|
|
18523
|
+
].join("\n");
|
|
18524
|
+
const fileName = `${name}.md`;
|
|
18525
|
+
return {
|
|
18526
|
+
name: fileName,
|
|
18527
|
+
summary: input.summary,
|
|
18528
|
+
files: [{
|
|
18529
|
+
path: fileName,
|
|
18530
|
+
content
|
|
18531
|
+
}]
|
|
18532
|
+
};
|
|
18533
|
+
};
|
|
18534
|
+
/** Every preset, in the order `p4c agents presets` lists them. */
|
|
18535
|
+
const AGENT_PRESETS = [buildPreset({
|
|
18536
|
+
name: "investigator",
|
|
18537
|
+
description: "Find where something lives in the codebase. Use when the answer is a set of locations rather than a change. Returns a fixed table of path:line rows and nothing else.",
|
|
18538
|
+
tools: "Read, Grep, Glob, Bash",
|
|
18539
|
+
summary: "Locates code and answers with a path:line table.",
|
|
18540
|
+
body: `You locate code. You do not change it, and you say no more about any location
|
|
18541
|
+
than the one clause that explains why it is in the list.
|
|
18542
|
+
|
|
18543
|
+
${INPUT_CONTRACT}
|
|
18544
|
+
|
|
18545
|
+
${outputContract("path:line - symbol - one clause on why it matters", [
|
|
18546
|
+
"",
|
|
18547
|
+
"Symbol is the function, type or constant the line belongs to, not a paraphrase",
|
|
18548
|
+
"of the line. Where there is genuinely no symbol, name the block: a route table,",
|
|
18549
|
+
"a config literal."
|
|
18550
|
+
])}`
|
|
18551
|
+
}), buildPreset({
|
|
18552
|
+
name: "reviewer",
|
|
18553
|
+
description: "Review named files or a diff for defects. Use when the answer is a list of findings rather than a change. Returns a fixed table of path:line findings and nothing else.",
|
|
18554
|
+
tools: "Read, Grep, Glob, Bash",
|
|
18555
|
+
summary: "Reviews named files and answers with a severity-tagged finding table.",
|
|
18556
|
+
body: `You review code you did not write. You do not change it, and you do not report
|
|
18557
|
+
what it does well - the orchestrator asked what is wrong, and a list padded with
|
|
18558
|
+
approval is a list that has to be re-read to find the three rows that matter.
|
|
18559
|
+
|
|
18560
|
+
${INPUT_CONTRACT}
|
|
18561
|
+
|
|
18562
|
+
${outputContract("path:line - severity - what is wrong, and what it breaks", [
|
|
18563
|
+
"",
|
|
18564
|
+
"Severity is one of three words:",
|
|
18565
|
+
"",
|
|
18566
|
+
" bug - wrong behaviour reachable in ordinary use. Name the input that reaches it.",
|
|
18567
|
+
" risk - correct today, and depends on something the code does not state.",
|
|
18568
|
+
" nit - style or clarity. Include only when the file is otherwise clean.",
|
|
18569
|
+
"",
|
|
18570
|
+
"A finding you cannot reach with a concrete input is a risk, not a bug. Saying",
|
|
18571
|
+
"so is what keeps the bug rows worth reading."
|
|
18572
|
+
])}`
|
|
18573
|
+
})];
|
|
18574
|
+
/**
|
|
18575
|
+
* Look one up by the name someone typed.
|
|
18576
|
+
*
|
|
18577
|
+
* Both spellings are accepted - `investigator` and `p4-investigator.md` - because
|
|
18578
|
+
* the listing shows the filename and the sentence people write is the bare word,
|
|
18579
|
+
* and refusing one of the two would be refusing whichever the person happened to
|
|
18580
|
+
* read most recently.
|
|
18581
|
+
*/
|
|
18582
|
+
const findAgentPreset = (requested) => {
|
|
18583
|
+
const wanted = requested.trim().toLowerCase();
|
|
18584
|
+
if (wanted.length === 0) return void 0;
|
|
18585
|
+
return AGENT_PRESETS.find((preset) => {
|
|
18586
|
+
const fileName = preset.name.toLowerCase();
|
|
18587
|
+
const bare = fileName.slice(3, -3);
|
|
18588
|
+
return wanted === fileName || wanted === bare || wanted === `${PRESET_NAME_PREFIX}${bare}`;
|
|
18589
|
+
});
|
|
18590
|
+
};
|
|
18591
|
+
//#endregion
|
|
18146
18592
|
//#region src/cli/assets.ts
|
|
18147
18593
|
const SyncServicesLive = layer$54.pipe(Layer.provideMerge(layer$55), Layer.provideMerge(layer$56), Layer.provideMerge(layer$60.pipe(Layer.provide(layer$67))), Layer.provideMerge(layer$67), Layer.provideMerge(FetchHttpClient.layer));
|
|
18148
18594
|
const runWithSync = (flags, run) => Effect.gen(function* () {
|
|
@@ -18186,6 +18632,14 @@ const MCP_WORDS = {
|
|
|
18186
18632
|
rootLabel: "MCP",
|
|
18187
18633
|
nameDescription: "Registration filename, for example linear.json."
|
|
18188
18634
|
};
|
|
18635
|
+
const AGENT_WORDS = {
|
|
18636
|
+
kind: "agent",
|
|
18637
|
+
command: "agents",
|
|
18638
|
+
singular: "agent definition",
|
|
18639
|
+
plural: "agent definitions",
|
|
18640
|
+
rootLabel: "Agents",
|
|
18641
|
+
nameDescription: "Definition filename, for example p4-reviewer.md."
|
|
18642
|
+
};
|
|
18189
18643
|
const rootFor = (status, kind) => status.roots.find((root) => root.kind === kind)?.path ?? null;
|
|
18190
18644
|
/**
|
|
18191
18645
|
* Exported for tests. These two are the only place a wrong report becomes a
|
|
@@ -18248,7 +18702,8 @@ const formatReport = (report) => {
|
|
|
18248
18702
|
const ALL_WORDS = [
|
|
18249
18703
|
SKILL_WORDS,
|
|
18250
18704
|
MEMORY_WORDS,
|
|
18251
|
-
MCP_WORDS
|
|
18705
|
+
MCP_WORDS,
|
|
18706
|
+
AGENT_WORDS
|
|
18252
18707
|
];
|
|
18253
18708
|
/**
|
|
18254
18709
|
* The whole picture, in one page.
|
|
@@ -18380,6 +18835,67 @@ const makeAssetCommands = (words) => {
|
|
|
18380
18835
|
const skillCommands = makeAssetCommands(SKILL_WORDS);
|
|
18381
18836
|
const memoryCommands = makeAssetCommands(MEMORY_WORDS);
|
|
18382
18837
|
const mcpCommands = makeAssetCommands(MCP_WORDS);
|
|
18838
|
+
const agentCommands = makeAssetCommands(AGENT_WORDS);
|
|
18839
|
+
/**
|
|
18840
|
+
* What one install attempt did, in a sentence.
|
|
18841
|
+
*
|
|
18842
|
+
* Exported for tests: `exists` in particular must never read as success. The
|
|
18843
|
+
* whole point of refusing to overwrite is that someone who has edited a preset
|
|
18844
|
+
* keeps their edits, and a line saying "installed" would tell them the opposite.
|
|
18845
|
+
*/
|
|
18846
|
+
const formatPresetInstall = (input) => {
|
|
18847
|
+
if (input.outcome === "created") return `Installed ${input.name}${input.path === null ? "" : ` at ${input.path}`}`;
|
|
18848
|
+
if (input.outcome === "exists") return `Skipped ${input.name}: it already exists here, and installing would overwrite your copy.`;
|
|
18849
|
+
return `Could not install ${input.name}: ${input.detail ?? input.outcome}`;
|
|
18850
|
+
};
|
|
18851
|
+
/**
|
|
18852
|
+
* `p4c agents presets` — what is on offer, before anything is written.
|
|
18853
|
+
*
|
|
18854
|
+
* Its own command rather than a flag on `install`, because reading the list is
|
|
18855
|
+
* what people do first and a listing that requires naming an install target is
|
|
18856
|
+
* not a listing.
|
|
18857
|
+
*/
|
|
18858
|
+
const presetsCommand = Command.make("presets", { ...authLocationFlags }).pipe(Command.withDescription("List the agent definitions p4code ships, without installing any."), Command.withHandler(() => Console.log([
|
|
18859
|
+
...AGENT_PRESETS.map((preset) => ` ${preset.name.padEnd(28)} ${preset.summary}`),
|
|
18860
|
+
"",
|
|
18861
|
+
"Install one with: p4c agents install <name>"
|
|
18862
|
+
].join("\n"))));
|
|
18863
|
+
/**
|
|
18864
|
+
* Install a shipped definition onto this machine, and only this machine.
|
|
18865
|
+
*
|
|
18866
|
+
* `createLocal` rather than a hub write, so an installed preset starts out
|
|
18867
|
+
* exactly like one written by hand: local, unmanaged, and shared only when
|
|
18868
|
+
* something explicitly publishes it. It also means this works with no hub
|
|
18869
|
+
* configured, which is when a fresh machine most wants it.
|
|
18870
|
+
*/
|
|
18871
|
+
const presetInstallCommand = Command.make("install", {
|
|
18872
|
+
...authLocationFlags,
|
|
18873
|
+
name: Argument.string("name").pipe(Argument.withDescription("Preset to install, for example investigator."), Argument.optional),
|
|
18874
|
+
all: Flag.boolean("all").pipe(Flag.withDescription("Install every preset p4code ships."))
|
|
18875
|
+
}).pipe(Command.withDescription("Install a p4code agent definition into this machine's agents directory."), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => Effect.gen(function* () {
|
|
18876
|
+
const requested = Option.getOrUndefined(flags.name);
|
|
18877
|
+
if (!flags.all && requested === void 0) return yield* Console.error("Name a preset, or pass --all to install every one.\nSee them with: p4c agents presets");
|
|
18878
|
+
const chosen = flags.all ? AGENT_PRESETS : (() => {
|
|
18879
|
+
const found = findAgentPreset(requested ?? "");
|
|
18880
|
+
return found === void 0 ? [] : [found];
|
|
18881
|
+
})();
|
|
18882
|
+
if (chosen.length === 0) return yield* Console.error(`No preset named ${requested ?? ""}. See them with: p4c agents presets`);
|
|
18883
|
+
const lines = [];
|
|
18884
|
+
for (const preset of chosen) {
|
|
18885
|
+
const result = yield* sync.createLocal({
|
|
18886
|
+
kind: AGENT_WORDS.kind,
|
|
18887
|
+
name: preset.name,
|
|
18888
|
+
files: preset.files
|
|
18889
|
+
});
|
|
18890
|
+
lines.push(formatPresetInstall({
|
|
18891
|
+
name: preset.name,
|
|
18892
|
+
outcome: result.outcome,
|
|
18893
|
+
path: result.path,
|
|
18894
|
+
detail: result.detail
|
|
18895
|
+
}));
|
|
18896
|
+
}
|
|
18897
|
+
return yield* Console.log(lines.join("\n"));
|
|
18898
|
+
}))));
|
|
18383
18899
|
const shareAllCommand = Command.make("share-all", { ...authLocationFlags }).pipe(Command.withDescription("Share everything, including new items, automatically. The default."), Command.withHandler((flags) => runWithSync(flags, ({ sync, link }) => Effect.gen(function* () {
|
|
18384
18900
|
yield* link.setShareMode("all");
|
|
18385
18901
|
yield* Console.log(formatStatus(yield* sync.status));
|
|
@@ -18432,6 +18948,23 @@ const mcpCommand = Command.make("mcp", {}).pipe(Command.withDescription("Share M
|
|
|
18432
18948
|
mcpCommands.showCommand,
|
|
18433
18949
|
mcpCommands.removeCommand
|
|
18434
18950
|
]));
|
|
18951
|
+
/**
|
|
18952
|
+
* `p4c agents` — the subagent definitions this machine runs.
|
|
18953
|
+
*
|
|
18954
|
+
* The same six subcommands every kind gets, plus the two that are particular to
|
|
18955
|
+
* this one: p4code ships definitions, and a definition is the only place a
|
|
18956
|
+
* subagent's input and output contract can be stated.
|
|
18957
|
+
*/
|
|
18958
|
+
const agentsCommand = Command.make("agents", {}).pipe(Command.withDescription("Share subagent definitions across your machines, and install p4code's."), Command.withSubcommands([
|
|
18959
|
+
agentCommands.statusCommand,
|
|
18960
|
+
agentCommands.syncCommand,
|
|
18961
|
+
agentCommands.publishCommand,
|
|
18962
|
+
agentCommands.unpublishCommand,
|
|
18963
|
+
agentCommands.showCommand,
|
|
18964
|
+
agentCommands.removeCommand,
|
|
18965
|
+
presetsCommand,
|
|
18966
|
+
presetInstallCommand
|
|
18967
|
+
]));
|
|
18435
18968
|
/** Exported for the hub command, which owns `connect`/`disconnect`. */
|
|
18436
18969
|
const hubLinkCommands = [hubConnectCommand, hubDisconnectCommand];
|
|
18437
18970
|
//#endregion
|
|
@@ -49044,88 +49577,6 @@ function formatAskUserQuestionAnswers(answers) {
|
|
|
49044
49577
|
return formatted;
|
|
49045
49578
|
}
|
|
49046
49579
|
//#endregion
|
|
49047
|
-
//#region src/provider/CompressPrompts.ts
|
|
49048
|
-
/**
|
|
49049
|
-
* Caveman-style response compression rulesets, adapted from the caveman
|
|
49050
|
-
* plugin's SKILL.md. Pre-baked per level so injection is a lookup, not a
|
|
49051
|
-
* runtime template. Applies to assistant prose only: code blocks, commits,
|
|
49052
|
-
* PRs, error strings, and safety-critical text stay uncompressed.
|
|
49053
|
-
*/
|
|
49054
|
-
const COMPRESS_SHARED_RULES = `Respond terse. All technical substance stays. Only fluff dies.
|
|
49055
|
-
|
|
49056
|
-
## Persistence
|
|
49057
|
-
|
|
49058
|
-
Active every response. No revert after many turns. No filler drift. Still active if unsure.
|
|
49059
|
-
|
|
49060
|
-
## Rules
|
|
49061
|
-
|
|
49062
|
-
Drop filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Short synonyms (big not extensive, fix not "implement a solution for"). No tool-call narration, no decorative tables/emoji, no dumping long raw error logs unless asked - quote shortest decisive line. Standard well-known tech acronyms OK (DB/API/HTTP); never invent new abbreviations (cfg/impl/req/res/fn) - tokenizers split them same as the full word: zero tokens saved, reader still decodes. No causal arrows (->) in prose. Technical terms exact. Code blocks unchanged. Errors quoted exact.
|
|
49063
|
-
|
|
49064
|
-
Preserve the user's dominant language: compress the style, not the language. Always keep technical terms, code, API names, CLI commands, commit-type keywords (feat/fix/...), and exact error strings verbatim unless the user explicitly asks for translation.
|
|
49065
|
-
|
|
49066
|
-
No self-reference. Never name or announce the style. No compressed answer plus normal recap - compressed output only. Exception: user explicitly asks what the mode is.
|
|
49067
|
-
|
|
49068
|
-
## Auto-Clarity
|
|
49069
|
-
|
|
49070
|
-
Drop compression and write normally when:
|
|
49071
|
-
- Security warnings
|
|
49072
|
-
- Irreversible action confirmations
|
|
49073
|
-
- Multi-step sequences where fragment order or omitted conjunctions risk misread
|
|
49074
|
-
- Compression itself creates technical ambiguity
|
|
49075
|
-
- User asks to clarify or repeats a question
|
|
49076
|
-
|
|
49077
|
-
Resume compression after the clear part is done.
|
|
49078
|
-
|
|
49079
|
-
## Boundaries
|
|
49080
|
-
|
|
49081
|
-
Code, commit messages, and PR descriptions: write normal, uncompressed.`;
|
|
49082
|
-
const COMPRESS_RULESETS = {
|
|
49083
|
-
lite: `## Response compression: lite
|
|
49084
|
-
|
|
49085
|
-
${COMPRESS_SHARED_RULES}
|
|
49086
|
-
|
|
49087
|
-
## Intensity: lite
|
|
49088
|
-
|
|
49089
|
-
No filler or hedging. Keep articles and full sentences. Professional but tight.
|
|
49090
|
-
|
|
49091
|
-
Example - "Why does my React component re-render?"
|
|
49092
|
-
"Your component re-renders because you create a new object reference each render. Wrap it in \`useMemo\`."`,
|
|
49093
|
-
full: `## Response compression: full
|
|
49094
|
-
|
|
49095
|
-
${COMPRESS_SHARED_RULES}
|
|
49096
|
-
|
|
49097
|
-
## Intensity: full
|
|
49098
|
-
|
|
49099
|
-
Drop articles (a/an/the), fragments OK, short synonyms. Pattern: [thing] [action] [reason]. [next step].
|
|
49100
|
-
|
|
49101
|
-
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
|
|
49102
|
-
Yes: "Bug in auth middleware. Token expiry check use \`<\` not \`<=\`. Fix:"
|
|
49103
|
-
|
|
49104
|
-
Example - "Why does my React component re-render?"
|
|
49105
|
-
"New object ref each render. Inline object prop = new ref = re-render. Wrap in \`useMemo\`."`,
|
|
49106
|
-
ultra: `## Response compression: ultra
|
|
49107
|
-
|
|
49108
|
-
${COMPRESS_SHARED_RULES}
|
|
49109
|
-
|
|
49110
|
-
## Intensity: ultra
|
|
49111
|
-
|
|
49112
|
-
Drop articles, fragments OK, short synonyms. Also strip conjunctions when cause-then-effect stays unambiguous. One word when one word is enough. State each fact once. Code symbols, function names, API names, error strings: never touch.
|
|
49113
|
-
|
|
49114
|
-
Example - "Why does my React component re-render?"
|
|
49115
|
-
"Inline object prop, new ref, re-render. \`useMemo\`."`
|
|
49116
|
-
};
|
|
49117
|
-
const COMPRESS_TURN_REMINDERS = {
|
|
49118
|
-
lite: "[response style: compressed lite - no filler/hedging, full sentences; code/commits/security text normal]",
|
|
49119
|
-
full: "[response style: compressed full - terse fragments, drop articles/filler; code/commits/security text normal]",
|
|
49120
|
-
ultra: "[response style: compressed ultra - maximum terseness, one word when enough; code/commits/security text normal]"
|
|
49121
|
-
};
|
|
49122
|
-
function compressRulesetFor(mode) {
|
|
49123
|
-
return mode === "off" ? void 0 : COMPRESS_RULESETS[mode];
|
|
49124
|
-
}
|
|
49125
|
-
function compressTurnReminderFor(mode) {
|
|
49126
|
-
return mode === "off" ? void 0 : COMPRESS_TURN_REMINDERS[mode];
|
|
49127
|
-
}
|
|
49128
|
-
//#endregion
|
|
49129
49580
|
//#region src/provider/Layers/ClaudeAdapter.ts
|
|
49130
49581
|
/**
|
|
49131
49582
|
* ClaudeAdapterLive - Scoped live implementation for the Claude Agent provider adapter.
|
|
@@ -49687,6 +50138,20 @@ const SUPPORTED_CLAUDE_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
|
49687
50138
|
/** Emitted by the CLI but absent from the SDK's `SDKMessage` union. */
|
|
49688
50139
|
const UNTYPED_COMMAND_LIFECYCLE_MESSAGE = "command_lifecycle";
|
|
49689
50140
|
const NARRATE_BEFORE_TOOLS_PROMPT = "Before your first tool call in a turn, write one short sentence saying what you are about to do. Keep it to a single line and skip it when you are answering without tools.";
|
|
50141
|
+
/**
|
|
50142
|
+
* Said whichever way the setting is set, and said as an override.
|
|
50143
|
+
*
|
|
50144
|
+
* The preset system prompt this adapter requests already carries an
|
|
50145
|
+
* instruction not to spawn subagents unless the user asked — and a preset is
|
|
50146
|
+
* taken wholesale, so there is no version of it with that line removed. An
|
|
50147
|
+
* append that merely stayed silent when subagents are allowed would therefore
|
|
50148
|
+
* leave the restriction standing, which is the one outcome the setting exists
|
|
50149
|
+
* to change. Both texts name the earlier instruction they replace, because a
|
|
50150
|
+
* later instruction that does not acknowledge the conflict reads as an
|
|
50151
|
+
* accident rather than a decision.
|
|
50152
|
+
*/
|
|
50153
|
+
const SUBAGENTS_ALLOWED_PROMPT = "You may spawn subagents with the Task tool whenever it helps, without asking the user first. This overrides any earlier instruction to use subagents only when they are requested. Prefer one for bounded search or review work whose result is much smaller than the reading it takes to produce; do the work inline when the answer is a single file or a single edit.";
|
|
50154
|
+
const SUBAGENTS_ON_REQUEST_PROMPT = "Do not spawn subagents with the Task tool unless the user asks for one. This overrides any earlier instruction permitting them on your own initiative. Do the work inline instead, and say so if it would have been better delegated.";
|
|
49690
50155
|
const CLAUDE_SETTING_SOURCES = [
|
|
49691
50156
|
"user",
|
|
49692
50157
|
"project",
|
|
@@ -49915,6 +50380,25 @@ function sdkNativeMethod(message) {
|
|
|
49915
50380
|
* would only duplicate a refresh that has already happened.
|
|
49916
50381
|
*/
|
|
49917
50382
|
const CONSUMED_UNDECLARED_SYSTEM_SUBTYPES = /* @__PURE__ */ new Set(["background_tasks_changed", "vcs_state_changed"]);
|
|
50383
|
+
/**
|
|
50384
|
+
* Another undeclared-but-real subtype, emitted once the CLI has pushed the
|
|
50385
|
+
* turn's changes to a hosting provider:
|
|
50386
|
+
* `{provider: "github", url: "https://github.com/org/repo/pull/96",
|
|
50387
|
+
* repo: "org/repo", identifier: "96"}`. Mapped to `code.published` so the
|
|
50388
|
+
* work log links the pull request instead of logging an unknown-subtype
|
|
50389
|
+
* warning.
|
|
50390
|
+
*/
|
|
50391
|
+
const CODE_CHANGE_PUBLISHED_SUBTYPE = "code_change_published";
|
|
50392
|
+
function codePublishedPayloadFromSdkMessage(message) {
|
|
50393
|
+
const record = message;
|
|
50394
|
+
if (typeof record.url !== "string" || record.url.length === 0) return;
|
|
50395
|
+
return {
|
|
50396
|
+
hostingProvider: typeof record.provider === "string" && record.provider.length > 0 ? record.provider : "unknown",
|
|
50397
|
+
url: record.url,
|
|
50398
|
+
...typeof record.repo === "string" && record.repo.length > 0 ? { repo: record.repo } : {},
|
|
50399
|
+
...typeof record.identifier === "string" && record.identifier.length > 0 ? { identifier: record.identifier } : {}
|
|
50400
|
+
};
|
|
50401
|
+
}
|
|
49918
50402
|
const SDK_MESSAGE_NOISE_KEYS = /* @__PURE__ */ new Set([
|
|
49919
50403
|
"type",
|
|
49920
50404
|
"subtype",
|
|
@@ -50886,6 +51370,17 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
50886
51370
|
}
|
|
50887
51371
|
};
|
|
50888
51372
|
if (CONSUMED_UNDECLARED_SYSTEM_SUBTYPES.has(message.subtype)) return;
|
|
51373
|
+
if (message.subtype === CODE_CHANGE_PUBLISHED_SUBTYPE) {
|
|
51374
|
+
const published = codePublishedPayloadFromSdkMessage(message);
|
|
51375
|
+
if (published) {
|
|
51376
|
+
yield* offerRuntimeEvent({
|
|
51377
|
+
...base,
|
|
51378
|
+
type: "code.published",
|
|
51379
|
+
payload: published
|
|
51380
|
+
});
|
|
51381
|
+
return;
|
|
51382
|
+
}
|
|
51383
|
+
}
|
|
50889
51384
|
switch (message.subtype) {
|
|
50890
51385
|
case "init":
|
|
50891
51386
|
yield* offerRuntimeEvent({
|
|
@@ -51520,8 +52015,13 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
51520
52015
|
const mcpSession = readMcpProviderSession(input.threadId);
|
|
51521
52016
|
const externalMcpServers = options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers;
|
|
51522
52017
|
const narrateBeforeTools = options?.resolveToolCallNarration === void 0 ? DEFAULT_SERVER_SETTINGS.enableToolCallNarration : yield* options.resolveToolCallNarration;
|
|
52018
|
+
const unpromptedSubagents = options?.resolveUnpromptedSubagents === void 0 ? DEFAULT_SERVER_SETTINGS.enableUnpromptedSubagents : yield* options.resolveUnpromptedSubagents;
|
|
51523
52019
|
const compressRuleset = compressRulesetFor(input.compressMode ?? "off");
|
|
51524
|
-
const systemPromptAppend = [
|
|
52020
|
+
const systemPromptAppend = [
|
|
52021
|
+
...narrateBeforeTools ? [NARRATE_BEFORE_TOOLS_PROMPT] : [],
|
|
52022
|
+
unpromptedSubagents ? SUBAGENTS_ALLOWED_PROMPT : SUBAGENTS_ON_REQUEST_PROMPT,
|
|
52023
|
+
...compressRuleset !== void 0 ? [compressRuleset] : []
|
|
52024
|
+
].join("\n\n");
|
|
51525
52025
|
const queryOptions = {
|
|
51526
52026
|
...input.cwd ? { cwd: input.cwd } : {},
|
|
51527
52027
|
...apiModelId ? { model: apiModelId } : {},
|
|
@@ -51983,12 +52483,14 @@ const ClaudeDriver = {
|
|
|
51983
52483
|
const mcpRegistry = yield* McpRegistry;
|
|
51984
52484
|
const resolveDisabledSkills = serverSettings.getSettings.pipe(Effect.map((settings) => settings.disabledSkills), Effect.orElseSucceed(() => []));
|
|
51985
52485
|
const resolveToolCallNarration = serverSettings.getSettings.pipe(Effect.map((settings) => settings.enableToolCallNarration), Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS.enableToolCallNarration));
|
|
52486
|
+
const resolveUnpromptedSubagents = serverSettings.getSettings.pipe(Effect.map((settings) => settings.enableUnpromptedSubagents), Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS.enableUnpromptedSubagents));
|
|
51986
52487
|
const adapter = yield* makeClaudeAdapter(effectiveConfig, {
|
|
51987
52488
|
instanceId,
|
|
51988
52489
|
environment: processEnv,
|
|
51989
52490
|
resolveMcpServers: mcpRegistry.resolveForSession,
|
|
51990
52491
|
resolveDisabledSkills,
|
|
51991
52492
|
resolveToolCallNarration,
|
|
52493
|
+
resolveUnpromptedSubagents,
|
|
51992
52494
|
...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}
|
|
51993
52495
|
});
|
|
51994
52496
|
const textGeneration = yield* makeClaudeTextGeneration(effectiveConfig, processEnv);
|
|
@@ -85633,6 +86135,24 @@ function runtimeEventToActivities(event, taskTitle) {
|
|
|
85633
86135
|
turnId: toTurnId$1(event.turnId) ?? null,
|
|
85634
86136
|
...maybeSequence
|
|
85635
86137
|
}];
|
|
86138
|
+
case "code.published": {
|
|
86139
|
+
const label = event.payload.repo ? event.payload.identifier ? `${event.payload.repo}#${event.payload.identifier}` : event.payload.repo : event.payload.url;
|
|
86140
|
+
return [{
|
|
86141
|
+
id: event.eventId,
|
|
86142
|
+
createdAt: event.createdAt,
|
|
86143
|
+
tone: "info",
|
|
86144
|
+
kind: "code.published",
|
|
86145
|
+
summary: `Published ${label}`,
|
|
86146
|
+
payload: {
|
|
86147
|
+
hostingProvider: event.payload.hostingProvider,
|
|
86148
|
+
url: event.payload.url,
|
|
86149
|
+
...event.payload.repo ? { repo: event.payload.repo } : {},
|
|
86150
|
+
...event.payload.identifier ? { identifier: event.payload.identifier } : {}
|
|
86151
|
+
},
|
|
86152
|
+
turnId: toTurnId$1(event.turnId) ?? null,
|
|
86153
|
+
...maybeSequence
|
|
86154
|
+
}];
|
|
86155
|
+
}
|
|
85636
86156
|
case "turn.plan.updated": return [{
|
|
85637
86157
|
id: event.eventId,
|
|
85638
86158
|
createdAt: event.createdAt,
|
|
@@ -88764,6 +89284,7 @@ const reportUpdateAvailable = Effect.gen(function* () {
|
|
|
88764
89284
|
const makeCli = () => Command.make("p4c", { ...sharedServerCommandFlags }).pipe(Command.withDescription("Run the P4Code server."), Command.withHandler((flags) => runServerCommand(flags)), Command.withSubcommands([
|
|
88765
89285
|
startCommand,
|
|
88766
89286
|
serveCommand,
|
|
89287
|
+
agentsCommand,
|
|
88767
89288
|
authCommand,
|
|
88768
89289
|
hubCommand,
|
|
88769
89290
|
mcpCommand,
|