@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.
- package/README.md +8 -9
- package/dist/cli/config-cmd.d.ts +29 -8
- package/dist/cli/config-cmd.d.ts.map +1 -1
- package/dist/cli/config-cmd.js +404 -29
- package/dist/cli/config-cmd.js.map +1 -1
- package/dist/cli/registry.d.ts +1 -1
- package/dist/cli/registry.js +1 -1
- package/dist/commands/config/discover.d.ts +1 -1
- package/dist/commands/config/discover.d.ts.map +1 -1
- package/dist/commands/config/discover.js +1 -1
- package/dist/commands/config/discover.js.map +1 -1
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +1 -1
- package/dist/commands/config/index.js.map +1 -1
- package/dist/commands/config/migrate.d.ts +3 -0
- package/dist/commands/config/migrate.d.ts.map +1 -0
- package/dist/commands/config/migrate.js +14 -0
- package/dist/commands/config/migrate.js.map +1 -0
- package/dist/commands/config/remove.d.ts +1 -1
- package/dist/commands/config/remove.d.ts.map +1 -1
- package/dist/commands/config/remove.js +2 -2
- package/dist/commands/config/remove.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +39 -2
- package/dist/config.js.map +1 -1
- package/dist/extensions/context.d.ts +2 -1
- package/dist/extensions/context.d.ts.map +1 -1
- package/dist/extensions/context.js +2 -1
- package/dist/extensions/context.js.map +1 -1
- package/dist/resources/index.d.ts +2 -2
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +0 -13
- package/dist/resources/index.js.map +1 -1
- package/dist/rlm/helpers.d.ts +3 -1
- package/dist/rlm/helpers.d.ts.map +1 -1
- package/dist/rlm/helpers.js +17 -7
- package/dist/rlm/helpers.js.map +1 -1
- package/dist/tools/create-plan.d.ts.map +1 -1
- package/dist/tools/create-plan.js +19 -8
- package/dist/tools/create-plan.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/tools/update-task-status.d.ts.map +1 -1
- package/dist/tools/update-task-status.js +51 -21
- package/dist/tools/update-task-status.js.map +1 -1
- package/package.json +3 -2
- package/dist/resources/agents-status.d.ts +0 -31
- package/dist/resources/agents-status.d.ts.map +0 -1
- package/dist/resources/agents-status.js +0 -28
- 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-
|
|
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-
|
|
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-
|
|
313
|
-
"
|
|
314
|
-
"cacheDir": "~/Library/Application Support/limps-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
package/dist/cli/config-cmd.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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(
|
|
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
|
|
104
|
-
*
|
|
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":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"config-cmd.d.ts","sourceRoot":"","sources":["../../src/cli/config-cmd.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/config-cmd.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
59
|
-
|
|
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
|
|
208
|
-
const basePath =
|
|
209
|
-
const configPath =
|
|
210
|
-
const dataPath =
|
|
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
|
-
*
|
|
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
|
|
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(
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
307
|
-
*
|
|
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
|
-
//
|
|
444
|
+
// Scan under limps/projects (Application Support/limps/projects/<name>/config.json)
|
|
313
445
|
// This respects any mocking of getOSBasePath for testing
|
|
314
|
-
const
|
|
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 =
|
|
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
|
-
//
|
|
462
|
+
// Only list configs that are actually limps configs (plansPath, dataPath, scoring)
|
|
332
463
|
try {
|
|
333
|
-
|
|
334
|
-
|
|
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
|
|
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
|
/**
|