@myagentroam/node 0.1.6 → 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.
@@ -0,0 +1,203 @@
1
+ import { homedir } from 'node:os';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { skillInstallMetadataSchema } from '@myagentroam/protocol';
4
+ import { SkillDirectoryService } from './skill-directory-service.js';
5
+ import { SkillInstallService, workspaceSkillTracked } from './skill-install-service.js';
6
+ export class SkillNodeOperationService {
7
+ database;
8
+ config;
9
+ directories = new SkillDirectoryService();
10
+ installer = new SkillInstallService();
11
+ activeTargets = new Set();
12
+ jobs = new Map();
13
+ constructor(database, config) {
14
+ this.database = database;
15
+ this.config = config;
16
+ }
17
+ operations() {
18
+ return {
19
+ 'node.skills.inspect': async () => {
20
+ await this.installer.recoverNodeHome(homedir());
21
+ return this.directories.inspectNodeHome(homedir());
22
+ },
23
+ 'node.skills.install': (data) => this.install('NODE', data),
24
+ 'node.skills.remove': (data) => this.remove('NODE', data),
25
+ 'workspace.skills.inspect': async (data) => {
26
+ const workspace = this.workspacePath(record(data)['workspaceId']);
27
+ await this.installer.recoverWorkspace(workspace);
28
+ return this.directories.inspectWorkspace(workspace);
29
+ },
30
+ 'workspace.skills.install': (data) => this.install('WORKSPACE', data),
31
+ 'workspace.skills.remove': (data) => this.remove('WORKSPACE', data),
32
+ 'workspace.skills.git-exclude-repair': async (data) => {
33
+ const workspace = this.workspacePath(record(data)['workspaceId']);
34
+ await this.installer.recoverWorkspace(workspace);
35
+ await this.installer.repairWorkspaceGitExclude(workspace);
36
+ return { repaired: true };
37
+ },
38
+ 'skill.install.job.get': (data) => this.job(data)
39
+ };
40
+ }
41
+ async install(target, data) {
42
+ const input = record(data);
43
+ const name = text(input['name'], 'SKILL_NAME_CONFLICT');
44
+ const ticket = text(input['artifactTicket'], 'SKILL_INSTALL_FAILED');
45
+ const metadata = skillInstallMetadataSchema.parse(input['metadata']);
46
+ const workspace = target === 'WORKSPACE' ? this.workspacePath(input['workspaceId']) : undefined;
47
+ if (workspace === undefined)
48
+ await this.installer.recoverNodeHome(homedir());
49
+ else
50
+ await this.installer.recoverWorkspace(workspace);
51
+ if (workspace !== undefined &&
52
+ input['confirmTracked'] !== true &&
53
+ (await workspaceSkillTracked(workspace, name)))
54
+ throw new Error('WORKSPACE_SKILL_TRACKED_CONFIRM_REQUIRED');
55
+ const targetKey = `${target}:${workspace ?? homedir()}`;
56
+ this.cleanupJobs();
57
+ if (this.activeTargets.has(targetKey))
58
+ throw new Error('WORKSPACE_BUSY');
59
+ if (this.activeTargets.size >= 50)
60
+ throw new Error('SKILL_TARGET_LIMIT_EXCEEDED');
61
+ const installJobId = randomUUID();
62
+ this.activeTargets.add(targetKey);
63
+ this.jobs.set(installJobId, { status: 'RUNNING', expiresAt: Date.now() + 5 * 60_000 });
64
+ void (async () => {
65
+ let terminal;
66
+ try {
67
+ await this.performInstall({
68
+ target,
69
+ name,
70
+ ticket,
71
+ metadata,
72
+ ...(workspace === undefined ? {} : { workspace })
73
+ });
74
+ terminal = { status: 'SUCCEEDED' };
75
+ }
76
+ catch (error) {
77
+ terminal = {
78
+ status: 'FAILED',
79
+ errorCode: error instanceof Error ? error.message : 'SKILL_INSTALL_FAILED'
80
+ };
81
+ }
82
+ this.activeTargets.delete(targetKey);
83
+ this.jobs.set(installJobId, { ...terminal, expiresAt: Date.now() + 5 * 60_000 });
84
+ })();
85
+ return { accepted: true, installJobId };
86
+ }
87
+ job(data) {
88
+ const installJobId = text(record(data)['installJobId'], 'MESSAGE_INVALID');
89
+ this.cleanupJobs();
90
+ const job = this.jobs.get(installJobId);
91
+ if (job === undefined)
92
+ throw new Error('SKILL_INSTALL_FAILED');
93
+ return {
94
+ installJobId,
95
+ status: job.status,
96
+ ...(job.errorCode === undefined ? {} : { errorCode: job.errorCode })
97
+ };
98
+ }
99
+ cleanupJobs() {
100
+ const now = Date.now();
101
+ for (const [id, job] of this.jobs)
102
+ if (job.expiresAt < now)
103
+ this.jobs.delete(id);
104
+ while (this.jobs.size > 1_000)
105
+ this.jobs.delete(this.jobs.keys().next().value);
106
+ }
107
+ async performInstall(input) {
108
+ const bundle = await this.download(input.ticket, input.metadata.contentHash);
109
+ if (input.target === 'NODE')
110
+ await this.installer.installNodeHome(homedir(), input.name, bundle, input.metadata);
111
+ else
112
+ await this.installer.installWorkspace(input.workspace, input.name, bundle, input.metadata, {
113
+ confirmTracked: true
114
+ });
115
+ }
116
+ async remove(target, data) {
117
+ const input = record(data);
118
+ const name = text(input['name'], 'SKILL_NAME_CONFLICT');
119
+ const workspace = target === 'WORKSPACE' ? this.workspacePath(input['workspaceId']) : undefined;
120
+ const targetKey = `${target}:${workspace ?? homedir()}`;
121
+ if (this.activeTargets.has(targetKey))
122
+ throw new Error('WORKSPACE_BUSY');
123
+ this.activeTargets.add(targetKey);
124
+ try {
125
+ if (target === 'NODE') {
126
+ await this.installer.recoverNodeHome(homedir());
127
+ await this.installer.removeNodeHome(homedir(), name);
128
+ }
129
+ else {
130
+ await this.installer.recoverWorkspace(workspace);
131
+ await this.installer.removeWorkspace(workspace, name);
132
+ }
133
+ }
134
+ finally {
135
+ this.activeTargets.delete(targetKey);
136
+ }
137
+ return { removed: true };
138
+ }
139
+ workspacePath(value) {
140
+ const workspace = this.database().getWorkspace(text(value, 'WORKSPACE_INVALID'));
141
+ if (workspace === undefined)
142
+ throw new Error('WORKSPACE_INVALID');
143
+ return workspace.path;
144
+ }
145
+ async download(ticket, expectedContentHash) {
146
+ const config = this.config();
147
+ if (config.nodeId === undefined || config.credential === undefined)
148
+ throw new Error('NODE_UNAUTHENTICATED');
149
+ const response = await fetch(`${config.serverUrl.replace(/\/$/u, '')}/api/skill-artifacts/${encodeURIComponent(ticket)}`, {
150
+ headers: {
151
+ 'x-mar-node-id': config.nodeId,
152
+ authorization: `Bearer ${config.credential}`
153
+ },
154
+ signal: AbortSignal.timeout(60_000)
155
+ });
156
+ if (!response.ok)
157
+ throw new Error('SKILL_INSTALL_FAILED');
158
+ const bundle = (await response.json());
159
+ validateBundle(bundle, expectedContentHash);
160
+ return bundle;
161
+ }
162
+ }
163
+ function validateBundle(bundle, expectedContentHash) {
164
+ if (bundle === null ||
165
+ typeof bundle !== 'object' ||
166
+ bundle.schemaVersion !== 1 ||
167
+ !Array.isArray(bundle.files) ||
168
+ bundle.files.length < 1 ||
169
+ bundle.files.length > 1_000)
170
+ throw new Error('SKILL_CONTENT_INVALID');
171
+ let total = 0;
172
+ const paths = new Set();
173
+ for (const file of bundle.files) {
174
+ if (file === null ||
175
+ typeof file !== 'object' ||
176
+ typeof file.path !== 'string' ||
177
+ typeof file.contentBase64 !== 'string' ||
178
+ paths.has(file.path))
179
+ throw new Error('SKILL_CONTENT_INVALID');
180
+ paths.add(file.path);
181
+ const content = Buffer.from(file.contentBase64, 'base64');
182
+ if (content.toString('base64') !== file.contentBase64 || content.length > 5 * 1024 * 1024)
183
+ throw new Error('SKILL_CONTENT_INVALID');
184
+ total += content.length;
185
+ if (total > 25 * 1024 * 1024)
186
+ throw new Error('SKILL_CONTENT_INVALID');
187
+ }
188
+ if (!paths.has('SKILL.md') || paths.has('.mar-skill-install.json'))
189
+ throw new Error('SKILL_CONTENT_INVALID');
190
+ const canonical = bundle.files.map((file) => `${file.path}\0${file.contentBase64}`).join('\0');
191
+ if (createHash('sha256').update(canonical).digest('hex') !== expectedContentHash)
192
+ throw new Error('SKILL_CONTENT_INVALID');
193
+ }
194
+ function record(value) {
195
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
196
+ throw new Error('MESSAGE_INVALID');
197
+ return value;
198
+ }
199
+ function text(value, code) {
200
+ if (typeof value !== 'string' || value.length === 0)
201
+ throw new Error(code);
202
+ return value;
203
+ }
@@ -36,16 +36,16 @@ export declare class WorkbenchManifestService {
36
36
  }[];
37
37
  accessOptions: {
38
38
  id: string;
39
- label: string;
40
39
  description: string;
40
+ label: string;
41
41
  }[];
42
42
  commands: {
43
43
  id: string;
44
+ description: string;
44
45
  available: boolean;
45
46
  runner: "codex" | "claude-code" | "opencode";
46
47
  reasonCode: string | null;
47
48
  label: string;
48
- description: string;
49
49
  inputHint: string;
50
50
  source: "CODEX_APP_SERVER" | "CLAUDE_AGENT_SDK" | "OPENCODE_SERVER";
51
51
  interaction: "RUNNER_TEXT" | "IMMEDIATE_ACTION" | "TOGGLE" | "VALUE";
@@ -35,7 +35,6 @@ export async function superviseNode(configPath, bootstrapEntrypoint) {
35
35
  const prefix = resolve(dataDirectory, 'node-versions', targetVersion);
36
36
  await rm(prefix, { recursive: true, force: true });
37
37
  await mkdir(prefix, { recursive: true, mode: 0o700 });
38
- const registry = validatedRegistry(config.upgrade?.registryUrl);
39
38
  const invocation = platformCliInvocation('npm', [
40
39
  'install',
41
40
  '--prefix',
@@ -43,7 +42,6 @@ export async function superviseNode(configPath, bootstrapEntrypoint) {
43
42
  '--ignore-scripts',
44
43
  '--no-audit',
45
44
  '--no-fund',
46
- ...(registry === undefined ? [] : ['--registry', registry]),
47
45
  `@myagentroam/node@${targetVersion}`
48
46
  ]);
49
47
  const installResult = await runProcess(invocation.command, invocation.args, false);
@@ -53,13 +51,7 @@ export async function superviseNode(configPath, bootstrapEntrypoint) {
53
51
  }
54
52
  if (installResult.code !== 0)
55
53
  throw new Error('NODE_UPGRADE_INSTALL_FAILED');
56
- const rebuild = platformCliInvocation('npm', [
57
- 'rebuild',
58
- '--prefix',
59
- prefix,
60
- ...(registry === undefined ? [] : ['--registry', registry]),
61
- 'node-pty'
62
- ]);
54
+ const rebuild = platformCliInvocation('npm', ['rebuild', '--prefix', prefix, 'node-pty']);
63
55
  const rebuildResult = await runProcess(rebuild.command, rebuild.args, false);
64
56
  if (rebuildResult.signalled) {
65
57
  stopping = true;
@@ -138,17 +130,6 @@ async function writeAtomic(path, contents) {
138
130
  await writeFile(temporary, contents, { mode: 0o600 });
139
131
  await rename(temporary, path);
140
132
  }
141
- function validatedRegistry(value) {
142
- if (value === undefined)
143
- return undefined;
144
- const url = new URL(value);
145
- if (url.protocol !== 'https:' &&
146
- !(url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname)))
147
- throw new Error('NODE_UPGRADE_REGISTRY_INVALID');
148
- if (url.username !== '' || url.password !== '' || url.search !== '' || url.hash !== '')
149
- throw new Error('NODE_UPGRADE_REGISTRY_INVALID');
150
- return url.toString().replace(/\/$/u, '');
151
- }
152
133
  function delay(milliseconds) {
153
134
  return new Promise((resolveDelay) => setTimeout(resolveDelay, milliseconds));
154
135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "type": "module",
6
6
  "files": [
@@ -24,7 +24,7 @@
24
24
  "node-pty": "1.1.0",
25
25
  "ws": "^8.21.3",
26
26
  "zod": "4.4.3",
27
- "@myagentroam/protocol": "^0.1.6"
27
+ "@myagentroam/protocol": "^0.1.8"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/ws": "^8.18.1"