devlyn-cli 4.2.5 → 4.2.7
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 +3 -1
- package/bin/devlyn.js +63 -25
- package/bin/instructions.js +23 -9
- package/bin/skill-history.json +568 -30
- package/bin/skill-ownership.js +22 -10
- package/config/skills/_shared/task-complete.py +144 -12
- package/config/skills/_shared/task-completion.md +5 -5
- package/config/skills/devlyn-ideate/SKILL.md +2 -2
- package/config/skills/devlyn-ideate/references/loop.md +12 -8
- package/config/skills/devlyn-ideate/scripts/queue.py +300 -100
- package/node_modules/marked/LICENSE.md +44 -0
- package/node_modules/marked/README.md +106 -0
- package/node_modules/marked/bin/main.js +282 -0
- package/node_modules/marked/bin/marked.js +15 -0
- package/node_modules/marked/lib/marked.cjs +2211 -0
- package/node_modules/marked/lib/marked.cjs.map +7 -0
- package/node_modules/marked/lib/marked.d.cts +728 -0
- package/node_modules/marked/lib/marked.d.ts +728 -0
- package/node_modules/marked/lib/marked.esm.js +2189 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +2213 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +111 -0
- package/node_modules/marked/man/marked.1.md +92 -0
- package/node_modules/marked/marked.min.js +69 -0
- package/node_modules/marked/package.json +111 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -215,7 +215,9 @@ Selected during install. Run `npx devlyn-cli` again to add more.
|
|
|
215
215
|
|
|
216
216
|
---
|
|
217
217
|
|
|
218
|
-
Installer updates preserve user work: before writing any selected target, devlyn refuses to replace or remove
|
|
218
|
+
Installer updates preserve user work: before writing any selected target, devlyn refuses to replace or remove edited skills, directories with extra files (including hidden files), links, or unknown copies. `-y` does not bypass this check. Unedited copies from every published version upgrade automatically, including committed copies, historical install-path stamps and generated Python bytecode caches. Release packaging derives the offline skill history from Git with `node scripts/update-skill-history.js`. Move conflicting folders to a backup and migrate them manually; also remove conflicting tracked paths from the Git index before rerunning.
|
|
219
|
+
|
|
220
|
+
Only `__pycache__` directories containing exclusively regular `*.pyc` files count as generated Python caches. Other entries make the directory ordinary content; links and special files are refused. Standalone `*.pyc` files count as content: none of the 113 published versions through 4.2.5 shipped them. Historical path normalization applies only to the escaped absolute assignment defaults written into Markdown by the 2.8.0–4.0.0 installers.
|
|
219
221
|
|
|
220
222
|
## Requirements
|
|
221
223
|
|
package/bin/devlyn.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const crypto = require('crypto');
|
|
4
3
|
const fs = require('fs');
|
|
5
4
|
const path = require('path');
|
|
6
5
|
const os = require('os');
|
|
@@ -10,7 +9,7 @@ const CONFIG_SOURCE = path.join(__dirname, '..', 'config');
|
|
|
10
9
|
const OPTIONAL_SKILLS_SOURCE = path.join(__dirname, '..', 'optional-skills');
|
|
11
10
|
const PKG = require('../package.json');
|
|
12
11
|
const { updateInstructions, InstructionError, holdsDevlynDefaults, importsAgentsMd } = require('./instructions');
|
|
13
|
-
const { contentHash, fingerprint,
|
|
12
|
+
const { contentHash, fingerprint, isIgnoredEntry, trackedPaths } = require('./skill-ownership');
|
|
14
13
|
const SKILL_HISTORY = require('./skill-history.json');
|
|
15
14
|
|
|
16
15
|
// The devlyn skill bundle installed into every skill-capable agent's loader
|
|
@@ -83,16 +82,15 @@ const RETIRED_SKILL_MD_SHA256 = {
|
|
|
83
82
|
]),
|
|
84
83
|
};
|
|
85
84
|
|
|
86
|
-
// Whether `dir` is a real folder whose only file, besides
|
|
85
|
+
// Whether `dir` is a real folder whose only file, besides generated metadata, is a SKILL.md
|
|
87
86
|
// with an LF-normalized SHA-256 in `hashes`: an unedited copy devlyn-cli shipped, not something
|
|
88
87
|
// the user added to or wrote.
|
|
89
88
|
function isShippedCopy(dir, hashes) {
|
|
90
89
|
const skill = path.join(dir, 'SKILL.md');
|
|
91
90
|
return fs.lstatSync(dir, { throwIfNoEntry: false })?.isDirectory() === true
|
|
92
|
-
&& fs.readdirSync(dir).filter((name) => !
|
|
91
|
+
&& fs.readdirSync(dir).filter((name) => !isIgnoredEntry(path.join(dir, name))).length === 1
|
|
93
92
|
&& fs.lstatSync(skill, { throwIfNoEntry: false })?.isFile() === true
|
|
94
|
-
&& hashes.has(
|
|
95
|
-
.update(fs.readFileSync(skill, 'utf8').replace(/\r\n/g, '\n')).digest('hex'));
|
|
93
|
+
&& hashes.has(contentHash(skill));
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
// Skill roots of each target. In a project, `.agents/skills` serves Codex, omp, Pi and Grok,
|
|
@@ -639,13 +637,18 @@ function installOptionalSkillInto(target, skillName) {
|
|
|
639
637
|
}
|
|
640
638
|
}
|
|
641
639
|
|
|
642
|
-
// A 0.6.0-0.7.1 copy is one SKILL.md, the file that identifies it.
|
|
643
|
-
//
|
|
640
|
+
// A 0.6.0-0.7.1 copy is one SKILL.md, the file that identifies it. A failed swap leaves
|
|
641
|
+
// that copy for the retry; a successful refresh discards its generated metadata too.
|
|
644
642
|
function refreshPreStandardCopy(target, skillName) {
|
|
645
|
-
const
|
|
643
|
+
const dir = path.join(target, skillName);
|
|
644
|
+
const skill = path.join(dir, 'SKILL.md');
|
|
646
645
|
const staged = `${skill}.${process.pid}.tmp`;
|
|
647
646
|
try {
|
|
648
647
|
fs.copyFileSync(path.join(OPTIONAL_SKILLS_SOURCE, skillName, 'SKILL.md'), staged);
|
|
648
|
+
for (const name of fs.readdirSync(dir)) {
|
|
649
|
+
const file = path.join(dir, name);
|
|
650
|
+
if (isIgnoredEntry(file)) fs.rmSync(file, { recursive: true });
|
|
651
|
+
}
|
|
649
652
|
fs.renameSync(staged, skill);
|
|
650
653
|
} finally {
|
|
651
654
|
fs.rmSync(staged, { force: true });
|
|
@@ -711,21 +714,55 @@ function installCoreSkills(skillsDir) {
|
|
|
711
714
|
writeInstallMarker(skillsDir, [...DEVLYN_CORE_SKILLS, ...refreshed], previous);
|
|
712
715
|
}
|
|
713
716
|
|
|
714
|
-
|
|
715
|
-
|
|
717
|
+
function readProjectGitignore() {
|
|
718
|
+
const gitignorePath = path.join(projectDir(), '.gitignore');
|
|
719
|
+
const stat = fs.lstatSync(gitignorePath, { throwIfNoEntry: false });
|
|
720
|
+
// Refuse symlinks and hardlinks even when no rules need changing, before any installation write.
|
|
721
|
+
if (stat && (!stat.isFile() || stat.nlink !== 1)) {
|
|
722
|
+
throw new InstructionError(`Cannot update ${gitignorePath}: not a regular file with a single link (links are refused).`);
|
|
723
|
+
}
|
|
724
|
+
return stat ? fs.readFileSync(gitignorePath) : Buffer.alloc(0);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Ignore runtime state; let the project's install marker travel with its skills.
|
|
728
|
+
function updateProjectGitignore(markerEntry) {
|
|
716
729
|
const gitignorePath = path.join(projectDir(), '.gitignore');
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
730
|
+
const original = readProjectGitignore();
|
|
731
|
+
const marker = Buffer.from(markerEntry);
|
|
732
|
+
const managedHeader = Buffer.from('# devlyn-cli pipeline state');
|
|
733
|
+
const runtimeRules = [Buffer.from('.devlyn/'), Buffer.from('.devlyn')];
|
|
734
|
+
const lines = [];
|
|
735
|
+
for (let start = 0; start < original.length;) {
|
|
736
|
+
const lf = original.indexOf(0x0a, start);
|
|
737
|
+
const end = lf === -1 ? original.length : lf + 1;
|
|
738
|
+
const patternEnd = lf === -1 ? end : original[lf - 1] === 0x0d ? lf - 1 : lf;
|
|
739
|
+
lines.push({ raw: original.subarray(start, end), pattern: original.subarray(start, patternEnd) });
|
|
740
|
+
start = end;
|
|
741
|
+
}
|
|
742
|
+
const kept = lines.filter(({ pattern }, index) => {
|
|
743
|
+
if (pattern.equals(marker)) return false;
|
|
744
|
+
if (!pattern.equals(managedHeader)) return true;
|
|
745
|
+
// A header owns contiguous patterns until a blank/comment; remove it only when marker removal empties that block.
|
|
746
|
+
const entries = [];
|
|
747
|
+
for (let next = index + 1; next < lines.length; next++) {
|
|
748
|
+
const entry = lines[next].pattern;
|
|
749
|
+
if (!entry.length || entry[0] === 0x23) break;
|
|
750
|
+
entries.push(entry);
|
|
751
|
+
}
|
|
752
|
+
return !entries.some((entry) => entry.equals(marker)) || entries.some((entry) => !entry.equals(marker));
|
|
753
|
+
});
|
|
754
|
+
let gitignoreContent = Buffer.concat(kept.map((line) => line.raw));
|
|
755
|
+
if (!kept.some(({ pattern }) => runtimeRules.some((rule) => pattern.equals(rule)))) {
|
|
756
|
+
const lf = original.indexOf(0x0a);
|
|
757
|
+
const eol = lf > 0 && original[lf - 1] === 0x0d ? '\r\n' : '\n';
|
|
758
|
+
const prefix = gitignoreContent.length && gitignoreContent.at(-1) !== 0x0a ? eol : '';
|
|
759
|
+
const header = kept.some(({ pattern }) => pattern.equals(managedHeader))
|
|
760
|
+
? '' : `${gitignoreContent.length ? eol : ''}${managedHeader}${eol}`;
|
|
761
|
+
gitignoreContent = Buffer.concat([gitignoreContent, Buffer.from(`${prefix}${header}.devlyn/${eol}`)]);
|
|
762
|
+
}
|
|
763
|
+
if (!gitignoreContent.equals(original)) {
|
|
764
|
+
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
765
|
+
log(' → .gitignore (updated project install rules)', 'dim');
|
|
729
766
|
}
|
|
730
767
|
}
|
|
731
768
|
|
|
@@ -756,7 +793,7 @@ function installAgentsProject(withClaude) {
|
|
|
756
793
|
+ '(npx devlyn-cli -y --claude) so the devlyn block is written once, into CLAUDE.md.');
|
|
757
794
|
}
|
|
758
795
|
installCoreSkills(skillRoots('agents', false)[0]);
|
|
759
|
-
|
|
796
|
+
updateProjectGitignore('.agents/skills/.devlyn-install.json');
|
|
760
797
|
}
|
|
761
798
|
|
|
762
799
|
// Whether Claude Code reads the devlyn defaults of AGENTS.md through an `@AGENTS.md` import in this CLAUDE.md text.
|
|
@@ -817,7 +854,7 @@ function installClaudeCore() {
|
|
|
817
854
|
}
|
|
818
855
|
}
|
|
819
856
|
installCoreSkills(skillsDir);
|
|
820
|
-
|
|
857
|
+
updateProjectGitignore('.claude/skills/.devlyn-install.json');
|
|
821
858
|
|
|
822
859
|
// One-hour foreground Bash calls and one-hour prompt caching serve a drain host that waits in the foreground.
|
|
823
860
|
let settingsChanged = false;
|
|
@@ -871,6 +908,7 @@ function install(targets, global, optionalNames = []) {
|
|
|
871
908
|
+ 'would apply to every project. Run from a project folder, or use --global for skills only.');
|
|
872
909
|
}
|
|
873
910
|
const roots = targets.flatMap((target) => skillRoots(target, global));
|
|
911
|
+
if (!global) readProjectGitignore();
|
|
874
912
|
preflightSkills(roots, optionalNames,
|
|
875
913
|
!global && targets.includes('claude') ? path.dirname(skillRoots('claude', false)[0]) : null);
|
|
876
914
|
// AGENTS.md first, whatever order the menu gave: a new CLAUDE.md holds the block only when AGENTS.md does not.
|
package/bin/instructions.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const { Lexer } = require('marked');
|
|
4
5
|
const legacyTemplates = require('./instruction-templates.json');
|
|
5
6
|
|
|
6
7
|
const BEGIN = '<!-- devlyn:instructions:begin';
|
|
@@ -119,16 +120,29 @@ function instructionParagraphs(text) {
|
|
|
119
120
|
return paragraphs;
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
//
|
|
123
|
+
// Recognize direct adjacent imports in Markdown text, including inline imports.
|
|
124
|
+
// Leaf tokens keep code, escapes and ordinary HTML from removing our only block.
|
|
123
125
|
function importsAgentsMd(text) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
// Match the complete native path token before dropping its optional fragment.
|
|
127
|
+
const matchesImport = (value) => [...value.matchAll(/(?:^|\s)@((?:[^\s\\]|\\ )+)/g)].some((match) => {
|
|
128
|
+
const file = match[1].split('#', 1)[0];
|
|
129
|
+
return file === 'AGENTS.md' || file === './AGENTS.md';
|
|
130
|
+
});
|
|
131
|
+
const containsImport = (tokens) => tokens.some((token) => {
|
|
132
|
+
if (['code', 'codespan'].includes(token.type)) return false;
|
|
133
|
+
if (token.type === 'html') {
|
|
134
|
+
const raw = token.raw || '';
|
|
135
|
+
// Native Claude scans text left after complete comments in a comment-shaped HTML token.
|
|
136
|
+
return raw.trimStart().startsWith('<!--') && raw.includes('-->')
|
|
137
|
+
&& matchesImport(raw.replace(/<!--[\s\S]*?-->/g, ''));
|
|
138
|
+
}
|
|
139
|
+
if (token.tokens) return containsImport(token.tokens);
|
|
140
|
+
if (token.items) return containsImport(token.items);
|
|
141
|
+
return token.type === 'text' && matchesImport(token.text);
|
|
142
|
+
});
|
|
143
|
+
// Claude strips this bounded frontmatter prefix before expanding imports.
|
|
144
|
+
const body = text.replace(/^\uFEFF/, '').replace(/^---\s*\n([\s\S]*?)---\s*\n?/, '');
|
|
145
|
+
return containsImport(new Lexer({ gfm: false }).lex(body));
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
function customInstructions(text, name, template, managed = false) {
|