@sudosandwich/limps 2.6.1 → 2.8.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 (51) hide show
  1. package/README.md +8 -9
  2. package/dist/cli/config-cmd.d.ts +29 -8
  3. package/dist/cli/config-cmd.d.ts.map +1 -1
  4. package/dist/cli/config-cmd.js +404 -29
  5. package/dist/cli/config-cmd.js.map +1 -1
  6. package/dist/cli/registry.d.ts +1 -1
  7. package/dist/cli/registry.js +1 -1
  8. package/dist/commands/config/discover.d.ts +1 -1
  9. package/dist/commands/config/discover.d.ts.map +1 -1
  10. package/dist/commands/config/discover.js +1 -1
  11. package/dist/commands/config/discover.js.map +1 -1
  12. package/dist/commands/config/index.d.ts.map +1 -1
  13. package/dist/commands/config/index.js +1 -1
  14. package/dist/commands/config/index.js.map +1 -1
  15. package/dist/commands/config/migrate.d.ts +3 -0
  16. package/dist/commands/config/migrate.d.ts.map +1 -0
  17. package/dist/commands/config/migrate.js +14 -0
  18. package/dist/commands/config/migrate.js.map +1 -0
  19. package/dist/commands/config/remove.d.ts +1 -1
  20. package/dist/commands/config/remove.d.ts.map +1 -1
  21. package/dist/commands/config/remove.js +2 -2
  22. package/dist/commands/config/remove.js.map +1 -1
  23. package/dist/config.d.ts +1 -0
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +39 -2
  26. package/dist/config.js.map +1 -1
  27. package/dist/extensions/context.d.ts +2 -1
  28. package/dist/extensions/context.d.ts.map +1 -1
  29. package/dist/extensions/context.js +2 -1
  30. package/dist/extensions/context.js.map +1 -1
  31. package/dist/resources/index.d.ts +2 -2
  32. package/dist/resources/index.d.ts.map +1 -1
  33. package/dist/resources/index.js +0 -13
  34. package/dist/resources/index.js.map +1 -1
  35. package/dist/rlm/helpers.d.ts +3 -1
  36. package/dist/rlm/helpers.d.ts.map +1 -1
  37. package/dist/rlm/helpers.js +17 -7
  38. package/dist/rlm/helpers.js.map +1 -1
  39. package/dist/tools/create-plan.d.ts.map +1 -1
  40. package/dist/tools/create-plan.js +19 -8
  41. package/dist/tools/create-plan.js.map +1 -1
  42. package/dist/tools/index.d.ts +1 -1
  43. package/dist/tools/index.js +1 -1
  44. package/dist/tools/update-task-status.d.ts.map +1 -1
  45. package/dist/tools/update-task-status.js +51 -21
  46. package/dist/tools/update-task-status.js.map +1 -1
  47. package/package.json +3 -2
  48. package/dist/resources/agents-status.d.ts +0 -31
  49. package/dist/resources/agents-status.d.ts.map +0 -1
  50. package/dist/resources/agents-status.js +0 -28
  51. package/dist/resources/agents-status.js.map +0 -1
package/README.md CHANGED
@@ -270,7 +270,7 @@ Config location varies by OS:
270
270
  "docsPaths": ["~/Documents/my-plans"],
271
271
  "fileExtensions": [".md"],
272
272
  "dataPath": "~/Library/Application Support/limps/data",
273
- "extensions": ["@sudosandwich/limps-radix"],
273
+ "extensions": ["@sudosandwich/limps-headless"],
274
274
  "scoring": {
275
275
  "weights": { "dependency": 40, "priority": 30, "workload": 30 },
276
276
  "biases": {}
@@ -302,23 +302,23 @@ limps exposes 15 MCP tools for AI assistants:
302
302
  Extensions add MCP tools and resources. Install from npm:
303
303
 
304
304
  ```bash
305
- npm install -g @sudosandwich/limps-radix
305
+ npm install -g @sudosandwich/limps-headless
306
306
  ```
307
307
 
308
308
  Add to config:
309
309
 
310
310
  ```json
311
311
  {
312
- "extensions": ["@sudosandwich/limps-radix"],
313
- "@sudosandwich/limps-radix": {
314
- "cacheDir": "~/Library/Application Support/limps-radix"
312
+ "extensions": ["@sudosandwich/limps-headless"],
313
+ "limps-headless": {
314
+ "cacheDir": "~/Library/Application Support/limps-headless"
315
315
  }
316
316
  }
317
317
  ```
318
318
 
319
319
  **Available extensions:**
320
320
 
321
- - `@sudosandwich/limps-radix` — Radix UI contract extraction and semantic analysis
321
+ - `@sudosandwich/limps-headless` — Headless UI contract extraction, semantic analysis, and drift detection (Radix UI and Base UI migration).
322
322
 
323
323
  ## Obsidian Compatibility
324
324
 
@@ -343,7 +343,7 @@ npm test
343
343
  This is a monorepo with:
344
344
 
345
345
  - `packages/limps` — Core MCP server
346
- - `packages/limps-radix` — Radix UI extension
346
+ - `packages/limps-headless` — Headless UI extension (Radix/Base UI contract extraction and audit)
347
347
 
348
348
  ## Used in Production
349
349
 
@@ -380,7 +380,7 @@ NNNN-descriptive-name/
380
380
 
381
381
  ### Why the prefixes?
382
382
 
383
- I chose this to keep things lexicographically ordered and easier to reference in chat. "Show me the next agent or agents we can run now in plan 25", and the MCP will run the tool to process the agents applying weights and biases to choose the next best task or tasks that can run in parallel.
383
+ I chose this to keep things lexicographically ordered and easier to reference in chat. "Show me the next agent or agents we can run now in plan NNNN-plan-name", and the MCP will run the tool to process the agents applying weights and biases to choose the next best task or tasks that can run in parallel.
384
384
 
385
385
  ## Deep Dive
386
386
 
@@ -490,7 +490,6 @@ Progressive disclosure via resources:
490
490
  | `plans://summary` | Plan summaries with key info |
491
491
  | `plans://full` | Full plan documents |
492
492
  | `decisions://log` | Decision log entries |
493
- | `agents://status` | Agent status and tasks |
494
493
 
495
494
  </details>
496
495
 
@@ -28,6 +28,8 @@ export declare function getProjectsData(): ProjectsData;
28
28
  export declare function configList(): string;
29
29
  /**
30
30
  * Switch to a different project.
31
+ * If the name is not in the registry but exists in the default discovery location
32
+ * (Application Support/<name>/config.json), registers it and sets as current.
31
33
  *
32
34
  * @param name - Project name to switch to
33
35
  * @returns Success message
@@ -80,14 +82,16 @@ export declare function configPath(resolveConfigPathFn: () => string): string;
80
82
  */
81
83
  export declare function configAdd(name: string, configFileOrDirPath: string): string;
82
84
  /**
83
- * Remove a project from the registry.
84
- * Does not delete any files.
85
+ * Remove a project from the registry and delete its config file and project directory
86
+ * when the config lives under limps/projects (Application Support/limps/projects/).
87
+ * Accepts either a project name (exact match, no path chars) or a path to a config file.
88
+ * Paths are strictly validated: must be under limps/projects and end with config.json.
85
89
  *
86
- * @param name - Project name to remove
90
+ * @param nameOrPath - Project name or path to config.json
87
91
  * @returns Success message
88
- * @throws Error if project not found
92
+ * @throws Error if project not found or path is invalid
89
93
  */
90
- export declare function configRemove(name: string): string;
94
+ export declare function configRemove(nameOrPath: string): string;
91
95
  /**
92
96
  * Set the current project from an existing config file path.
93
97
  * Auto-derives the project name from the parent directory.
@@ -100,12 +104,29 @@ export declare function configRemove(name: string): string;
100
104
  export declare function configSet(configFilePath: string): string;
101
105
  import { LocalMcpAdapter, type McpClientAdapter, type McpServerConfig } from './mcp-client-adapter.js';
102
106
  /**
103
- * Discover and register config files from default OS locations.
104
- * Scans the OS-specific application support directories for config.json files.
107
+ * Discover config files under limps/projects (Application Support/limps/projects/<name>/config.json).
108
+ * Does not auto-register; use `limps config use <name>` to register and switch.
105
109
  *
106
- * @returns Summary of discovered projects
110
+ * @returns Summary of discovered (unregistered) projects
107
111
  */
108
112
  export declare function configDiscover(): string;
113
+ /**
114
+ * Migrate known (registered) configs and configs from old locations into limps/projects/
115
+ * so the limps root is not polluted. Ensures limps/projects exists, then:
116
+ * 1. For each registered project: if config is not under limps/projects/<name>/, copy it there and update registry.
117
+ * 2. Pull from old sibling layout (Application Support/<name>/config.json): copy to limps/projects/<name>/, register.
118
+ * 3. Pull from flat limps layout (limps/<name>/config.json): copy to limps/projects/<name>/, register if not already.
119
+ * Repairs all paths inside each config JSON (plansPath, dataPath, docsPaths) and MCP client references.
120
+ *
121
+ * @returns Summary of migration (migrated count and paths)
122
+ */
123
+ /** One recorded move for migration reversal. */
124
+ export interface MigrationMove {
125
+ name: string;
126
+ sourcePath: string;
127
+ targetPath: string;
128
+ }
129
+ export declare function configMigrate(): string;
109
130
  /**
110
131
  * Generate MCP server configuration JSON for limps projects.
111
132
  * Returns the JSON structure that should be added to the client config.
@@ -1 +1 @@
1
- {"version":3,"file":"config-cmd.d.ts","sourceRoot":"","sources":["../../src/cli/config-cmd.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config-cmd.d.ts","sourceRoot":"","sources":["../../src/cli/config-cmd.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAmCH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,OAAO,CAAC;KACjB,EAAE,CAAC;IACJ,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,YAAY,CAY9C;AAED;;;;GAIG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAmBnC;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA6B9C;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE;QACN,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QACrB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;KAC3B,CAAC;CACH;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,mBAAmB,EAAE,MAAM,MAAM,GAAG,UAAU,CAkB3E;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,mBAAmB,EAAE,MAAM,MAAM,GAAG,MAAM,CA0FpE;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,mBAAmB,EAAE,MAAM,MAAM,GAAG,MAAM,CAEpE;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,GAAG,MAAM,CAqF3E;AA+BD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA+FvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAmCxD;AAED,OAAO,EAEL,eAAe,EAEf,KAAK,gBAAgB,EAErB,KAAK,eAAe,EACrB,MAAM,yBAAyB,CAAC;AAEjC;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAkDvC;AAwFD;;;;;;;;;GASG;AACH,gDAAgD;AAChD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAyBD,wBAAgB,aAAa,IAAI,MAAM,CAsItC;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,gBAAgB,EACzB,mBAAmB,EAAE,MAAM,MAAM,EACjC,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB;IACD,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACzC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC,CAuEA;AAqGD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,gBAAgB,EACzB,mBAAmB,EAAE,MAAM,MAAM,EACjC,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB;IACD,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB,CAoDA;AAuED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,gBAAgB,EACzB,mBAAmB,EAAE,MAAM,MAAM,EACjC,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,MAAM,CAoBR;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,mBAAmB,EAAE,MAAM,MAAM,EACjC,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,MAAM,CAGR;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,mBAAmB,EAAE,MAAM,MAAM,EACjC,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,MAAM,CAGR;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,mBAAmB,EAAE,MAAM,MAAM,EACjC,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,MAAM,CAGR;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,mBAAmB,EAAE,MAAM,MAAM,EACjC,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,MAAM,CAGR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,mBAAmB,EAAE,MAAM,MAAM,EACjC,aAAa,CAAC,EAAE,MAAM,EAAE,EACxB,aAAa,CAAC,EAAE,eAAe,GAAG,MAAM,GACvC,MAAM,CAWR;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAG7D;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,mBAAmB,EAAE,MAAM,MAAM,EACjC,aAAa,CAAC,EAAE,MAAM,EAAE,GACvB,MAAM,CAkDR;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAgDtF"}
@@ -2,13 +2,21 @@
2
2
  * Config subcommand handlers for limps CLI.
3
3
  * Provides commands to manage the project registry and view configuration.
4
4
  */
5
- import { existsSync, readdirSync, readFileSync, writeFileSync, statSync, mkdirSync } from 'fs';
5
+ import { existsSync, readdirSync, readFileSync, writeFileSync, statSync, mkdirSync, realpathSync, rmSync, } from 'fs';
6
6
  import * as jsonpatch from 'fast-json-patch';
7
- import { resolve, dirname, basename, join } from 'path';
7
+ import { resolve, dirname, basename, join, relative, sep } from 'path';
8
8
  import * as toml from '@iarna/toml';
9
9
  import { registerProject, unregisterProject, setCurrentProject, listProjects, loadRegistry, } from './registry.js';
10
- import { loadConfig } from '../config.js';
11
- import { getOSBasePath, getOSConfigPath, getOSDataPath } from '../utils/os-paths.js';
10
+ import { loadConfig, validateConfig } from '../config.js';
11
+ import { getOSBasePath } from '../utils/os-paths.js';
12
+ /** Child dir under limps for project configs (limps/projects/<name>/config.json). */
13
+ const PROJECTS_DIR = 'projects';
14
+ /**
15
+ * Root directory for project configs under limps (Application Support/limps/projects/).
16
+ */
17
+ function getProjectsRoot() {
18
+ return join(getOSBasePath('limps'), PROJECTS_DIR);
19
+ }
12
20
  /**
13
21
  * Get projects data for JSON output.
14
22
  *
@@ -49,14 +57,39 @@ export function configList() {
49
57
  }
50
58
  /**
51
59
  * Switch to a different project.
60
+ * If the name is not in the registry but exists in the default discovery location
61
+ * (Application Support/<name>/config.json), registers it and sets as current.
52
62
  *
53
63
  * @param name - Project name to switch to
54
64
  * @returns Success message
55
65
  * @throws Error if project not found
56
66
  */
57
67
  export function configUse(name) {
58
- setCurrentProject(name);
59
- return `Switched to project "${name}"`;
68
+ if (!name || !SAFE_NAME_REGEX.test(name)) {
69
+ throw new Error(`Invalid project name: must not be empty and must not contain path separators.\nRun \`limps config list\` to see available projects.`);
70
+ }
71
+ const registry = loadRegistry();
72
+ if (registry.projects[name]) {
73
+ setCurrentProject(name);
74
+ return `Switched to project "${name}"`;
75
+ }
76
+ // Not registered: try default discovery location so "discover" + "use <name>" works
77
+ const searchDir = getProjectsRoot();
78
+ const configPath = join(searchDir, name, 'config.json');
79
+ if (existsSync(configPath)) {
80
+ try {
81
+ const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
82
+ if (validateConfig(raw)) {
83
+ registerProject(name, configPath);
84
+ setCurrentProject(name);
85
+ return `Registered and switched to project "${name}"`;
86
+ }
87
+ }
88
+ catch {
89
+ // Not valid JSON or not a limps config, fall through to throw
90
+ }
91
+ }
92
+ throw new Error(`Project not found: ${name}\nRun \`limps config list\` to see available projects.`);
60
93
  }
61
94
  /**
62
95
  * Get configuration data for JSON output.
@@ -204,10 +237,10 @@ export function configAdd(name, configFileOrDirPath) {
204
237
  registerProject(name, configInDir);
205
238
  return `Registered project "${name}" with config: ${configInDir}`;
206
239
  }
207
- // No config.json found - create one in OS standard location
208
- const basePath = getOSBasePath(name);
209
- const configPath = getOSConfigPath(name);
210
- const dataPath = getOSDataPath(name);
240
+ // No config.json found - create one under limps/projects/<name>/
241
+ const basePath = join(getProjectsRoot(), name);
242
+ const configPath = join(basePath, 'config.json');
243
+ const dataPath = join(basePath, 'data');
211
244
  // Check if config already exists in OS location
212
245
  if (existsSync(configPath)) {
213
246
  throw new Error(`Config already exists at: ${configPath}\nTo reconfigure, delete the existing config file first.`);
@@ -253,17 +286,116 @@ export function configAdd(name, configFileOrDirPath) {
253
286
  registerProject(name, absolutePath);
254
287
  return `Registered project "${name}" with config: ${absolutePath}`;
255
288
  }
289
+ /** Project names must not contain path separators or traversal (prompt injection / misuse). */
290
+ const SAFE_NAME_REGEX = /^[^/\\]+$/;
291
+ /**
292
+ * Delete the config file and its project directory when under discovery root.
293
+ * Only deletes when the config path is under discovery root (application config directory).
294
+ */
295
+ function deleteConfigAndProjectDir(canonicalConfigPath, discoveryRootCanonical) {
296
+ const rel = relative(discoveryRootCanonical, canonicalConfigPath);
297
+ if (rel.split(sep).includes('..'))
298
+ return;
299
+ if (!existsSync(canonicalConfigPath))
300
+ return;
301
+ rmSync(canonicalConfigPath, { force: true });
302
+ const parentDir = dirname(canonicalConfigPath);
303
+ const parentRel = relative(discoveryRootCanonical, parentDir);
304
+ // Only delete parent when it is a project dir: discoveryRoot/<name>/config.json
305
+ if (basename(canonicalConfigPath) === 'config.json' &&
306
+ !parentRel.startsWith('..') &&
307
+ !parentRel.includes(sep) &&
308
+ parentRel !== '' &&
309
+ existsSync(parentDir)) {
310
+ rmSync(parentDir, { recursive: true, force: true });
311
+ }
312
+ }
256
313
  /**
257
- * Remove a project from the registry.
258
- * Does not delete any files.
314
+ * Remove a project from the registry and delete its config file and project directory
315
+ * when the config lives under limps/projects (Application Support/limps/projects/).
316
+ * Accepts either a project name (exact match, no path chars) or a path to a config file.
317
+ * Paths are strictly validated: must be under limps/projects and end with config.json.
259
318
  *
260
- * @param name - Project name to remove
319
+ * @param nameOrPath - Project name or path to config.json
261
320
  * @returns Success message
262
- * @throws Error if project not found
321
+ * @throws Error if project not found or path is invalid
263
322
  */
264
- export function configRemove(name) {
265
- unregisterProject(name);
266
- return `Removed project "${name}" from registry (files not deleted)`;
323
+ export function configRemove(nameOrPath) {
324
+ const registry = loadRegistry();
325
+ const discoveryRoot = getProjectsRoot();
326
+ let discoveryRootCanonical;
327
+ try {
328
+ discoveryRootCanonical = realpathSync(discoveryRoot);
329
+ }
330
+ catch {
331
+ discoveryRootCanonical = discoveryRoot;
332
+ }
333
+ if (SAFE_NAME_REGEX.test(nameOrPath)) {
334
+ if (registry.projects[nameOrPath]) {
335
+ const configPath = registry.projects[nameOrPath].configPath;
336
+ let canonicalPath = null;
337
+ try {
338
+ canonicalPath = realpathSync(resolve(configPath));
339
+ }
340
+ catch {
341
+ // Config file missing, just unregister
342
+ }
343
+ const underDiscovery = canonicalPath !== null &&
344
+ !relative(discoveryRootCanonical, canonicalPath).split(sep).includes('..');
345
+ if (underDiscovery && canonicalPath) {
346
+ deleteConfigAndProjectDir(canonicalPath, discoveryRootCanonical);
347
+ }
348
+ unregisterProject(nameOrPath);
349
+ return underDiscovery
350
+ ? `Removed project "${nameOrPath}" and deleted config and project directory.`
351
+ : `Removed project "${nameOrPath}" from registry.`;
352
+ }
353
+ throw new Error(`Project not found: ${nameOrPath}\nRun \`limps config list\` to see registered projects.`);
354
+ }
355
+ const absolutePath = resolve(nameOrPath);
356
+ if (basename(absolutePath) !== 'config.json') {
357
+ throw new Error(`Invalid path: must point to a config.json file.\nRun \`limps config list\` to see registered projects.`);
358
+ }
359
+ if (!existsSync(absolutePath)) {
360
+ throw new Error(`Project not found: ${nameOrPath}\nRun \`limps config list\` to see registered projects.`);
361
+ }
362
+ try {
363
+ const stat = statSync(absolutePath);
364
+ if (!stat.isFile()) {
365
+ throw new Error(`Invalid path: not a file.\nRun \`limps config list\` to see registered projects.`);
366
+ }
367
+ }
368
+ catch (err) {
369
+ if (err instanceof Error && err.message.startsWith('Invalid path:'))
370
+ throw err;
371
+ throw new Error(`Project not found: ${nameOrPath}\nRun \`limps config list\` to see registered projects.`);
372
+ }
373
+ let canonicalPath;
374
+ try {
375
+ canonicalPath = realpathSync(absolutePath);
376
+ }
377
+ catch {
378
+ throw new Error(`Project not found: ${nameOrPath}\nRun \`limps config list\` to see registered projects.`);
379
+ }
380
+ const rel = relative(discoveryRootCanonical, canonicalPath);
381
+ if (rel.split(sep).includes('..')) {
382
+ throw new Error(`Invalid path: must be under application config directory.\nRun \`limps config list\` to see registered projects.`);
383
+ }
384
+ const entry = Object.entries(registry.projects).find(([, p]) => {
385
+ try {
386
+ return realpathSync(resolve(p.configPath)) === canonicalPath;
387
+ }
388
+ catch {
389
+ return false;
390
+ }
391
+ });
392
+ if (entry) {
393
+ const [name] = entry;
394
+ deleteConfigAndProjectDir(canonicalPath, discoveryRootCanonical);
395
+ unregisterProject(name);
396
+ return `Removed project "${name}" and deleted config and project directory.`;
397
+ }
398
+ throw new Error(`Project not found: ${nameOrPath}\nRun \`limps config list\` to see registered projects.`);
267
399
  }
268
400
  /**
269
401
  * Set the current project from an existing config file path.
@@ -303,16 +435,15 @@ export function configSet(configFilePath) {
303
435
  }
304
436
  import { getAdapter, LocalMcpAdapter, } from './mcp-client-adapter.js';
305
437
  /**
306
- * Discover and register config files from default OS locations.
307
- * Scans the OS-specific application support directories for config.json files.
438
+ * Discover config files under limps/projects (Application Support/limps/projects/<name>/config.json).
439
+ * Does not auto-register; use `limps config use <name>` to register and switch.
308
440
  *
309
- * @returns Summary of discovered projects
441
+ * @returns Summary of discovered (unregistered) projects
310
442
  */
311
443
  export function configDiscover() {
312
- // Get the parent directory of where limps stores its config
444
+ // Scan under limps/projects (Application Support/limps/projects/<name>/config.json)
313
445
  // This respects any mocking of getOSBasePath for testing
314
- const limpsBasePath = getOSBasePath('limps');
315
- const searchDir = dirname(limpsBasePath);
446
+ const searchDir = getProjectsRoot();
316
447
  const registry = loadRegistry();
317
448
  const registeredPaths = new Set(Object.values(registry.projects).map((p) => p.configPath));
318
449
  const discovered = [];
@@ -324,18 +455,19 @@ export function configDiscover() {
324
455
  for (const entry of entries) {
325
456
  if (!entry.isDirectory())
326
457
  continue;
327
- const configPath = `${searchDir}/${entry.name}/config.json`;
458
+ const configPath = join(searchDir, entry.name, 'config.json');
328
459
  // Skip if already registered or doesn't exist
329
460
  if (registeredPaths.has(configPath) || !existsSync(configPath))
330
461
  continue;
331
- // Validate it's a valid limps config
462
+ // Only list configs that are actually limps configs (plansPath, dataPath, scoring)
332
463
  try {
333
- loadConfig(configPath);
334
- registerProject(entry.name, configPath);
464
+ const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
465
+ if (!validateConfig(raw))
466
+ continue;
335
467
  discovered.push({ name: entry.name, path: configPath });
336
468
  }
337
469
  catch {
338
- // Not a valid limps config, skip
470
+ // Not valid JSON or not a limps config, skip
339
471
  }
340
472
  }
341
473
  }
@@ -350,7 +482,250 @@ export function configDiscover() {
350
482
  lines.push(` ${name}: ${path}`);
351
483
  }
352
484
  lines.push('');
353
- lines.push('Run `limps config use <name>` to switch to a project.');
485
+ lines.push('Run `limps config use <name>` to register and switch to a project.');
486
+ return lines.join('\n');
487
+ }
488
+ /**
489
+ * Repair MCP client config references: replace old config path with new path in all
490
+ * global MCP client configs (Claude Desktop, Cursor, Claude Code, Codex).
491
+ */
492
+ function repairMcpConfigReferences(oldPath, newPath) {
493
+ const oldResolved = resolve(oldPath);
494
+ const newResolved = resolve(newPath);
495
+ const adapters = ['claude', 'cursor', 'claude-code', 'codex'];
496
+ for (const clientType of adapters) {
497
+ try {
498
+ const adapter = getAdapter(clientType);
499
+ const config = adapter.readConfig();
500
+ const serversKey = adapter.getServersKey();
501
+ let servers;
502
+ if (adapter.useFlatKey?.()) {
503
+ servers = config[serversKey];
504
+ }
505
+ else {
506
+ const keyParts = serversKey.split('.');
507
+ let current = config;
508
+ for (let i = 0; i < keyParts.length - 1; i++) {
509
+ const part = keyParts[i];
510
+ if (!current[part] || typeof current[part] !== 'object')
511
+ break;
512
+ current = current[part];
513
+ }
514
+ servers = current[keyParts[keyParts.length - 1]];
515
+ }
516
+ if (!servers || typeof servers !== 'object')
517
+ continue;
518
+ let changed = false;
519
+ for (const server of Object.values(servers)) {
520
+ if (!server ||
521
+ typeof server !== 'object' ||
522
+ !Array.isArray(server.args))
523
+ continue;
524
+ const args = server.args;
525
+ for (let i = 0; i < args.length; i++) {
526
+ const arg = args[i];
527
+ if (typeof arg !== 'string')
528
+ continue;
529
+ if (arg === oldPath || resolve(arg) === oldResolved) {
530
+ args[i] = newResolved;
531
+ changed = true;
532
+ }
533
+ }
534
+ }
535
+ if (changed)
536
+ adapter.writeConfig(config);
537
+ }
538
+ catch {
539
+ // Skip if config missing or invalid
540
+ }
541
+ }
542
+ }
543
+ /**
544
+ * Repair all path fields in a config JSON so paths under oldDir are rewritten under targetDir.
545
+ * Handles plansPath, dataPath, docsPaths (array). Preserves relative structure under the project dir.
546
+ */
547
+ function repairConfigPaths(raw, oldDir, targetDir) {
548
+ const oldDirResolved = resolve(oldDir);
549
+ const targetDirResolved = resolve(targetDir);
550
+ function rewritePath(pathStr) {
551
+ const resolved = resolve(pathStr);
552
+ if (resolved === oldDirResolved || resolved.startsWith(oldDirResolved + sep)) {
553
+ const rel = relative(oldDirResolved, resolved);
554
+ return rel === '' ? targetDirResolved : join(targetDirResolved, rel);
555
+ }
556
+ return pathStr;
557
+ }
558
+ if (raw.plansPath && typeof raw.plansPath === 'string') {
559
+ raw.plansPath = rewritePath(raw.plansPath);
560
+ }
561
+ if (raw.dataPath && typeof raw.dataPath === 'string') {
562
+ raw.dataPath = rewritePath(raw.dataPath);
563
+ }
564
+ if (raw.docsPaths && Array.isArray(raw.docsPaths)) {
565
+ raw.docsPaths = raw.docsPaths.map((p) => (typeof p === 'string' ? rewritePath(p) : p));
566
+ }
567
+ }
568
+ /** Write migration log so the run can be reversed. */
569
+ function writeMigrationLog(limpsRoot, moves) {
570
+ const timestamp = new Date().toISOString();
571
+ const safeTimestamp = timestamp.replace(/[:.]/g, '-');
572
+ const logPath = join(limpsRoot, `migration-${safeTimestamp}.json`);
573
+ const reverseSteps = moves.map((m) => ` ${m.name}: copy "${m.targetPath}" → "${m.sourcePath}", then \`limps config set "${m.sourcePath}"\`, then remove "${m.targetPath}" and its directory.`);
574
+ const log = {
575
+ timestamp,
576
+ command: 'limps config migrate',
577
+ moves,
578
+ reverseInstructions: [
579
+ 'To reverse this migration, for each move:',
580
+ ...reverseSteps,
581
+ 'Then run `limps config remove <name>` for each project and re-add with the source path if desired.',
582
+ ].join('\n'),
583
+ };
584
+ writeFileSync(logPath, JSON.stringify(log, null, 2));
585
+ return logPath;
586
+ }
587
+ export function configMigrate() {
588
+ const projectsRoot = getProjectsRoot();
589
+ const limpsRoot = getOSBasePath('limps');
590
+ const parentOfLimps = dirname(limpsRoot);
591
+ mkdirSync(projectsRoot, { recursive: true });
592
+ const registry = loadRegistry();
593
+ const lines = ['Migrating configs into limps/projects/', ''];
594
+ const moves = [];
595
+ let migrated = 0;
596
+ function copyConfigToProjectDir(name, sourcePath) {
597
+ const targetDir = join(projectsRoot, name);
598
+ const targetPath = join(targetDir, 'config.json');
599
+ if (!existsSync(sourcePath))
600
+ return false;
601
+ try {
602
+ const raw = JSON.parse(readFileSync(sourcePath, 'utf-8'));
603
+ if (!validateConfig(raw))
604
+ return false;
605
+ const oldDir = dirname(sourcePath);
606
+ repairConfigPaths(raw, oldDir, targetDir);
607
+ mkdirSync(targetDir, { recursive: true });
608
+ writeFileSync(targetPath, JSON.stringify(raw, null, 2));
609
+ registerProject(name, targetPath);
610
+ repairMcpConfigReferences(sourcePath, targetPath);
611
+ rmSync(sourcePath, { force: true });
612
+ if (basename(sourcePath) === 'config.json' && existsSync(oldDir)) {
613
+ rmSync(oldDir, { recursive: true, force: true });
614
+ }
615
+ migrated++;
616
+ moves.push({ name, sourcePath, targetPath });
617
+ lines.push(` ${name}: ${sourcePath} → ${targetPath}`);
618
+ return true;
619
+ }
620
+ catch {
621
+ return false;
622
+ }
623
+ }
624
+ // 1. Registered projects not already under limps/projects/<name>/
625
+ for (const [name, project] of Object.entries(registry.projects)) {
626
+ const currentPath = resolve(project.configPath);
627
+ const targetPath = join(projectsRoot, name, 'config.json');
628
+ let currentCanonical;
629
+ let targetCanonical;
630
+ try {
631
+ currentCanonical = realpathSync(currentPath);
632
+ targetCanonical = existsSync(targetPath) ? realpathSync(targetPath) : targetPath;
633
+ }
634
+ catch {
635
+ continue;
636
+ }
637
+ if (currentCanonical === targetCanonical)
638
+ continue;
639
+ const rel = relative(projectsRoot, currentPath);
640
+ if (!rel.split(sep).includes('..'))
641
+ continue; // already under projects
642
+ copyConfigToProjectDir(name, currentPath);
643
+ }
644
+ // 2. Old sibling layout: Application Support/<name>/config.json
645
+ try {
646
+ if (existsSync(parentOfLimps)) {
647
+ const entries = readdirSync(parentOfLimps, { withFileTypes: true });
648
+ for (const entry of entries) {
649
+ if (!entry.isDirectory() || entry.name === 'limps')
650
+ continue;
651
+ const sourcePath = join(parentOfLimps, entry.name, 'config.json');
652
+ if (!existsSync(sourcePath))
653
+ continue;
654
+ if (registry.projects[entry.name])
655
+ continue; // already registered and possibly migrated
656
+ copyConfigToProjectDir(entry.name, sourcePath);
657
+ }
658
+ }
659
+ }
660
+ catch {
661
+ // ignore
662
+ }
663
+ // 3. Flat limps layout: limps/<name>/config.json (excluding projects)
664
+ try {
665
+ if (existsSync(limpsRoot)) {
666
+ const entries = readdirSync(limpsRoot, { withFileTypes: true });
667
+ for (const entry of entries) {
668
+ if (!entry.isDirectory() || entry.name === PROJECTS_DIR)
669
+ continue;
670
+ const sourcePath = join(limpsRoot, entry.name, 'config.json');
671
+ if (!existsSync(sourcePath))
672
+ continue;
673
+ const targetPath = join(projectsRoot, entry.name, 'config.json');
674
+ if (existsSync(targetPath))
675
+ continue; // already in projects
676
+ copyConfigToProjectDir(entry.name, sourcePath);
677
+ }
678
+ }
679
+ }
680
+ catch {
681
+ // ignore
682
+ }
683
+ // 4. Strip coordination keys from all project configs under limps/projects/
684
+ const deprecatedCoordinationKeys = [
685
+ 'coordinationPath',
686
+ 'heartbeatTimeout',
687
+ 'debounceDelay',
688
+ 'maxHandoffIterations',
689
+ ];
690
+ let coordinationCleaned = 0;
691
+ try {
692
+ const projectDirs = readdirSync(projectsRoot, { withFileTypes: true });
693
+ for (const entry of projectDirs) {
694
+ if (!entry.isDirectory())
695
+ continue;
696
+ const configPath = join(projectsRoot, entry.name, 'config.json');
697
+ if (!existsSync(configPath))
698
+ continue;
699
+ try {
700
+ const raw = readFileSync(configPath, 'utf-8');
701
+ const hadDeprecated = deprecatedCoordinationKeys.some((k) => raw.includes(`"${k}"`));
702
+ if (hadDeprecated) {
703
+ loadConfig(configPath);
704
+ coordinationCleaned++;
705
+ }
706
+ }
707
+ catch {
708
+ // ignore
709
+ }
710
+ }
711
+ }
712
+ catch {
713
+ // ignore
714
+ }
715
+ if (coordinationCleaned > 0) {
716
+ lines.push('');
717
+ lines.push(`Coordination keys removed from ${coordinationCleaned} config(s).`);
718
+ }
719
+ if (migrated === 0) {
720
+ if (coordinationCleaned === 0) {
721
+ return 'No configs to migrate. All known configs are already under limps/projects/.';
722
+ }
723
+ return lines.join('\n');
724
+ }
725
+ const logPath = writeMigrationLog(limpsRoot, moves);
726
+ lines.push('');
727
+ lines.push(`Migrated ${migrated} project(s). Run \`limps config list\` to see current projects.`);
728
+ lines.push(`Reversal log: ${logPath}`);
354
729
  return lines.join('\n');
355
730
  }
356
731
  /**