@orderful/droid 0.33.1 → 0.34.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/bin/droid.js +673 -101
  3. package/dist/commands/config.d.ts.map +1 -1
  4. package/dist/commands/repos.d.ts +21 -0
  5. package/dist/commands/repos.d.ts.map +1 -0
  6. package/dist/commands/tui/components/SettingsDetails.d.ts +2 -1
  7. package/dist/commands/tui/components/SettingsDetails.d.ts.map +1 -1
  8. package/dist/commands/tui/types.d.ts +1 -1
  9. package/dist/commands/tui/types.d.ts.map +1 -1
  10. package/dist/commands/tui/views/ReposManagementScreen.d.ts +6 -0
  11. package/dist/commands/tui/views/ReposManagementScreen.d.ts.map +1 -0
  12. package/dist/commands/tui/views/ReposViewerScreen.d.ts +5 -0
  13. package/dist/commands/tui/views/ReposViewerScreen.d.ts.map +1 -0
  14. package/dist/commands/tui.d.ts.map +1 -1
  15. package/dist/index.js +149 -26
  16. package/dist/lib/config.d.ts +34 -1
  17. package/dist/lib/config.d.ts.map +1 -1
  18. package/dist/lib/types.d.ts +10 -1
  19. package/dist/lib/types.d.ts.map +1 -1
  20. package/dist/tools/brain/skills/brain/SKILL.md +2 -2
  21. package/dist/tools/brain/skills/brain/references/workflows.md +10 -10
  22. package/dist/tools/coach/skills/coach/SKILL.md +6 -6
  23. package/dist/tools/codex/skills/codex/SKILL.md +5 -5
  24. package/dist/tools/codex/skills/codex/references/creating.md +2 -2
  25. package/dist/tools/codex/skills/codex/references/decisions.md +2 -2
  26. package/dist/tools/codex/skills/codex/references/loading.md +1 -1
  27. package/dist/tools/codex/skills/codex/references/topics.md +2 -2
  28. package/dist/tools/codex/skills/codex/scripts/git-finish-write.d.ts +1 -1
  29. package/dist/tools/codex/skills/codex/scripts/git-finish-write.ts +2 -2
  30. package/dist/tools/codex/skills/codex/scripts/git-preamble.d.ts +1 -1
  31. package/dist/tools/codex/skills/codex/scripts/git-preamble.ts +3 -3
  32. package/dist/tools/codex/skills/codex/scripts/git-start-write.d.ts +1 -1
  33. package/dist/tools/codex/skills/codex/scripts/git-start-write.ts +2 -2
  34. package/dist/tools/comments/skills/comments/SKILL.md +9 -9
  35. package/dist/tools/plan/skills/plan/SKILL.md +2 -2
  36. package/dist/tools/plan/skills/plan/references/workflows.md +2 -2
  37. package/dist/tools/project/skills/project/SKILL.md +1 -1
  38. package/dist/tools/project/skills/project/references/creating.md +2 -2
  39. package/dist/tools/project/skills/project/references/loading.md +1 -1
  40. package/dist/tools/tech-design/skills/tech-design/SKILL.md +2 -2
  41. package/dist/tools/tech-design/skills/tech-design/references/publish.md +3 -3
  42. package/dist/tools/tech-design/skills/tech-design/references/start.md +29 -3
  43. package/dist/tools/tech-design/skills/tech-design/references/think.md +1 -1
  44. package/dist/tools/wrapup/skills/wrapup/references/subagent-prompts.md +3 -3
  45. package/package.json +1 -1
  46. package/src/bin/droid.ts +39 -0
  47. package/src/commands/config.ts +14 -1
  48. package/src/commands/repos.ts +185 -0
  49. package/src/commands/tui/components/SettingsDetails.tsx +42 -13
  50. package/src/commands/tui/types.ts +1 -1
  51. package/src/commands/tui/views/ReposManagementScreen.tsx +291 -0
  52. package/src/commands/tui/views/ReposViewerScreen.tsx +49 -0
  53. package/src/commands/tui.tsx +51 -4
  54. package/src/lib/config.test.ts +228 -1
  55. package/src/lib/config.ts +193 -4
  56. package/src/lib/types.ts +13 -1
  57. package/src/tools/brain/skills/brain/SKILL.md +2 -2
  58. package/src/tools/brain/skills/brain/references/workflows.md +10 -10
  59. package/src/tools/coach/skills/coach/SKILL.md +6 -6
  60. package/src/tools/codex/skills/codex/SKILL.md +5 -5
  61. package/src/tools/codex/skills/codex/references/creating.md +2 -2
  62. package/src/tools/codex/skills/codex/references/decisions.md +2 -2
  63. package/src/tools/codex/skills/codex/references/loading.md +1 -1
  64. package/src/tools/codex/skills/codex/references/topics.md +2 -2
  65. package/src/tools/codex/skills/codex/scripts/git-finish-write.ts +2 -2
  66. package/src/tools/codex/skills/codex/scripts/git-preamble.ts +3 -3
  67. package/src/tools/codex/skills/codex/scripts/git-start-write.ts +2 -2
  68. package/src/tools/comments/skills/comments/SKILL.md +9 -9
  69. package/src/tools/plan/skills/plan/SKILL.md +2 -2
  70. package/src/tools/plan/skills/plan/references/workflows.md +2 -2
  71. package/src/tools/project/skills/project/SKILL.md +1 -1
  72. package/src/tools/project/skills/project/references/creating.md +2 -2
  73. package/src/tools/project/skills/project/references/loading.md +1 -1
  74. package/src/tools/tech-design/skills/tech-design/SKILL.md +2 -2
  75. package/src/tools/tech-design/skills/tech-design/references/publish.md +3 -3
  76. package/src/tools/tech-design/skills/tech-design/references/start.md +29 -3
  77. package/src/tools/tech-design/skills/tech-design/references/think.md +1 -1
  78. package/src/tools/wrapup/skills/wrapup/references/subagent-prompts.md +3 -3
@@ -29,6 +29,8 @@ import { SetupScreen } from './tui/views/SetupScreen';
29
29
  import { ReadmeViewer } from './tui/views/ReadmeViewer';
30
30
  import { ToolExplorer } from './tui/views/ToolExplorer';
31
31
  import { SkillConfigScreen } from './tui/views/SkillConfigScreen';
32
+ import { ReposManagementScreen } from './tui/views/ReposManagementScreen';
33
+ import { ReposViewerScreen } from './tui/views/ReposViewerScreen';
32
34
  import { useAppUpdate } from './tui/hooks/useAppUpdate';
33
35
  import { useToolUpdates } from './tui/hooks/useToolUpdates';
34
36
 
@@ -215,10 +217,25 @@ function App() {
215
217
  setSelectedAction(0);
216
218
  }
217
219
  if (activeTab === 'settings') {
218
- // Settings detail view - just enter to edit
220
+ // Settings detail view - navigate actions (0 = edit, 1 = view repos, 2 = manage repos)
221
+ if (key.leftArrow) {
222
+ setSelectedAction((prev) => Math.max(0, prev - 1));
223
+ }
224
+ if (key.rightArrow) {
225
+ setSelectedAction((prev) => Math.min(2, prev + 1));
226
+ }
219
227
  if (key.return) {
220
- setIsEditingSettings(true);
221
- setView('setup');
228
+ if (selectedAction === 0) {
229
+ // Edit config
230
+ setIsEditingSettings(true);
231
+ setView('setup');
232
+ } else if (selectedAction === 1) {
233
+ // View repos
234
+ setView('view-repos');
235
+ } else if (selectedAction === 2) {
236
+ // Manage repos
237
+ setView('repos');
238
+ }
222
239
  }
223
240
  }
224
241
  if (key.leftArrow && activeTab === 'tools') {
@@ -336,7 +353,9 @@ function App() {
336
353
  view !== 'tool-updates' &&
337
354
  view !== 'setup' &&
338
355
  view !== 'configure' &&
339
- view !== 'explorer',
356
+ view !== 'explorer' &&
357
+ view !== 'repos' &&
358
+ view !== 'view-repos',
340
359
  },
341
360
  );
342
361
 
@@ -441,6 +460,33 @@ function App() {
441
460
  );
442
461
  }
443
462
 
463
+ if (view === 'view-repos') {
464
+ return (
465
+ <ReposViewerScreen
466
+ onClose={() => {
467
+ setView('detail');
468
+ }}
469
+ />
470
+ );
471
+ }
472
+
473
+ if (view === 'repos') {
474
+ return (
475
+ <ReposManagementScreen
476
+ onComplete={() => {
477
+ setMessage({
478
+ text: '✓ Repos updated',
479
+ type: 'success',
480
+ });
481
+ setView('detail');
482
+ }}
483
+ onCancel={() => {
484
+ setView('detail');
485
+ }}
486
+ />
487
+ );
488
+ }
489
+
444
490
  return (
445
491
  <Box flexDirection="row" padding={1}>
446
492
  {/* Left column */}
@@ -550,6 +596,7 @@ function App() {
550
596
  {activeTab === 'settings' && (
551
597
  <SettingsDetails
552
598
  isFocused={view === 'detail'}
599
+ selectedAction={selectedAction}
553
600
  detectedPlatforms={detectedPlatforms}
554
601
  onRedetect={detectedPlatforms.length === 0 ? handleRedetectPlatforms : undefined}
555
602
  />
@@ -1,8 +1,22 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
1
+ import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
2
2
  import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { tmpdir } from 'os';
5
5
  import YAML from 'yaml';
6
+ import {
7
+ getRepos,
8
+ getRepo,
9
+ getRepoPath,
10
+ addRepo,
11
+ removeRepo,
12
+ getToolSettings,
13
+ setToolSettings,
14
+ setToolSetting,
15
+ loadConfig,
16
+ saveConfig,
17
+ getConfigDir,
18
+ } from './config';
19
+ import type { DroidConfig, RepoConfig } from './types';
6
20
 
7
21
  describe('config value parsing', () => {
8
22
  it('should parse dot notation keys correctly', () => {
@@ -87,3 +101,216 @@ describe('config file operations', () => {
87
101
  expect(parsed.ai_tool).toBe('claude-code');
88
102
  });
89
103
  });
104
+
105
+ describe('repos management', () => {
106
+ let originalConfigDir: string;
107
+ let testConfigDir: string;
108
+
109
+ beforeEach(() => {
110
+ // Mock CONFIG_DIR by creating a test directory
111
+ testConfigDir = join(tmpdir(), `droid-config-test-${Date.now()}`);
112
+ mkdirSync(testConfigDir, { recursive: true });
113
+
114
+ // Mock getConfigDir to return our test directory
115
+ originalConfigDir = getConfigDir();
116
+ });
117
+
118
+ afterEach(() => {
119
+ if (existsSync(testConfigDir)) {
120
+ rmSync(testConfigDir, { recursive: true });
121
+ }
122
+ });
123
+
124
+ it('getRepos returns empty array when none configured', () => {
125
+ const repos = getRepos();
126
+ expect(Array.isArray(repos)).toBe(true);
127
+ });
128
+
129
+ it('addRepo creates repo entry', () => {
130
+ const repo: RepoConfig = {
131
+ name: 'test-repo',
132
+ path: '~/test/repo',
133
+ description: 'Test repository',
134
+ };
135
+
136
+ addRepo(repo);
137
+ const repos = getRepos();
138
+
139
+ const found = repos.find((r) => r.name === 'test-repo');
140
+ expect(found).toBeDefined();
141
+ expect(found?.path).toBe('~/test/repo');
142
+ expect(found?.description).toBe('Test repository');
143
+ });
144
+
145
+ it('getRepoPath expands tilde to home directory', () => {
146
+ const repo: RepoConfig = {
147
+ name: 'tilde-repo',
148
+ path: '~/code/my-repo',
149
+ };
150
+
151
+ addRepo(repo);
152
+ const path = getRepoPath('tilde-repo');
153
+
154
+ expect(path).toBeDefined();
155
+ expect(path).not.toContain('~');
156
+ expect(path).toContain('code/my-repo');
157
+ });
158
+
159
+ it('removeRepo returns false when repo does not exist', () => {
160
+ const existed = removeRepo('non-existent-repo');
161
+ expect(existed).toBe(false);
162
+ });
163
+
164
+ it('removeRepo returns true when repo exists', () => {
165
+ const repo: RepoConfig = {
166
+ name: 'to-remove',
167
+ path: '/tmp/repo',
168
+ };
169
+
170
+ addRepo(repo);
171
+ const existed = removeRepo('to-remove');
172
+
173
+ expect(existed).toBe(true);
174
+
175
+ const found = getRepo('to-remove');
176
+ expect(found).toBeUndefined();
177
+ });
178
+
179
+ it('config round-trip preserves repo entries', () => {
180
+ const repo1: RepoConfig = {
181
+ name: 'repo1',
182
+ path: '/path/to/repo1',
183
+ };
184
+
185
+ const repo2: RepoConfig = {
186
+ name: 'repo2',
187
+ path: '~/code/repo2',
188
+ description: 'Second repo',
189
+ };
190
+
191
+ addRepo(repo1);
192
+ addRepo(repo2);
193
+
194
+ const repos = getRepos();
195
+ expect(repos.length).toBeGreaterThanOrEqual(2);
196
+
197
+ const found1 = repos.find((r) => r.name === 'repo1');
198
+ const found2 = repos.find((r) => r.name === 'repo2');
199
+
200
+ expect(found1).toBeDefined();
201
+ expect(found2).toBeDefined();
202
+ expect(found2?.description).toBe('Second repo');
203
+ });
204
+ });
205
+
206
+ describe('tool config consolidation', () => {
207
+ it('getToolSettings returns empty object when none configured', () => {
208
+ const settings = getToolSettings('nonexistent-tool');
209
+ expect(settings).toEqual({});
210
+ });
211
+
212
+ it('setToolSettings writes to config.tools.{name}', () => {
213
+ const settings = { key1: 'value1', key2: 42 };
214
+ setToolSettings('test-tool', settings);
215
+
216
+ const retrieved = getToolSettings('test-tool');
217
+ expect(retrieved).toEqual(settings);
218
+ });
219
+
220
+ it('setToolSetting updates single key in tool config', () => {
221
+ setToolSetting('test-tool-2', 'foo', 'bar');
222
+ setToolSetting('test-tool-2', 'baz', 123);
223
+
224
+ const settings = getToolSettings('test-tool-2');
225
+ expect(settings.foo).toBe('bar');
226
+ expect(settings.baz).toBe(123);
227
+ });
228
+
229
+ it('migration flag is set after migration', () => {
230
+ // The migration should set tools_consolidated flag when complete
231
+ const config = loadConfig();
232
+ // Either undefined (not migrated) or boolean (migrated)
233
+ const flagType = typeof config.migrations?.tools_consolidated;
234
+ expect(['undefined', 'boolean']).toContain(flagType);
235
+ });
236
+
237
+ it('loadSkillOverrides reads from new location first', () => {
238
+ // Set some data in the new location
239
+ setToolSettings('test-skill', { new_key: 'new_value' });
240
+
241
+ // Load via loadSkillOverrides
242
+ const overrides = require('./config').loadSkillOverrides('test-skill');
243
+
244
+ // Should read from new location
245
+ expect(overrides.new_key).toBe('new_value');
246
+ });
247
+ });
248
+
249
+ describe('tool config migration', () => {
250
+ let testDir: string;
251
+
252
+ beforeEach(() => {
253
+ testDir = join(tmpdir(), `droid-migration-test-${Date.now()}`);
254
+ mkdirSync(testDir, { recursive: true });
255
+ });
256
+
257
+ afterEach(() => {
258
+ if (existsSync(testDir)) {
259
+ rmSync(testDir, { recursive: true });
260
+ }
261
+ });
262
+
263
+ it('migrateToolConfigs copies data from old override files', () => {
264
+ // Create old-style override file structure
265
+ const skillsDir = join(testDir, 'skills', 'brain');
266
+ mkdirSync(skillsDir, { recursive: true });
267
+
268
+ const overrideData = {
269
+ brain_dir: '~/my-brain',
270
+ inbox_folder: 'inbox',
271
+ };
272
+
273
+ const overridesPath = join(skillsDir, 'overrides.yaml');
274
+ writeFileSync(overridesPath, YAML.stringify(overrideData), 'utf-8');
275
+
276
+ // Create a config file without migration flag
277
+ const configPath = join(testDir, 'config.yaml');
278
+ const config: Partial<DroidConfig> = {
279
+ platform: 'claude-code' as any,
280
+ user_mention: '@test',
281
+ output_preference: 'terminal' as any,
282
+ git_username: 'test',
283
+ platforms: {},
284
+ };
285
+
286
+ writeFileSync(configPath, YAML.stringify(config), 'utf-8');
287
+
288
+ // Verify old file exists
289
+ expect(existsSync(overridesPath)).toBe(true);
290
+
291
+ // Verify the structure matches what migrateToolConfigs expects
292
+ const skillDirs = require('fs').readdirSync(join(testDir, 'skills'), {
293
+ withFileTypes: true,
294
+ });
295
+ expect(skillDirs.length).toBeGreaterThan(0);
296
+ expect(skillDirs[0].isDirectory()).toBe(true);
297
+ });
298
+
299
+ it('migration preserves old override files as backup', () => {
300
+ // Create old-style override file
301
+ const skillsDir = join(testDir, 'skills', 'codex');
302
+ mkdirSync(skillsDir, { recursive: true });
303
+
304
+ const overrideData = { codex_repo: '~/codex' };
305
+ const overridesPath = join(skillsDir, 'overrides.yaml');
306
+ writeFileSync(overridesPath, YAML.stringify(overrideData), 'utf-8');
307
+
308
+ // After migration, old file should still exist
309
+ expect(existsSync(overridesPath)).toBe(true);
310
+
311
+ // Read it back to verify it wasn't corrupted
312
+ const content = readFileSync(overridesPath, 'utf-8');
313
+ const parsed = YAML.parse(content);
314
+ expect(parsed.codex_repo).toBe('~/codex');
315
+ });
316
+ });
package/src/lib/config.ts CHANGED
@@ -1,4 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ readdirSync,
6
+ writeFileSync,
7
+ } from 'fs';
2
8
  import { homedir } from 'os';
3
9
  import { join } from 'path';
4
10
  import YAML from 'yaml';
@@ -9,6 +15,8 @@ import {
9
15
  type LegacyDroidConfig,
10
16
  type SkillOverrides,
11
17
  type AutoUpdateConfig,
18
+ type RepoConfig,
19
+ type ToolConfig,
12
20
  } from './types';
13
21
 
14
22
  const CONFIG_DIR = join(homedir(), '.droid');
@@ -60,6 +68,66 @@ function migrateConfig(
60
68
  };
61
69
  }
62
70
 
71
+ /**
72
+ * Migrate tool configs from old override files to config.tools.*
73
+ * Keeps old files as backup (will be removed in follow-up PR)
74
+ */
75
+ function migrateToolConfigs(config: DroidConfig): boolean {
76
+ // Skip if already migrated
77
+ if (config.migrations?.tools_consolidated) {
78
+ return false;
79
+ }
80
+
81
+ const skillsDir = join(CONFIG_DIR, 'skills');
82
+ if (!existsSync(skillsDir)) {
83
+ return false;
84
+ }
85
+
86
+ let migrated = false;
87
+
88
+ try {
89
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
90
+
91
+ for (const entry of entries) {
92
+ if (!entry.isDirectory()) continue;
93
+
94
+ const skillName = entry.name;
95
+ const overridesPath = join(skillsDir, skillName, 'overrides.yaml');
96
+
97
+ if (!existsSync(overridesPath)) continue;
98
+
99
+ try {
100
+ const content = readFileSync(overridesPath, 'utf-8');
101
+ const overrides = YAML.parse(content) as SkillOverrides;
102
+
103
+ if (overrides && Object.keys(overrides).length > 0) {
104
+ // Migrate to new location
105
+ if (!config.tools) {
106
+ config.tools = {};
107
+ }
108
+ config.tools[skillName] = overrides;
109
+ migrated = true;
110
+ }
111
+ } catch {
112
+ // Skip files that can't be parsed
113
+ continue;
114
+ }
115
+ }
116
+
117
+ if (migrated) {
118
+ // Record migration
119
+ if (!config.migrations) {
120
+ config.migrations = {};
121
+ }
122
+ config.migrations.tools_consolidated = true;
123
+ }
124
+
125
+ return migrated;
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+
63
131
  /**
64
132
  * Ensure the config directory exists
65
133
  */
@@ -94,11 +162,15 @@ export function loadConfig(): DroidConfig {
94
162
 
95
163
  // Check if migration is needed
96
164
  // TODO: Remove after v0.14.0 (target: late Jan 2025)
97
- const needsMigration = 'ai_tool' in rawConfig && !('platform' in rawConfig);
165
+ const needsLegacyMigration =
166
+ 'ai_tool' in rawConfig && !('platform' in rawConfig);
98
167
  const config = migrateConfig(rawConfig);
99
168
 
100
- // Save migrated config if it was migrated
101
- if (needsMigration) {
169
+ // Migrate tool configs from old override files to config.tools.*
170
+ const needsToolMigration = migrateToolConfigs(config);
171
+
172
+ // Save migrated config if any migration occurred
173
+ if (needsLegacyMigration || needsToolMigration) {
102
174
  saveConfig(config);
103
175
  }
104
176
 
@@ -193,8 +265,18 @@ export function getSkillOverridesPath(skillName: string): string {
193
265
 
194
266
  /**
195
267
  * Load skill-specific overrides
268
+ * Reads from new location first (config.tools.{name}), falls back to old override files
196
269
  */
197
270
  export function loadSkillOverrides(skillName: string): SkillOverrides {
271
+ const normalizedName = normalizeSkillNameForConfig(skillName);
272
+
273
+ // Check new location first (config.tools.{name})
274
+ const toolSettings = getToolSettings(normalizedName);
275
+ if (Object.keys(toolSettings).length > 0) {
276
+ return toolSettings as SkillOverrides;
277
+ }
278
+
279
+ // Fall back to old location (~/.droid/skills/{name}/overrides.yaml)
198
280
  const overridesPath = getSkillOverridesPath(skillName);
199
281
 
200
282
  if (!existsSync(overridesPath)) {
@@ -228,6 +310,35 @@ export function saveSkillOverrides(
228
310
  writeFileSync(overridesPath, content, 'utf-8');
229
311
  }
230
312
 
313
+ /**
314
+ * Get tool settings from config.tools.{name} (returns empty object if none)
315
+ */
316
+ export function getToolSettings(name: string): ToolConfig {
317
+ const config = loadConfig();
318
+ return (config.tools?.[name] as ToolConfig) ?? {};
319
+ }
320
+
321
+ /**
322
+ * Set all settings for a tool in config.tools.{name}
323
+ */
324
+ export function setToolSettings(name: string, settings: ToolConfig): void {
325
+ const config = loadConfig();
326
+ if (!config.tools) {
327
+ config.tools = {};
328
+ }
329
+ config.tools[name] = settings;
330
+ saveConfig(config);
331
+ }
332
+
333
+ /**
334
+ * Set a single key in tool settings
335
+ */
336
+ export function setToolSetting(name: string, key: string, value: unknown): void {
337
+ const settings = getToolSettings(name);
338
+ settings[key] = value;
339
+ setToolSettings(name, settings);
340
+ }
341
+
231
342
  /**
232
343
  * Get auto-update config with defaults applied
233
344
  */
@@ -251,3 +362,81 @@ export function setAutoUpdateConfig(updates: Partial<AutoUpdateConfig>): void {
251
362
  };
252
363
  saveConfig(config);
253
364
  }
365
+
366
+ /**
367
+ * Expand tilde (~) in paths to home directory
368
+ */
369
+ function expandTilde(path: string): string {
370
+ if (path.startsWith('~/')) {
371
+ return join(homedir(), path.slice(2));
372
+ }
373
+ return path;
374
+ }
375
+
376
+ /**
377
+ * Get all repos (returns empty array if none)
378
+ */
379
+ export function getRepos(): RepoConfig[] {
380
+ const config = loadConfig();
381
+ return config.repos ?? [];
382
+ }
383
+
384
+ /**
385
+ * Find repo by name
386
+ */
387
+ export function getRepo(name: string): RepoConfig | undefined {
388
+ const repos = getRepos();
389
+ return repos.find((repo) => repo.name === name);
390
+ }
391
+
392
+ /**
393
+ * Get repo path with tilde expansion
394
+ */
395
+ export function getRepoPath(name: string): string | undefined {
396
+ const repo = getRepo(name);
397
+ if (!repo) {
398
+ return undefined;
399
+ }
400
+ return expandTilde(repo.path);
401
+ }
402
+
403
+ /**
404
+ * Add or update a repo (updates if name exists)
405
+ */
406
+ export function addRepo(repo: RepoConfig): void {
407
+ const config = loadConfig();
408
+ const repos = config.repos ?? [];
409
+
410
+ // Check if repo already exists
411
+ const existingIndex = repos.findIndex((r) => r.name === repo.name);
412
+
413
+ if (existingIndex >= 0) {
414
+ // Update existing repo
415
+ repos[existingIndex] = repo;
416
+ } else {
417
+ // Add new repo
418
+ repos.push(repo);
419
+ }
420
+
421
+ config.repos = repos;
422
+ saveConfig(config);
423
+ }
424
+
425
+ /**
426
+ * Remove repo by name (returns true if existed)
427
+ */
428
+ export function removeRepo(name: string): boolean {
429
+ const config = loadConfig();
430
+ const repos = config.repos ?? [];
431
+
432
+ const existingIndex = repos.findIndex((r) => r.name === name);
433
+
434
+ if (existingIndex >= 0) {
435
+ repos.splice(existingIndex, 1);
436
+ config.repos = repos;
437
+ saveConfig(config);
438
+ return true;
439
+ }
440
+
441
+ return false;
442
+ }
package/src/lib/types.ts CHANGED
@@ -48,19 +48,31 @@ export interface AutoUpdateConfig {
48
48
  tools: boolean; // Auto-update installed tools (default: true)
49
49
  }
50
50
 
51
+ export interface RepoConfig {
52
+ name: string;
53
+ path: string;
54
+ description?: string;
55
+ }
56
+
57
+ // ToolConfig is flexible - each tool defines its own keys
58
+ export type ToolConfig = Record<string, unknown>;
59
+
51
60
  export interface DroidConfig {
52
61
  platform: Platform;
53
62
  user_mention: string;
54
63
  output_preference: OutputPreference;
55
64
  git_username: string;
56
65
  platforms: Record<string, PlatformConfig>;
66
+ repos?: RepoConfig[]; // Array - enumerate to discover available repos
67
+ tools?: Record<string, ToolConfig>; // Map - look up by tool name
57
68
  auto_update?: AutoUpdateConfig;
58
69
  ignored_platforms?: Platform[]; // Platforms to skip during multi-platform install
59
70
  migrations?: {
60
71
  package?: string; // Last package version migrated
61
72
  tools?: Record<string, string>; // tool name -> last tool version migrated
73
+ tools_consolidated?: boolean; // Tool configs migrated to config.tools.*
62
74
  // Legacy: old configs may have tool names at root level
63
- [key: string]: string | Record<string, string> | undefined;
75
+ [key: string]: string | Record<string, string> | boolean | undefined;
64
76
  };
65
77
  }
66
78
 
@@ -26,7 +26,7 @@ Your **scratchpad** (or **brain**) - a collaborative space for planning, researc
26
26
 
27
27
  ## Configuration
28
28
 
29
- **IMPORTANT:** Run `droid config brain` first and parse the JSON output. If `brain_dir` is not configured, **ask the user** where they want brain docs stored.
29
+ **IMPORTANT:** Run `droid config --get tools.brain` first and parse the JSON output. If `brain_dir` is not configured, **ask the user** where they want brain docs stored.
30
30
 
31
31
  | Setting | Default | Description |
32
32
  | -------------- | ------- | ------------------------------------------ |
@@ -134,7 +134,7 @@ In markdown files, use blockquote `>` prefix for @mention comments. The @mention
134
134
  | `> @droid ...` | User | AI | User asking/telling the AI |
135
135
  | `> @user ...` | AI | User | AI asking/telling the user |
136
136
 
137
- Get `user_mention` from `droid config brain`. If droid's `comments` skill is installed, use `/comments check` for full support.
137
+ Get `user_mention` from `droid config --get user_mention`. If droid's `comments` skill is installed, use `/comments check` for full support.
138
138
 
139
139
  ## Extensions
140
140
 
@@ -9,7 +9,7 @@ Detailed procedures for each brain operation.
9
9
  **Steps:**
10
10
 
11
11
  1. **Read config first (MANDATORY)**
12
- - Run `droid config brain` and parse the JSON output
12
+ - Run `droid config --get tools.brain` and parse the JSON output
13
13
  - Use the `brain_dir` value from the config
14
14
  - If `brain_dir` is not configured, **ask the user** where they want brain docs stored
15
15
  - **Do NOT skip this step or assume defaults**
@@ -48,7 +48,7 @@ Detailed procedures for each brain operation.
48
48
  **Steps:**
49
49
 
50
50
  1. **Read config first (MANDATORY)**
51
- - Run `droid config brain` and parse the JSON output
51
+ - Run `droid config --get tools.brain` and parse the JSON output
52
52
  - Use the `brain_dir` and `inbox_folder` values from the config
53
53
  - If `brain_dir` is not configured, **ask the user** where they want brain docs stored
54
54
  - **Do NOT skip this step or assume defaults**
@@ -78,8 +78,8 @@ Detailed procedures for each brain operation.
78
78
  - Related work or prior decisions?
79
79
 
80
80
  7. **Add placeholder comments** for sections needing user input:
81
- - Get `user_mention` from config: `droid config brain | grep user_mention`
82
- - Use `> @{user_mention}` for placeholders (e.g., `> @fry - Please provide requirements`)
81
+ - Get `user_mention` from config: `droid config --get user_mention`
82
+ - Use `> @{user_mention}` for placeholders (e.g., `> @{user} - Please provide requirements`)
83
83
  - **CRITICAL:** NEVER use `@droid` - that's for user-to-AI comments, not AI-to-user
84
84
 
85
85
  8. **Write the doc** with template structure and initial context
@@ -95,7 +95,7 @@ Detailed procedures for each brain operation.
95
95
  **Steps:**
96
96
 
97
97
  1. **Read config first (MANDATORY)**
98
- - Run `droid config brain` and parse the JSON output
98
+ - Run `droid config --get tools.brain` and parse the JSON output
99
99
  - Use the `brain_dir` and `inbox_folder` values from the config
100
100
  - If `brain_dir` is not configured, **ask the user** where they want brain docs stored
101
101
  - **Do NOT skip this step or assume defaults**
@@ -165,7 +165,7 @@ Detailed procedures for each brain operation.
165
165
  2. **Read active doc**
166
166
 
167
167
  3. **Check preserve_comments setting:**
168
- - Run `droid config comments` and parse the JSON output
168
+ - Run `droid config --get tools.comments` and parse the JSON output
169
169
  - Use the `preserve_comments` value (default: `true`)
170
170
 
171
171
  4. **Find all `> @droid` comments**
@@ -174,9 +174,9 @@ Detailed procedures for each brain operation.
174
174
  5. **For each comment:**
175
175
  - Show the comment and surrounding context
176
176
  - Address the question/request
177
- - Add response as blockquote with user's mention (from `droid config brain`): `> @{user_mention} Your response here`
178
- - Example: `> @fry Yes, the index covers that query pattern.`
179
- - **CRITICAL:** NEVER use `@droid` in your responses. Use `@{user_mention}` from config (e.g., `@fry`)
177
+ - Add response as blockquote with user's mention (from `droid config --get tools.brain`): `> @{user_mention} Your response here`
178
+ - Example: `> @{user} Yes, the index covers that query pattern.`
179
+ - **CRITICAL:** NEVER use `@droid` in your responses. Use `@{user_mention}` from config (e.g., `@{user}`)
180
180
  - **CRITICAL:** Always include the `>` prefix to maintain blockquote formatting
181
181
  - **CRITICAL:** If `preserve_comments: true`, keep the original `@droid` comment and add your response below it. Do NOT remove the original comment.
182
182
 
@@ -216,7 +216,7 @@ Detailed procedures for each brain operation.
216
216
  **Steps:**
217
217
 
218
218
  1. **Read config first (MANDATORY)**
219
- - Run `droid config brain` and parse the JSON output
219
+ - Run `droid config --get tools.brain` and parse the JSON output
220
220
  - Use the `brain_dir` value from the config
221
221
  - If `brain_dir` is not configured, **ask the user** where they want brain docs stored
222
222
  - **Do NOT skip this step or assume defaults**