@myagentroam/node 0.1.7 → 0.1.8

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/config.d.ts CHANGED
@@ -2,19 +2,17 @@ export interface NodeConfig {
2
2
  readonly serverUrl: string;
3
3
  /** Explicit roots or the platform default resolved while loading the config. */
4
4
  readonly allowedRoots: readonly string[];
5
- /** Absolute or config-relative location of this Node's private runtime SQLite. */
6
- readonly databasePath?: string;
5
+ /** Absolute or config-relative root for SQLite, upgrades and other private Node data. */
6
+ readonly dataDirectory?: string;
7
7
  readonly nodeId?: string;
8
8
  readonly credential?: string;
9
9
  readonly registrationToken?: string;
10
- readonly upgrade?: {
11
- readonly registryUrl?: string;
12
- };
13
10
  }
14
11
  export declare function nodeConfigPath(): string;
15
12
  export declare function loadNodeConfig(path?: string): Promise<NodeConfig>;
16
13
  /** Uses the Node service account's scope when no Workspace roots are configured. */
17
14
  export declare function defaultAllowedRoots(platform?: NodeJS.Platform, homePath?: string, currentDirectory?: string): readonly string[];
18
15
  export declare function nodeDatabasePath(config: NodeConfig, configPath?: string): string;
16
+ export declare function nodeDataDirectory(config: NodeConfig, configPath?: string): string;
19
17
  /** Credentials are always written with owner-only POSIX permissions where supported. */
20
18
  export declare function saveNodeConfig(config: NodeConfig, path?: string): Promise<void>;
package/dist/config.js CHANGED
@@ -10,6 +10,7 @@ export async function loadNodeConfig(path = nodeConfigPath()) {
10
10
  parsed === null ||
11
11
  !('serverUrl' in parsed) ||
12
12
  typeof parsed.serverUrl !== 'string' ||
13
+ 'databasePath' in parsed ||
13
14
  ('allowedRoots' in parsed &&
14
15
  (!Array.isArray(parsed.allowedRoots) ||
15
16
  !parsed.allowedRoots.every((root) => typeof root === 'string')))) {
@@ -20,15 +21,7 @@ export async function loadNodeConfig(path = nodeConfigPath()) {
20
21
  const nodeId = optionalString('nodeId');
21
22
  const credential = optionalString('credential');
22
23
  const registrationToken = optionalString('registrationToken');
23
- const databasePath = optionalString('databasePath');
24
- const upgradeInput = input['upgrade'];
25
- if (upgradeInput !== undefined &&
26
- (upgradeInput === null ||
27
- typeof upgradeInput !== 'object' ||
28
- Array.isArray(upgradeInput) ||
29
- ('registryUrl' in upgradeInput &&
30
- typeof upgradeInput['registryUrl'] !== 'string')))
31
- throw new Error('NODE_CONFIG_INVALID');
24
+ const dataDirectory = optionalString('dataDirectory');
32
25
  const allowedRoots = Array.isArray(input['allowedRoots'])
33
26
  ? input['allowedRoots']
34
27
  : defaultAllowedRoots();
@@ -38,16 +31,7 @@ export async function loadNodeConfig(path = nodeConfigPath()) {
38
31
  ...(nodeId === undefined ? {} : { nodeId }),
39
32
  ...(credential === undefined ? {} : { credential }),
40
33
  ...(registrationToken === undefined ? {} : { registrationToken }),
41
- ...(databasePath === undefined ? {} : { databasePath }),
42
- ...(upgradeInput === undefined
43
- ? {}
44
- : {
45
- upgrade: {
46
- ...(upgradeInput['registryUrl'] === undefined
47
- ? {}
48
- : { registryUrl: upgradeInput['registryUrl'] })
49
- }
50
- })
34
+ ...(dataDirectory === undefined ? {} : { dataDirectory })
51
35
  };
52
36
  }
53
37
  /** Uses the Node service account's scope when no Workspace roots are configured. */
@@ -55,7 +39,12 @@ export function defaultAllowedRoots(platform = process.platform, homePath = home
55
39
  return platform === 'win32' ? [win32.parse(currentDirectory).root] : [homePath];
56
40
  }
57
41
  export function nodeDatabasePath(config, configPath = nodeConfigPath()) {
58
- return config.databasePath ?? resolve(dirname(configPath), 'node-runtime.sqlite');
42
+ return resolve(nodeDataDirectory(config, configPath), 'node-runtime.sqlite');
43
+ }
44
+ export function nodeDataDirectory(config, configPath = nodeConfigPath()) {
45
+ if (config.dataDirectory !== undefined)
46
+ return resolve(dirname(configPath), config.dataDirectory);
47
+ return resolve(dirname(configPath));
59
48
  }
60
49
  /** Credentials are always written with owner-only POSIX permissions where supported. */
61
50
  export async function saveNodeConfig(config, path = nodeConfigPath()) {
package/dist/connector.js CHANGED
@@ -18,6 +18,7 @@ import { RunnerRegistry } from './runner/runner-registry.js';
18
18
  import { OpenCodeRunner } from './runner/opencode-runner.js';
19
19
  import { OpenCodeManagedRunController } from './runner/opencode/managed-run-controller.js';
20
20
  import { NodeOperationRouter } from './connector/node-operation-router.js';
21
+ import { SkillNodeOperationService } from './service/skill-node-operation-service.js';
21
22
  import { RunnerService } from './service/runner-service.js';
22
23
  import { TerminalService } from './service/terminal-service.js';
23
24
  import { WorkspaceService } from './service/workspace-service.js';
@@ -313,9 +314,9 @@ export class NodeConnector {
313
314
  commandStates: (sessionId) => this.commandStates.list(sessionId)
314
315
  });
315
316
  this.runners = new RunnerRegistry([
317
+ this.openCodeClient,
316
318
  this.codexClient,
317
319
  this.claudeClient,
318
- this.openCodeClient,
319
320
  ...(this.fakeRunner === undefined ? [] : [this.fakeRunner])
320
321
  ]);
321
322
  this.managedRunnerSessions = new ManagedRunnerSessionService(this.runtime, this.runners, (session) => this.emitWorkbenchEvent('session', {
@@ -708,6 +709,11 @@ export class NodeConnector {
708
709
  this.operationRouter.registerAll(this.sessionCatalogService.operations());
709
710
  this.operationRouter.registerAll(this.sessionCommandService.operations());
710
711
  this.operationRouter.registerAll(this.workspaceQueueWorkbenchService.operations());
712
+ this.operationRouter.registerAll(new SkillNodeOperationService(requireDatabase, () => {
713
+ if (this.config === undefined)
714
+ throw new Error('NODE_CONFIG_UNAVAILABLE');
715
+ return this.config;
716
+ }).operations());
711
717
  this.operationRouter.register('node.metrics', () => ({
712
718
  metrics: {
713
719
  ...this.metrics.snapshot(),
package/dist/main.js CHANGED
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { dirname, resolve } from 'node:path';
3
3
  import { NodeConnector } from './connector.js';
4
- import { loadNodeConfig, nodeConfigPath, nodeDatabasePath } from './config.js';
4
+ import { loadNodeConfig, nodeConfigPath, nodeDatabasePath, nodeDataDirectory } from './config.js';
5
5
  import { getNodeHealth } from './health.js';
6
6
  import { removeServiceDefinition, writeServiceDefinition } from './service.js';
7
7
  import { superviseNode } from './supervisor.js';
8
+ import { configureNodeLog } from './rotating-log.js';
8
9
  const [command = 'status', ...args] = process.argv.slice(2);
9
10
  const config = readOption(args, '--config') ?? nodeConfigPath();
10
11
  const output = readOption(args, '--output') ?? defaultServicePath();
11
12
  if (command === 'run') {
13
+ configureNodeLog(nodeDataDirectory(await loadNodeConfig(config), config));
12
14
  const connector = new NodeConnector({ configPath: config });
13
15
  await connector.start();
14
16
  for (const signal of ['SIGINT', 'SIGTERM']) {
@@ -18,6 +20,7 @@ if (command === 'run') {
18
20
  else if (command === 'supervise') {
19
21
  if (process.argv[1] === undefined)
20
22
  throw new Error('NODE_ENTRYPOINT_UNAVAILABLE');
23
+ configureNodeLog(nodeDataDirectory(await loadNodeConfig(config), config));
21
24
  await superviseNode(config, process.argv[1]);
22
25
  }
23
26
  else if (command === 'install') {
@@ -1,7 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { randomBytes } from 'node:crypto';
3
3
  import { createServer } from 'node:net';
4
- import { createOpencodeClient } from '@opencode-ai/sdk/v2/client';
5
4
  import { minimalRunnerEnvironment, platformCliInvocation } from './operational.js';
6
5
  import { terminateProcessTree } from './process-tree.js';
7
6
  const START_TIMEOUT_MS = 10_000;
@@ -35,6 +34,7 @@ export class OpenCodeServerClient {
35
34
  }
36
35
  }
37
36
  async startServer() {
37
+ const { createOpencodeClient } = await import('@opencode-ai/sdk/v2/client');
38
38
  const port = await availablePort();
39
39
  const password = randomBytes(24).toString('base64url');
40
40
  const invocation = platformCliInvocation(this.options.command ?? 'opencode', [
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { writeNodeLog } from './rotating-log.js';
2
3
  const secretKey = /(?:password|credential|token|cookie|authorization|secret|api[_-]?key)/i;
3
4
  export function runRunnerProbe(command, args, options) {
4
5
  return new Promise((resolve) => {
@@ -115,5 +116,7 @@ export function platformCliInvocation(command, args, platform = process.platform
115
116
  }
116
117
  export function nodeLog(event, fields = {}) {
117
118
  const safeFields = redactLogValue(fields);
118
- process.stdout.write(`${JSON.stringify({ timestamp: new Date().toISOString(), level: 'info', component: 'mar-node', event, ...safeFields })}\n`);
119
+ const line = `${JSON.stringify({ timestamp: new Date().toISOString(), level: 'info', component: 'mar-node', event, ...safeFields })}\n`;
120
+ process.stdout.write(line);
121
+ writeNodeLog(line);
119
122
  }
@@ -0,0 +1,17 @@
1
+ export declare function configureNodeLog(dataDirectory: string): void;
2
+ export declare function writeNodeLog(content: string): void;
3
+ export declare class RotatingNodeLog {
4
+ private readonly maxBytes;
5
+ private readonly retentionMs;
6
+ private readonly now;
7
+ private readonly activePath;
8
+ private bytes;
9
+ private day;
10
+ private rotation;
11
+ private lastCleanup;
12
+ constructor(directory: string, maxBytes?: number, retentionMs?: number, now?: () => number);
13
+ write(content: string): void;
14
+ private currentDay;
15
+ private rotate;
16
+ private cleanup;
17
+ }
@@ -0,0 +1,67 @@
1
+ import { appendFileSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ const DAY_MS = 24 * 60 * 60 * 1_000;
4
+ let sink;
5
+ export function configureNodeLog(dataDirectory) {
6
+ sink = new RotatingNodeLog(resolve(dataDirectory, 'logs'));
7
+ }
8
+ export function writeNodeLog(content) {
9
+ sink?.write(content);
10
+ }
11
+ export class RotatingNodeLog {
12
+ maxBytes;
13
+ retentionMs;
14
+ now;
15
+ activePath;
16
+ bytes = 0;
17
+ day = '';
18
+ rotation = 0;
19
+ lastCleanup = 0;
20
+ constructor(directory, maxBytes = 100 * 1024 * 1024, retentionMs = 7 * DAY_MS, now = Date.now) {
21
+ this.maxBytes = maxBytes;
22
+ this.retentionMs = retentionMs;
23
+ this.now = now;
24
+ mkdirSync(directory, { recursive: true });
25
+ this.activePath = resolve(directory, 'node.log');
26
+ try {
27
+ const stat = statSync(this.activePath);
28
+ this.bytes = stat.size;
29
+ this.day = new Date(stat.mtimeMs).toISOString().slice(0, 10);
30
+ }
31
+ catch {
32
+ this.day = this.currentDay();
33
+ }
34
+ this.cleanup();
35
+ }
36
+ write(content) {
37
+ const length = Buffer.byteLength(content);
38
+ const day = this.currentDay();
39
+ if (this.bytes > 0 && (this.bytes + length > this.maxBytes || day !== this.day))
40
+ this.rotate();
41
+ appendFileSync(this.activePath, content, { encoding: 'utf8', mode: 0o600 });
42
+ this.bytes += length;
43
+ this.day = day;
44
+ if (this.now() - this.lastCleanup >= DAY_MS)
45
+ this.cleanup();
46
+ }
47
+ currentDay() {
48
+ return new Date(this.now()).toISOString().slice(0, 10);
49
+ }
50
+ rotate() {
51
+ const suffix = `${this.day}-${this.now()}-${String(this.rotation++).padStart(3, '0')}`;
52
+ renameSync(this.activePath, resolve(this.activePath, '..', `node-${suffix}.log`));
53
+ this.bytes = 0;
54
+ }
55
+ cleanup() {
56
+ const directory = resolve(this.activePath, '..');
57
+ const cutoff = this.now() - this.retentionMs;
58
+ for (const entry of readdirSync(directory)) {
59
+ if (!entry.startsWith('node-') || !entry.endsWith('.log'))
60
+ continue;
61
+ const path = resolve(directory, entry);
62
+ if (statSync(path).mtimeMs < cutoff)
63
+ unlinkSync(path);
64
+ }
65
+ this.lastCleanup = this.now();
66
+ }
67
+ }
@@ -33,8 +33,7 @@ export class NodeConnectionLifecycleService {
33
33
  const registered = {
34
34
  serverUrl: config.serverUrl,
35
35
  allowedRoots: config.allowedRoots,
36
- ...(config.databasePath === undefined ? {} : { databasePath: config.databasePath }),
37
- ...(config.upgrade === undefined ? {} : { upgrade: config.upgrade }),
36
+ ...(config.dataDirectory === undefined ? {} : { dataDirectory: config.dataDirectory }),
38
37
  nodeId,
39
38
  credential
40
39
  };
@@ -20,16 +20,16 @@ export declare class RunnerService {
20
20
  }[];
21
21
  accessOptions: {
22
22
  id: string;
23
- label: string;
24
23
  description: string;
24
+ label: string;
25
25
  }[];
26
26
  commands: {
27
27
  id: string;
28
+ description: string;
28
29
  available: boolean;
29
30
  runner: "codex" | "claude-code" | "opencode";
30
31
  reasonCode: string | null;
31
32
  label: string;
32
- description: string;
33
33
  inputHint: string;
34
34
  source: "CODEX_APP_SERVER" | "CLAUDE_AGENT_SDK" | "OPENCODE_SERVER";
35
35
  interaction: "RUNNER_TEXT" | "IMMEDIATE_ACTION" | "TOGGLE" | "VALUE";
@@ -0,0 +1,17 @@
1
+ import { type SkillInstallMetadata } from '@myagentroam/protocol';
2
+ export type LocalSkillStatus = 'VALID' | 'UNKNOWN_VERSION' | 'PARTIAL' | 'INVALID' | 'INVALID_LINK' | 'GIT_EXCLUDE_BROKEN';
3
+ export interface LocalSkillInspection {
4
+ readonly name: string;
5
+ readonly description: string;
6
+ readonly localStatus: LocalSkillStatus;
7
+ readonly install?: SkillInstallMetadata;
8
+ }
9
+ export interface LocalSkillTargetInspection {
10
+ readonly targetKind: 'NODE' | 'WORKSPACE';
11
+ readonly compatibilityMode: 'SYMLINK' | 'JUNCTION' | 'MANAGED_COPY' | 'UNAVAILABLE';
12
+ readonly skills: readonly LocalSkillInspection[];
13
+ }
14
+ export declare class SkillDirectoryService {
15
+ inspectNodeHome(home: string): Promise<LocalSkillTargetInspection>;
16
+ inspectWorkspace(workspace: string): Promise<LocalSkillTargetInspection>;
17
+ }
@@ -0,0 +1,203 @@
1
+ import { lstat, open, readFile, readdir, readlink, realpath } from 'node:fs/promises';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import path from 'node:path';
5
+ import { skillInstallMetadataSchema } from '@myagentroam/protocol';
6
+ const MAX_METADATA_BYTES = 64 * 1024;
7
+ const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
8
+ const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/u;
9
+ export class SkillDirectoryService {
10
+ async inspectNodeHome(home) {
11
+ const agentsRoot = path.join(home, '.agents', 'skills');
12
+ const claudeRoot = path.join(home, '.claude', 'skills');
13
+ const skills = await scanRoot(agentsRoot, 'AGENTS');
14
+ return {
15
+ targetKind: 'NODE',
16
+ compatibilityMode: await nodeCompatibilityMode(agentsRoot, claudeRoot),
17
+ skills
18
+ };
19
+ }
20
+ async inspectWorkspace(workspace) {
21
+ const agents = await scanRoot(path.join(workspace, '.agents', 'skills'), 'AGENTS');
22
+ const claude = await scanRoot(path.join(workspace, '.claude', 'skills'), 'CLAUDE');
23
+ const skills = mergeWorkspaceCopies(agents, claude);
24
+ const gitExcludeBroken = await hasBrokenGitExclude(workspace, skills.map((skill) => skill.name));
25
+ return {
26
+ targetKind: 'WORKSPACE',
27
+ compatibilityMode: 'MANAGED_COPY',
28
+ skills: gitExcludeBroken
29
+ ? skills.map((skill) => ({ ...skill, localStatus: 'GIT_EXCLUDE_BROKEN' }))
30
+ : skills
31
+ };
32
+ }
33
+ }
34
+ async function hasBrokenGitExclude(workspace, names) {
35
+ if (names.length === 0)
36
+ return false;
37
+ try {
38
+ await lstat(path.join(workspace, '.git'));
39
+ }
40
+ catch (error) {
41
+ if (isMissing(error))
42
+ return false;
43
+ return true;
44
+ }
45
+ try {
46
+ const result = await promisify(execFile)('git', ['-C', workspace, 'rev-parse', '--git-path', 'info/exclude'], { timeout: 5_000, windowsHide: true });
47
+ const exclude = path.resolve(workspace, result.stdout.trim());
48
+ const content = await readFile(exclude, 'utf8');
49
+ return names.some((name) => !content.includes(`/.agents/skills/${name}/`) ||
50
+ !content.includes(`/.claude/skills/${name}/`));
51
+ }
52
+ catch {
53
+ return true;
54
+ }
55
+ }
56
+ async function scanRoot(root, source) {
57
+ let entries;
58
+ try {
59
+ entries = await readdir(root, { withFileTypes: true });
60
+ }
61
+ catch (error) {
62
+ if (isMissing(error))
63
+ return [];
64
+ throw error;
65
+ }
66
+ const results = [];
67
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
68
+ if (entry.name.startsWith('.mar-'))
69
+ continue;
70
+ const itemPath = path.join(root, entry.name);
71
+ const stat = await lstat(itemPath);
72
+ if (stat.isSymbolicLink() || (!stat.isDirectory() && process.platform === 'win32')) {
73
+ results.push({ name: entry.name, description: '', localStatus: 'INVALID_LINK', source });
74
+ continue;
75
+ }
76
+ if (!stat.isDirectory())
77
+ continue;
78
+ if (!SKILL_NAME.test(entry.name) || WINDOWS_DEVICE_NAME.test(entry.name)) {
79
+ results.push({ name: entry.name, description: '', localStatus: 'INVALID', source });
80
+ continue;
81
+ }
82
+ results.push({ ...(await inspectSkill(itemPath, entry.name)), source });
83
+ }
84
+ return results;
85
+ }
86
+ async function inspectSkill(directory, directoryName) {
87
+ try {
88
+ const frontmatter = parseSkillFrontmatter(await readBounded(path.join(directory, 'SKILL.md')));
89
+ if (frontmatter.name !== directoryName ||
90
+ !SKILL_NAME.test(frontmatter.name) ||
91
+ WINDOWS_DEVICE_NAME.test(frontmatter.name))
92
+ return { name: directoryName, description: frontmatter.description, localStatus: 'INVALID' };
93
+ const install = await readInstallMetadata(path.join(directory, '.mar-skill-install.json'));
94
+ return {
95
+ name: directoryName,
96
+ description: frontmatter.description,
97
+ localStatus: install === undefined ? 'UNKNOWN_VERSION' : 'VALID',
98
+ ...(install === undefined ? {} : { install })
99
+ };
100
+ }
101
+ catch {
102
+ return { name: directoryName, description: '', localStatus: 'INVALID' };
103
+ }
104
+ }
105
+ async function readInstallMetadata(file) {
106
+ try {
107
+ return skillInstallMetadataSchema.parse(JSON.parse(await readBounded(file)));
108
+ }
109
+ catch (error) {
110
+ if (isMissing(error))
111
+ return undefined;
112
+ return undefined;
113
+ }
114
+ }
115
+ async function readBounded(file) {
116
+ const handle = await open(file, 'r');
117
+ try {
118
+ const stat = await handle.stat();
119
+ if (!stat.isFile() || stat.size > MAX_METADATA_BYTES)
120
+ throw new Error('SKILL_METADATA_INVALID');
121
+ const buffer = Buffer.alloc(stat.size);
122
+ await handle.read(buffer, 0, buffer.length, 0);
123
+ return buffer.toString('utf8');
124
+ }
125
+ finally {
126
+ await handle.close();
127
+ }
128
+ }
129
+ function parseSkillFrontmatter(content) {
130
+ const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(content);
131
+ if (match === null)
132
+ throw new Error('SKILL_METADATA_INVALID');
133
+ const values = new Map();
134
+ for (const line of match[1].split(/\r?\n/u)) {
135
+ const field = /^(name|description):\s*(.*?)\s*$/u.exec(line);
136
+ if (field !== null)
137
+ values.set(field[1], unquote(field[2]));
138
+ }
139
+ const name = values.get('name');
140
+ const description = values.get('description');
141
+ if (name === undefined || description === undefined || description.length > 4096)
142
+ throw new Error('SKILL_METADATA_INVALID');
143
+ return { name, description };
144
+ }
145
+ function unquote(value) {
146
+ if (value.length >= 2 &&
147
+ ((value.startsWith('"') && value.endsWith('"')) ||
148
+ (value.startsWith("'") && value.endsWith("'"))))
149
+ return value.slice(1, -1);
150
+ return value;
151
+ }
152
+ function mergeWorkspaceCopies(agents, claude) {
153
+ const byName = new Map();
154
+ for (const item of agents)
155
+ byName.set(item.name, { agents: item });
156
+ for (const item of claude)
157
+ byName.set(item.name, { ...byName.get(item.name), claude: item });
158
+ return [...byName.entries()]
159
+ .sort(([left], [right]) => left.localeCompare(right))
160
+ .map(([name, copies]) => {
161
+ if (copies.agents === undefined || copies.claude === undefined)
162
+ return {
163
+ ...withoutSource((copies.agents ?? copies.claude)),
164
+ name,
165
+ localStatus: 'PARTIAL'
166
+ };
167
+ if (copies.agents.localStatus !== copies.claude.localStatus ||
168
+ JSON.stringify(copies.agents.install) !== JSON.stringify(copies.claude.install))
169
+ return { name, description: copies.agents.description, localStatus: 'PARTIAL' };
170
+ return withoutSource(copies.agents);
171
+ });
172
+ }
173
+ function withoutSource(entry) {
174
+ return {
175
+ name: entry.name,
176
+ description: entry.description,
177
+ localStatus: entry.localStatus,
178
+ ...(entry.install === undefined ? {} : { install: entry.install })
179
+ };
180
+ }
181
+ async function nodeCompatibilityMode(agentsRoot, claudeRoot) {
182
+ try {
183
+ const stat = await lstat(claudeRoot);
184
+ if (stat.isSymbolicLink()) {
185
+ const target = await realpath(path.resolve(path.dirname(claudeRoot), await readlink(claudeRoot)));
186
+ const agents = await realpath(agentsRoot);
187
+ return target === agents
188
+ ? process.platform === 'win32'
189
+ ? 'JUNCTION'
190
+ : 'SYMLINK'
191
+ : 'UNAVAILABLE';
192
+ }
193
+ return stat.isDirectory() ? 'MANAGED_COPY' : 'UNAVAILABLE';
194
+ }
195
+ catch (error) {
196
+ if (isMissing(error))
197
+ return 'UNAVAILABLE';
198
+ return 'UNAVAILABLE';
199
+ }
200
+ }
201
+ function isMissing(error) {
202
+ return (error instanceof Error && 'code' in error && error.code === 'ENOENT');
203
+ }
@@ -0,0 +1,21 @@
1
+ import { type SkillInstallMetadata } from '@myagentroam/protocol';
2
+ export interface SkillBundleFile {
3
+ readonly path: string;
4
+ readonly contentBase64: string;
5
+ }
6
+ export interface InstallableSkillBundle {
7
+ readonly schemaVersion: 1;
8
+ readonly files: readonly SkillBundleFile[];
9
+ }
10
+ export declare class SkillInstallService {
11
+ recoverNodeHome(home: string): Promise<void>;
12
+ recoverWorkspace(workspace: string): Promise<void>;
13
+ installNodeHome(home: string, name: string, bundle: InstallableSkillBundle, metadata: SkillInstallMetadata): Promise<void>;
14
+ installWorkspace(workspace: string, name: string, bundle: InstallableSkillBundle, metadata: SkillInstallMetadata, options?: {
15
+ readonly confirmTracked?: boolean;
16
+ }): Promise<void>;
17
+ removeWorkspace(workspace: string, name: string): Promise<void>;
18
+ removeNodeHome(home: string, name: string): Promise<void>;
19
+ repairWorkspaceGitExclude(workspace: string): Promise<void>;
20
+ }
21
+ export declare function workspaceSkillTracked(workspace: string, name: string): Promise<boolean>;