@platless/axom-cli 0.1.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 +64 -0
- package/dist/args.js +29 -0
- package/dist/cli.js +76 -0
- package/dist/commands/auth.js +52 -0
- package/dist/commands/execute.js +40 -0
- package/dist/commands/guide.js +38 -0
- package/dist/commands/record.js +99 -0
- package/dist/commands/skills.js +69 -0
- package/dist/commands/struct.js +50 -0
- package/dist/commands/update.js +88 -0
- package/dist/commands/whoami.js +8 -0
- package/dist/commands/workstation.js +171 -0
- package/dist/config.js +131 -0
- package/dist/credentialBroker.js +340 -0
- package/dist/credentials.js +211 -0
- package/dist/install/agentsDir.js +35 -0
- package/dist/install/claude/commands.js +89 -0
- package/dist/install/claude/materialize.js +42 -0
- package/dist/install/installer.js +45 -0
- package/dist/install/targets/claude.js +154 -0
- package/dist/mcp.js +152 -0
- package/dist/output.js +37 -0
- package/dist/workstation/acp/acpAgents.js +1287 -0
- package/dist/workstation/acp/acpClient.js +691 -0
- package/dist/workstation/acp/acpSession.js +1047 -0
- package/dist/workstation/acp/adoption.js +396 -0
- package/dist/workstation/acp/auth.js +80 -0
- package/dist/workstation/acp/authUrlSniffer.js +52 -0
- package/dist/workstation/acp/binaryInstall.js +411 -0
- package/dist/workstation/acp/configOptions.js +257 -0
- package/dist/workstation/acp/contract.js +494 -0
- package/dist/workstation/acp/draftInterceptor.js +154 -0
- package/dist/workstation/acp/draftSubagents.js +38 -0
- package/dist/workstation/acp/installErrors.js +89 -0
- package/dist/workstation/acp/installProbe.js +363 -0
- package/dist/workstation/acp/permission.js +327 -0
- package/dist/workstation/acp/registry/catalog.js +29 -0
- package/dist/workstation/acp/registry/launch.js +128 -0
- package/dist/workstation/acp/registry/registryClient.js +160 -0
- package/dist/workstation/acp/registry/registryTypes.js +8 -0
- package/dist/workstation/acp/sessionUpdateMapper.js +602 -0
- package/dist/workstation/acp/test/fakeAgent.js +435 -0
- package/dist/workstation/acp/test/sessionUpdateFixtures.js +165 -0
- package/dist/workstation/acp/timedCommand.js +47 -0
- package/dist/workstation/acp/uvxInstall.js +248 -0
- package/dist/workstation/acp/validate.js +134 -0
- package/dist/workstation/agentSessions.js +1628 -0
- package/dist/workstation/channels/agent.js +96 -0
- package/dist/workstation/channels/computer.js +91 -0
- package/dist/workstation/channels/management.js +231 -0
- package/dist/workstation/channels/url.js +42 -0
- package/dist/workstation/config.js +47 -0
- package/dist/workstation/connection/encryptedChannel.js +261 -0
- package/dist/workstation/connection/retryPolicy.js +14 -0
- package/dist/workstation/connection/supervisor.js +353 -0
- package/dist/workstation/crypto.js +63 -0
- package/dist/workstation/daemon.js +225 -0
- package/dist/workstation/daemonctl.js +95 -0
- package/dist/workstation/exec.js +112 -0
- package/dist/workstation/journal.js +257 -0
- package/dist/workstation/names.js +17 -0
- package/dist/workstation/oauth/browser.js +24 -0
- package/dist/workstation/oauth/deviceAuthorization.js +198 -0
- package/dist/workstation/osinfo.js +4 -0
- package/dist/workstation/protocol/agent.js +606 -0
- package/dist/workstation/protocol/common.js +136 -0
- package/dist/workstation/protocol/computer.js +31 -0
- package/dist/workstation/protocol/management.js +116 -0
- package/dist/workstation/register.js +42 -0
- package/dist/workstation/service/installer.js +362 -0
- package/dist/workstation/service/launchd.js +46 -0
- package/dist/workstation/service/loginShell.js +36 -0
- package/dist/workstation/service/systemd.js +48 -0
- package/dist/workstation/service/types.js +73 -0
- package/dist/workstation/supervision.js +52 -0
- package/dist/workstation/systemMetrics.js +271 -0
- package/dist/workstation/update/artifact.js +61 -0
- package/dist/workstation/update/coordinator.js +240 -0
- package/dist/workstation/update/npmInstaller.js +91 -0
- package/dist/workstation/update/redact.js +36 -0
- package/dist/workstation/update/runUpdate.js +215 -0
- package/dist/workstation/update/stateStore.js +87 -0
- package/dist/workstation/update/taskRegistry.js +142 -0
- package/dist/workstation/update/types.js +3 -0
- package/dist/workstation/update/watchdog.js +311 -0
- package/dist/workstation/version.js +12 -0
- package/package.json +39 -0
- package/skills/axom/SKILL.md +83 -0
- package/skills/axom-definition/SKILL.md +74 -0
- package/skills/axom-platform/SKILL.md +78 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createReadStream, promises as fs } from 'node:fs';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
export const RESTART_READY_TIMEOUT_MS = 120_000;
|
|
7
|
+
const MARKER_FILE = 'restart-ready.json';
|
|
8
|
+
/** The npm package this running binary was installed from. Never the retired `axom-workstation`. */
|
|
9
|
+
const PACKAGE_NAME = '@platless/axom-cli';
|
|
10
|
+
function safeId(value) {
|
|
11
|
+
return createHash('sha256').update(value).digest('hex');
|
|
12
|
+
}
|
|
13
|
+
function updateDirectory(configDir) { return path.join(configDir, 'update'); }
|
|
14
|
+
export function readinessMarkerPath(configDir) { return path.join(updateDirectory(configDir), MARKER_FILE); }
|
|
15
|
+
async function atomicWrite(file, value) {
|
|
16
|
+
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
17
|
+
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
18
|
+
try {
|
|
19
|
+
await fs.writeFile(temporary, value, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
20
|
+
await fs.chmod(temporary, 0o600);
|
|
21
|
+
await fs.rename(temporary, file);
|
|
22
|
+
await fs.chmod(file, 0o600);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Writes evidence from a successfully encrypted management generation, never from a browser. */
|
|
30
|
+
export async function writeReadinessMarker(configDir, record) {
|
|
31
|
+
if (!record.commandId || !record.version || !record.generation)
|
|
32
|
+
throw new Error('Readiness marker requires command, version, and generation');
|
|
33
|
+
const marker = readinessMarkerPath(configDir);
|
|
34
|
+
await atomicWrite(marker, `${JSON.stringify(record)}\n`);
|
|
35
|
+
return marker;
|
|
36
|
+
}
|
|
37
|
+
async function isReady(marker, commandId, targetVersion) {
|
|
38
|
+
try {
|
|
39
|
+
const value = JSON.parse(await fs.readFile(marker, 'utf8'));
|
|
40
|
+
return typeof value === 'object' && value !== null
|
|
41
|
+
&& value.commandId === commandId
|
|
42
|
+
&& value.version === targetVersion
|
|
43
|
+
&& typeof value.generation === 'string';
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
50
|
+
/**
|
|
51
|
+
* A watchdog process has exactly one recovery transition. A successful readiness record returns;
|
|
52
|
+
* otherwise it installs the immutable last-known-good archive once and persists the final result.
|
|
53
|
+
*/
|
|
54
|
+
export async function runWatchdog(options) {
|
|
55
|
+
const now = options.now ?? Date.now;
|
|
56
|
+
const wait = options.wait ?? delay;
|
|
57
|
+
const deadline = now() + RESTART_READY_TIMEOUT_MS;
|
|
58
|
+
while (now() < deadline) {
|
|
59
|
+
if (await isReady(options.marker, options.commandId, options.targetVersion))
|
|
60
|
+
return;
|
|
61
|
+
await wait(Math.min(1_000, Math.max(1, deadline - now())));
|
|
62
|
+
}
|
|
63
|
+
if (await isReady(options.marker, options.commandId, options.targetVersion))
|
|
64
|
+
return;
|
|
65
|
+
const priorError = (await options.loadState?.().catch(() => null))?.error;
|
|
66
|
+
const fail = (error) => options.saveState({
|
|
67
|
+
phase: 'FAILED', commandId: options.commandId, version: options.targetVersion, error,
|
|
68
|
+
}).catch(() => undefined);
|
|
69
|
+
try {
|
|
70
|
+
await options.install(options.archive);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// Nothing was restored, so there is nothing to restart into either: this is the one case the
|
|
74
|
+
// words "Rollback failed" actually describe.
|
|
75
|
+
await fail(`Rollback failed: ${describe(error).slice(0, 512)}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await options.saveState({ phase: 'ROLLED_BACK', commandId: options.commandId, version: options.targetVersion });
|
|
79
|
+
try {
|
|
80
|
+
await options.restart?.();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// The package *is* rolled back; only putting it back under supervision failed. Calling that a
|
|
84
|
+
// failed rollback -- and overwriting the update's own error with a bare service-manager exit
|
|
85
|
+
// code -- is how `Rollback failed: service restart failed with exit 113` became the only thing
|
|
86
|
+
// two stranded workstations had to say for themselves.
|
|
87
|
+
await fail(rollbackRestartFailure(options.targetVersion, error, priorError));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const describe = (error) => (error instanceof Error ? error.message : String(error));
|
|
91
|
+
/**
|
|
92
|
+
* The sentence that names the repair for a workstation whose service registration is gone. Shared
|
|
93
|
+
* with `runUpdate.ts`'s provisioning failure (which imports it) because it is the same end state
|
|
94
|
+
* reached from either side, and a machine nobody can restart deserves one wording, not two.
|
|
95
|
+
*/
|
|
96
|
+
export const SERVICE_UNREGISTERED_REMEDIATION = 'The workstation service may no longer be registered with the OS service manager, in which case '
|
|
97
|
+
+ 'nothing will restart it. Run `axom workstation install` on this machine to restore it.';
|
|
98
|
+
function rollbackRestartFailure(version, error, priorError) {
|
|
99
|
+
const original = priorError ? ` The update itself failed with: ${priorError}` : '';
|
|
100
|
+
return `Rolled back to ${version}, but the service could not be restarted: ${describe(error)}. `
|
|
101
|
+
.concat(SERVICE_UNREGISTERED_REMEDIATION, original)
|
|
102
|
+
.slice(0, 512);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Copies the executable watchdog out of the npm package before replacement, then starts it as an
|
|
106
|
+
* independent process. The staged plan contains only private local paths and no bearer material.
|
|
107
|
+
*/
|
|
108
|
+
export async function stageWatchdog(command, lastKnownGood, service, deps = {}) {
|
|
109
|
+
const staged = await prepareWatchdog(command, lastKnownGood, service, deps);
|
|
110
|
+
staged.arm();
|
|
111
|
+
return { program: staged.program, plan: staged.plan };
|
|
112
|
+
}
|
|
113
|
+
async function prepareWatchdog(command, lastKnownGood, service, deps = {}) {
|
|
114
|
+
const directory = updateDirectory(service.configDir);
|
|
115
|
+
const program = path.join(directory, 'watchdog.mjs');
|
|
116
|
+
const plan = path.join(directory, `watchdog-${safeId(command.commandId)}.json`);
|
|
117
|
+
const source = deps.source ?? fileURLToPath(import.meta.url);
|
|
118
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
119
|
+
await fs.copyFile(source, program);
|
|
120
|
+
await fs.chmod(program, 0o700);
|
|
121
|
+
const value = {
|
|
122
|
+
commandId: command.commandId, targetVersion: command.release.version, archive: lastKnownGood,
|
|
123
|
+
marker: readinessMarkerPath(service.configDir), state: path.join(service.configDir, 'update-state.json'),
|
|
124
|
+
npmPath: service.npmPath, npmPrefix: service.npmPrefix, cliPath: service.cliPath,
|
|
125
|
+
};
|
|
126
|
+
await atomicWrite(plan, `${JSON.stringify(value)}\n`);
|
|
127
|
+
return {
|
|
128
|
+
program, plan,
|
|
129
|
+
arm: () => {
|
|
130
|
+
const child = (deps.spawn ?? ((exe, args, options) => nodeSpawn(exe, args, options)))(service.nodePath, [program, plan], {
|
|
131
|
+
detached: true, shell: false, stdio: 'ignore',
|
|
132
|
+
});
|
|
133
|
+
child?.unref?.();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Maintains the one archive allowed to roll the daemon back. A cache entry is reusable only when
|
|
139
|
+
* it was packed from this running package version; metadata is checked before npm can mutate it.
|
|
140
|
+
*/
|
|
141
|
+
export class LastKnownGoodCache {
|
|
142
|
+
options;
|
|
143
|
+
constructor(options) {
|
|
144
|
+
this.options = options;
|
|
145
|
+
}
|
|
146
|
+
get clientVersion() { return this.options.runningVersion; }
|
|
147
|
+
async prepare(command) {
|
|
148
|
+
const directory = path.join(updateDirectory(this.options.configDir), 'last-known-good');
|
|
149
|
+
const archive = await this.ensureArchive(directory);
|
|
150
|
+
const watchdog = await prepareWatchdog(command, archive, this.options, this.options);
|
|
151
|
+
return {
|
|
152
|
+
arm: async () => watchdog.arm(),
|
|
153
|
+
// An npm install failure happens while the old daemon is still alive. Restore synchronously
|
|
154
|
+
// here instead of arming a second watchdog, so no later timer can roll back twice.
|
|
155
|
+
restore: async () => npmInstall(this.options.npmPath, this.options.npmPrefix, archive),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* A current-version release is already authenticated by management. Download it only in the
|
|
160
|
+
* background, then independently hash the cached bytes before atomically publishing metadata.
|
|
161
|
+
*/
|
|
162
|
+
async promoteAuthenticatedRelease(release) {
|
|
163
|
+
if (release.version !== this.options.runningVersion || !this.options.download)
|
|
164
|
+
return;
|
|
165
|
+
const directory = path.join(updateDirectory(this.options.configDir), 'last-known-good');
|
|
166
|
+
const archive = path.join(directory, 'last-known-good.tgz');
|
|
167
|
+
if (await this.matchesCache(archive, path.join(directory, 'last-known-good.json'), release.version, release.sha512))
|
|
168
|
+
return;
|
|
169
|
+
const downloaded = await this.options.download(release);
|
|
170
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
171
|
+
const temporary = path.join(directory, `.release-${process.pid}-${Date.now()}.tgz`);
|
|
172
|
+
try {
|
|
173
|
+
await fs.copyFile(downloaded.path, temporary);
|
|
174
|
+
if (await sha512(temporary) !== release.sha512.toLowerCase())
|
|
175
|
+
throw new Error('Authenticated release cache digest changed while promoting');
|
|
176
|
+
await fs.rename(temporary, archive);
|
|
177
|
+
await atomicWrite(path.join(directory, 'last-known-good.json'), `${JSON.stringify({ name: PACKAGE_NAME, version: release.version, sha512: release.sha512.toLowerCase() })}\n`);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
await fs.rm(temporary, { force: true }).catch(() => undefined);
|
|
181
|
+
await downloaded.cleanup().catch(() => undefined);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async ensureArchive(directory) {
|
|
185
|
+
const archive = path.join(directory, 'last-known-good.tgz');
|
|
186
|
+
const metadata = path.join(directory, 'last-known-good.json');
|
|
187
|
+
try {
|
|
188
|
+
const saved = JSON.parse(await fs.readFile(metadata, 'utf8'));
|
|
189
|
+
await fs.access(archive);
|
|
190
|
+
if (await this.matchesCache(archive, metadata, this.options.runningVersion))
|
|
191
|
+
return archive;
|
|
192
|
+
}
|
|
193
|
+
catch { /* cache missing/corrupt: repack below */ }
|
|
194
|
+
const staging = path.join(directory, `.pack-${process.pid}-${Date.now()}`);
|
|
195
|
+
await fs.mkdir(staging, { recursive: true, mode: 0o700 });
|
|
196
|
+
try {
|
|
197
|
+
const packed = await this.options.pack(this.options.packageRoot, staging);
|
|
198
|
+
if (packed.name !== PACKAGE_NAME || packed.version !== this.options.runningVersion) {
|
|
199
|
+
throw new Error(`Refusing last-known-good package ${packed.name}@${packed.version}; expected ${PACKAGE_NAME}@${this.options.runningVersion}`);
|
|
200
|
+
}
|
|
201
|
+
if (!path.isAbsolute(packed.path) || path.dirname(packed.path) !== staging)
|
|
202
|
+
throw new Error('npm pack returned an unsafe archive path');
|
|
203
|
+
await fs.rename(packed.path, archive);
|
|
204
|
+
await atomicWrite(metadata, `${JSON.stringify({ name: packed.name, version: packed.version, sha512: await sha512(archive) })}\n`);
|
|
205
|
+
return archive;
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
await fs.rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async matchesCache(archive, metadata, version, expectedSha512) {
|
|
212
|
+
try {
|
|
213
|
+
const saved = JSON.parse(await fs.readFile(metadata, 'utf8'));
|
|
214
|
+
if (typeof saved !== 'object' || saved === null
|
|
215
|
+
|| saved.name !== PACKAGE_NAME
|
|
216
|
+
|| saved.version !== version
|
|
217
|
+
|| typeof saved.sha512 !== 'string')
|
|
218
|
+
return false;
|
|
219
|
+
const digest = await sha512(archive);
|
|
220
|
+
const declared = saved.sha512.toLowerCase();
|
|
221
|
+
return digest === declared && (!expectedSha512 || digest === expectedSha512.toLowerCase());
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function sha512(file) {
|
|
229
|
+
const hash = createHash('sha512');
|
|
230
|
+
await new Promise((resolve, reject) => {
|
|
231
|
+
const stream = createReadStream(file);
|
|
232
|
+
stream.on('data', (chunk) => { hash.update(chunk); });
|
|
233
|
+
stream.once('error', reject);
|
|
234
|
+
stream.once('end', resolve);
|
|
235
|
+
});
|
|
236
|
+
return hash.digest('hex');
|
|
237
|
+
}
|
|
238
|
+
async function npmInstall(npmPath, npmPrefix, archive) {
|
|
239
|
+
await new Promise((resolve, reject) => {
|
|
240
|
+
// Same as NpmInstaller: no `--prefix`, so a rollback lands where the forward install did.
|
|
241
|
+
const child = nodeSpawn(npmPath, ['install', '--global', archive, '--ignore-scripts', '--no-audit', '--no-fund'], { shell: false, stdio: 'ignore' });
|
|
242
|
+
child.once('error', reject);
|
|
243
|
+
child.once('close', (code) => code === 0 ? resolve() : reject(new Error(`npm rollback failed with exit ${code ?? 'unknown'}`)));
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/** How much of the child's own complaint is worth carrying into a 512-character status field. */
|
|
247
|
+
const CAPTURED_STDERR_LIMIT = 300;
|
|
248
|
+
async function defaultServiceRunner(command, args) {
|
|
249
|
+
await new Promise((resolve, reject) => {
|
|
250
|
+
// Not `stdio: 'ignore'`. This runs in a detached process whose only way of reporting anything
|
|
251
|
+
// is the status file, so discarding the child's stderr left a bare exit code standing in for a
|
|
252
|
+
// diagnosis -- which is precisely what `exit 113` was.
|
|
253
|
+
const child = nodeSpawn(command, args, { shell: false, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
254
|
+
let stderr = '';
|
|
255
|
+
child.stderr?.on('data', (chunk) => {
|
|
256
|
+
if (stderr.length < CAPTURED_STDERR_LIMIT)
|
|
257
|
+
stderr += String(chunk);
|
|
258
|
+
});
|
|
259
|
+
child.once('error', reject);
|
|
260
|
+
child.once('close', (code) => {
|
|
261
|
+
if (code === 0)
|
|
262
|
+
return resolve();
|
|
263
|
+
const said = stderr.trim().replace(/\s+/g, ' ').slice(0, CAPTURED_STDERR_LIMIT);
|
|
264
|
+
reject(new Error(`\`${path.basename(command)} ${args.slice(1).join(' ')}\` failed with exit ${code ?? 'unknown'}${said ? `: ${said}` : ''}`));
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/** The subcommand a rollback re-provisions with; the same one a person is told to run by hand. */
|
|
269
|
+
export const REPROVISION_ARGV = ['workstation', 'install'];
|
|
270
|
+
/**
|
|
271
|
+
* Puts the rolled-back client back under service supervision.
|
|
272
|
+
*
|
|
273
|
+
* Deliberately **not** `launchctl kickstart` / `systemctl restart`, which was the previous
|
|
274
|
+
* behaviour and could not work in the very situation this watchdog exists for. Provisioning boots
|
|
275
|
+
* the old job out *before* bootstrapping its replacement, so an update that dies in between leaves
|
|
276
|
+
* the machine with no registered service at all -- and kickstarting a job the service manager does
|
|
277
|
+
* not have answers 113, "Could not find specified service", by construction. Both production
|
|
278
|
+
* workstations recorded exactly that, one after the other.
|
|
279
|
+
*
|
|
280
|
+
* Re-running the installed package's own `workstation install` is correct in both end states: it
|
|
281
|
+
* kickstarts a job that is still registered and bootstraps one that is not, it already carries the
|
|
282
|
+
* hardened launchd transition handling (see `service/installer.ts`), and it works out the platform
|
|
283
|
+
* for itself -- so the plan no longer has to carry one. By this point npm has restored the
|
|
284
|
+
* last-known-good package, so [cliPath] is the *rolled-back* CLI, which is the one we want to run.
|
|
285
|
+
*
|
|
286
|
+
* It is intentionally unbounded. `install` performs no network or npm work, its launchd retries are
|
|
287
|
+
* already bounded, and killing it part-way through a `bootstrap` is itself how a machine loses its
|
|
288
|
+
* service definition -- the same reason `runUpdate.ts` refuses to kill the provisioning child it
|
|
289
|
+
* gives up waiting on.
|
|
290
|
+
*/
|
|
291
|
+
export async function reprovisionManagedService(nodePath, cliPath, run = defaultServiceRunner) {
|
|
292
|
+
await run(nodePath, [cliPath, ...REPROVISION_ARGV]);
|
|
293
|
+
}
|
|
294
|
+
async function main() {
|
|
295
|
+
const planPath = process.argv[2];
|
|
296
|
+
if (!planPath)
|
|
297
|
+
return;
|
|
298
|
+
const plan = JSON.parse(await fs.readFile(planPath, 'utf8'));
|
|
299
|
+
await runWatchdog({
|
|
300
|
+
...plan,
|
|
301
|
+
install: (archive) => npmInstall(plan.npmPath, plan.npmPrefix, archive),
|
|
302
|
+
saveState: async (status) => atomicWrite(plan.state, `${JSON.stringify(status)}\n`),
|
|
303
|
+
loadState: async () => JSON.parse(await fs.readFile(plan.state, 'utf8')),
|
|
304
|
+
// `process.execPath` is the node the daemon armed this watchdog with, which is the node the
|
|
305
|
+
// service definition itself runs -- never a `node` resolved from this process's PATH.
|
|
306
|
+
restart: () => reprovisionManagedService(process.execPath, plan.cliPath),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
310
|
+
void main().catch(() => process.exitCode = 1);
|
|
311
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
const packageMetadata = require('../../package.json');
|
|
4
|
+
/** Read once at process load from the installed npm package, never duplicated in source. */
|
|
5
|
+
export const CLIENT_VERSION = packageMetadata.version;
|
|
6
|
+
/**
|
|
7
|
+
* This package's own npm name, read from the same package.json as CLIENT_VERSION.
|
|
8
|
+
*
|
|
9
|
+
* Sent as the required `clientPackage` field on `management_hello` so the server can tell which
|
|
10
|
+
* npm package a connected workstation is running.
|
|
11
|
+
*/
|
|
12
|
+
export const CLIENT_PACKAGE_NAME = packageMetadata.name;
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@platless/axom-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Axomwork CLI - manage Axomwork structures, records and executions over MCP",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"axom": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"skills"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "rm -rf dist && tsc",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@agentclientprotocol/sdk": "^1.4.0",
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
24
|
+
"@napi-rs/keyring": "^1.1.3",
|
|
25
|
+
"commander": "^15.0.0",
|
|
26
|
+
"js-yaml": "^4.1.0",
|
|
27
|
+
"semver": "^7.8.5",
|
|
28
|
+
"ws": "^8.18.0",
|
|
29
|
+
"zod": "^4.4.3"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/js-yaml": "^4.0.9",
|
|
33
|
+
"@types/node": "^20.11.0",
|
|
34
|
+
"@types/semver": "^7.8.0",
|
|
35
|
+
"@types/ws": "^8.5.13",
|
|
36
|
+
"typescript": "^5.2.2",
|
|
37
|
+
"vitest": "^2.1.9"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: axom
|
|
3
|
+
description: "Work with Axomwork applications: inspect structures, query and mutate records, execute operations and services via Axomwork MCP tools"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Axomwork
|
|
7
|
+
|
|
8
|
+
Use this skill when an Axomwork MCP server is connected and you need to inspect application structure, read or change business records, or execute model operations and services.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
Start by discovering structure before changing records. Use `axom__struct__list_applications()` to find applications, then `axom__struct__get_model({ applicationName, modelName })` to inspect fields, operations, and record shape. Only then query, create, update, delete, upsert, or execute.
|
|
13
|
+
|
|
14
|
+
## Core Tools
|
|
15
|
+
|
|
16
|
+
- `axom__system__whoami()` - returns the current authenticated user and role.
|
|
17
|
+
- `axom__struct__list_applications()` - lists available applications.
|
|
18
|
+
- `axom__struct__get_application({ applicationName })` - reads one application definition.
|
|
19
|
+
- `axom__struct__list_models({ applicationName })` - lists models in an application.
|
|
20
|
+
- `axom__struct__get_model({ applicationName, modelName })` - reads one model definition, including attributes and operations.
|
|
21
|
+
- `axom__record__query({ applicationName, modelName, criteria?, page? })` - queries records using REST-compatible filters and sorts in `criteria`.
|
|
22
|
+
- `axom__record__get({ applicationName, modelName, id })` - reads one record by id.
|
|
23
|
+
- `axom__record__create({ applicationName, modelName, record })` - creates a record; `record` contains model field values.
|
|
24
|
+
- `axom__record__update({ applicationName, modelName, id, changes })` - updates one record; `changes` contains only the fields to modify.
|
|
25
|
+
- `axom__record__upsert({ applicationName, modelName, identityField, record })` - creates or updates a record matched by `identityField`; `record` must include that field. The model must declare a `unique` validation on `identityField`, or the call throws — check `get_model` for that before calling upsert on an unfamiliar model.
|
|
26
|
+
- `axom__record__delete({ applicationName, modelName, id })` - deletes one record by id.
|
|
27
|
+
- `axom__operation__execute({ applicationName, modelName, operationName, input })` - executes a model operation and waits for the result. Use `axom__operation__execute_async` with the same arguments when asynchronous continuation is intended; it returns a running status and a child run id.
|
|
28
|
+
- `axom__service__execute({ applicationName?, serviceName, input })` - executes a reusable platform or application-scoped service, with `axom__service__execute_async` as its non-waiting counterpart.
|
|
29
|
+
|
|
30
|
+
## Query Criteria
|
|
31
|
+
|
|
32
|
+
For the complete filter, sort, and page contract, load guide
|
|
33
|
+
`agent-integration/mcp/record-query-criteria.md` with `axom__guide__load`.
|
|
34
|
+
|
|
35
|
+
Pass filters and sorts inside `criteria`, with `page` as a separate top-level field:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"applicationName": "demo",
|
|
40
|
+
"modelName": "Article",
|
|
41
|
+
"criteria": {
|
|
42
|
+
"filters": [{"op":"eq","field":"status","value":"active"}],
|
|
43
|
+
"sorts": [{"field":"createdAt","direction":"DESC"}]
|
|
44
|
+
},
|
|
45
|
+
"page": {"number":1,"size":20}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Filter `op` values are `eq|gt|lt|like|in|and|or`. Use nested `filters` for `and` and `or`; use `values` for `in`. Page `size` must be one of `20`, `50`, or `100`.
|
|
50
|
+
|
|
51
|
+
## Upserting a Record
|
|
52
|
+
|
|
53
|
+
Use `axom__record__upsert` when a stable external identifier should select an existing record or create a new one. Supply the canonical fields in this order, and include the identity value inside `record`:
|
|
54
|
+
For create/update/upsert safety, load guide
|
|
55
|
+
`agent-integration/operating-patterns/safe-record-mutations.md`; before deletion, load
|
|
56
|
+
`agent-integration/operating-patterns/destructive-actions.md`.
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"applicationName": "demo",
|
|
61
|
+
"modelName": "Task",
|
|
62
|
+
"identityField": "externalId",
|
|
63
|
+
"record": {
|
|
64
|
+
"externalId": "task-42",
|
|
65
|
+
"title": "Reconcile invoices"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Execution Results
|
|
71
|
+
|
|
72
|
+
`axom__operation__execute` and `axom__service__execute` return an OperationResult-style payload. Interpret `success` as the primary outcome, `text` as user-facing output, `data` as structured result data, `message` as diagnostic detail, `processing` as whether work continues asynchronously, and `runId` as the related run identifier when one is started.
|
|
73
|
+
For asynchronous continuation, load guides
|
|
74
|
+
`concepts/execution/synchronous-and-task-execution.md` and
|
|
75
|
+
`agent-integration/operating-patterns/following-runs.md`.
|
|
76
|
+
|
|
77
|
+
## Guide
|
|
78
|
+
|
|
79
|
+
Axomwork ships an AI-first manual. Call `axom__guide__load` with no arguments for the table of
|
|
80
|
+
contents, `axom__guide__load` with `path` (e.g. `script-language/sdk/records.md`) for one page, and
|
|
81
|
+
`axom__guide__search` with `query` for ranked snippets. Guide references omit the locale and include
|
|
82
|
+
`.md`, for example `script-language/sdk/records.md`. When a tool description references a guide
|
|
83
|
+
path, load that page first.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: axom-definition
|
|
3
|
+
description: "Develop Axomwork application definitions with independently callable canonical definition MCP tools (admin role required)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Axomwork Definition
|
|
7
|
+
|
|
8
|
+
Use this skill to edit application definitions with the canonical `axom__definition__*` MCP tools. These tools require an ADMIN token. Check `tools/list` for the specific tool before calling it.
|
|
9
|
+
|
|
10
|
+
Call each tool directly with camelCase arguments:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{"applicationName":"demo","modelName":"Article"}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## ETag workflow
|
|
17
|
+
|
|
18
|
+
Read the target resource or its list before a mutation, then send the returned `etag` as `expectedEtag`. Body updates use `expectedBodyEtag`. If a conflict occurs, reread the resource, merge intentionally, and retry. Confirm delete actions with the user.
|
|
19
|
+
Load guides `agent-integration/operating-patterns/etag-write-workflow.md` and
|
|
20
|
+
`agent-integration/operating-patterns/destructive-actions.md` for the complete safety contract.
|
|
21
|
+
|
|
22
|
+
## Applications
|
|
23
|
+
|
|
24
|
+
- `axom__definition__list_applications`
|
|
25
|
+
- `axom__definition__get_application` — `applicationName`
|
|
26
|
+
- `axom__definition__create_application` — `application`, `expectedEtag`
|
|
27
|
+
- `axom__definition__update_application` — `applicationName`, `application`, `expectedEtag`
|
|
28
|
+
- `axom__definition__delete_application` — `applicationName`, `expectedEtag`
|
|
29
|
+
|
|
30
|
+
## Models
|
|
31
|
+
|
|
32
|
+
- `axom__definition__list_models` — `applicationName`
|
|
33
|
+
- `axom__definition__get_model` — `applicationName`, `modelName`
|
|
34
|
+
- `axom__definition__create_model` — `applicationName`, `model`, `expectedEtag`
|
|
35
|
+
- `axom__definition__update_model` — `applicationName`, `modelName`, `model`, `expectedEtag`
|
|
36
|
+
- `axom__definition__delete_model` — `applicationName`, `modelName`, `expectedEtag`
|
|
37
|
+
|
|
38
|
+
## Attributes and operations
|
|
39
|
+
|
|
40
|
+
- `axom__definition__get_attribute` — `applicationName`, `modelName`, `attributeName`
|
|
41
|
+
- `axom__definition__create_attribute` — `applicationName`, `modelName`, `attribute`, `expectedEtag`
|
|
42
|
+
- `axom__definition__update_attribute` — `applicationName`, `modelName`, `attributeName`, `attribute`, `expectedEtag`
|
|
43
|
+
- `axom__definition__delete_attribute` — `applicationName`, `modelName`, `attributeName`, `expectedEtag`
|
|
44
|
+
- `axom__definition__get_operation` — `applicationName`, `modelName`, `operationName`
|
|
45
|
+
- `axom__definition__create_operation` — `applicationName`, `modelName`, `operation`, `expectedEtag`
|
|
46
|
+
- `axom__definition__update_operation` — `applicationName`, `modelName`, `operationName`, `operation`, `expectedEtag`
|
|
47
|
+
- `axom__definition__delete_operation` — `applicationName`, `modelName`, `operationName`, `expectedEtag`
|
|
48
|
+
- `axom__definition__get_operation_body` — `applicationName`, `modelName`, `operationName`
|
|
49
|
+
- `axom__definition__update_operation_body` — `applicationName`, `modelName`, `operationName`, `content`, `expectedBodyEtag`
|
|
50
|
+
|
|
51
|
+
## Triggers and lifecycle
|
|
52
|
+
|
|
53
|
+
- `axom__definition__update_triggers` — `applicationName`, `modelName`, `triggers`, `expectedEtag`
|
|
54
|
+
- `axom__definition__get_lifecycle` — `applicationName`, `modelName`
|
|
55
|
+
- `axom__definition__update_lifecycle` — `applicationName`, `modelName`, `lifecycle`, `expectedEtag`
|
|
56
|
+
- `axom__definition__delete_lifecycle` — `applicationName`, `modelName`, `expectedEtag`
|
|
57
|
+
|
|
58
|
+
## Workflows
|
|
59
|
+
|
|
60
|
+
- `axom__definition__list_workflows` — `applicationName`, `modelName`
|
|
61
|
+
- `axom__definition__get_workflow` — `applicationName`, `modelName`, `workflowName`
|
|
62
|
+
- `axom__definition__create_workflow` — `applicationName`, `modelName`, `workflow`, `expectedEtag`
|
|
63
|
+
- `axom__definition__update_workflow` — `applicationName`, `modelName`, `workflowName`, `workflow`, `expectedEtag`
|
|
64
|
+
- `axom__definition__delete_workflow` — `applicationName`, `modelName`, `workflowName`, `expectedEtag`
|
|
65
|
+
- `axom__definition__get_workflow_body` — `applicationName`, `modelName`, `workflowName`
|
|
66
|
+
- `axom__definition__update_workflow_body` — `applicationName`, `modelName`, `workflowName`, `content`, `expectedBodyEtag`
|
|
67
|
+
|
|
68
|
+
## Guide
|
|
69
|
+
|
|
70
|
+
Before authoring `.axom.kts` bodies, load the relevant guide pages with `axom__guide__load`:
|
|
71
|
+
`script-language/sdk/index.md` (script SDK),
|
|
72
|
+
`script-language/workflows/workflow-script.md` (workflow DSL),
|
|
73
|
+
`script-language/patterns/index.md` (sync/task/streaming patterns), and
|
|
74
|
+
`application-development/index.md` (definition semantics).
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: axom-platform
|
|
3
|
+
description: "Operate Axomwork platform configuration with independently callable canonical operations MCP tools (admin role required)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Axomwork Platform
|
|
7
|
+
|
|
8
|
+
Use the canonical `axom__platform__*` MCP tools (platform skills, AI configuration, cowork agents, data sources, and platform settings). These tools require an ADMIN token. Check `tools/list` for the specific tool before calling it.
|
|
9
|
+
|
|
10
|
+
Call each tool directly with camelCase arguments:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{"dataSourceName":"analytics","expectedEtag":"current-etag"}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## ETag workflow
|
|
17
|
+
|
|
18
|
+
Read the target resource or list before a mutation, then send its `etag` as `expectedEtag`. Prompt and workflow-body updates use `expectedBodyEtag`. On conflict, reread and merge intentionally. Confirm delete actions with the user. Report the returned `requiresRestart` value for every datasource or settings mutation.
|
|
19
|
+
For the full concurrency workflow, load guide
|
|
20
|
+
`agent-integration/operating-patterns/etag-write-workflow.md`.
|
|
21
|
+
|
|
22
|
+
## Platform skills
|
|
23
|
+
|
|
24
|
+
- `axom__platform__list_skills`
|
|
25
|
+
- `axom__platform__get_skill`
|
|
26
|
+
- `axom__platform__create_skill`
|
|
27
|
+
- `axom__platform__update_skill`
|
|
28
|
+
- `axom__platform__delete_skill`
|
|
29
|
+
|
|
30
|
+
## AI configuration
|
|
31
|
+
|
|
32
|
+
- `axom__platform__get_ai_config`
|
|
33
|
+
- `axom__platform__upsert_ai_vendor`
|
|
34
|
+
- `axom__platform__delete_ai_vendor`
|
|
35
|
+
- `axom__platform__upsert_ai_profile`
|
|
36
|
+
- `axom__platform__delete_ai_profile`
|
|
37
|
+
- `axom__platform__update_ai_memory`
|
|
38
|
+
|
|
39
|
+
## Cowork agents
|
|
40
|
+
|
|
41
|
+
- `axom__platform__list_cowork_agents`
|
|
42
|
+
- `axom__platform__get_cowork_agent`
|
|
43
|
+
- `axom__platform__create_cowork_agent`
|
|
44
|
+
- `axom__platform__update_cowork_agent`
|
|
45
|
+
- `axom__platform__delete_cowork_agent`
|
|
46
|
+
- `axom__platform__get_cowork_agent_prompt`
|
|
47
|
+
- `axom__platform__update_cowork_agent_prompt`
|
|
48
|
+
- `axom__platform__list_cowork_agent_skills`
|
|
49
|
+
- `axom__platform__get_cowork_agent_skill`
|
|
50
|
+
- `axom__platform__create_cowork_agent_skill`
|
|
51
|
+
- `axom__platform__update_cowork_agent_skill`
|
|
52
|
+
- `axom__platform__delete_cowork_agent_skill`
|
|
53
|
+
- `axom__platform__list_cowork_agent_workflows`
|
|
54
|
+
- `axom__platform__get_cowork_agent_workflow`
|
|
55
|
+
- `axom__platform__create_cowork_agent_workflow`
|
|
56
|
+
- `axom__platform__update_cowork_agent_workflow`
|
|
57
|
+
- `axom__platform__delete_cowork_agent_workflow`
|
|
58
|
+
- `axom__platform__get_cowork_agent_workflow_body`
|
|
59
|
+
- `axom__platform__update_cowork_agent_workflow_body`
|
|
60
|
+
|
|
61
|
+
## Datasources and platform settings
|
|
62
|
+
|
|
63
|
+
- `axom__platform__list_data_sources`
|
|
64
|
+
- `axom__platform__get_data_source`
|
|
65
|
+
- `axom__platform__create_data_source`
|
|
66
|
+
- `axom__platform__update_data_source`
|
|
67
|
+
- `axom__platform__delete_data_source`
|
|
68
|
+
- `axom__platform__get_settings`
|
|
69
|
+
- `axom__platform__update_settings`
|
|
70
|
+
|
|
71
|
+
## Guide
|
|
72
|
+
|
|
73
|
+
Before editing, load the matching guide with `axom__guide__load`:
|
|
74
|
+
`platform-operations/ai-vendors-and-profiles.md`,
|
|
75
|
+
`platform-operations/cowork-agent-configuration.md`,
|
|
76
|
+
`platform-operations/datasource-configuration.md`,
|
|
77
|
+
`platform-operations/service-configuration.md`, or
|
|
78
|
+
`platform-operations/platform-skills.md`.
|