dsh-loop-engine 1.0.0-rc3 → 1.0.0-rc5

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/lib/index.js CHANGED
@@ -45,10 +45,10 @@ var __callDispose = (stack, error, hasError) => {
45
45
  };
46
46
 
47
47
  // src/index.ts
48
- import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
48
+ import { mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
49
49
  import { mkdir, readFile as readFile4, rename, writeFile } from "node:fs/promises";
50
50
  import { randomUUID } from "node:crypto";
51
- import { dirname as dirname3, join as join6 } from "node:path";
51
+ import { dirname as dirname4, join as join8 } from "node:path";
52
52
  import z5 from "@deepseek-ai/schemastery";
53
53
  import { installSettingsSection } from "@deepseek-ai/dsh-settings";
54
54
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
@@ -3650,7 +3650,8 @@ var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
3650
3650
  // src/settings.ts
3651
3651
  var LOOP_ENGINE_IDS = ["in-process", "claude-code", "codex", "pi"];
3652
3652
  var LOOP_ENGINE_SETTINGS_SCHEMA = z4.object({
3653
- engine: z4.union([z4.const("in-process"), z4.const("claude-code"), z4.const("codex"), z4.const("pi")]).default("in-process")
3653
+ engine: z4.union([z4.const("in-process"), z4.const("claude-code"), z4.const("codex"), z4.const("pi")]).default("in-process"),
3654
+ showInComposer: z4.boolean().default(true)
3654
3655
  });
3655
3656
  function loopEngineSettingsNamespace() {
3656
3657
  return settingsNamespace(LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL);
@@ -3694,64 +3695,120 @@ function ensureTrailingNewline(text) {
3694
3695
  return text.endsWith("\n") ? text : `${text}
3695
3696
  `;
3696
3697
  }
3698
+ function hasRootEntry(text) {
3699
+ return /^(?:- |\[)/m.test(text);
3700
+ }
3701
+ function dropSeedPlaceholder(text) {
3702
+ return text.replace(/^\[\]\n/m, "");
3703
+ }
3704
+ function seedEmptyArray(text) {
3705
+ const head = text.replace(/\n+$/, "");
3706
+ return head === "" ? "[]\n" : `${head}
3707
+ []
3708
+ `;
3709
+ }
3697
3710
  function applyManagedBlock(text, engine) {
3698
3711
  const block = renderManagedBlock(engine);
3699
3712
  const span = managedSpan(text);
3713
+ let result;
3700
3714
  if (!span.present) {
3701
3715
  if (block === "") return text;
3702
3716
  const base = ensureTrailingNewline(text);
3703
- return `${base}
3717
+ result = `${base}
3704
3718
  ${block}`;
3719
+ } else if (block === "") {
3720
+ result = span.tail.startsWith("\n") ? `${span.head}${span.tail.slice(1)}` : `${span.head}${span.tail}`;
3721
+ } else {
3722
+ result = `${span.head}${span.blankBefore ? "\n" : ""}${block}${span.tail}`;
3705
3723
  }
3706
- if (block === "") {
3707
- return span.tail.startsWith("\n") ? `${span.head}${span.tail.slice(1)}` : `${span.head}${span.tail}`;
3708
- }
3709
- return `${span.head}${span.blankBefore ? "\n" : ""}${block}${span.tail}`;
3724
+ if (block !== "") return dropSeedPlaceholder(result);
3725
+ return hasRootEntry(result) ? result : seedEmptyArray(result);
3710
3726
  }
3711
3727
 
3712
3728
  // src/commands.ts
3729
+ import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
3730
+ import { homedir } from "node:os";
3731
+ import { join as join3 } from "node:path";
3732
+ import { createUserMessage as createUserMessage4 } from "@deepseek-ai/dsh-llm";
3733
+ var COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
3734
+ function forwardClaudeCodeCommand(name2) {
3735
+ return (invocation) => {
3736
+ invocation.agent.followup(createUserMessage4({
3737
+ content: [{ type: "text", text: `/${name2}${invocation.rawInput}` }],
3738
+ source: { kind: "user" }
3739
+ }));
3740
+ return { kind: "success" };
3741
+ };
3742
+ }
3743
+ function builtin(name2, description) {
3744
+ return { name: name2, description, handler: forwardClaudeCodeCommand(name2) };
3745
+ }
3713
3746
  var CLAUDE_CODE_COMMANDS = [
3714
- {
3715
- name: "help",
3716
- description: "Show help about Claude Code commands",
3717
- handler: async () => ({ kind: "success" })
3718
- },
3719
- {
3720
- name: "compact",
3721
- description: "Compact the conversation to reduce context usage",
3722
- handler: async () => ({ kind: "success" })
3723
- },
3724
- {
3725
- name: "clear",
3726
- description: "Clear the conversation and start fresh",
3727
- handler: async () => ({ kind: "success" })
3728
- },
3729
- {
3730
- name: "review",
3731
- description: "Review recent changes (git diff)",
3732
- handler: async () => ({ kind: "success" })
3733
- },
3734
- {
3735
- name: "explain",
3736
- description: "Explain the selected code",
3737
- handler: async () => ({ kind: "success" })
3738
- },
3739
- {
3740
- name: "fix",
3741
- description: "Fix issues in the code",
3742
- handler: async () => ({ kind: "success" })
3743
- },
3744
- {
3745
- name: "tests",
3746
- description: "Add tests for the selected code",
3747
- handler: async () => ({ kind: "success" })
3748
- }
3747
+ builtin("help", "Show help about Claude Code commands"),
3748
+ builtin("compact", "Compact the conversation to reduce context usage"),
3749
+ builtin("clear", "Clear the conversation and start fresh"),
3750
+ builtin("review", "Review recent changes (git diff)"),
3751
+ builtin("explain", "Explain the selected code"),
3752
+ builtin("fix", "Fix issues in the code"),
3753
+ builtin("tests", "Add tests for the selected code")
3749
3754
  ];
3755
+ function discoverUserSlashCommands() {
3756
+ let entries;
3757
+ try {
3758
+ entries = readdirSync(userCommandsDir(), { encoding: "utf8" });
3759
+ } catch {
3760
+ return [];
3761
+ }
3762
+ const definitions = [];
3763
+ const seen = new Set(CLAUDE_CODE_COMMANDS.map((command) => command.name));
3764
+ for (const entry of entries.sort()) {
3765
+ if (!entry.endsWith(".md")) continue;
3766
+ const name2 = entry.slice(0, -".md".length);
3767
+ if (!COMMAND_NAME.test(name2) || seen.has(name2)) continue;
3768
+ const path = join3(userCommandsDir(), entry);
3769
+ let raw;
3770
+ try {
3771
+ raw = readFileSync2(path, "utf8");
3772
+ } catch {
3773
+ continue;
3774
+ }
3775
+ const description = commandDescription(raw);
3776
+ if (description === void 0) continue;
3777
+ seen.add(name2);
3778
+ definitions.push({ name: name2, description, handler: forwardClaudeCodeCommand(name2) });
3779
+ }
3780
+ return definitions;
3781
+ }
3782
+ function userCommandsDir() {
3783
+ return join3(homedir(), ".claude", "commands");
3784
+ }
3785
+ function commandDescription(raw) {
3786
+ const trimmed = raw.trim();
3787
+ if (trimmed.length === 0) return void 0;
3788
+ let body = trimmed;
3789
+ if (trimmed.startsWith("---\n")) {
3790
+ const closing = trimmed.indexOf("\n---");
3791
+ if (closing <= 0) return void 0;
3792
+ for (const line of trimmed.slice(4, closing).split("\n")) {
3793
+ const colon = line.indexOf(":");
3794
+ if (colon < 0 || line.slice(0, colon).trim() !== "description") continue;
3795
+ const value = line.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
3796
+ if (value.length > 0) return value;
3797
+ }
3798
+ body = trimmed.slice(closing + 4);
3799
+ }
3800
+ for (const line of body.split("\n")) {
3801
+ const candidate = line.trim();
3802
+ if (candidate.length === 0 || candidate.startsWith("#")) continue;
3803
+ return candidate.length > 120 ? `${candidate.slice(0, 119)}\u2026` : candidate;
3804
+ }
3805
+ return void 0;
3806
+ }
3750
3807
 
3751
3808
  // src/skills.ts
3752
3809
  import { readFile, readdir, stat } from "node:fs/promises";
3753
- import { homedir } from "node:os";
3754
- import { join as join3, resolve } from "node:path";
3810
+ import { homedir as homedir2 } from "node:os";
3811
+ import { join as join4, resolve } from "node:path";
3755
3812
  var SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3756
3813
  var PROVIDER_NAME = "claude-code";
3757
3814
  var CLAUDE_CODE_RANK = 150;
@@ -3861,10 +3918,10 @@ var ClaudeCodeSkillProvider = class {
3861
3918
  const cwd = options.cwd;
3862
3919
  if (cwd !== void 0) {
3863
3920
  const projectRoot = await findProjectRoot(resolve(cwd));
3864
- await collectSkillsDir(join3(projectRoot, ".claude", "skills"), CLAUDE_CODE_RANK, candidates);
3921
+ await collectSkillsDir(join4(projectRoot, ".claude", "skills"), CLAUDE_CODE_RANK, candidates);
3865
3922
  await collectClaudeMd(projectRoot, candidates);
3866
3923
  }
3867
- await collectSkillsDir(join3(homedir(), ".claude", "skills"), CLAUDE_CODE_USER_RANK, candidates);
3924
+ await collectSkillsDir(join4(homedir2(), ".claude", "skills"), CLAUDE_CODE_USER_RANK, candidates);
3868
3925
  if (this.control.signal.aborted) return [];
3869
3926
  return candidates;
3870
3927
  }
@@ -3898,11 +3955,11 @@ async function collectSkillsDir(skillsDir, rank, candidates) {
3898
3955
  return;
3899
3956
  }
3900
3957
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3901
- const entryPath = join3(skillsDir, entry.name);
3958
+ const entryPath = join4(skillsDir, entry.name);
3902
3959
  const info = await stat(entryPath).catch(() => void 0);
3903
3960
  if (info === void 0) continue;
3904
3961
  if (info.isDirectory()) {
3905
- const path = join3(entryPath, "SKILL.md");
3962
+ const path = join4(entryPath, "SKILL.md");
3906
3963
  const skill2 = await tryParseSkill(path);
3907
3964
  if (skill2 === void 0) continue;
3908
3965
  candidates.push(toCandidate(skill2, path, rank, entryPath));
@@ -3915,7 +3972,7 @@ async function collectSkillsDir(skillsDir, rank, candidates) {
3915
3972
  }
3916
3973
  }
3917
3974
  async function collectClaudeMd(projectRoot, candidates) {
3918
- const claudeMd = join3(projectRoot, "CLAUDE.md");
3975
+ const claudeMd = join4(projectRoot, "CLAUDE.md");
3919
3976
  try {
3920
3977
  const info = await stat(claudeMd);
3921
3978
  if (!info.isFile()) return;
@@ -3952,7 +4009,7 @@ async function findProjectRoot(cwd) {
3952
4009
  let current = cwd;
3953
4010
  while (true) {
3954
4011
  try {
3955
- await stat(join3(current, ".git"));
4012
+ await stat(join4(current, ".git"));
3956
4013
  return current;
3957
4014
  } catch {
3958
4015
  }
@@ -3963,12 +4020,81 @@ async function findProjectRoot(cwd) {
3963
4020
  }
3964
4021
 
3965
4022
  // src/engine-codex/skills.ts
3966
- import { readFile as readFile2 } from "node:fs/promises";
3967
- import { homedir as homedir2 } from "node:os";
3968
- import { join as join4, resolve as resolve2 } from "node:path";
4023
+ import { homedir as homedir3 } from "node:os";
4024
+ import { join as join6 } from "node:path";
4025
+
4026
+ // src/driver-core/context-files.ts
4027
+ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
4028
+ import { join as join5, resolve as resolve2 } from "node:path";
4029
+ async function projectAncestors(cwd) {
4030
+ const root = await findProjectRoot(resolve2(cwd));
4031
+ const dirs = [];
4032
+ let current = resolve2(cwd);
4033
+ while (true) {
4034
+ dirs.push(current);
4035
+ if (current === root) return dirs;
4036
+ current = resolve2(current, "..");
4037
+ }
4038
+ }
4039
+ async function collectProjectContextFiles(cwd, policy) {
4040
+ const files = [];
4041
+ for (const dir of await projectAncestors(cwd)) {
4042
+ const chosen = await dirContextFile(dir, policy);
4043
+ if (chosen !== void 0) files.push(chosen);
4044
+ }
4045
+ return files;
4046
+ }
4047
+ async function dirContextFile(dir, policy) {
4048
+ if (policy.override !== void 0) {
4049
+ const override = join5(dir, policy.override);
4050
+ if (await pathExists(override)) return override;
4051
+ }
4052
+ for (const name2 of policy.primary) {
4053
+ const candidate = join5(dir, name2);
4054
+ if (await pathExists(candidate)) return candidate;
4055
+ }
4056
+ return void 0;
4057
+ }
4058
+ async function pathExists(path) {
4059
+ try {
4060
+ await stat2(path);
4061
+ return true;
4062
+ } catch {
4063
+ return false;
4064
+ }
4065
+ }
4066
+ async function readOptionalFile(path) {
4067
+ try {
4068
+ return await readFile2(path, { encoding: "utf8" });
4069
+ } catch {
4070
+ return void 0;
4071
+ }
4072
+ }
4073
+ async function anySourceNonEmpty(paths) {
4074
+ for (const path of paths) {
4075
+ const raw = await readOptionalFile(path);
4076
+ if (raw !== void 0 && raw.trim().length > 0) return true;
4077
+ }
4078
+ return false;
4079
+ }
4080
+ async function fileNonEmpty(path) {
4081
+ const raw = await readOptionalFile(path);
4082
+ return raw !== void 0 && raw.trim().length > 0;
4083
+ }
4084
+ async function readSources(paths) {
4085
+ const parts = [];
4086
+ for (const path of paths) {
4087
+ const raw = await readOptionalFile(path);
4088
+ if (raw !== void 0 && raw.trim().length > 0) parts.push(raw);
4089
+ }
4090
+ return parts.length > 0 ? parts.join("\n\n") : void 0;
4091
+ }
4092
+
4093
+ // src/engine-codex/skills.ts
3969
4094
  var PROVIDER_NAME2 = "codex";
3970
4095
  var CODEX_PROJECT_RANK = 140;
3971
4096
  var CODEX_USER_RANK = 160;
4097
+ var CODEX_CONTEXT_POLICY = { primary: ["AGENTS.md"] };
3972
4098
  var CodexSkillProvider = class {
3973
4099
  constructor(control) {
3974
4100
  this.control = control;
@@ -3979,60 +4105,65 @@ var CodexSkillProvider = class {
3979
4105
  const candidates = [];
3980
4106
  const cwd = options.cwd;
3981
4107
  if (cwd !== void 0) {
3982
- const projectRoot = await findProjectRoot(resolve2(cwd));
3983
- await this.collectAgentsMd(join4(projectRoot, "AGENTS.md"), CODEX_PROJECT_RANK, candidates);
4108
+ const paths = await collectProjectContextFiles(cwd, CODEX_CONTEXT_POLICY);
4109
+ if (await anySourceNonEmpty(paths)) candidates.push(this.agentsCandidate(paths, CODEX_PROJECT_RANK));
3984
4110
  }
3985
- await this.collectAgentsMd(join4(homedir2(), ".codex", "AGENTS.md"), CODEX_USER_RANK, candidates);
4111
+ const userPath = join6(homedir3(), ".codex", "AGENTS.md");
4112
+ if (await fileNonEmpty(userPath)) candidates.push(this.agentsCandidate([userPath], CODEX_USER_RANK));
3986
4113
  if (this.control.signal.aborted) return [];
3987
4114
  return candidates;
3988
4115
  }
3989
4116
  async get(candidate, _options) {
3990
4117
  const locator = candidate.locator;
3991
- try {
3992
- const content = await readFile2(locator.path, { encoding: "utf8" });
3993
- return {
3994
- name: candidate.name,
3995
- description: candidate.description,
3996
- invocation: candidate.invocation,
3997
- source: candidate.source,
3998
- provider: this.name,
3999
- content,
4000
- path: locator.path,
4001
- /* v8 ignore next -- every candidate from collectAgentsMd carries a resourceBase */
4002
- ...candidate.resourceBase !== void 0 ? { resourceBase: candidate.resourceBase } : {}
4003
- };
4004
- } catch {
4005
- return void 0;
4006
- }
4118
+ const content = await readSources(locator.paths);
4119
+ if (content === void 0) return void 0;
4120
+ const first = locator.paths[0];
4121
+ return {
4122
+ name: candidate.name,
4123
+ description: candidate.description,
4124
+ invocation: candidate.invocation,
4125
+ source: candidate.source,
4126
+ provider: this.name,
4127
+ content,
4128
+ path: first,
4129
+ resourceBase: { kind: "file", path: first }
4130
+ };
4007
4131
  }
4008
- /** Read one AGENTS.md file and push a candidate when it exists. */
4009
- async collectAgentsMd(path, rank, candidates) {
4010
- try {
4011
- const content = await readFile2(path, { encoding: "utf8" });
4012
- if (content.trim().length === 0) return;
4013
- candidates.push({
4014
- name: "agents-md",
4015
- description: "Codex project/user instructions (AGENTS.md)",
4016
- invocation: { modelInvocable: true, userInvocable: true },
4017
- source: "custom",
4018
- provider: this.name,
4019
- rank,
4020
- locator: { kind: "agents-md", path },
4021
- path,
4022
- resourceBase: { kind: "file", path }
4023
- });
4024
- } catch {
4025
- }
4132
+ /** One merged `agents-md` candidate for a ranked file set. */
4133
+ agentsCandidate(paths, rank) {
4134
+ const first = paths[0];
4135
+ return {
4136
+ name: "agents-md",
4137
+ description: "Codex project/user instructions (AGENTS.md)",
4138
+ invocation: { modelInvocable: true, userInvocable: true },
4139
+ source: "custom",
4140
+ provider: this.name,
4141
+ rank,
4142
+ locator: { kind: "agents-md", paths },
4143
+ path: first,
4144
+ resourceBase: { kind: "file", path: first }
4145
+ };
4026
4146
  }
4027
4147
  };
4028
4148
 
4029
4149
  // src/engine-pi/skills.ts
4030
- import { readFile as readFile3 } from "node:fs/promises";
4031
- import { homedir as homedir3 } from "node:os";
4032
- import { join as join5, resolve as resolve3 } from "node:path";
4150
+ import { readdir as readdir2, readFile as readFile3, stat as stat3 } from "node:fs/promises";
4151
+ import { homedir as homedir4 } from "node:os";
4152
+ import { dirname as dirname3, join as join7, resolve as resolve3 } from "node:path";
4033
4153
  var PROVIDER_NAME3 = "pi";
4034
- var PI_PROJECT_RANK = 140;
4035
- var PI_USER_RANK = 160;
4154
+ var PI_AGENTS_PROJECT_RANK = 140;
4155
+ var PI_SKILL_PROJECT_RANK = 150;
4156
+ var PI_AGENTS_USER_RANK = 160;
4157
+ var PI_SKILL_USER_RANK = 170;
4158
+ var PI_CONTEXT_POLICY = {
4159
+ override: "AGENTS.override.md",
4160
+ primary: ["AGENTS.md", "CLAUDE.md"]
4161
+ };
4162
+ function piAgentDir() {
4163
+ const override = process.env.PI_CODING_AGENT_DIR;
4164
+ if (override !== void 0 && override.length > 0) return resolve3(override);
4165
+ return join7(homedir4(), ".pi", "agent");
4166
+ }
4036
4167
  var PiSkillProvider = class {
4037
4168
  constructor(control) {
4038
4169
  this.control = control;
@@ -4043,49 +4174,113 @@ var PiSkillProvider = class {
4043
4174
  const candidates = [];
4044
4175
  const cwd = options.cwd;
4045
4176
  if (cwd !== void 0) {
4046
- const projectRoot = await findProjectRoot(resolve3(cwd));
4047
- await this.collectAgentsMd(join5(projectRoot, "AGENTS.md"), PI_PROJECT_RANK, candidates);
4177
+ const projectDirs = await projectAncestors(cwd);
4178
+ const contextPaths = await collectProjectContextFiles(cwd, PI_CONTEXT_POLICY);
4179
+ if (await anySourceNonEmpty(contextPaths)) candidates.push(this.agentsCandidate(contextPaths, PI_AGENTS_PROJECT_RANK));
4180
+ for (const dir of projectDirs) {
4181
+ await this.collectSkillsDir(join7(dir, ".pi", "skills"), PI_SKILL_PROJECT_RANK, candidates);
4182
+ }
4048
4183
  }
4049
- await this.collectAgentsMd(join5(homedir3(), ".pi", "AGENTS.md"), PI_USER_RANK, candidates);
4184
+ const userAgentDir = piAgentDir();
4185
+ const userContext = join7(userAgentDir, "AGENTS.md");
4186
+ if (await fileNonEmpty(userContext)) candidates.push(this.agentsCandidate([userContext], PI_AGENTS_USER_RANK));
4187
+ await this.collectSkillsDir(join7(userAgentDir, "skills"), PI_SKILL_USER_RANK, candidates);
4050
4188
  if (this.control.signal.aborted) return [];
4051
4189
  return candidates;
4052
4190
  }
4053
4191
  async get(candidate, _options) {
4054
4192
  const locator = candidate.locator;
4055
- try {
4056
- const content = await readFile3(locator.path, { encoding: "utf8" });
4193
+ if (locator.kind === "skill-file") {
4194
+ const parsed = await this.tryParse(locator.path);
4195
+ if (parsed === void 0) return void 0;
4057
4196
  return {
4058
- name: candidate.name,
4059
- description: candidate.description,
4060
- invocation: candidate.invocation,
4197
+ name: parsed.name,
4198
+ description: parsed.description,
4199
+ ...parsed.whenToUse === void 0 ? {} : { whenToUse: parsed.whenToUse },
4200
+ invocation: parsed.invocation,
4061
4201
  source: candidate.source,
4062
4202
  provider: this.name,
4063
- content,
4203
+ content: parsed.content,
4064
4204
  path: locator.path,
4065
- /* v8 ignore next -- every candidate from collectAgentsMd carries a resourceBase */
4066
- ...candidate.resourceBase !== void 0 ? { resourceBase: candidate.resourceBase } : {}
4205
+ resourceBase: { kind: "directory", path: dirname3(locator.path) }
4067
4206
  };
4207
+ }
4208
+ const content = await readSources(locator.paths);
4209
+ if (content === void 0) return void 0;
4210
+ const first = locator.paths[0];
4211
+ return {
4212
+ name: candidate.name,
4213
+ description: candidate.description,
4214
+ invocation: candidate.invocation,
4215
+ source: candidate.source,
4216
+ provider: this.name,
4217
+ content,
4218
+ path: first,
4219
+ resourceBase: { kind: "file", path: first }
4220
+ };
4221
+ }
4222
+ /** One merged `agents-md` candidate for a ranked file set. */
4223
+ agentsCandidate(paths, rank) {
4224
+ const first = paths[0];
4225
+ return {
4226
+ name: "agents-md",
4227
+ description: "Pi project/user instructions (AGENTS.md / CLAUDE.md)",
4228
+ invocation: { modelInvocable: true, userInvocable: true },
4229
+ source: "custom",
4230
+ provider: this.name,
4231
+ rank,
4232
+ locator: { kind: "agents-md", paths },
4233
+ path: first,
4234
+ resourceBase: { kind: "file", path: first }
4235
+ };
4236
+ }
4237
+ /** Collect every skill in one skills directory, both pi layouts. */
4238
+ async collectSkillsDir(skillsDir, rank, candidates) {
4239
+ let entries;
4240
+ try {
4241
+ entries = await readdir2(skillsDir, { withFileTypes: true, encoding: "utf8" });
4068
4242
  } catch {
4069
- return void 0;
4243
+ return;
4244
+ }
4245
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
4246
+ const entryPath = join7(skillsDir, entry.name);
4247
+ const info = await stat3(entryPath).catch(() => void 0);
4248
+ if (info === void 0) continue;
4249
+ if (info.isDirectory()) {
4250
+ const path = join7(entryPath, "SKILL.md");
4251
+ const parsed2 = await this.tryParse(path);
4252
+ if (parsed2 === void 0) continue;
4253
+ candidates.push(this.skillCandidate(parsed2, path, rank, entryPath));
4254
+ continue;
4255
+ }
4256
+ if (!entry.name.endsWith(".md")) continue;
4257
+ const parsed = await this.tryParse(entryPath);
4258
+ if (parsed === void 0) continue;
4259
+ candidates.push(this.skillCandidate(parsed, entryPath, rank, skillsDir));
4070
4260
  }
4071
4261
  }
4072
- /** Read one AGENTS.md file and push a candidate when it exists. */
4073
- async collectAgentsMd(path, rank, candidates) {
4262
+ /** One parsed skill as a ranked candidate. */
4263
+ skillCandidate(skill, path, rank, resourceDir) {
4264
+ return {
4265
+ name: skill.name,
4266
+ description: skill.description,
4267
+ ...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
4268
+ invocation: skill.invocation,
4269
+ source: "custom",
4270
+ provider: this.name,
4271
+ rank,
4272
+ locator: { kind: "skill-file", path },
4273
+ path,
4274
+ resourceBase: { kind: "directory", path: resourceDir }
4275
+ };
4276
+ }
4277
+ /** Parse one SKILL.md file, or `undefined` when it is unreadable or invalid. */
4278
+ async tryParse(path) {
4074
4279
  try {
4075
- const content = await readFile3(path, { encoding: "utf8" });
4076
- if (content.trim().length === 0) return;
4077
- candidates.push({
4078
- name: "agents-md",
4079
- description: "Pi project/user instructions (AGENTS.md)",
4080
- invocation: { modelInvocable: true, userInvocable: true },
4081
- source: "custom",
4082
- provider: this.name,
4083
- rank,
4084
- locator: { kind: "agents-md", path },
4085
- path,
4086
- resourceBase: { kind: "file", path }
4087
- });
4280
+ const raw = await readFile3(path, { encoding: "utf8" });
4281
+ return parseSkillFile(raw);
4088
4282
  } catch {
4283
+ return void 0;
4089
4284
  }
4090
4285
  }
4091
4286
  };
@@ -4111,7 +4306,7 @@ var Config4 = z5.object({
4111
4306
  });
4112
4307
  function resolvePatchPath(config) {
4113
4308
  if (config.patchPath !== void 0 && config.patchPath !== "") return config.patchPath;
4114
- return join6(
4309
+ return join8(
4115
4310
  resolveDshHome(),
4116
4311
  "profiles",
4117
4312
  config.profile ?? "web",
@@ -4130,13 +4325,13 @@ async function readPatchOrUndefined(path) {
4130
4325
  }
4131
4326
  }
4132
4327
  async function writePatchFile(path, text) {
4133
- await mkdir(dirname3(path), { recursive: true });
4328
+ await mkdir(dirname4(path), { recursive: true });
4134
4329
  const tmp = `${path}.tmp-${randomUUID()}`;
4135
4330
  await writeFile(tmp, text, "utf8");
4136
4331
  await rename(tmp, path);
4137
4332
  }
4138
4333
  function writePatchFileSync(path, text) {
4139
- mkdirSync(dirname3(path), { recursive: true });
4334
+ mkdirSync(dirname4(path), { recursive: true });
4140
4335
  const tmp = `${path}.tmp-${randomUUID()}`;
4141
4336
  writeFileSync(tmp, text, "utf8");
4142
4337
  renameSync(tmp, path);
@@ -4150,7 +4345,7 @@ async function syncManagedBlock(path, engine) {
4150
4345
  }
4151
4346
  function readPatchFileSync(path) {
4152
4347
  try {
4153
- return readFileSync2(path, "utf8");
4348
+ return readFileSync3(path, "utf8");
4154
4349
  } catch (error) {
4155
4350
  if (isMissing(error)) return "";
4156
4351
  throw error;
@@ -4231,8 +4426,12 @@ function apply(ctx, config) {
4231
4426
  const commands = ctx.get("commands");
4232
4427
  if (commands !== void 0) {
4233
4428
  const disposers = [];
4234
- for (const cmd of CLAUDE_CODE_COMMANDS) {
4235
- disposers.push(commands.register(cmd));
4429
+ for (const command of [...CLAUDE_CODE_COMMANDS, ...discoverUserSlashCommands()]) {
4430
+ try {
4431
+ disposers.push(commands.register(command));
4432
+ } catch (error) {
4433
+ ctx.logger.warn(`loop-engine: skip claude-code command /${command.name}: ${String(error)}`);
4434
+ }
4236
4435
  }
4237
4436
  commandDisposers = disposers;
4238
4437
  }
@@ -4276,7 +4475,7 @@ function apply(ctx, config) {
4276
4475
  mountEngine(fileEngine);
4277
4476
  ctx.effect(() => () => CLEAR_RETRY(), "loop-engine: mount retry cleanup");
4278
4477
  let source;
4279
- installSettingsSection(ctx, loopEngineSettingsNamespace(), LOOP_ENGINE_SETTINGS_SCHEMA, { engine: fileEngine }, {
4478
+ installSettingsSection(ctx, loopEngineSettingsNamespace(), LOOP_ENGINE_SETTINGS_SCHEMA, { engine: fileEngine, showInComposer: true }, {
4280
4479
  setSource: (current) => {
4281
4480
  source = current;
4282
4481
  },
package/lib/invariant.js CHANGED
@@ -3,7 +3,8 @@ import z from "@deepseek-ai/schemastery";
3
3
  import { settingsNamespace } from "@deepseek-ai/dsh-settings";
4
4
  var LOOP_ENGINE_IDS = ["in-process", "claude-code", "codex", "pi"];
5
5
  var LOOP_ENGINE_SETTINGS_SCHEMA = z.object({
6
- engine: z.union([z.const("in-process"), z.const("claude-code"), z.const("codex"), z.const("pi")]).default("in-process")
6
+ engine: z.union([z.const("in-process"), z.const("claude-code"), z.const("codex"), z.const("pi")]).default("in-process"),
7
+ showInComposer: z.boolean().default(true)
7
8
  });
8
9
 
9
10
  // src/patch-manager.ts
@@ -44,19 +45,34 @@ function ensureTrailingNewline(text) {
44
45
  return text.endsWith("\n") ? text : `${text}
45
46
  `;
46
47
  }
48
+ function hasRootEntry(text) {
49
+ return /^(?:- |\[)/m.test(text);
50
+ }
51
+ function dropSeedPlaceholder(text) {
52
+ return text.replace(/^\[\]\n/m, "");
53
+ }
54
+ function seedEmptyArray(text) {
55
+ const head = text.replace(/\n+$/, "");
56
+ return head === "" ? "[]\n" : `${head}
57
+ []
58
+ `;
59
+ }
47
60
  function applyManagedBlock(text, engine) {
48
61
  const block = renderManagedBlock(engine);
49
62
  const span = managedSpan(text);
63
+ let result;
50
64
  if (!span.present) {
51
65
  if (block === "") return text;
52
66
  const base = ensureTrailingNewline(text);
53
- return `${base}
67
+ result = `${base}
54
68
  ${block}`;
69
+ } else if (block === "") {
70
+ result = span.tail.startsWith("\n") ? `${span.head}${span.tail.slice(1)}` : `${span.head}${span.tail}`;
71
+ } else {
72
+ result = `${span.head}${span.blankBefore ? "\n" : ""}${block}${span.tail}`;
55
73
  }
56
- if (block === "") {
57
- return span.tail.startsWith("\n") ? `${span.head}${span.tail.slice(1)}` : `${span.head}${span.tail}`;
58
- }
59
- return `${span.head}${span.blankBefore ? "\n" : ""}${block}${span.tail}`;
74
+ if (block !== "") return dropSeedPlaceholder(result);
75
+ return hasRootEntry(result) ? result : seedEmptyArray(result);
60
76
  }
61
77
 
62
78
  // src/invariant.ts