devflow-kit 1.0.0 → 1.2.0
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/CHANGELOG.md +69 -0
- package/README.md +35 -11
- package/dist/cli.js +5 -1
- package/dist/commands/ambient.d.ts +18 -0
- package/dist/commands/ambient.js +136 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +97 -10
- package/dist/commands/memory.d.ts +22 -0
- package/dist/commands/memory.js +175 -0
- package/dist/commands/uninstall.js +72 -5
- package/dist/plugins.js +74 -3
- package/dist/utils/post-install.d.ts +12 -0
- package/dist/utils/post-install.js +82 -1
- package/dist/utils/safe-delete-install.d.ts +7 -0
- package/dist/utils/safe-delete-install.js +40 -5
- package/package.json +2 -1
- package/plugins/devflow-accessibility/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-ambient/.claude-plugin/plugin.json +7 -0
- package/plugins/devflow-ambient/README.md +49 -0
- package/plugins/devflow-ambient/commands/ambient.md +110 -0
- package/plugins/devflow-ambient/skills/ambient-router/SKILL.md +89 -0
- package/plugins/devflow-ambient/skills/ambient-router/references/skill-catalog.md +68 -0
- package/plugins/devflow-audit-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-code-review/.claude-plugin/plugin.json +1 -4
- package/plugins/devflow-code-review/agents/reviewer.md +8 -0
- package/plugins/devflow-code-review/commands/code-review-teams.md +11 -1
- package/plugins/devflow-code-review/commands/code-review.md +12 -2
- package/plugins/devflow-core-skills/.claude-plugin/plugin.json +3 -6
- package/plugins/devflow-core-skills/skills/docs-framework/SKILL.md +10 -6
- package/plugins/devflow-core-skills/skills/test-driven-development/SKILL.md +139 -0
- package/plugins/devflow-core-skills/skills/test-driven-development/references/rationalization-prevention.md +111 -0
- package/plugins/devflow-debug/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-frontend-design/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-go/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-go/skills/go/SKILL.md +187 -0
- package/plugins/devflow-go/skills/go/references/concurrency.md +312 -0
- package/plugins/devflow-go/skills/go/references/detection.md +129 -0
- package/plugins/devflow-go/skills/go/references/patterns.md +232 -0
- package/plugins/devflow-go/skills/go/references/violations.md +205 -0
- package/plugins/devflow-implement/.claude-plugin/plugin.json +1 -3
- package/plugins/devflow-implement/agents/coder.md +11 -6
- package/plugins/devflow-java/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-java/skills/java/SKILL.md +183 -0
- package/plugins/devflow-java/skills/java/references/detection.md +120 -0
- package/plugins/devflow-java/skills/java/references/modern-java.md +270 -0
- package/plugins/devflow-java/skills/java/references/patterns.md +235 -0
- package/plugins/devflow-java/skills/java/references/violations.md +213 -0
- package/plugins/devflow-python/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-python/skills/python/SKILL.md +188 -0
- package/plugins/devflow-python/skills/python/references/async.md +220 -0
- package/plugins/devflow-python/skills/python/references/detection.md +128 -0
- package/plugins/devflow-python/skills/python/references/patterns.md +226 -0
- package/plugins/devflow-python/skills/python/references/violations.md +204 -0
- package/plugins/devflow-react/.claude-plugin/plugin.json +15 -0
- package/plugins/{devflow-core-skills → devflow-react}/skills/react/SKILL.md +1 -1
- package/plugins/{devflow-core-skills → devflow-react}/skills/react/references/patterns.md +3 -3
- package/plugins/devflow-resolve/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-rust/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-rust/skills/rust/SKILL.md +193 -0
- package/plugins/devflow-rust/skills/rust/references/detection.md +131 -0
- package/plugins/devflow-rust/skills/rust/references/ownership.md +242 -0
- package/plugins/devflow-rust/skills/rust/references/patterns.md +210 -0
- package/plugins/devflow-rust/skills/rust/references/violations.md +191 -0
- package/plugins/devflow-self-review/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-specify/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-typescript/.claude-plugin/plugin.json +15 -0
- package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/references/patterns.md +3 -3
- package/scripts/hooks/ambient-prompt.sh +48 -0
- package/scripts/hooks/background-memory-update.sh +49 -8
- package/scripts/hooks/ensure-memory-gitignore.sh +17 -0
- package/scripts/hooks/pre-compact-memory.sh +12 -6
- package/scripts/hooks/session-start-memory.sh +50 -8
- package/scripts/hooks/stop-update-memory.sh +10 -6
- package/shared/agents/coder.md +11 -6
- package/shared/agents/reviewer.md +8 -0
- package/shared/skills/ambient-router/SKILL.md +89 -0
- package/shared/skills/ambient-router/references/skill-catalog.md +68 -0
- package/shared/skills/docs-framework/SKILL.md +10 -6
- package/shared/skills/go/SKILL.md +187 -0
- package/shared/skills/go/references/concurrency.md +312 -0
- package/shared/skills/go/references/detection.md +129 -0
- package/shared/skills/go/references/patterns.md +232 -0
- package/shared/skills/go/references/violations.md +205 -0
- package/shared/skills/java/SKILL.md +183 -0
- package/shared/skills/java/references/detection.md +120 -0
- package/shared/skills/java/references/modern-java.md +270 -0
- package/shared/skills/java/references/patterns.md +235 -0
- package/shared/skills/java/references/violations.md +213 -0
- package/shared/skills/python/SKILL.md +188 -0
- package/shared/skills/python/references/async.md +220 -0
- package/shared/skills/python/references/detection.md +128 -0
- package/shared/skills/python/references/patterns.md +226 -0
- package/shared/skills/python/references/violations.md +204 -0
- package/shared/skills/react/SKILL.md +1 -1
- package/shared/skills/react/references/patterns.md +3 -3
- package/shared/skills/rust/SKILL.md +193 -0
- package/shared/skills/rust/references/detection.md +131 -0
- package/shared/skills/rust/references/ownership.md +242 -0
- package/shared/skills/rust/references/patterns.md +210 -0
- package/shared/skills/rust/references/violations.md +191 -0
- package/shared/skills/test-driven-development/SKILL.md +139 -0
- package/shared/skills/test-driven-development/references/rationalization-prevention.md +111 -0
- package/shared/skills/typescript/references/patterns.md +3 -3
- package/src/templates/managed-settings.json +14 -0
- package/plugins/devflow-code-review/skills/react/SKILL.md +0 -276
- package/plugins/devflow-code-review/skills/react/references/patterns.md +0 -1331
- package/plugins/devflow-core-skills/skills/accessibility/SKILL.md +0 -229
- package/plugins/devflow-core-skills/skills/accessibility/references/detection.md +0 -171
- package/plugins/devflow-core-skills/skills/accessibility/references/patterns.md +0 -670
- package/plugins/devflow-core-skills/skills/accessibility/references/violations.md +0 -419
- package/plugins/devflow-core-skills/skills/frontend-design/SKILL.md +0 -254
- package/plugins/devflow-core-skills/skills/frontend-design/references/detection.md +0 -184
- package/plugins/devflow-core-skills/skills/frontend-design/references/patterns.md +0 -511
- package/plugins/devflow-core-skills/skills/frontend-design/references/violations.md +0 -453
- package/plugins/devflow-core-skills/skills/react/references/violations.md +0 -565
- package/plugins/devflow-implement/skills/accessibility/SKILL.md +0 -229
- package/plugins/devflow-implement/skills/accessibility/references/detection.md +0 -171
- package/plugins/devflow-implement/skills/accessibility/references/patterns.md +0 -670
- package/plugins/devflow-implement/skills/accessibility/references/violations.md +0 -419
- package/plugins/devflow-implement/skills/frontend-design/SKILL.md +0 -254
- package/plugins/devflow-implement/skills/frontend-design/references/detection.md +0 -184
- package/plugins/devflow-implement/skills/frontend-design/references/patterns.md +0 -511
- package/plugins/devflow-implement/skills/frontend-design/references/violations.md +0 -453
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/SKILL.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/detection.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/patterns.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/violations.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/SKILL.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/detection.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/patterns.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/violations.md +0 -0
- /package/plugins/{devflow-code-review → devflow-react}/skills/react/references/violations.md +0 -0
- /package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/SKILL.md +0 -0
- /package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/references/violations.md +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import color from 'picocolors';
|
|
6
|
+
import { getClaudeDirectory, getDevFlowDirectory } from '../utils/paths.js';
|
|
7
|
+
import { createMemoryDir, migrateMemoryFiles } from '../utils/post-install.js';
|
|
8
|
+
/**
|
|
9
|
+
* Map of hook event type → filename marker for the 3 memory hooks.
|
|
10
|
+
*/
|
|
11
|
+
const MEMORY_HOOK_CONFIG = {
|
|
12
|
+
Stop: 'stop-update-memory.sh',
|
|
13
|
+
SessionStart: 'session-start-memory.sh',
|
|
14
|
+
PreCompact: 'pre-compact-memory.sh',
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Add all 3 memory hooks (Stop, SessionStart, PreCompact) to settings JSON.
|
|
18
|
+
* Idempotent — skips hooks that already exist. Returns unchanged JSON if all 3 present.
|
|
19
|
+
*/
|
|
20
|
+
export function addMemoryHooks(settingsJson, devflowDir) {
|
|
21
|
+
const settings = JSON.parse(settingsJson);
|
|
22
|
+
if (hasMemoryHooks(settingsJson)) {
|
|
23
|
+
return settingsJson;
|
|
24
|
+
}
|
|
25
|
+
if (!settings.hooks) {
|
|
26
|
+
settings.hooks = {};
|
|
27
|
+
}
|
|
28
|
+
let changed = false;
|
|
29
|
+
for (const [hookType, marker] of Object.entries(MEMORY_HOOK_CONFIG)) {
|
|
30
|
+
const existing = settings.hooks[hookType] ?? [];
|
|
31
|
+
const alreadyPresent = existing.some((matcher) => matcher.hooks.some((h) => h.command.includes(marker)));
|
|
32
|
+
if (!alreadyPresent) {
|
|
33
|
+
const hookCommand = path.join(devflowDir, 'scripts', 'hooks', marker);
|
|
34
|
+
const newEntry = {
|
|
35
|
+
hooks: [
|
|
36
|
+
{
|
|
37
|
+
type: 'command',
|
|
38
|
+
command: hookCommand,
|
|
39
|
+
timeout: 10,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
if (!settings.hooks[hookType]) {
|
|
44
|
+
settings.hooks[hookType] = [];
|
|
45
|
+
}
|
|
46
|
+
settings.hooks[hookType].push(newEntry);
|
|
47
|
+
changed = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!changed) {
|
|
51
|
+
return settingsJson;
|
|
52
|
+
}
|
|
53
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Remove all memory hooks (Stop, SessionStart, PreCompact) from settings JSON.
|
|
57
|
+
* Idempotent — returns unchanged JSON if no memory hooks present.
|
|
58
|
+
* Preserves non-memory hooks. Cleans empty arrays/objects.
|
|
59
|
+
*/
|
|
60
|
+
export function removeMemoryHooks(settingsJson) {
|
|
61
|
+
const settings = JSON.parse(settingsJson);
|
|
62
|
+
if (!settings.hooks) {
|
|
63
|
+
return settingsJson;
|
|
64
|
+
}
|
|
65
|
+
let changed = false;
|
|
66
|
+
for (const [hookType, marker] of Object.entries(MEMORY_HOOK_CONFIG)) {
|
|
67
|
+
if (!settings.hooks[hookType]) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const before = settings.hooks[hookType].length;
|
|
71
|
+
settings.hooks[hookType] = settings.hooks[hookType].filter((matcher) => !matcher.hooks.some((h) => h.command.includes(marker)));
|
|
72
|
+
if (settings.hooks[hookType].length !== before) {
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
if (settings.hooks[hookType].length === 0) {
|
|
76
|
+
delete settings.hooks[hookType];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
80
|
+
delete settings.hooks;
|
|
81
|
+
}
|
|
82
|
+
if (!changed) {
|
|
83
|
+
return settingsJson;
|
|
84
|
+
}
|
|
85
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if ALL 3 memory hooks are registered in settings JSON.
|
|
89
|
+
*/
|
|
90
|
+
export function hasMemoryHooks(settingsJson) {
|
|
91
|
+
return countMemoryHooks(settingsJson) === 3;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Count how many of the 3 memory hooks are present (0-3).
|
|
95
|
+
*/
|
|
96
|
+
export function countMemoryHooks(settingsJson) {
|
|
97
|
+
const settings = JSON.parse(settingsJson);
|
|
98
|
+
if (!settings.hooks) {
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
let count = 0;
|
|
102
|
+
for (const [hookType, marker] of Object.entries(MEMORY_HOOK_CONFIG)) {
|
|
103
|
+
const matchers = settings.hooks[hookType] ?? [];
|
|
104
|
+
if (matchers.some((matcher) => matcher.hooks.some((h) => h.command.includes(marker)))) {
|
|
105
|
+
count++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return count;
|
|
109
|
+
}
|
|
110
|
+
export const memoryCommand = new Command('memory')
|
|
111
|
+
.description('Enable or disable working memory (session context preservation)')
|
|
112
|
+
.option('--enable', 'Add Stop/SessionStart/PreCompact hooks')
|
|
113
|
+
.option('--disable', 'Remove memory hooks')
|
|
114
|
+
.option('--status', 'Show current state')
|
|
115
|
+
.action(async (options) => {
|
|
116
|
+
const hasFlag = options.enable || options.disable || options.status;
|
|
117
|
+
if (!hasFlag) {
|
|
118
|
+
p.intro(color.bgCyan(color.white(' Working Memory ')));
|
|
119
|
+
p.note(`${color.cyan('devflow memory --enable')} Add memory hooks\n` +
|
|
120
|
+
`${color.cyan('devflow memory --disable')} Remove memory hooks\n` +
|
|
121
|
+
`${color.cyan('devflow memory --status')} Check current state`, 'Usage');
|
|
122
|
+
p.outro(color.dim('Memory hooks provide automatic session context preservation'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const claudeDir = getClaudeDirectory();
|
|
126
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
127
|
+
let settingsContent;
|
|
128
|
+
try {
|
|
129
|
+
settingsContent = await fs.readFile(settingsPath, 'utf-8');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
if (options.status) {
|
|
133
|
+
p.log.info('Working memory: disabled (no settings.json found)');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// Create minimal settings.json
|
|
137
|
+
settingsContent = '{}';
|
|
138
|
+
}
|
|
139
|
+
if (options.status) {
|
|
140
|
+
const count = countMemoryHooks(settingsContent);
|
|
141
|
+
if (count === 3) {
|
|
142
|
+
p.log.info(`Working memory: ${color.green('enabled')} (3/3 hooks)`);
|
|
143
|
+
}
|
|
144
|
+
else if (count === 0) {
|
|
145
|
+
p.log.info(`Working memory: ${color.dim('disabled')}`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
p.log.info(`Working memory: ${color.yellow(`partial (${count}/3 hooks)`)} — run --enable to fix`);
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const devflowDir = getDevFlowDirectory();
|
|
153
|
+
if (options.enable) {
|
|
154
|
+
const updated = addMemoryHooks(settingsContent, devflowDir);
|
|
155
|
+
if (updated === settingsContent) {
|
|
156
|
+
p.log.info('Working memory already enabled');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await fs.writeFile(settingsPath, updated, 'utf-8');
|
|
160
|
+
await createMemoryDir(false);
|
|
161
|
+
await migrateMemoryFiles(true);
|
|
162
|
+
p.log.success('Working memory enabled — Stop/SessionStart/PreCompact hooks registered');
|
|
163
|
+
p.log.info(color.dim('Session context will be automatically preserved across conversations'));
|
|
164
|
+
}
|
|
165
|
+
if (options.disable) {
|
|
166
|
+
const updated = removeMemoryHooks(settingsContent);
|
|
167
|
+
if (updated === settingsContent) {
|
|
168
|
+
p.log.info('Working memory already disabled');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await fs.writeFile(settingsPath, updated, 'utf-8');
|
|
172
|
+
p.log.success('Working memory disabled — hooks removed');
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -9,6 +9,8 @@ import { getInstallationPaths, getClaudeDirectory, getManagedSettingsPath } from
|
|
|
9
9
|
import { getGitRoot } from '../utils/git.js';
|
|
10
10
|
import { isClaudeCliAvailable } from '../utils/cli.js';
|
|
11
11
|
import { DEVFLOW_PLUGINS, getAllSkillNames, LEGACY_SKILL_NAMES } from '../plugins.js';
|
|
12
|
+
import { removeAmbientHook } from './ambient.js';
|
|
13
|
+
import { removeMemoryHooks } from './memory.js';
|
|
12
14
|
import { detectShell, getProfilePath } from '../utils/safe-delete.js';
|
|
13
15
|
import { isAlreadyInstalled, removeFromProfile } from '../utils/safe-delete-install.js';
|
|
14
16
|
import { removeManagedSettings } from '../utils/post-install.js';
|
|
@@ -176,6 +178,21 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
176
178
|
if (!usedCli) {
|
|
177
179
|
if (isSelectiveUninstall) {
|
|
178
180
|
await removeSelectedPlugins(claudeDir, selectedPlugins, verbose);
|
|
181
|
+
// Clean up ambient hook if ambient plugin is being removed
|
|
182
|
+
if (selectedPlugins.some(sp => sp.name === 'devflow-ambient')) {
|
|
183
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
184
|
+
try {
|
|
185
|
+
const settings = await fs.readFile(settingsPath, 'utf-8');
|
|
186
|
+
const updated = removeAmbientHook(settings);
|
|
187
|
+
if (updated !== settings) {
|
|
188
|
+
await fs.writeFile(settingsPath, updated, 'utf-8');
|
|
189
|
+
if (verbose) {
|
|
190
|
+
p.log.success('Ambient mode hook removed from settings.json');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch { /* settings.json may not exist */ }
|
|
195
|
+
}
|
|
179
196
|
}
|
|
180
197
|
else {
|
|
181
198
|
await removeAllDevFlow(claudeDir, devflowScriptsDir, verbose);
|
|
@@ -221,7 +238,39 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
221
238
|
p.log.info('.docs/ preserved');
|
|
222
239
|
}
|
|
223
240
|
}
|
|
224
|
-
// 2. .
|
|
241
|
+
// 2. .memory/ directory
|
|
242
|
+
const memoryDir = path.join(process.cwd(), '.memory');
|
|
243
|
+
let memoryExist = false;
|
|
244
|
+
try {
|
|
245
|
+
await fs.access(memoryDir);
|
|
246
|
+
memoryExist = true;
|
|
247
|
+
}
|
|
248
|
+
catch { /* .memory doesn't exist */ }
|
|
249
|
+
if (memoryExist) {
|
|
250
|
+
let shouldRemoveMemory = false;
|
|
251
|
+
if (options.keepDocs) {
|
|
252
|
+
shouldRemoveMemory = false;
|
|
253
|
+
}
|
|
254
|
+
else if (process.stdin.isTTY) {
|
|
255
|
+
const removeMemory = await p.confirm({
|
|
256
|
+
message: '.memory/ directory found. Remove working memory files?',
|
|
257
|
+
initialValue: false,
|
|
258
|
+
});
|
|
259
|
+
if (p.isCancel(removeMemory)) {
|
|
260
|
+
p.cancel('Uninstall cancelled.');
|
|
261
|
+
process.exit(0);
|
|
262
|
+
}
|
|
263
|
+
shouldRemoveMemory = removeMemory;
|
|
264
|
+
}
|
|
265
|
+
if (shouldRemoveMemory) {
|
|
266
|
+
await fs.rm(memoryDir, { recursive: true, force: true });
|
|
267
|
+
p.log.success('.memory/ removed');
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
p.log.info('.memory/ preserved');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// 4. .claudeignore
|
|
225
274
|
const claudeignorePath = gitRoot
|
|
226
275
|
? path.join(gitRoot, '.claudeignore')
|
|
227
276
|
: path.join(process.cwd(), '.claudeignore');
|
|
@@ -249,12 +298,30 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
249
298
|
p.log.info('.claudeignore preserved (non-interactive mode)');
|
|
250
299
|
}
|
|
251
300
|
}
|
|
252
|
-
//
|
|
301
|
+
// 5. settings.json (DevFlow hooks)
|
|
253
302
|
for (const scope of scopesToUninstall) {
|
|
254
303
|
try {
|
|
255
304
|
const paths = await getInstallationPaths(scope);
|
|
256
305
|
const settingsPath = path.join(paths.claudeDir, 'settings.json');
|
|
257
|
-
|
|
306
|
+
let settingsContent = await fs.readFile(settingsPath, 'utf-8');
|
|
307
|
+
// Always remove ambient hook on full uninstall (idempotent)
|
|
308
|
+
const withoutAmbient = removeAmbientHook(settingsContent);
|
|
309
|
+
if (withoutAmbient !== settingsContent) {
|
|
310
|
+
await fs.writeFile(settingsPath, withoutAmbient, 'utf-8');
|
|
311
|
+
settingsContent = withoutAmbient;
|
|
312
|
+
if (verbose) {
|
|
313
|
+
p.log.success(`Ambient mode hook removed from settings.json (${scope})`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Always remove memory hooks on full uninstall (idempotent)
|
|
317
|
+
const withoutMemory = removeMemoryHooks(settingsContent);
|
|
318
|
+
if (withoutMemory !== settingsContent) {
|
|
319
|
+
await fs.writeFile(settingsPath, withoutMemory, 'utf-8');
|
|
320
|
+
settingsContent = withoutMemory;
|
|
321
|
+
if (verbose) {
|
|
322
|
+
p.log.success(`Memory hooks removed from settings.json (${scope})`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
258
325
|
const settings = JSON.parse(settingsContent);
|
|
259
326
|
if (settings.hooks) {
|
|
260
327
|
if (process.stdin.isTTY) {
|
|
@@ -280,7 +347,7 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
280
347
|
// settings.json doesn't exist or can't be parsed — skip
|
|
281
348
|
}
|
|
282
349
|
}
|
|
283
|
-
//
|
|
350
|
+
// 6. Managed settings (security deny list)
|
|
284
351
|
let managedSettingsExist = false;
|
|
285
352
|
try {
|
|
286
353
|
const managedPath = getManagedSettingsPath();
|
|
@@ -309,7 +376,7 @@ export const uninstallCommand = new Command('uninstall')
|
|
|
309
376
|
p.log.info('Managed settings preserved (non-interactive mode)');
|
|
310
377
|
}
|
|
311
378
|
}
|
|
312
|
-
//
|
|
379
|
+
// 7. Safe-delete shell function
|
|
313
380
|
const shell = detectShell();
|
|
314
381
|
const profilePath = getProfilePath(shell);
|
|
315
382
|
if (profilePath && await isAlreadyInstalled(profilePath)) {
|
package/dist/plugins.js
CHANGED
|
@@ -10,7 +10,7 @@ export const DEVFLOW_PLUGINS = [
|
|
|
10
10
|
description: 'Auto-activating quality enforcement (foundation layer)',
|
|
11
11
|
commands: [],
|
|
12
12
|
agents: [],
|
|
13
|
-
skills: ['
|
|
13
|
+
skills: ['core-patterns', 'docs-framework', 'git-safety', 'git-workflow', 'github-patterns', 'input-validation', 'test-driven-development', 'test-patterns'],
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
name: 'devflow-specify',
|
|
@@ -24,14 +24,14 @@ export const DEVFLOW_PLUGINS = [
|
|
|
24
24
|
description: 'Complete task implementation workflow',
|
|
25
25
|
commands: ['/implement'],
|
|
26
26
|
agents: ['git', 'skimmer', 'synthesizer', 'coder', 'simplifier', 'scrutinizer', 'shepherd', 'validator'],
|
|
27
|
-
skills: ['
|
|
27
|
+
skills: ['agent-teams', 'implementation-patterns', 'self-review'],
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
name: 'devflow-code-review',
|
|
31
31
|
description: 'Comprehensive code review',
|
|
32
32
|
commands: ['/code-review'],
|
|
33
33
|
agents: ['git', 'reviewer', 'synthesizer'],
|
|
34
|
-
skills: ['
|
|
34
|
+
skills: ['agent-teams', 'architecture-patterns', 'complexity-patterns', 'consistency-patterns', 'database-patterns', 'dependencies-patterns', 'documentation-patterns', 'performance-patterns', 'regression-patterns', 'review-methodology', 'security-patterns', 'test-patterns'],
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
name: 'devflow-resolve',
|
|
@@ -54,6 +54,13 @@ export const DEVFLOW_PLUGINS = [
|
|
|
54
54
|
agents: ['simplifier', 'scrutinizer', 'validator'],
|
|
55
55
|
skills: ['self-review', 'core-patterns'],
|
|
56
56
|
},
|
|
57
|
+
{
|
|
58
|
+
name: 'devflow-ambient',
|
|
59
|
+
description: 'Ambient mode — auto-loads relevant skills based on each prompt',
|
|
60
|
+
commands: ['/ambient'],
|
|
61
|
+
agents: [],
|
|
62
|
+
skills: ['ambient-router'],
|
|
63
|
+
},
|
|
57
64
|
{
|
|
58
65
|
name: 'devflow-audit-claude',
|
|
59
66
|
description: 'Audit CLAUDE.md files against Anthropic best practices',
|
|
@@ -62,6 +69,70 @@ export const DEVFLOW_PLUGINS = [
|
|
|
62
69
|
skills: [],
|
|
63
70
|
optional: true,
|
|
64
71
|
},
|
|
72
|
+
{
|
|
73
|
+
name: 'devflow-typescript',
|
|
74
|
+
description: 'TypeScript language patterns (type safety, generics, utility types)',
|
|
75
|
+
commands: [],
|
|
76
|
+
agents: [],
|
|
77
|
+
skills: ['typescript'],
|
|
78
|
+
optional: true,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'devflow-react',
|
|
82
|
+
description: 'React framework patterns (hooks, state, composition, performance)',
|
|
83
|
+
commands: [],
|
|
84
|
+
agents: [],
|
|
85
|
+
skills: ['react'],
|
|
86
|
+
optional: true,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'devflow-accessibility',
|
|
90
|
+
description: 'Web accessibility patterns (WCAG, ARIA, keyboard navigation)',
|
|
91
|
+
commands: [],
|
|
92
|
+
agents: [],
|
|
93
|
+
skills: ['accessibility'],
|
|
94
|
+
optional: true,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'devflow-frontend-design',
|
|
98
|
+
description: 'Frontend design patterns (typography, color, spacing, motion)',
|
|
99
|
+
commands: [],
|
|
100
|
+
agents: [],
|
|
101
|
+
skills: ['frontend-design'],
|
|
102
|
+
optional: true,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'devflow-go',
|
|
106
|
+
description: 'Go language patterns (error handling, interfaces, concurrency)',
|
|
107
|
+
commands: [],
|
|
108
|
+
agents: [],
|
|
109
|
+
skills: ['go'],
|
|
110
|
+
optional: true,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'devflow-java',
|
|
114
|
+
description: 'Java language patterns (records, sealed classes, composition)',
|
|
115
|
+
commands: [],
|
|
116
|
+
agents: [],
|
|
117
|
+
skills: ['java'],
|
|
118
|
+
optional: true,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'devflow-python',
|
|
122
|
+
description: 'Python language patterns (type hints, protocols, data modeling)',
|
|
123
|
+
commands: [],
|
|
124
|
+
agents: [],
|
|
125
|
+
skills: ['python'],
|
|
126
|
+
optional: true,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'devflow-rust',
|
|
130
|
+
description: 'Rust language patterns (ownership, error handling, type system)',
|
|
131
|
+
commands: [],
|
|
132
|
+
agents: [],
|
|
133
|
+
skills: ['rust'],
|
|
134
|
+
optional: true,
|
|
135
|
+
},
|
|
65
136
|
];
|
|
66
137
|
/**
|
|
67
138
|
* Deprecated command names from old installations.
|
|
@@ -65,4 +65,16 @@ export declare function updateGitignore(gitRoot: string, verbose: boolean): Prom
|
|
|
65
65
|
* Create .docs/ directory structure for DevFlow artifacts.
|
|
66
66
|
*/
|
|
67
67
|
export declare function createDocsStructure(verbose: boolean): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Create .memory/ directory for working memory files.
|
|
70
|
+
* Separate from .docs/ which is for reviews/releases.
|
|
71
|
+
*/
|
|
72
|
+
export declare function createMemoryDir(verbose: boolean, cwd?: string): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Migrate memory files from .docs/ to .memory/.
|
|
75
|
+
* One-time migration for existing users. Skips if destination exists (no clobber).
|
|
76
|
+
* Also cleans up ephemeral files from .docs/.
|
|
77
|
+
* Returns count of migrated files.
|
|
78
|
+
*/
|
|
79
|
+
export declare function migrateMemoryFiles(verbose: boolean, cwd?: string): Promise<number>;
|
|
68
80
|
//# sourceMappingURL=post-install.d.ts.map
|
|
@@ -386,7 +386,7 @@ export async function installClaudeignore(gitRoot, rootDir, verbose) {
|
|
|
386
386
|
export async function updateGitignore(gitRoot, verbose) {
|
|
387
387
|
try {
|
|
388
388
|
const gitignorePath = path.join(gitRoot, '.gitignore');
|
|
389
|
-
const entriesToAdd = ['.claude/', '.devflow/'];
|
|
389
|
+
const entriesToAdd = ['.claude/', '.devflow/', '.memory/', '.docs/'];
|
|
390
390
|
let gitignoreContent = '';
|
|
391
391
|
try {
|
|
392
392
|
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
@@ -424,4 +424,85 @@ export async function createDocsStructure(verbose) {
|
|
|
424
424
|
}
|
|
425
425
|
catch { /* may already exist */ }
|
|
426
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Create .memory/ directory for working memory files.
|
|
429
|
+
* Separate from .docs/ which is for reviews/releases.
|
|
430
|
+
*/
|
|
431
|
+
export async function createMemoryDir(verbose, cwd) {
|
|
432
|
+
const memoryDir = path.join(cwd ?? process.cwd(), '.memory');
|
|
433
|
+
try {
|
|
434
|
+
await fs.mkdir(memoryDir, { recursive: true });
|
|
435
|
+
if (verbose) {
|
|
436
|
+
p.log.success('.memory/ directory ready');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch { /* may already exist */ }
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Migrate memory files from .docs/ to .memory/.
|
|
443
|
+
* One-time migration for existing users. Skips if destination exists (no clobber).
|
|
444
|
+
* Also cleans up ephemeral files from .docs/.
|
|
445
|
+
* Returns count of migrated files.
|
|
446
|
+
*/
|
|
447
|
+
export async function migrateMemoryFiles(verbose, cwd) {
|
|
448
|
+
const root = cwd ?? process.cwd();
|
|
449
|
+
const docsDir = path.join(root, '.docs');
|
|
450
|
+
const memoryDir = path.join(root, '.memory');
|
|
451
|
+
const migrations = [
|
|
452
|
+
{ src: path.join(docsDir, 'WORKING-MEMORY.md'), dest: path.join(memoryDir, 'WORKING-MEMORY.md') },
|
|
453
|
+
{ src: path.join(docsDir, 'patterns.md'), dest: path.join(memoryDir, 'PROJECT-PATTERNS.md') },
|
|
454
|
+
{ src: path.join(docsDir, 'working-memory-backup.json'), dest: path.join(memoryDir, 'backup.json') },
|
|
455
|
+
];
|
|
456
|
+
let migrated = 0;
|
|
457
|
+
for (const { src, dest } of migrations) {
|
|
458
|
+
try {
|
|
459
|
+
await fs.access(src);
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
continue; // Source doesn't exist
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
await fs.access(dest);
|
|
466
|
+
continue; // Destination already exists — no clobber
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// Destination doesn't exist — proceed with migration
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
await fs.rename(src, dest);
|
|
473
|
+
migrated++;
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Cross-device or permission error — try copy+delete
|
|
477
|
+
try {
|
|
478
|
+
await fs.copyFile(src, dest);
|
|
479
|
+
await fs.rm(src, { force: true });
|
|
480
|
+
migrated++;
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// Migration failed for this file — skip silently
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Clean up ephemeral files from .docs/
|
|
488
|
+
const ephemeralFiles = [
|
|
489
|
+
path.join(docsDir, '.working-memory-update.log'),
|
|
490
|
+
path.join(docsDir, '.working-memory-last-trigger'),
|
|
491
|
+
];
|
|
492
|
+
for (const file of ephemeralFiles) {
|
|
493
|
+
try {
|
|
494
|
+
await fs.rm(file, { force: true });
|
|
495
|
+
}
|
|
496
|
+
catch { /* doesn't exist or can't remove */ }
|
|
497
|
+
}
|
|
498
|
+
// Clean up lock directory
|
|
499
|
+
try {
|
|
500
|
+
await fs.rmdir(path.join(docsDir, '.working-memory.lock'));
|
|
501
|
+
}
|
|
502
|
+
catch { /* doesn't exist or not empty */ }
|
|
503
|
+
if (migrated > 0 && verbose) {
|
|
504
|
+
p.log.success(`Migrated ${migrated} memory file(s) from .docs/ to .memory/`);
|
|
505
|
+
}
|
|
506
|
+
return migrated;
|
|
507
|
+
}
|
|
427
508
|
//# sourceMappingURL=post-install.js.map
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Shell } from './safe-delete.js';
|
|
2
|
+
/** Bump this when the safe-delete block changes. */
|
|
3
|
+
export declare const SAFE_DELETE_BLOCK_VERSION = 2;
|
|
2
4
|
/**
|
|
3
5
|
* Generate the safe-delete shell function block with markers.
|
|
4
6
|
* Returns null for unsupported shells.
|
|
@@ -13,6 +15,11 @@ export declare function isAlreadyInstalled(profilePath: string): Promise<boolean
|
|
|
13
15
|
* Creates parent directories and the file if they don't exist.
|
|
14
16
|
*/
|
|
15
17
|
export declare function installToProfile(profilePath: string, block: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Extract the installed safe-delete block version from a profile file.
|
|
20
|
+
* Returns 0 (not installed), 1 (legacy block without version stamp), or N (versioned block).
|
|
21
|
+
*/
|
|
22
|
+
export declare function getInstalledVersion(profilePath: string): Promise<number>;
|
|
16
23
|
/**
|
|
17
24
|
* Remove the safe-delete block from a profile file.
|
|
18
25
|
* Returns true if the block was found and removed, false otherwise.
|
|
@@ -2,6 +2,8 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
const START_MARKER = '# >>> DevFlow safe-delete >>>';
|
|
4
4
|
const END_MARKER = '# <<< DevFlow safe-delete <<<';
|
|
5
|
+
/** Bump this when the safe-delete block changes. */
|
|
6
|
+
export const SAFE_DELETE_BLOCK_VERSION = 2;
|
|
5
7
|
/**
|
|
6
8
|
* Generate the safe-delete shell function block with markers.
|
|
7
9
|
* Returns null for unsupported shells.
|
|
@@ -13,13 +15,18 @@ export function generateSafeDeleteBlock(shell, platform, trashCommand) {
|
|
|
13
15
|
const cmd = trashCommand ?? 'trash';
|
|
14
16
|
return [
|
|
15
17
|
START_MARKER,
|
|
18
|
+
`# v${SAFE_DELETE_BLOCK_VERSION}`,
|
|
16
19
|
`rm() {`,
|
|
17
20
|
` local files=()`,
|
|
18
21
|
` for arg in "$@"; do`,
|
|
19
22
|
` [[ "$arg" =~ ^- ]] || files+=("$arg")`,
|
|
20
23
|
` done`,
|
|
21
|
-
`
|
|
22
|
-
`
|
|
24
|
+
` local existing=()`,
|
|
25
|
+
` for f in "\${files[@]}"; do`,
|
|
26
|
+
` [ -e "$f" ] || [ -L "$f" ] && existing+=("$f")`,
|
|
27
|
+
` done`,
|
|
28
|
+
` if (( \${#existing[@]} > 0 )); then`,
|
|
29
|
+
` ${cmd} "\${existing[@]}"`,
|
|
23
30
|
` fi`,
|
|
24
31
|
`}`,
|
|
25
32
|
`command() {`,
|
|
@@ -36,6 +43,7 @@ export function generateSafeDeleteBlock(shell, platform, trashCommand) {
|
|
|
36
43
|
const cmd = trashCommand ?? 'trash';
|
|
37
44
|
return [
|
|
38
45
|
START_MARKER,
|
|
46
|
+
`# v${SAFE_DELETE_BLOCK_VERSION}`,
|
|
39
47
|
`function rm --description "Safe delete via trash"`,
|
|
40
48
|
` set -l files`,
|
|
41
49
|
` for arg in $argv`,
|
|
@@ -43,8 +51,14 @@ export function generateSafeDeleteBlock(shell, platform, trashCommand) {
|
|
|
43
51
|
` set files $files $arg`,
|
|
44
52
|
` end`,
|
|
45
53
|
` end`,
|
|
46
|
-
`
|
|
47
|
-
`
|
|
54
|
+
` set -l existing`,
|
|
55
|
+
` for f in $files`,
|
|
56
|
+
` if test -e $f; or test -L $f`,
|
|
57
|
+
` set existing $existing $f`,
|
|
58
|
+
` end`,
|
|
59
|
+
` end`,
|
|
60
|
+
` if test (count $existing) -gt 0`,
|
|
61
|
+
` ${cmd} $existing`,
|
|
48
62
|
` end`,
|
|
49
63
|
`end`,
|
|
50
64
|
END_MARKER,
|
|
@@ -54,6 +68,7 @@ export function generateSafeDeleteBlock(shell, platform, trashCommand) {
|
|
|
54
68
|
if (platform === 'win32') {
|
|
55
69
|
return [
|
|
56
70
|
START_MARKER,
|
|
71
|
+
`# v${SAFE_DELETE_BLOCK_VERSION}`,
|
|
57
72
|
`if (Get-Alias rm -ErrorAction SilentlyContinue) {`,
|
|
58
73
|
` Remove-Alias rm -Force -Scope Global`,
|
|
59
74
|
`}`,
|
|
@@ -82,12 +97,14 @@ export function generateSafeDeleteBlock(shell, platform, trashCommand) {
|
|
|
82
97
|
const cmd = trashCommand ?? 'trash';
|
|
83
98
|
return [
|
|
84
99
|
START_MARKER,
|
|
100
|
+
`# v${SAFE_DELETE_BLOCK_VERSION}`,
|
|
85
101
|
`if (Get-Alias rm -ErrorAction SilentlyContinue) {`,
|
|
86
102
|
` Remove-Alias rm -Force -Scope Global`,
|
|
87
103
|
`}`,
|
|
88
104
|
`function rm {`,
|
|
89
105
|
` $files = $args | Where-Object { $_ -notlike '-*' }`,
|
|
90
|
-
`
|
|
106
|
+
` $existing = $files | Where-Object { Test-Path $_ }`,
|
|
107
|
+
` if ($existing) { & ${cmd} @existing }`,
|
|
91
108
|
`}`,
|
|
92
109
|
END_MARKER,
|
|
93
110
|
].join('\n');
|
|
@@ -123,6 +140,24 @@ export async function installToProfile(profilePath, block) {
|
|
|
123
140
|
const content = existing.length > 0 ? existing + separator + block + '\n' : block + '\n';
|
|
124
141
|
await fs.writeFile(profilePath, content, 'utf-8');
|
|
125
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Extract the installed safe-delete block version from a profile file.
|
|
145
|
+
* Returns 0 (not installed), 1 (legacy block without version stamp), or N (versioned block).
|
|
146
|
+
*/
|
|
147
|
+
export async function getInstalledVersion(profilePath) {
|
|
148
|
+
try {
|
|
149
|
+
const content = await fs.readFile(profilePath, 'utf-8');
|
|
150
|
+
const startIdx = content.indexOf(START_MARKER);
|
|
151
|
+
if (startIdx === -1)
|
|
152
|
+
return 0;
|
|
153
|
+
const afterMarker = content.slice(startIdx + START_MARKER.length);
|
|
154
|
+
const match = afterMarker.match(/^\n# v(\d+)/);
|
|
155
|
+
return match ? parseInt(match[1], 10) : 1;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
126
161
|
/**
|
|
127
162
|
* Remove the safe-delete block from a profile file.
|
|
128
163
|
* Returns true if the block was found and removed, false otherwise.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devflow-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Agentic Development Toolkit for Claude Code - Enhance AI-assisted development with intelligent commands and workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"dev": "tsc --watch",
|
|
25
25
|
"cli": "node dist/cli.js",
|
|
26
26
|
"prepublishOnly": "npm run build",
|
|
27
|
+
"version:bump": "npx tsx scripts/bump-version.ts",
|
|
27
28
|
"test": "vitest run",
|
|
28
29
|
"test:watch": "vitest"
|
|
29
30
|
},
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devflow-accessibility",
|
|
3
|
+
"description": "Web accessibility patterns - WCAG compliance, ARIA roles, keyboard navigation, focus management",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "DevFlow Contributors",
|
|
6
|
+
"email": "dean@keren.dev"
|
|
7
|
+
},
|
|
8
|
+
"version": "1.2.0",
|
|
9
|
+
"homepage": "https://github.com/dean0x/devflow",
|
|
10
|
+
"repository": "https://github.com/dean0x/devflow",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": ["accessibility", "wcag", "aria", "a11y"],
|
|
13
|
+
"agents": [],
|
|
14
|
+
"skills": ["accessibility"]
|
|
15
|
+
}
|