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/README.md +25 -103
- package/README.zh.md +9 -51
- package/lib/client.js +212 -3
- package/lib/index.js +329 -130
- package/lib/invariant.js +22 -6
- package/lib/types/client/LoopEngineComposerSelect.d.ts +40 -0
- package/lib/types/client/index.d.ts +1 -0
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/store.d.ts +13 -1
- package/lib/types/commands.d.ts +43 -6
- package/lib/types/driver-core/context-files.d.ts +62 -0
- package/lib/types/engine-codex/skills.d.ts +13 -10
- package/lib/types/engine-pi/skills.d.ts +40 -11
- package/lib/types/patch-manager.d.ts +4 -1
- package/lib/types/settings.d.ts +2 -0
- package/lib/types/skills.d.ts +16 -0
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
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
|
|
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(
|
|
3921
|
+
await collectSkillsDir(join4(projectRoot, ".claude", "skills"), CLAUDE_CODE_RANK, candidates);
|
|
3865
3922
|
await collectClaudeMd(projectRoot, candidates);
|
|
3866
3923
|
}
|
|
3867
|
-
await collectSkillsDir(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 {
|
|
3967
|
-
import {
|
|
3968
|
-
|
|
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
|
|
3983
|
-
await this.
|
|
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
|
-
|
|
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
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
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
|
-
/**
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
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
|
|
4032
|
-
import { join as
|
|
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
|
|
4035
|
-
var
|
|
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
|
|
4047
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
4056
|
-
const
|
|
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:
|
|
4059
|
-
description:
|
|
4060
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
4073
|
-
|
|
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
|
|
4076
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
4235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|