@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
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @platless/axom-cli
|
|
2
|
+
|
|
3
|
+
One npm package, one bin: `axom`. An authenticated MCP client for Axomwork (`struct`/`record`/`execute`/`guide`), plus the personal Workstation daemon — registration, encrypted tunnels, self-update, and OS service lifecycle — merged into the same package.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Install the build your Axomwork server publishes, so the CLI and the server always agree on the wire
|
|
8
|
+
format:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install -g http://<your-axom-server>/download/client/axom-cli-<version>.tgz
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The exact filename is in the manifest the server publishes:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
curl -s http://<your-axom-server>/download/client/manifest.json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A plain `-g` install on purpose: it puts `axom` on your `PATH` the way npm puts every other global
|
|
21
|
+
binary there. Do **not** add `--prefix` — the CLI's own self-update runs the same `npm install
|
|
22
|
+
--global`, and a private prefix leaves the binary somewhere no shell looks.
|
|
23
|
+
|
|
24
|
+
Verify before going further — this is the step that catches a broken install:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
axom --version
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quickstart
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
axom auth --server http://localhost:8080
|
|
34
|
+
axom whoami
|
|
35
|
+
|
|
36
|
+
axom struct list app
|
|
37
|
+
axom struct get model demo/Article
|
|
38
|
+
|
|
39
|
+
axom record query demo/Article '{"filters":[{"op":"eq","field":"status","value":"active"}],"page":{"number":1,"size":20}}'
|
|
40
|
+
axom record create demo/Article '{"title":"Hello"}'
|
|
41
|
+
|
|
42
|
+
axom execute demo/Article/publish '{"id":"rec-1"}'
|
|
43
|
+
axom execute healthCheck '{}'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Skills
|
|
47
|
+
|
|
48
|
+
`axom skills add [--claude] [--project]` installs the bundled `axom`/`axom-definition`/`axom-platform` skills. By default it writes them to `.agents/skills/` in the current directory — the convention Codex, Cursor, OpenCode, Pi and others already read on their own. With `--claude` it installs the full plugin through Claude Code's plugin marketplace instead. `axom skills remove` reverses either path.
|
|
49
|
+
|
|
50
|
+
## Workstation
|
|
51
|
+
|
|
52
|
+
`axom workstation start` registers this machine (reusing the `axom auth` login) and installs/starts the daemon as an OS service; `stop`/`restart`/`status` control or query it; `install`/`uninstall` manage just the service definition (launchd/systemd) for manual repair.
|
|
53
|
+
|
|
54
|
+
## Updating
|
|
55
|
+
|
|
56
|
+
`axom update [--version x.y.z]` upgrades `axom` itself to the release the connected server currently publishes. It only installs the new package and prints a hint — it does not restart the workstation daemon (that would kill every active agent session on the machine), so run `axom workstation restart` afterward to apply it.
|
|
57
|
+
|
|
58
|
+
## Config
|
|
59
|
+
|
|
60
|
+
Non-secret CLI auth state is stored in `~/.axom/config.json` with mode `0600`. The file keeps one `defaultServer` and a `servers` map whose authenticated entries contain only `{ "authenticated": true }`; that value is a marker, not a credential. The refresh token is stored in the operating system's credential store (isolated behind the credential broker on macOS), while the access token and its expiry stay only in the credential process's in-memory cache. Config entries containing the obsolete `clientId`, `accessToken`, `refreshToken`, or `expiresAt` fields are rejected and must be removed before running `axom auth` again. Use `--server <url>` to override the default server for any command.
|
|
61
|
+
|
|
62
|
+
## Learn more
|
|
63
|
+
|
|
64
|
+
This README is a front page, not the manual. For the full command reference (arguments, exit codes, examples) and architecture notes, see `docs/modules/cli.md` and `guide/en/agent-integration/axom-cli/` in the Axomwork monorepo this package is built from.
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { UsageError } from './config.js';
|
|
2
|
+
export function splitPath(input, segments, usage) {
|
|
3
|
+
const parts = input.split('/');
|
|
4
|
+
if (parts.length !== segments || parts.some((part) => part.length === 0)) {
|
|
5
|
+
throw new UsageError(`Expected ${usage}, got "${input}"`);
|
|
6
|
+
}
|
|
7
|
+
return parts;
|
|
8
|
+
}
|
|
9
|
+
export function splitPathFlexible(input, usage) {
|
|
10
|
+
const parts = input.split('/');
|
|
11
|
+
if (parts.length < 1 || parts.length > 3 || parts.some((part) => part.length === 0)) {
|
|
12
|
+
throw new UsageError(`Expected ${usage}, got "${input}"`);
|
|
13
|
+
}
|
|
14
|
+
return parts;
|
|
15
|
+
}
|
|
16
|
+
export function parseJsonArg(input, what, example) {
|
|
17
|
+
if (input === undefined)
|
|
18
|
+
return {};
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(input);
|
|
21
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
22
|
+
throw new Error('Expected a JSON object');
|
|
23
|
+
}
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new UsageError(`Invalid JSON for ${what}. Example: ${example}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { authAction, logoutAction } from './commands/auth.js';
|
|
6
|
+
import { registerExecuteCommand } from './commands/execute.js';
|
|
7
|
+
import { registerGuideCommands } from './commands/guide.js';
|
|
8
|
+
import { registerRecordCommands } from './commands/record.js';
|
|
9
|
+
import { registerSkillsCommands } from './commands/skills.js';
|
|
10
|
+
import { registerStructCommands } from './commands/struct.js';
|
|
11
|
+
import { registerUpdateCommand } from './commands/update.js';
|
|
12
|
+
import { registerWorkstationCommands } from './commands/workstation.js';
|
|
13
|
+
import { whoamiAction } from './commands/whoami.js';
|
|
14
|
+
import { runCredentialBroker } from './credentials.js';
|
|
15
|
+
import { runCommand } from './output.js';
|
|
16
|
+
import { install, restart, start, status, stop, uninstall } from './workstation/daemonctl.js';
|
|
17
|
+
import { runDaemon } from './workstation/daemon.js';
|
|
18
|
+
import { registerWorkstation } from './workstation/register.js';
|
|
19
|
+
import { detectSupervision } from './workstation/supervision.js';
|
|
20
|
+
import { CLIENT_VERSION } from './workstation/version.js';
|
|
21
|
+
export function buildProgram() {
|
|
22
|
+
const program = new Command().name('axom').version(CLIENT_VERSION).option('--server <url>', 'Axomwork server URL');
|
|
23
|
+
const serverOption = () => program.opts().server;
|
|
24
|
+
program
|
|
25
|
+
.command('auth')
|
|
26
|
+
.description('Authorize axom-cli with an Axomwork server')
|
|
27
|
+
.option('--server <url>', 'Axomwork server URL')
|
|
28
|
+
.action((options) => runCommand(() => authAction({ server: options.server ?? serverOption() })));
|
|
29
|
+
program
|
|
30
|
+
.command('whoami')
|
|
31
|
+
.description('Print the authenticated Axomwork user')
|
|
32
|
+
.action(() => runCommand(() => whoamiAction({ server: serverOption() })));
|
|
33
|
+
program
|
|
34
|
+
.command('logout')
|
|
35
|
+
.description('Revoke and remove axom-cli credentials for an Axomwork server')
|
|
36
|
+
.option('--server <url>', 'Axomwork server URL')
|
|
37
|
+
.action((options) => runCommand(() => logoutAction({ server: options.server ?? serverOption() })));
|
|
38
|
+
program
|
|
39
|
+
.command('__credential-broker', { hidden: true })
|
|
40
|
+
.action(() => runCredentialBroker());
|
|
41
|
+
registerStructCommands(program);
|
|
42
|
+
registerRecordCommands(program);
|
|
43
|
+
registerExecuteCommand(program);
|
|
44
|
+
registerGuideCommands(program);
|
|
45
|
+
registerSkillsCommands(program);
|
|
46
|
+
registerUpdateCommand(program);
|
|
47
|
+
registerWorkstationCommands(program, {
|
|
48
|
+
start, stop, restart, install, uninstall, runDaemon, registerWorkstation,
|
|
49
|
+
status: async () => {
|
|
50
|
+
const current = await status();
|
|
51
|
+
return current.running
|
|
52
|
+
? `running connected to ${current.server ?? 'unknown server'} as ${current.workstationId ?? 'unknown workstation'}`
|
|
53
|
+
: 'stopped';
|
|
54
|
+
},
|
|
55
|
+
log: console.log,
|
|
56
|
+
authenticate: (server) => authAction({ server }),
|
|
57
|
+
detectSupervision,
|
|
58
|
+
isInteractive: () => Boolean(process.stdin.isTTY),
|
|
59
|
+
});
|
|
60
|
+
return program;
|
|
61
|
+
}
|
|
62
|
+
export function isMainModule(argv1) {
|
|
63
|
+
if (!argv1)
|
|
64
|
+
return false;
|
|
65
|
+
try {
|
|
66
|
+
// A `.bin` entry is usually a symlink; compare the resolved target.
|
|
67
|
+
return realpathSync(argv1) === fileURLToPath(import.meta.url);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// argv[1] does not exist on disk (or is not resolvable) — fall back to comparing URLs.
|
|
71
|
+
return import.meta.url === pathToFileURL(argv1).href;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (isMainModule(process.argv[1])) {
|
|
75
|
+
await buildProgram().parseAsync();
|
|
76
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
import { loadConfig, resolveServer, saveConfig } from '../config.js';
|
|
3
|
+
import { callAxomTool } from '../mcp.js';
|
|
4
|
+
import { authorizeDevice } from '../workstation/oauth/deviceAuthorization.js';
|
|
5
|
+
import { revokeAndClearTokens, saveTokens } from '../credentials.js';
|
|
6
|
+
export const DEFAULT_SERVER_URL = 'http://localhost:8080';
|
|
7
|
+
export async function authAction(options, deps = {}) {
|
|
8
|
+
const config = await loadConfig();
|
|
9
|
+
const server = resolveServer(config, options.server ?? config.defaultServer ?? DEFAULT_SERVER_URL);
|
|
10
|
+
const host = hostname();
|
|
11
|
+
const { accessToken, refreshToken, expiresInSeconds } = await authorizeDevice(server, `Axomwork CLI (${host})`, host, deps);
|
|
12
|
+
const updated = await loadConfig();
|
|
13
|
+
try {
|
|
14
|
+
await saveTokens(server, { accessToken, refreshToken, expiresAt: new Date(Date.now() + expiresInSeconds * 1000).toISOString() });
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
await revokeRefreshToken(server, refreshToken);
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
updated.servers[server] = { authenticated: true };
|
|
21
|
+
if (!updated.defaultServer) {
|
|
22
|
+
updated.defaultServer = server;
|
|
23
|
+
}
|
|
24
|
+
await saveConfig(updated);
|
|
25
|
+
const outcome = await callAxomTool(server, 'axom__system__whoami', {});
|
|
26
|
+
if (outcome.isError) {
|
|
27
|
+
throw new Error(outcome.text);
|
|
28
|
+
}
|
|
29
|
+
console.log(`Logged in as ${outcome.text}`);
|
|
30
|
+
}
|
|
31
|
+
export async function logoutAction(options) {
|
|
32
|
+
const config = await loadConfig();
|
|
33
|
+
const server = resolveServer(config, options.server ?? config.defaultServer ?? DEFAULT_SERVER_URL);
|
|
34
|
+
await revokeAndClearTokens(server);
|
|
35
|
+
delete config.servers[server];
|
|
36
|
+
if (config.defaultServer === server) {
|
|
37
|
+
config.defaultServer = Object.keys(config.servers)[0];
|
|
38
|
+
}
|
|
39
|
+
await saveConfig(config);
|
|
40
|
+
console.log(`Logged out from ${server}`);
|
|
41
|
+
}
|
|
42
|
+
async function revokeRefreshToken(server, refreshToken) {
|
|
43
|
+
await fetch(`${server}/oauth/revoke`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
46
|
+
body: new URLSearchParams({
|
|
47
|
+
token: refreshToken,
|
|
48
|
+
token_type_hint: 'refresh_token',
|
|
49
|
+
client_id: 'axom-cli',
|
|
50
|
+
}),
|
|
51
|
+
}).catch(() => undefined);
|
|
52
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { parseJsonArg, splitPathFlexible } from '../args.js';
|
|
2
|
+
import { loadConfig, resolveServer } from '../config.js';
|
|
3
|
+
import { callAxomTool } from '../mcp.js';
|
|
4
|
+
import { printOutcome, runCommand } from '../output.js';
|
|
5
|
+
const executeUsage = '<app>/<model>/<operation> | [<app>/]<serviceName>';
|
|
6
|
+
const executeExample = '{"id":"rec-1"}';
|
|
7
|
+
export function registerExecuteCommand(program) {
|
|
8
|
+
const serverOption = () => program.opts().server;
|
|
9
|
+
program
|
|
10
|
+
.command('execute')
|
|
11
|
+
.description('Execute an Axomwork operation or service')
|
|
12
|
+
.argument('<path>', executeUsage)
|
|
13
|
+
.argument('[json]')
|
|
14
|
+
.action((path, json) => runCommand(() => executeAction({ path, json, server: serverOption() })));
|
|
15
|
+
}
|
|
16
|
+
export async function executeAction(options) {
|
|
17
|
+
const server = resolveServer(await loadConfig(), options.server);
|
|
18
|
+
const parts = splitPathFlexible(options.path, executeUsage);
|
|
19
|
+
const input = parseJsonArg(options.json, 'execute input', executeExample);
|
|
20
|
+
if (parts.length === 3) {
|
|
21
|
+
const [applicationName, modelName, operationName] = parts;
|
|
22
|
+
printOutcome(
|
|
23
|
+
// 同步是 axom__*__execute 的固有语义;awaitResult 参数已随 InvocationMode 重构移除,
|
|
24
|
+
// 继续传会被工具的封闭 inputSchema 直接拒掉。
|
|
25
|
+
await callAxomTool(server, 'axom__operation__execute', {
|
|
26
|
+
applicationName,
|
|
27
|
+
modelName,
|
|
28
|
+
operationName,
|
|
29
|
+
input,
|
|
30
|
+
}));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (parts.length === 2) {
|
|
34
|
+
const [applicationName, serviceName] = parts;
|
|
35
|
+
printOutcome(await callAxomTool(server, 'axom__service__execute', { applicationName, serviceName, input }));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const [serviceName] = parts;
|
|
39
|
+
printOutcome(await callAxomTool(server, 'axom__service__execute', { serviceName, input }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { loadConfig, resolveServer } from '../config.js';
|
|
2
|
+
import { callAxomTool } from '../mcp.js';
|
|
3
|
+
import { printOutcome, runCommand } from '../output.js';
|
|
4
|
+
export function registerGuideCommands(program) {
|
|
5
|
+
const guide = program.command('guide').description('Read and search the Axomwork Guide');
|
|
6
|
+
const serverOption = () => program.opts().server;
|
|
7
|
+
guide
|
|
8
|
+
.command('load')
|
|
9
|
+
.description('Load a guide page, or the table of contents when no path is given')
|
|
10
|
+
.argument('[path]', 'guide page path, e.g. script-language/sdk/records')
|
|
11
|
+
.option('--locale <locale>', 'guide locale (en|zh)')
|
|
12
|
+
.action((path, options) => runCommand(() => guideLoadAction({ path, locale: options.locale, server: serverOption() })));
|
|
13
|
+
guide
|
|
14
|
+
.command('search')
|
|
15
|
+
.description('Search the guide with keywords or a question')
|
|
16
|
+
.argument('<query>')
|
|
17
|
+
.option('--locale <locale>', 'guide locale (en|zh)')
|
|
18
|
+
.option('--limit <n>', 'maximum hits')
|
|
19
|
+
.action((query, options) => runCommand(() => guideSearchAction({ query, locale: options.locale, limit: options.limit, server: serverOption() })));
|
|
20
|
+
}
|
|
21
|
+
export async function guideLoadAction(options) {
|
|
22
|
+
const server = resolveServer(await loadConfig(), options.server);
|
|
23
|
+
const args = {};
|
|
24
|
+
if (options.path !== undefined)
|
|
25
|
+
args.path = options.path;
|
|
26
|
+
if (options.locale !== undefined)
|
|
27
|
+
args.locale = options.locale;
|
|
28
|
+
printOutcome(await callAxomTool(server, 'axom__guide__load', args));
|
|
29
|
+
}
|
|
30
|
+
export async function guideSearchAction(options) {
|
|
31
|
+
const server = resolveServer(await loadConfig(), options.server);
|
|
32
|
+
const args = { query: options.query };
|
|
33
|
+
if (options.locale !== undefined)
|
|
34
|
+
args.locale = options.locale;
|
|
35
|
+
if (options.limit !== undefined)
|
|
36
|
+
args.limit = Number(options.limit);
|
|
37
|
+
printOutcome(await callAxomTool(server, 'axom__guide__search', args));
|
|
38
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { parseJsonArg, splitPath } from '../args.js';
|
|
2
|
+
import { UsageError, loadConfig, resolveServer } from '../config.js';
|
|
3
|
+
import { callAxomTool } from '../mcp.js';
|
|
4
|
+
import { printOutcome, runCommand } from '../output.js';
|
|
5
|
+
const recordExample = '{"title":"Hello"}';
|
|
6
|
+
const queryExample = '{"filters":[{"op":"eq","field":"status","value":"active"}],"page":{"number":1,"size":20}}';
|
|
7
|
+
const queryKeys = ['filters', 'sorts', 'page'];
|
|
8
|
+
export function registerRecordCommands(program) {
|
|
9
|
+
const record = program.command('record').description('Read and mutate Axomwork records');
|
|
10
|
+
const serverOption = () => program.opts().server;
|
|
11
|
+
record
|
|
12
|
+
.command('get')
|
|
13
|
+
.argument('<path>', '<app>/<model>')
|
|
14
|
+
.argument('<recordId>')
|
|
15
|
+
.action((path, id) => runCommand(() => recordGetAction({ path, id, server: serverOption() })));
|
|
16
|
+
record
|
|
17
|
+
.command('create')
|
|
18
|
+
.argument('<path>', '<app>/<model>')
|
|
19
|
+
.argument('<json>')
|
|
20
|
+
.action((path, json) => runCommand(() => recordCreateAction({ path, json, server: serverOption() })));
|
|
21
|
+
record
|
|
22
|
+
.command('update')
|
|
23
|
+
.argument('<path>', '<app>/<model>')
|
|
24
|
+
.argument('<recordId>')
|
|
25
|
+
.argument('<json>')
|
|
26
|
+
.action((path, id, json) => runCommand(() => recordUpdateAction({ path, id, json, server: serverOption() })));
|
|
27
|
+
record
|
|
28
|
+
.command('upsert')
|
|
29
|
+
.argument('<application>')
|
|
30
|
+
.argument('<model>')
|
|
31
|
+
.requiredOption('--identity-field <field>', 'Field used to match an existing record')
|
|
32
|
+
.requiredOption('--record <json>', 'JSON record payload')
|
|
33
|
+
.action((applicationName, modelName, options) => runCommand(() => recordUpsertAction({ ...options, applicationName, modelName, server: serverOption() })));
|
|
34
|
+
record
|
|
35
|
+
.command('delete')
|
|
36
|
+
.argument('<path>', '<app>/<model>')
|
|
37
|
+
.argument('<recordId>')
|
|
38
|
+
.action((path, id) => runCommand(() => recordDeleteAction({ path, id, server: serverOption() })));
|
|
39
|
+
record
|
|
40
|
+
.command('query')
|
|
41
|
+
.argument('<path>', '<app>/<model>')
|
|
42
|
+
.argument('[criteriaJson]')
|
|
43
|
+
.action((path, json) => runCommand(() => recordQueryAction({ path, json, server: serverOption() })));
|
|
44
|
+
}
|
|
45
|
+
export async function recordGetAction(options) {
|
|
46
|
+
const server = await resolveActionServer(options.server);
|
|
47
|
+
const [applicationName, modelName] = splitPath(options.path, 2, '<app>/<model>');
|
|
48
|
+
printOutcome(await callAxomTool(server, 'axom__record__get', { applicationName, modelName, id: options.id }));
|
|
49
|
+
}
|
|
50
|
+
export async function recordCreateAction(options) {
|
|
51
|
+
const server = await resolveActionServer(options.server);
|
|
52
|
+
const [applicationName, modelName] = splitPath(options.path, 2, '<app>/<model>');
|
|
53
|
+
const record = parseJsonArg(options.json, 'record', recordExample);
|
|
54
|
+
printOutcome(await callAxomTool(server, 'axom__record__create', { applicationName, modelName, record }));
|
|
55
|
+
}
|
|
56
|
+
export async function recordUpdateAction(options) {
|
|
57
|
+
const server = await resolveActionServer(options.server);
|
|
58
|
+
const [applicationName, modelName] = splitPath(options.path, 2, '<app>/<model>');
|
|
59
|
+
const changes = parseJsonArg(options.json, 'record update changes', recordExample);
|
|
60
|
+
printOutcome(await callAxomTool(server, 'axom__record__update', { applicationName, modelName, id: options.id, changes }));
|
|
61
|
+
}
|
|
62
|
+
export async function recordUpsertAction(options) {
|
|
63
|
+
const server = await resolveActionServer(options.server);
|
|
64
|
+
const record = parseJsonArg(options.record, 'record', recordExample);
|
|
65
|
+
printOutcome(await callAxomTool(server, 'axom__record__upsert', {
|
|
66
|
+
applicationName: options.applicationName,
|
|
67
|
+
modelName: options.modelName,
|
|
68
|
+
identityField: options.identityField,
|
|
69
|
+
record,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
export async function recordDeleteAction(options) {
|
|
73
|
+
const server = await resolveActionServer(options.server);
|
|
74
|
+
const [applicationName, modelName] = splitPath(options.path, 2, '<app>/<model>');
|
|
75
|
+
printOutcome(await callAxomTool(server, 'axom__record__delete', { applicationName, modelName, id: options.id }));
|
|
76
|
+
}
|
|
77
|
+
export async function recordQueryAction(options) {
|
|
78
|
+
const server = await resolveActionServer(options.server);
|
|
79
|
+
const [applicationName, modelName] = splitPath(options.path, 2, '<app>/<model>');
|
|
80
|
+
const criteria = parseJsonArg(options.json, 'record query criteria', queryExample);
|
|
81
|
+
const invalidKeys = Object.keys(criteria).filter((key) => !queryKeys.includes(key));
|
|
82
|
+
if (invalidKeys.length > 0) {
|
|
83
|
+
throw new UsageError(`Invalid record query criteria keys: ${invalidKeys.join(', ')}. Valid keys: filters, sorts, page. Example: ${queryExample}`);
|
|
84
|
+
}
|
|
85
|
+
const args = { applicationName, modelName };
|
|
86
|
+
const queryCriteria = {};
|
|
87
|
+
for (const key of ['filters', 'sorts']) {
|
|
88
|
+
if (key in criteria)
|
|
89
|
+
queryCriteria[key] = criteria[key];
|
|
90
|
+
}
|
|
91
|
+
if (Object.keys(queryCriteria).length > 0)
|
|
92
|
+
args.criteria = queryCriteria;
|
|
93
|
+
if ('page' in criteria)
|
|
94
|
+
args.page = criteria.page;
|
|
95
|
+
printOutcome(await callAxomTool(server, 'axom__record__query', args));
|
|
96
|
+
}
|
|
97
|
+
async function resolveActionServer(server) {
|
|
98
|
+
return resolveServer(await loadConfig(), server);
|
|
99
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { UsageError, loadConfig, resolveServer } from '../config.js';
|
|
2
|
+
import { materializeAgentsSkills, removeAgentsSkills } from '../install/agentsDir.js';
|
|
3
|
+
import { installAgent, skillsSourceDir, uninstallAgent } from '../install/installer.js';
|
|
4
|
+
import { runCommand } from '../output.js';
|
|
5
|
+
/**
|
|
6
|
+
* `axom skills add|remove`: materializes the bundled skills into `.agents/skills/` under the
|
|
7
|
+
* current directory — the convention Codex, Cursor, OpenCode, Pi and others already read, no
|
|
8
|
+
* per-agent code needed. `--claude` is the one exception, since Claude Code reads skills through
|
|
9
|
+
* its own plugin/marketplace mechanism instead; that path delegates to the existing
|
|
10
|
+
* `installAgent`/`uninstallAgent('claude', ...)`.
|
|
11
|
+
*
|
|
12
|
+
* `--project` only means something on the `--claude` path (Claude's user-wide-vs-project
|
|
13
|
+
* registration switch). The default `.agents/skills` path always writes under the current
|
|
14
|
+
* working directory, so `--project` there is meaningless and is rejected rather than ignored.
|
|
15
|
+
*/
|
|
16
|
+
export function registerSkillsCommands(program) {
|
|
17
|
+
const serverOption = () => program.opts().server;
|
|
18
|
+
const skills = program.command('skills').description('Materialize Axomwork skills for AI coding agents');
|
|
19
|
+
skills
|
|
20
|
+
.command('add')
|
|
21
|
+
.description('Add Axomwork skills to .agents/skills in the current directory (or --claude for Claude Code)')
|
|
22
|
+
.option('--claude', 'install the Claude Code plugin instead of writing .agents/skills')
|
|
23
|
+
.option('--project', 'Claude Code only: register for the current project instead of user-wide')
|
|
24
|
+
.action((options) => runCommand(async () => {
|
|
25
|
+
if (options.claude) {
|
|
26
|
+
const server = resolveServer(await loadConfig(), serverOption());
|
|
27
|
+
const report = await installAgent('claude', {
|
|
28
|
+
serverUrl: server,
|
|
29
|
+
scope: options.project ? 'project' : 'user',
|
|
30
|
+
});
|
|
31
|
+
console.log(`target: ${report.target}`);
|
|
32
|
+
for (const file of report.installedFiles)
|
|
33
|
+
console.log(`installed: ${file}`);
|
|
34
|
+
console.log(report.mcpRegistration);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
rejectProjectWithoutClaude(options);
|
|
38
|
+
const { files } = await materializeAgentsSkills(skillsSourceDir(), process.cwd());
|
|
39
|
+
for (const file of files)
|
|
40
|
+
console.log(`installed: ${file}`);
|
|
41
|
+
}));
|
|
42
|
+
skills
|
|
43
|
+
.command('remove')
|
|
44
|
+
.description('Remove Axomwork skills from .agents/skills in the current directory (or --claude for Claude Code)')
|
|
45
|
+
.option('--claude', 'remove the Claude Code plugin instead of .agents/skills')
|
|
46
|
+
.option('--project', 'Claude Code only: matches the scope used with `add --project`')
|
|
47
|
+
.action((options) => runCommand(async () => {
|
|
48
|
+
if (options.claude) {
|
|
49
|
+
const report = await uninstallAgent('claude', {
|
|
50
|
+
serverUrl: '',
|
|
51
|
+
scope: options.project ? 'project' : 'user',
|
|
52
|
+
});
|
|
53
|
+
console.log(`target: ${report.target}`);
|
|
54
|
+
for (const file of report.removed)
|
|
55
|
+
console.log(`removed: ${file}`);
|
|
56
|
+
console.log(report.notes);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
rejectProjectWithoutClaude(options);
|
|
60
|
+
const { removed } = await removeAgentsSkills(process.cwd());
|
|
61
|
+
for (const file of removed)
|
|
62
|
+
console.log(`removed: ${file}`);
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
function rejectProjectWithoutClaude(options) {
|
|
66
|
+
if (options.project) {
|
|
67
|
+
throw new UsageError('--project is only meaningful with --claude; the default .agents/skills path always writes to the current directory.');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { splitPath } from '../args.js';
|
|
2
|
+
import { UsageError, loadConfig, resolveServer } from '../config.js';
|
|
3
|
+
import { callAxomTool } from '../mcp.js';
|
|
4
|
+
import { printOutcome, runCommand } from '../output.js';
|
|
5
|
+
export function registerStructCommands(program) {
|
|
6
|
+
const struct = program.command('struct').description('Inspect Axomwork app and model structure');
|
|
7
|
+
const serverOption = () => program.opts().server;
|
|
8
|
+
struct
|
|
9
|
+
.command('list')
|
|
10
|
+
.argument('<kind>', 'app|model')
|
|
11
|
+
.argument('[target]', 'app name for model listing')
|
|
12
|
+
.action((kind, target) => runCommand(() => structListAction({ kind, target, server: serverOption() })));
|
|
13
|
+
struct
|
|
14
|
+
.command('get')
|
|
15
|
+
.argument('<kind>', 'app|model')
|
|
16
|
+
.argument('<target>', 'app name or app/model path')
|
|
17
|
+
.action((kind, target) => runCommand(() => structGetAction({ kind, target, server: serverOption() })));
|
|
18
|
+
}
|
|
19
|
+
export async function structListAction(options) {
|
|
20
|
+
const server = await resolveActionServer(options.server);
|
|
21
|
+
const kind = requireStructKind(options.kind);
|
|
22
|
+
if (kind === 'app') {
|
|
23
|
+
if (options.target !== undefined) {
|
|
24
|
+
throw new UsageError('Expected axom struct list app with no target.');
|
|
25
|
+
}
|
|
26
|
+
printOutcome(await callAxomTool(server, 'axom__struct__list_applications', {}));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const [applicationName] = splitPath(options.target ?? '', 1, '<applicationName>');
|
|
30
|
+
printOutcome(await callAxomTool(server, 'axom__struct__list_models', { applicationName }));
|
|
31
|
+
}
|
|
32
|
+
export async function structGetAction(options) {
|
|
33
|
+
const server = await resolveActionServer(options.server);
|
|
34
|
+
const kind = requireStructKind(options.kind);
|
|
35
|
+
if (kind === 'app') {
|
|
36
|
+
const [applicationName] = splitPath(options.target ?? '', 1, '<applicationName>');
|
|
37
|
+
printOutcome(await callAxomTool(server, 'axom__struct__get_application', { applicationName }));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const [applicationName, modelName] = splitPath(options.target ?? '', 2, '<applicationName>/<modelName>');
|
|
41
|
+
printOutcome(await callAxomTool(server, 'axom__struct__get_model', { applicationName, modelName }));
|
|
42
|
+
}
|
|
43
|
+
async function resolveActionServer(server) {
|
|
44
|
+
return resolveServer(await loadConfig(), server);
|
|
45
|
+
}
|
|
46
|
+
function requireStructKind(kind) {
|
|
47
|
+
if (kind === 'app' || kind === 'model')
|
|
48
|
+
return kind;
|
|
49
|
+
throw new UsageError(`Unknown struct kind "${kind}". Valid values: app|model`);
|
|
50
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { UsageError, loadConfig, resolveServer } from '../config.js';
|
|
3
|
+
import { runCommand } from '../output.js';
|
|
4
|
+
import { clientDir } from '../workstation/config.js';
|
|
5
|
+
import { CLI_PATH } from '../workstation/daemonctl.js';
|
|
6
|
+
import { createUpdateCoordinator } from '../workstation/update/runUpdate.js';
|
|
7
|
+
function isManifest(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null
|
|
9
|
+
&& typeof value.version === 'string'
|
|
10
|
+
&& typeof value.url === 'string'
|
|
11
|
+
&& typeof value.size === 'number'
|
|
12
|
+
&& typeof value.sha512 === 'string';
|
|
13
|
+
}
|
|
14
|
+
/** Fetches the release the server currently publishes for this exact deployment. */
|
|
15
|
+
async function fetchManifest(server, fetchImpl) {
|
|
16
|
+
const url = new URL('/download/client/manifest.json', server).toString();
|
|
17
|
+
const response = await fetchImpl(url);
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
throw new Error(`Fetching the release manifest failed with HTTP ${response.status}`);
|
|
20
|
+
}
|
|
21
|
+
const parsed = await response.json();
|
|
22
|
+
if (!isManifest(parsed)) {
|
|
23
|
+
throw new Error('Release manifest is malformed');
|
|
24
|
+
}
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
function defaultCoordinator(dir, server) {
|
|
28
|
+
return createUpdateCoordinator({
|
|
29
|
+
dir, server,
|
|
30
|
+
// `CLI_PATH` is resolved once, from daemonctl.js's own compiled location — not re-derived here
|
|
31
|
+
// from this module's own URL. See CLI_PATH's doc in daemonctl.ts for why that would be the
|
|
32
|
+
// wrong thing to do even though, at update.js's current depth under dist/, it would happen to
|
|
33
|
+
// produce the right answer.
|
|
34
|
+
cliPath: CLI_PATH,
|
|
35
|
+
// A one-shot CLI invocation with no supervisor and, on a CLI-only machine, no daemon to
|
|
36
|
+
// restart into: it must not provision the OS service or arm the rollback watchdog. See
|
|
37
|
+
// UpdateCoordinatorDeps.serviceLifecycle in runUpdate.ts for why.
|
|
38
|
+
serviceLifecycle: 'none',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `axom update [--version x.y.z]`: pulls the release manifest the configured server currently
|
|
43
|
+
* publishes and runs it through the exact same `UpdateCoordinator` a server-triggered update uses
|
|
44
|
+
* (see runUpdate.ts) — there is no second update implementation for the manual path.
|
|
45
|
+
*/
|
|
46
|
+
export async function updateAction(options, deps = {}) {
|
|
47
|
+
const config = await loadConfig();
|
|
48
|
+
const server = resolveServer(config, options.server ?? config.defaultServer);
|
|
49
|
+
const fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
50
|
+
const manifest = await fetchManifest(server, fetchImpl);
|
|
51
|
+
if (options.version && options.version !== manifest.version) {
|
|
52
|
+
throw new UsageError(`Requested version ${options.version} does not match the server's published release ${manifest.version}.`);
|
|
53
|
+
}
|
|
54
|
+
const release = {
|
|
55
|
+
version: manifest.version, url: manifest.url, size: manifest.size, sha512: manifest.sha512,
|
|
56
|
+
};
|
|
57
|
+
const dir = deps.dir ?? clientDir();
|
|
58
|
+
const coordinator = deps.createCoordinator ? deps.createCoordinator({ dir, server }) : defaultCoordinator(dir, server);
|
|
59
|
+
const command = { commandId: `manual-${randomUUID()}`, release, force: false };
|
|
60
|
+
const status = await coordinator.handle(command);
|
|
61
|
+
const log = deps.log ?? console.log;
|
|
62
|
+
// 'RESTARTING' and 'BUSY' are not handled as separate cases: neither is reachable from
|
|
63
|
+
// `defaultCoordinator`. 'RESTARTING' is the phase of an update that hands off by exiting, and
|
|
64
|
+
// `serviceLifecycle: 'none'` above is exactly the lifecycle that has nothing to hand off to —
|
|
65
|
+
// the coordinator ends such an update at 'SUCCEEDED' instead (see the `restart`-less branch in
|
|
66
|
+
// UpdateCoordinator.execute), which is why the successful case below is that phase and why this
|
|
67
|
+
// command, not the coordinator, is what tells the operator a restart is still owed. 'BUSY' would
|
|
68
|
+
// require `TaskRegistry.canUpdate()` to return false, but the manual path always hands the
|
|
69
|
+
// coordinator a fresh, empty, in-process `TaskRegistry` (there is nothing else in this process
|
|
70
|
+
// that could be running a task), so it is always true. Keeping dedicated cases for phases the
|
|
71
|
+
// real assembly can never produce is exactly the untestable-branch shape this command was
|
|
72
|
+
// flagged for; if a future caller makes either reachable, restore its case then.
|
|
73
|
+
switch (status.phase) {
|
|
74
|
+
case 'SUCCEEDED':
|
|
75
|
+
log(`Installed ${release.version}. Run \`axom workstation restart\` to apply it.`);
|
|
76
|
+
return;
|
|
77
|
+
default:
|
|
78
|
+
throw new Error(status.error ?? `Update did not complete (phase: ${status.phase}).`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function registerUpdateCommand(program) {
|
|
82
|
+
const serverOption = () => program.opts().server;
|
|
83
|
+
program
|
|
84
|
+
.command('update')
|
|
85
|
+
.description('Update axom-cli to the release the configured server currently publishes')
|
|
86
|
+
.option('--version <version>', 'Fail unless the server-published release matches this exact version')
|
|
87
|
+
.action((options) => runCommand(() => updateAction({ server: serverOption(), version: options.version })));
|
|
88
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { loadConfig, resolveServer } from '../config.js';
|
|
2
|
+
import { callAxomTool } from '../mcp.js';
|
|
3
|
+
import { printOutcome } from '../output.js';
|
|
4
|
+
export async function whoamiAction(options) {
|
|
5
|
+
const config = await loadConfig();
|
|
6
|
+
const server = resolveServer(config, options.server);
|
|
7
|
+
printOutcome(await callAxomTool(server, 'axom__system__whoami', {}));
|
|
8
|
+
}
|