dsh-loop-engine 1.0.0-rc2 → 1.0.0-rc4

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);
@@ -3710,48 +3711,89 @@ ${block}`;
3710
3711
  }
3711
3712
 
3712
3713
  // src/commands.ts
3714
+ import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
3715
+ import { homedir } from "node:os";
3716
+ import { join as join3 } from "node:path";
3717
+ import { createUserMessage as createUserMessage4 } from "@deepseek-ai/dsh-llm";
3718
+ var COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
3719
+ function forwardClaudeCodeCommand(name2) {
3720
+ return (invocation) => {
3721
+ invocation.agent.followup(createUserMessage4({
3722
+ content: [{ type: "text", text: `/${name2}${invocation.rawInput}` }],
3723
+ source: { kind: "user" }
3724
+ }));
3725
+ return { kind: "success" };
3726
+ };
3727
+ }
3728
+ function builtin(name2, description) {
3729
+ return { name: name2, description, handler: forwardClaudeCodeCommand(name2) };
3730
+ }
3713
3731
  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
- }
3732
+ builtin("help", "Show help about Claude Code commands"),
3733
+ builtin("compact", "Compact the conversation to reduce context usage"),
3734
+ builtin("clear", "Clear the conversation and start fresh"),
3735
+ builtin("review", "Review recent changes (git diff)"),
3736
+ builtin("explain", "Explain the selected code"),
3737
+ builtin("fix", "Fix issues in the code"),
3738
+ builtin("tests", "Add tests for the selected code")
3749
3739
  ];
3740
+ function discoverUserSlashCommands() {
3741
+ let entries;
3742
+ try {
3743
+ entries = readdirSync(userCommandsDir(), { encoding: "utf8" });
3744
+ } catch {
3745
+ return [];
3746
+ }
3747
+ const definitions = [];
3748
+ const seen = new Set(CLAUDE_CODE_COMMANDS.map((command) => command.name));
3749
+ for (const entry of entries.sort()) {
3750
+ if (!entry.endsWith(".md")) continue;
3751
+ const name2 = entry.slice(0, -".md".length);
3752
+ if (!COMMAND_NAME.test(name2) || seen.has(name2)) continue;
3753
+ const path = join3(userCommandsDir(), entry);
3754
+ let raw;
3755
+ try {
3756
+ raw = readFileSync2(path, "utf8");
3757
+ } catch {
3758
+ continue;
3759
+ }
3760
+ const description = commandDescription(raw);
3761
+ if (description === void 0) continue;
3762
+ seen.add(name2);
3763
+ definitions.push({ name: name2, description, handler: forwardClaudeCodeCommand(name2) });
3764
+ }
3765
+ return definitions;
3766
+ }
3767
+ function userCommandsDir() {
3768
+ return join3(homedir(), ".claude", "commands");
3769
+ }
3770
+ function commandDescription(raw) {
3771
+ const trimmed = raw.trim();
3772
+ if (trimmed.length === 0) return void 0;
3773
+ let body = trimmed;
3774
+ if (trimmed.startsWith("---\n")) {
3775
+ const closing = trimmed.indexOf("\n---");
3776
+ if (closing <= 0) return void 0;
3777
+ for (const line of trimmed.slice(4, closing).split("\n")) {
3778
+ const colon = line.indexOf(":");
3779
+ if (colon < 0 || line.slice(0, colon).trim() !== "description") continue;
3780
+ const value = line.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
3781
+ if (value.length > 0) return value;
3782
+ }
3783
+ body = trimmed.slice(closing + 4);
3784
+ }
3785
+ for (const line of body.split("\n")) {
3786
+ const candidate = line.trim();
3787
+ if (candidate.length === 0 || candidate.startsWith("#")) continue;
3788
+ return candidate.length > 120 ? `${candidate.slice(0, 119)}\u2026` : candidate;
3789
+ }
3790
+ return void 0;
3791
+ }
3750
3792
 
3751
3793
  // src/skills.ts
3752
3794
  import { readFile, readdir, stat } from "node:fs/promises";
3753
- import { homedir } from "node:os";
3754
- import { join as join3, resolve } from "node:path";
3795
+ import { homedir as homedir2 } from "node:os";
3796
+ import { join as join4, resolve } from "node:path";
3755
3797
  var SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3756
3798
  var PROVIDER_NAME = "claude-code";
3757
3799
  var CLAUDE_CODE_RANK = 150;
@@ -3861,10 +3903,10 @@ var ClaudeCodeSkillProvider = class {
3861
3903
  const cwd = options.cwd;
3862
3904
  if (cwd !== void 0) {
3863
3905
  const projectRoot = await findProjectRoot(resolve(cwd));
3864
- await collectSkillsDir(join3(projectRoot, ".claude", "skills"), CLAUDE_CODE_RANK, candidates);
3906
+ await collectSkillsDir(join4(projectRoot, ".claude", "skills"), CLAUDE_CODE_RANK, candidates);
3865
3907
  await collectClaudeMd(projectRoot, candidates);
3866
3908
  }
3867
- await collectSkillsDir(join3(homedir(), ".claude", "skills"), CLAUDE_CODE_USER_RANK, candidates);
3909
+ await collectSkillsDir(join4(homedir2(), ".claude", "skills"), CLAUDE_CODE_USER_RANK, candidates);
3868
3910
  if (this.control.signal.aborted) return [];
3869
3911
  return candidates;
3870
3912
  }
@@ -3898,11 +3940,11 @@ async function collectSkillsDir(skillsDir, rank, candidates) {
3898
3940
  return;
3899
3941
  }
3900
3942
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3901
- const entryPath = join3(skillsDir, entry.name);
3943
+ const entryPath = join4(skillsDir, entry.name);
3902
3944
  const info = await stat(entryPath).catch(() => void 0);
3903
3945
  if (info === void 0) continue;
3904
3946
  if (info.isDirectory()) {
3905
- const path = join3(entryPath, "SKILL.md");
3947
+ const path = join4(entryPath, "SKILL.md");
3906
3948
  const skill2 = await tryParseSkill(path);
3907
3949
  if (skill2 === void 0) continue;
3908
3950
  candidates.push(toCandidate(skill2, path, rank, entryPath));
@@ -3915,7 +3957,7 @@ async function collectSkillsDir(skillsDir, rank, candidates) {
3915
3957
  }
3916
3958
  }
3917
3959
  async function collectClaudeMd(projectRoot, candidates) {
3918
- const claudeMd = join3(projectRoot, "CLAUDE.md");
3960
+ const claudeMd = join4(projectRoot, "CLAUDE.md");
3919
3961
  try {
3920
3962
  const info = await stat(claudeMd);
3921
3963
  if (!info.isFile()) return;
@@ -3952,7 +3994,7 @@ async function findProjectRoot(cwd) {
3952
3994
  let current = cwd;
3953
3995
  while (true) {
3954
3996
  try {
3955
- await stat(join3(current, ".git"));
3997
+ await stat(join4(current, ".git"));
3956
3998
  return current;
3957
3999
  } catch {
3958
4000
  }
@@ -3963,12 +4005,81 @@ async function findProjectRoot(cwd) {
3963
4005
  }
3964
4006
 
3965
4007
  // 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";
4008
+ import { homedir as homedir3 } from "node:os";
4009
+ import { join as join6 } from "node:path";
4010
+
4011
+ // src/driver-core/context-files.ts
4012
+ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
4013
+ import { join as join5, resolve as resolve2 } from "node:path";
4014
+ async function projectAncestors(cwd) {
4015
+ const root = await findProjectRoot(resolve2(cwd));
4016
+ const dirs = [];
4017
+ let current = resolve2(cwd);
4018
+ while (true) {
4019
+ dirs.push(current);
4020
+ if (current === root) return dirs;
4021
+ current = resolve2(current, "..");
4022
+ }
4023
+ }
4024
+ async function collectProjectContextFiles(cwd, policy) {
4025
+ const files = [];
4026
+ for (const dir of await projectAncestors(cwd)) {
4027
+ const chosen = await dirContextFile(dir, policy);
4028
+ if (chosen !== void 0) files.push(chosen);
4029
+ }
4030
+ return files;
4031
+ }
4032
+ async function dirContextFile(dir, policy) {
4033
+ if (policy.override !== void 0) {
4034
+ const override = join5(dir, policy.override);
4035
+ if (await pathExists(override)) return override;
4036
+ }
4037
+ for (const name2 of policy.primary) {
4038
+ const candidate = join5(dir, name2);
4039
+ if (await pathExists(candidate)) return candidate;
4040
+ }
4041
+ return void 0;
4042
+ }
4043
+ async function pathExists(path) {
4044
+ try {
4045
+ await stat2(path);
4046
+ return true;
4047
+ } catch {
4048
+ return false;
4049
+ }
4050
+ }
4051
+ async function readOptionalFile(path) {
4052
+ try {
4053
+ return await readFile2(path, { encoding: "utf8" });
4054
+ } catch {
4055
+ return void 0;
4056
+ }
4057
+ }
4058
+ async function anySourceNonEmpty(paths) {
4059
+ for (const path of paths) {
4060
+ const raw = await readOptionalFile(path);
4061
+ if (raw !== void 0 && raw.trim().length > 0) return true;
4062
+ }
4063
+ return false;
4064
+ }
4065
+ async function fileNonEmpty(path) {
4066
+ const raw = await readOptionalFile(path);
4067
+ return raw !== void 0 && raw.trim().length > 0;
4068
+ }
4069
+ async function readSources(paths) {
4070
+ const parts = [];
4071
+ for (const path of paths) {
4072
+ const raw = await readOptionalFile(path);
4073
+ if (raw !== void 0 && raw.trim().length > 0) parts.push(raw);
4074
+ }
4075
+ return parts.length > 0 ? parts.join("\n\n") : void 0;
4076
+ }
4077
+
4078
+ // src/engine-codex/skills.ts
3969
4079
  var PROVIDER_NAME2 = "codex";
3970
4080
  var CODEX_PROJECT_RANK = 140;
3971
4081
  var CODEX_USER_RANK = 160;
4082
+ var CODEX_CONTEXT_POLICY = { primary: ["AGENTS.md"] };
3972
4083
  var CodexSkillProvider = class {
3973
4084
  constructor(control) {
3974
4085
  this.control = control;
@@ -3979,60 +4090,65 @@ var CodexSkillProvider = class {
3979
4090
  const candidates = [];
3980
4091
  const cwd = options.cwd;
3981
4092
  if (cwd !== void 0) {
3982
- const projectRoot = await findProjectRoot(resolve2(cwd));
3983
- await this.collectAgentsMd(join4(projectRoot, "AGENTS.md"), CODEX_PROJECT_RANK, candidates);
4093
+ const paths = await collectProjectContextFiles(cwd, CODEX_CONTEXT_POLICY);
4094
+ if (await anySourceNonEmpty(paths)) candidates.push(this.agentsCandidate(paths, CODEX_PROJECT_RANK));
3984
4095
  }
3985
- await this.collectAgentsMd(join4(homedir2(), ".codex", "AGENTS.md"), CODEX_USER_RANK, candidates);
4096
+ const userPath = join6(homedir3(), ".codex", "AGENTS.md");
4097
+ if (await fileNonEmpty(userPath)) candidates.push(this.agentsCandidate([userPath], CODEX_USER_RANK));
3986
4098
  if (this.control.signal.aborted) return [];
3987
4099
  return candidates;
3988
4100
  }
3989
4101
  async get(candidate, _options) {
3990
4102
  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
- }
4103
+ const content = await readSources(locator.paths);
4104
+ if (content === void 0) return void 0;
4105
+ const first = locator.paths[0];
4106
+ return {
4107
+ name: candidate.name,
4108
+ description: candidate.description,
4109
+ invocation: candidate.invocation,
4110
+ source: candidate.source,
4111
+ provider: this.name,
4112
+ content,
4113
+ path: first,
4114
+ resourceBase: { kind: "file", path: first }
4115
+ };
4007
4116
  }
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
- }
4117
+ /** One merged `agents-md` candidate for a ranked file set. */
4118
+ agentsCandidate(paths, rank) {
4119
+ const first = paths[0];
4120
+ return {
4121
+ name: "agents-md",
4122
+ description: "Codex project/user instructions (AGENTS.md)",
4123
+ invocation: { modelInvocable: true, userInvocable: true },
4124
+ source: "custom",
4125
+ provider: this.name,
4126
+ rank,
4127
+ locator: { kind: "agents-md", paths },
4128
+ path: first,
4129
+ resourceBase: { kind: "file", path: first }
4130
+ };
4026
4131
  }
4027
4132
  };
4028
4133
 
4029
4134
  // 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";
4135
+ import { readdir as readdir2, readFile as readFile3, stat as stat3 } from "node:fs/promises";
4136
+ import { homedir as homedir4 } from "node:os";
4137
+ import { dirname as dirname3, join as join7, resolve as resolve3 } from "node:path";
4033
4138
  var PROVIDER_NAME3 = "pi";
4034
- var PI_PROJECT_RANK = 140;
4035
- var PI_USER_RANK = 160;
4139
+ var PI_AGENTS_PROJECT_RANK = 140;
4140
+ var PI_SKILL_PROJECT_RANK = 150;
4141
+ var PI_AGENTS_USER_RANK = 160;
4142
+ var PI_SKILL_USER_RANK = 170;
4143
+ var PI_CONTEXT_POLICY = {
4144
+ override: "AGENTS.override.md",
4145
+ primary: ["AGENTS.md", "CLAUDE.md"]
4146
+ };
4147
+ function piAgentDir() {
4148
+ const override = process.env.PI_CODING_AGENT_DIR;
4149
+ if (override !== void 0 && override.length > 0) return resolve3(override);
4150
+ return join7(homedir4(), ".pi", "agent");
4151
+ }
4036
4152
  var PiSkillProvider = class {
4037
4153
  constructor(control) {
4038
4154
  this.control = control;
@@ -4043,49 +4159,113 @@ var PiSkillProvider = class {
4043
4159
  const candidates = [];
4044
4160
  const cwd = options.cwd;
4045
4161
  if (cwd !== void 0) {
4046
- const projectRoot = await findProjectRoot(resolve3(cwd));
4047
- await this.collectAgentsMd(join5(projectRoot, "AGENTS.md"), PI_PROJECT_RANK, candidates);
4162
+ const projectDirs = await projectAncestors(cwd);
4163
+ const contextPaths = await collectProjectContextFiles(cwd, PI_CONTEXT_POLICY);
4164
+ if (await anySourceNonEmpty(contextPaths)) candidates.push(this.agentsCandidate(contextPaths, PI_AGENTS_PROJECT_RANK));
4165
+ for (const dir of projectDirs) {
4166
+ await this.collectSkillsDir(join7(dir, ".pi", "skills"), PI_SKILL_PROJECT_RANK, candidates);
4167
+ }
4048
4168
  }
4049
- await this.collectAgentsMd(join5(homedir3(), ".pi", "AGENTS.md"), PI_USER_RANK, candidates);
4169
+ const userAgentDir = piAgentDir();
4170
+ const userContext = join7(userAgentDir, "AGENTS.md");
4171
+ if (await fileNonEmpty(userContext)) candidates.push(this.agentsCandidate([userContext], PI_AGENTS_USER_RANK));
4172
+ await this.collectSkillsDir(join7(userAgentDir, "skills"), PI_SKILL_USER_RANK, candidates);
4050
4173
  if (this.control.signal.aborted) return [];
4051
4174
  return candidates;
4052
4175
  }
4053
4176
  async get(candidate, _options) {
4054
4177
  const locator = candidate.locator;
4055
- try {
4056
- const content = await readFile3(locator.path, { encoding: "utf8" });
4178
+ if (locator.kind === "skill-file") {
4179
+ const parsed = await this.tryParse(locator.path);
4180
+ if (parsed === void 0) return void 0;
4057
4181
  return {
4058
- name: candidate.name,
4059
- description: candidate.description,
4060
- invocation: candidate.invocation,
4182
+ name: parsed.name,
4183
+ description: parsed.description,
4184
+ ...parsed.whenToUse === void 0 ? {} : { whenToUse: parsed.whenToUse },
4185
+ invocation: parsed.invocation,
4061
4186
  source: candidate.source,
4062
4187
  provider: this.name,
4063
- content,
4188
+ content: parsed.content,
4064
4189
  path: locator.path,
4065
- /* v8 ignore next -- every candidate from collectAgentsMd carries a resourceBase */
4066
- ...candidate.resourceBase !== void 0 ? { resourceBase: candidate.resourceBase } : {}
4190
+ resourceBase: { kind: "directory", path: dirname3(locator.path) }
4067
4191
  };
4192
+ }
4193
+ const content = await readSources(locator.paths);
4194
+ if (content === void 0) return void 0;
4195
+ const first = locator.paths[0];
4196
+ return {
4197
+ name: candidate.name,
4198
+ description: candidate.description,
4199
+ invocation: candidate.invocation,
4200
+ source: candidate.source,
4201
+ provider: this.name,
4202
+ content,
4203
+ path: first,
4204
+ resourceBase: { kind: "file", path: first }
4205
+ };
4206
+ }
4207
+ /** One merged `agents-md` candidate for a ranked file set. */
4208
+ agentsCandidate(paths, rank) {
4209
+ const first = paths[0];
4210
+ return {
4211
+ name: "agents-md",
4212
+ description: "Pi project/user instructions (AGENTS.md / CLAUDE.md)",
4213
+ invocation: { modelInvocable: true, userInvocable: true },
4214
+ source: "custom",
4215
+ provider: this.name,
4216
+ rank,
4217
+ locator: { kind: "agents-md", paths },
4218
+ path: first,
4219
+ resourceBase: { kind: "file", path: first }
4220
+ };
4221
+ }
4222
+ /** Collect every skill in one skills directory, both pi layouts. */
4223
+ async collectSkillsDir(skillsDir, rank, candidates) {
4224
+ let entries;
4225
+ try {
4226
+ entries = await readdir2(skillsDir, { withFileTypes: true, encoding: "utf8" });
4068
4227
  } catch {
4069
- return void 0;
4228
+ return;
4229
+ }
4230
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
4231
+ const entryPath = join7(skillsDir, entry.name);
4232
+ const info = await stat3(entryPath).catch(() => void 0);
4233
+ if (info === void 0) continue;
4234
+ if (info.isDirectory()) {
4235
+ const path = join7(entryPath, "SKILL.md");
4236
+ const parsed2 = await this.tryParse(path);
4237
+ if (parsed2 === void 0) continue;
4238
+ candidates.push(this.skillCandidate(parsed2, path, rank, entryPath));
4239
+ continue;
4240
+ }
4241
+ if (!entry.name.endsWith(".md")) continue;
4242
+ const parsed = await this.tryParse(entryPath);
4243
+ if (parsed === void 0) continue;
4244
+ candidates.push(this.skillCandidate(parsed, entryPath, rank, skillsDir));
4070
4245
  }
4071
4246
  }
4072
- /** Read one AGENTS.md file and push a candidate when it exists. */
4073
- async collectAgentsMd(path, rank, candidates) {
4247
+ /** One parsed skill as a ranked candidate. */
4248
+ skillCandidate(skill, path, rank, resourceDir) {
4249
+ return {
4250
+ name: skill.name,
4251
+ description: skill.description,
4252
+ ...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
4253
+ invocation: skill.invocation,
4254
+ source: "custom",
4255
+ provider: this.name,
4256
+ rank,
4257
+ locator: { kind: "skill-file", path },
4258
+ path,
4259
+ resourceBase: { kind: "directory", path: resourceDir }
4260
+ };
4261
+ }
4262
+ /** Parse one SKILL.md file, or `undefined` when it is unreadable or invalid. */
4263
+ async tryParse(path) {
4074
4264
  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
- });
4265
+ const raw = await readFile3(path, { encoding: "utf8" });
4266
+ return parseSkillFile(raw);
4088
4267
  } catch {
4268
+ return void 0;
4089
4269
  }
4090
4270
  }
4091
4271
  };
@@ -4111,7 +4291,7 @@ var Config4 = z5.object({
4111
4291
  });
4112
4292
  function resolvePatchPath(config) {
4113
4293
  if (config.patchPath !== void 0 && config.patchPath !== "") return config.patchPath;
4114
- return join6(
4294
+ return join8(
4115
4295
  resolveDshHome(),
4116
4296
  "profiles",
4117
4297
  config.profile ?? "web",
@@ -4130,13 +4310,13 @@ async function readPatchOrUndefined(path) {
4130
4310
  }
4131
4311
  }
4132
4312
  async function writePatchFile(path, text) {
4133
- await mkdir(dirname3(path), { recursive: true });
4313
+ await mkdir(dirname4(path), { recursive: true });
4134
4314
  const tmp = `${path}.tmp-${randomUUID()}`;
4135
4315
  await writeFile(tmp, text, "utf8");
4136
4316
  await rename(tmp, path);
4137
4317
  }
4138
4318
  function writePatchFileSync(path, text) {
4139
- mkdirSync(dirname3(path), { recursive: true });
4319
+ mkdirSync(dirname4(path), { recursive: true });
4140
4320
  const tmp = `${path}.tmp-${randomUUID()}`;
4141
4321
  writeFileSync(tmp, text, "utf8");
4142
4322
  renameSync(tmp, path);
@@ -4150,7 +4330,7 @@ async function syncManagedBlock(path, engine) {
4150
4330
  }
4151
4331
  function readPatchFileSync(path) {
4152
4332
  try {
4153
- return readFileSync2(path, "utf8");
4333
+ return readFileSync3(path, "utf8");
4154
4334
  } catch (error) {
4155
4335
  if (isMissing(error)) return "";
4156
4336
  throw error;
@@ -4231,8 +4411,12 @@ function apply(ctx, config) {
4231
4411
  const commands = ctx.get("commands");
4232
4412
  if (commands !== void 0) {
4233
4413
  const disposers = [];
4234
- for (const cmd of CLAUDE_CODE_COMMANDS) {
4235
- disposers.push(commands.register(cmd));
4414
+ for (const command of [...CLAUDE_CODE_COMMANDS, ...discoverUserSlashCommands()]) {
4415
+ try {
4416
+ disposers.push(commands.register(command));
4417
+ } catch (error) {
4418
+ ctx.logger.warn(`loop-engine: skip claude-code command /${command.name}: ${String(error)}`);
4419
+ }
4236
4420
  }
4237
4421
  commandDisposers = disposers;
4238
4422
  }
@@ -4276,7 +4460,7 @@ function apply(ctx, config) {
4276
4460
  mountEngine(fileEngine);
4277
4461
  ctx.effect(() => () => CLEAR_RETRY(), "loop-engine: mount retry cleanup");
4278
4462
  let source;
4279
- installSettingsSection(ctx, loopEngineSettingsNamespace(), LOOP_ENGINE_SETTINGS_SCHEMA, { engine: fileEngine }, {
4463
+ installSettingsSection(ctx, loopEngineSettingsNamespace(), LOOP_ENGINE_SETTINGS_SCHEMA, { engine: fileEngine, showInComposer: true }, {
4280
4464
  setSource: (current) => {
4281
4465
  source = current;
4282
4466
  },
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
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Composer loop-engine picker: a compact dropdown registered at the
3
+ * `conversation.input.right` seat, so it sits immediately left of the model
4
+ * select in the composer's tool row. The engine is a deployment-level choice,
5
+ * so this surface shares the same settings-backed {@link LoopEngineStore} as
6
+ * the settings section and the header badge — a change in any one is what the
7
+ * others show next. Switching still asks for confirmation first (it interrupts
8
+ * sessions still running on the previous engine) and reloads the page once the
9
+ * commit lands, matching the settings section's semantics.
10
+ *
11
+ * Styling is token-driven inline styles like the badge and section (the
12
+ * client-module bundle is esbuild-built without a CSS loader).
13
+ * @module dsh-loop-engine/client/composer
14
+ */
15
+ import { type JSX } from 'react';
16
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
17
+ import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots';
18
+ import type { LoopEngineStore, LoopEngineState } from './store.ts';
19
+ import type { en } from './locales.ts';
20
+ /** Injected dependencies of {@link LoopEngineComposerSelect} (slot `inject`). */
21
+ export interface LoopEngineComposerSelectInjected {
22
+ /** The selection store (loaded on mount, refreshed by scope pushes). */
23
+ controller: LoopEngineStore;
24
+ hooks: {
25
+ /** Engine snapshot bound by the UI renderer as useSnapshot. */
26
+ snapshot: SnapshotStore<LoopEngineState>;
27
+ };
28
+ /** Composer copy bound to the loop engine dictionaries. */
29
+ t: (key: keyof typeof en) => string;
30
+ }
31
+ /** Props delivered by the slot outlet (the renderer erases the share boundary). */
32
+ export type LoopEngineComposerSelectProps = Partial<InjectFace<LoopEngineComposerSelectInjected>>;
33
+ /**
34
+ * Render the composer's loop-engine dropdown. Hides until the settings scope
35
+ * settles, so the composer never flashes a provisional engine.
36
+ * @param props - composed slot props.
37
+ * @returns the picker, or null while the engine is unknown.
38
+ */
39
+ export declare function LoopEngineComposerSelect(props: LoopEngineComposerSelectProps): JSX.Element | null;
40
+ //# sourceMappingURL=LoopEngineComposerSelect.d.ts.map
@@ -9,6 +9,7 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
9
9
  import { type LoopEngineKey } from './locales.ts';
10
10
  export type { LoopEngineSectionInjected, LoopEngineSectionProps } from './LoopEngineSection.tsx';
11
11
  export type { LoopEngineBadgeInjected, LoopEngineBadgeProps } from './LoopEngineBadge.tsx';
12
+ export type { LoopEngineComposerSelectInjected, LoopEngineComposerSelectProps } from './LoopEngineComposerSelect.tsx';
12
13
  export type { LoopEngineState } from './store.ts';
13
14
  declare module '@deepseek-ai/dsh-client-ui-slots' {
14
15
  interface LocaleNamespaceMap {
@@ -16,6 +16,8 @@ export interface LoopEngineKey {
16
16
  engineCodex: string;
17
17
  /** Option label: the Pi CLI driver. */
18
18
  enginePi: string;
19
+ /** Settings toggle: show the engine picker in the chat page composer. */
20
+ showInComposerLabel: string;
19
21
  /** Unavailable-state message. */
20
22
  unavailable: string;
21
23
  /** Notice shown when the selection would interrupt running agents. */