ide-agents 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/scan.js CHANGED
@@ -15,7 +15,7 @@ function parseAllowedScope(data) {
15
15
  if (scope === "global" || scope === "project" || scope === "any") {
16
16
  return scope;
17
17
  }
18
- return null;
18
+ return "any";
19
19
  }
20
20
  function parseAgentDescription(content, fallbackName) {
21
21
  const parsed = matter(content);
@@ -37,45 +37,74 @@ function parseAgentDescription(content, fallbackName) {
37
37
  }
38
38
  return fallbackName;
39
39
  }
40
- async function scanSkills(repoPath) {
41
- const skillsDir = path.join(repoPath, "skills");
42
- if (!(await pathExists(skillsDir))) {
40
+ async function parseSkillDirectory(skillDir, dirName) {
41
+ const skillMdPath = path.join(skillDir, "SKILL.md");
42
+ const hasSkillMd = await pathExists(skillMdPath);
43
+ let name = dirName;
44
+ let description = "";
45
+ let allowedScope = "any";
46
+ if (hasSkillMd) {
47
+ const raw = await readFile(skillMdPath, "utf8");
48
+ const parsed = matter(raw);
49
+ if (typeof parsed.data.name === "string" && parsed.data.name.trim()) {
50
+ name = parsed.data.name.trim();
51
+ }
52
+ if (typeof parsed.data.description === "string") {
53
+ description = parsed.data.description.trim();
54
+ }
55
+ allowedScope = parseAllowedScope(parsed.data);
56
+ }
57
+ return { id: dirName, name, description, hasSkillMd, allowedScope };
58
+ }
59
+ function toSkillArtifact(sourcePath, parsed) {
60
+ return {
61
+ id: parsed.id,
62
+ kind: "skill",
63
+ sourcePath,
64
+ name: parsed.name,
65
+ description: parsed.description,
66
+ hasSkillMd: parsed.hasSkillMd,
67
+ allowedScope: parsed.allowedScope,
68
+ };
69
+ }
70
+ async function scanSkillsInDirectory(parentDir, sourcePathPrefix) {
71
+ if (!(await pathExists(parentDir))) {
43
72
  return [];
44
73
  }
45
- const entries = await readdir(skillsDir, { withFileTypes: true });
74
+ const entries = await readdir(parentDir, { withFileTypes: true });
46
75
  const artifacts = [];
47
76
  for (const entry of entries) {
48
77
  if (!entry.isDirectory())
49
78
  continue;
50
- const skillDir = path.join(skillsDir, entry.name);
79
+ if (entry.name.startsWith("."))
80
+ continue;
81
+ const skillDir = path.join(parentDir, entry.name);
51
82
  const skillMdPath = path.join(skillDir, "SKILL.md");
52
- const hasSkillMd = await pathExists(skillMdPath);
53
- let name = entry.name;
54
- let description = "";
55
- let allowedScope = null;
56
- if (hasSkillMd) {
57
- const raw = await readFile(skillMdPath, "utf8");
58
- const parsed = matter(raw);
59
- if (typeof parsed.data.name === "string" && parsed.data.name.trim()) {
60
- name = parsed.data.name.trim();
61
- }
62
- if (typeof parsed.data.description === "string") {
63
- description = parsed.data.description.trim();
64
- }
65
- allowedScope = parseAllowedScope(parsed.data);
83
+ if (!(await pathExists(skillMdPath))) {
84
+ continue;
66
85
  }
67
- artifacts.push({
68
- id: entry.name,
69
- kind: "skill",
70
- sourcePath: path.join("skills", entry.name),
71
- name,
72
- description,
73
- hasSkillMd,
74
- allowedScope,
75
- });
86
+ const parsed = await parseSkillDirectory(skillDir, entry.name);
87
+ const relativeSource = sourcePathPrefix
88
+ ? path.join(sourcePathPrefix, entry.name)
89
+ : entry.name;
90
+ artifacts.push(toSkillArtifact(relativeSource, parsed));
76
91
  }
77
92
  return artifacts.sort((a, b) => a.id.localeCompare(b.id));
78
93
  }
94
+ async function scanSkillsNested(repoPath) {
95
+ return scanSkillsInDirectory(path.join(repoPath, "skills"), "skills");
96
+ }
97
+ /** Repos like bluriesophos/cursorskills: `<repo>/<skill-name>/SKILL.md` at root. */
98
+ async function scanSkillsFlat(repoPath) {
99
+ return scanSkillsInDirectory(repoPath, "");
100
+ }
101
+ async function scanSkills(repoPath) {
102
+ const nested = await scanSkillsNested(repoPath);
103
+ if (nested.length > 0) {
104
+ return nested;
105
+ }
106
+ return scanSkillsFlat(repoPath);
107
+ }
79
108
  async function scanAgents(repoPath) {
80
109
  const agentsDir = path.join(repoPath, "agents");
81
110
  if (!(await pathExists(agentsDir))) {
package/dist/server.js CHANGED
@@ -6,8 +6,9 @@ import { cloneRepo, fetchRepo, getGitStatusWithoutFetch, pullRepo, } from "./git
6
6
  import { applyInstallations } from "./apply.js";
7
7
  import { scanRepoArtifacts } from "./scan.js";
8
8
  import { getArtifactTargets } from "./targets.js";
9
- import { getIdeAgentsHome, getRepoPath, getWebDistDir, slugFromUrl, } from "./paths.js";
10
- import { PACKAGE_VERSION as VERSION } from "./types.js";
9
+ import { expandUserPath, getIdeAgentsHome, getRepoPath, getWebDistDir, slugFromUrl, } from "./paths.js";
10
+ import { defaultAdapterFromIdes, getDefaultIdes } from "./ides.js";
11
+ import { PACKAGE_VERSION as VERSION } from "./version.js";
11
12
  export async function createServer(options) {
12
13
  const app = Fastify({ logger: false });
13
14
  app.setErrorHandler((error, _request, reply) => {
@@ -19,21 +20,70 @@ export async function createServer(options) {
19
20
  const config = await readConfig();
20
21
  return {
21
22
  home: getIdeAgentsHome(),
22
- adapter: config.adapter,
23
23
  port: options.port,
24
24
  version: VERSION,
25
25
  defaultProjectPath: options.launchCwd ?? null,
26
+ ides: config.ides,
26
27
  };
27
28
  });
29
+ app.get("/api/settings", async () => {
30
+ const config = await readConfig();
31
+ return {
32
+ ides: config.ides,
33
+ defaults: getDefaultIdes(),
34
+ home: getIdeAgentsHome(),
35
+ version: VERSION,
36
+ };
37
+ });
38
+ app.put("/api/settings", async (request, reply) => {
39
+ const { ides } = request.body ?? {};
40
+ if (!ides) {
41
+ return reply.status(400).send({ error: "ides is required" });
42
+ }
43
+ for (const key of ["cursor", "claude", "codex"]) {
44
+ const entry = ides[key];
45
+ if (!entry || typeof entry.configPath !== "string" || !entry.configPath.trim()) {
46
+ return reply.status(400).send({ error: `Invalid config for ${key}` });
47
+ }
48
+ }
49
+ const normalized = {
50
+ cursor: {
51
+ enabled: Boolean(ides.cursor.enabled),
52
+ configPath: expandUserPath(ides.cursor.configPath),
53
+ },
54
+ claude: {
55
+ enabled: Boolean(ides.claude.enabled),
56
+ configPath: expandUserPath(ides.claude.configPath),
57
+ },
58
+ codex: {
59
+ enabled: Boolean(ides.codex.enabled),
60
+ configPath: expandUserPath(ides.codex.configPath),
61
+ },
62
+ };
63
+ if (!normalized.cursor.enabled &&
64
+ !normalized.claude.enabled &&
65
+ !normalized.codex.enabled) {
66
+ return reply.status(400).send({ error: "At least one IDE must be enabled" });
67
+ }
68
+ const config = await readConfig();
69
+ config.ides = normalized;
70
+ config.adapter = defaultAdapterFromIdes(config.ides);
71
+ await writeConfig(config);
72
+ return { ides: config.ides };
73
+ });
28
74
  app.get("/api/repos", async () => {
29
75
  const config = await readConfig();
30
76
  const repos = [];
31
77
  for (const repo of config.repos) {
32
78
  const git = await getGitStatusWithoutFetch(repo.slug, repo.ref);
79
+ const repoPath = getRepoPath(repo.slug);
80
+ const artifacts = await scanRepoArtifacts(repoPath);
33
81
  repos.push({
34
82
  ...repo,
35
- localPath: getRepoPath(repo.slug),
83
+ localPath: repoPath,
36
84
  git,
85
+ skillCount: artifacts.filter((a) => a.kind === "skill").length,
86
+ agentCount: artifacts.filter((a) => a.kind === "agent").length,
37
87
  });
38
88
  }
39
89
  return { repos };
@@ -97,6 +147,23 @@ export async function createServer(options) {
97
147
  const git = await getGitStatusWithoutFetch(repo.slug, repo.ref);
98
148
  return { git };
99
149
  });
150
+ app.post("/api/repos/:id/check-updates", async (request, reply) => {
151
+ const config = await readConfig();
152
+ const repo = config.repos.find((r) => r.id === request.params.id);
153
+ if (!repo) {
154
+ return reply.status(404).send({ error: "Repository not found" });
155
+ }
156
+ try {
157
+ await fetchRepo(repo.slug);
158
+ }
159
+ catch (err) {
160
+ return reply.status(400).send({
161
+ error: err instanceof Error ? err.message : String(err),
162
+ });
163
+ }
164
+ const git = await getGitStatusWithoutFetch(repo.slug, repo.ref);
165
+ return { git };
166
+ });
100
167
  app.post("/api/repos/:id/pull", async (request, reply) => {
101
168
  const config = await readConfig();
102
169
  const repo = config.repos.find((r) => r.id === request.params.id);
@@ -124,7 +191,7 @@ export async function createServer(options) {
124
191
  const scanned = await scanRepoArtifacts(getRepoPath(repo.slug));
125
192
  const artifacts = await Promise.all(scanned.map(async (artifact) => ({
126
193
  ...artifact,
127
- targets: await getArtifactTargets(artifact, projectPath, config.adapter),
194
+ targets: await getArtifactTargets(artifact, projectPath, config),
128
195
  })));
129
196
  return { artifacts };
130
197
  });
package/dist/targets.d.ts CHANGED
@@ -1,4 +1,5 @@
1
+ import type { IdeAgentsConfig } from "./types.js";
1
2
  import type { Artifact, ArtifactTargets, Installation, TargetStatus } from "./types.js";
2
3
  export declare function getTargetStatus(targetPath: string): Promise<TargetStatus>;
3
- export declare function getArtifactTargets(artifact: Pick<Artifact, "kind" | "id">, projectPath: string | null, adapterId: string): Promise<ArtifactTargets>;
4
+ export declare function getArtifactTargets(artifact: Pick<Artifact, "kind" | "id">, projectPath: string | null, config: IdeAgentsConfig): Promise<ArtifactTargets>;
4
5
  export declare function installationStub(artifact: Pick<Artifact, "kind" | "id" | "sourcePath">, projectPath: string | null): Pick<Installation, "kind" | "targetName" | "sourcePath" | "projectPath">;
package/dist/targets.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { lstat } from "node:fs/promises";
2
- import { getAdapter } from "./adapters/cursor.js";
2
+ import { getEnabledAdapters } from "./adapters/index.js";
3
3
  export async function getTargetStatus(targetPath) {
4
4
  try {
5
5
  const stats = await lstat(targetPath);
@@ -18,17 +18,35 @@ export async function getTargetStatus(targetPath) {
18
18
  };
19
19
  }
20
20
  }
21
- export async function getArtifactTargets(artifact, projectPath, adapterId) {
22
- const adapter = getAdapter(adapterId);
21
+ function mergeTargetStatus(current, next) {
22
+ return {
23
+ exists: current.exists || next.exists,
24
+ isSymlink: current.isSymlink || next.isSymlink,
25
+ blocked: current.blocked || next.blocked,
26
+ };
27
+ }
28
+ export async function getArtifactTargets(artifact, projectPath, config) {
29
+ const adapters = getEnabledAdapters(config);
23
30
  const installation = {
24
31
  kind: artifact.kind,
25
32
  targetName: artifact.id,
26
33
  projectPath,
27
34
  };
28
- const global = await getTargetStatus(adapter.getGlobalTargetPath(installation));
35
+ let global = {
36
+ exists: false,
37
+ isSymlink: false,
38
+ blocked: false,
39
+ };
29
40
  let project = null;
30
- if (projectPath) {
31
- project = await getTargetStatus(adapter.getProjectTargetPath(installation));
41
+ for (const adapter of adapters) {
42
+ const globalStatus = await getTargetStatus(adapter.getGlobalTargetPath(installation));
43
+ global = mergeTargetStatus(global, globalStatus);
44
+ if (projectPath) {
45
+ const projectStatus = await getTargetStatus(adapter.getProjectTargetPath(installation));
46
+ project = project
47
+ ? mergeTargetStatus(project, projectStatus)
48
+ : projectStatus;
49
+ }
32
50
  }
33
51
  return { global, project };
34
52
  }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,17 @@
1
1
  export type ArtifactKind = "skill" | "agent";
2
2
  export type ArtifactAllowedScope = "global" | "project" | "any";
3
- export type AdapterId = "cursor";
3
+ export type IdeId = "cursor" | "claude" | "codex";
4
+ /** @deprecated use IdeId */
5
+ export type AdapterId = IdeId;
6
+ export interface IdeToolConfig {
7
+ enabled: boolean;
8
+ configPath: string;
9
+ }
10
+ export interface IdesConfig {
11
+ cursor: IdeToolConfig;
12
+ claude: IdeToolConfig;
13
+ codex: IdeToolConfig;
14
+ }
4
15
  export interface ServerConfig {
5
16
  port: number;
6
17
  }
@@ -23,7 +34,9 @@ export interface Installation {
23
34
  }
24
35
  export interface IdeAgentsConfig {
25
36
  version: 1;
37
+ /** @deprecated kept for migration; use ides */
26
38
  adapter: AdapterId;
39
+ ides: IdesConfig;
27
40
  server: ServerConfig;
28
41
  repos: RepoConfig[];
29
42
  installations: Installation[];
@@ -61,6 +74,8 @@ export interface GitStatus {
61
74
  export interface RepoWithStatus extends RepoConfig {
62
75
  localPath: string;
63
76
  git: GitStatus;
77
+ skillCount: number;
78
+ agentCount: number;
64
79
  }
65
80
  export interface ApplyResultItem {
66
81
  path: string;
@@ -70,6 +85,5 @@ export interface ApplyResultItem {
70
85
  export interface ApplyResult {
71
86
  results: ApplyResultItem[];
72
87
  }
73
- export declare const PACKAGE_VERSION = "0.1.0";
74
88
  /** @deprecated legacy config field */
75
89
  export type Scope = "global" | "project" | "off";
package/dist/types.js CHANGED
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = "0.1.0";
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare const PACKAGE_VERSION: string;
@@ -0,0 +1,6 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const require = createRequire(import.meta.url);
5
+ const { version } = require(path.join(fileURLToPath(new URL(".", import.meta.url)), "../package.json"));
6
+ export const PACKAGE_VERSION = version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ide-agents",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Local admin for IDE agents and skills from git repos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,15 +43,15 @@
43
43
  "license": "MIT",
44
44
  "repository": {
45
45
  "type": "git",
46
- "url": "git+https://github.com/sergeychernov/agentdesk.git"
46
+ "url": "git+https://github.com/sergeychernov/ide-agents.git"
47
47
  },
48
- "homepage": "https://github.com/sergeychernov/agentdesk#readme",
48
+ "homepage": "https://ide-agents.vercel.app/",
49
49
  "bugs": {
50
- "url": "https://github.com/sergeychernov/agentdesk/issues"
50
+ "url": "https://github.com/sergeychernov/ide-agents/issues"
51
51
  },
52
52
  "dependencies": {
53
53
  "@fastify/static": "^8.1.1",
54
- "fastify": "^5.2.1",
54
+ "fastify": "^5.8.5",
55
55
  "gray-matter": "^4.0.3",
56
56
  "uuid": "^11.1.0"
57
57
  },
@@ -60,18 +60,18 @@
60
60
  "@mantine/hooks": "^9.2.2",
61
61
  "@tabler/icons-react": "^3.44.0",
62
62
  "@types/node": "^22.13.10",
63
- "@types/react": "^19.0.10",
64
- "@types/react-dom": "^19.0.4",
65
- "@vitejs/plugin-react": "^4.3.4",
63
+ "@types/react": "^19.2.15",
64
+ "@types/react-dom": "^19.2.3",
65
+ "@vitejs/plugin-react": "^6.0.2",
66
66
  "concurrently": "^9.1.2",
67
67
  "postcss": "^8.5.15",
68
68
  "postcss-preset-mantine": "^1.18.0",
69
69
  "postcss-simple-vars": "^7.0.1",
70
- "react": "^19.0.0",
71
- "react-dom": "^19.0.0",
70
+ "react": "^19.2.6",
71
+ "react-dom": "^19.2.6",
72
72
  "react-router-dom": "^7.3.0",
73
73
  "tsx": "^4.19.3",
74
74
  "typescript": "^5.8.2",
75
- "vite": "^6.2.1"
75
+ "vite": "^8.0.14"
76
76
  }
77
77
  }