@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.
Files changed (90) hide show
  1. package/README.md +64 -0
  2. package/dist/args.js +29 -0
  3. package/dist/cli.js +76 -0
  4. package/dist/commands/auth.js +52 -0
  5. package/dist/commands/execute.js +40 -0
  6. package/dist/commands/guide.js +38 -0
  7. package/dist/commands/record.js +99 -0
  8. package/dist/commands/skills.js +69 -0
  9. package/dist/commands/struct.js +50 -0
  10. package/dist/commands/update.js +88 -0
  11. package/dist/commands/whoami.js +8 -0
  12. package/dist/commands/workstation.js +171 -0
  13. package/dist/config.js +131 -0
  14. package/dist/credentialBroker.js +340 -0
  15. package/dist/credentials.js +211 -0
  16. package/dist/install/agentsDir.js +35 -0
  17. package/dist/install/claude/commands.js +89 -0
  18. package/dist/install/claude/materialize.js +42 -0
  19. package/dist/install/installer.js +45 -0
  20. package/dist/install/targets/claude.js +154 -0
  21. package/dist/mcp.js +152 -0
  22. package/dist/output.js +37 -0
  23. package/dist/workstation/acp/acpAgents.js +1287 -0
  24. package/dist/workstation/acp/acpClient.js +691 -0
  25. package/dist/workstation/acp/acpSession.js +1047 -0
  26. package/dist/workstation/acp/adoption.js +396 -0
  27. package/dist/workstation/acp/auth.js +80 -0
  28. package/dist/workstation/acp/authUrlSniffer.js +52 -0
  29. package/dist/workstation/acp/binaryInstall.js +411 -0
  30. package/dist/workstation/acp/configOptions.js +257 -0
  31. package/dist/workstation/acp/contract.js +494 -0
  32. package/dist/workstation/acp/draftInterceptor.js +154 -0
  33. package/dist/workstation/acp/draftSubagents.js +38 -0
  34. package/dist/workstation/acp/installErrors.js +89 -0
  35. package/dist/workstation/acp/installProbe.js +363 -0
  36. package/dist/workstation/acp/permission.js +327 -0
  37. package/dist/workstation/acp/registry/catalog.js +29 -0
  38. package/dist/workstation/acp/registry/launch.js +128 -0
  39. package/dist/workstation/acp/registry/registryClient.js +160 -0
  40. package/dist/workstation/acp/registry/registryTypes.js +8 -0
  41. package/dist/workstation/acp/sessionUpdateMapper.js +602 -0
  42. package/dist/workstation/acp/test/fakeAgent.js +435 -0
  43. package/dist/workstation/acp/test/sessionUpdateFixtures.js +165 -0
  44. package/dist/workstation/acp/timedCommand.js +47 -0
  45. package/dist/workstation/acp/uvxInstall.js +248 -0
  46. package/dist/workstation/acp/validate.js +134 -0
  47. package/dist/workstation/agentSessions.js +1628 -0
  48. package/dist/workstation/channels/agent.js +96 -0
  49. package/dist/workstation/channels/computer.js +91 -0
  50. package/dist/workstation/channels/management.js +231 -0
  51. package/dist/workstation/channels/url.js +42 -0
  52. package/dist/workstation/config.js +47 -0
  53. package/dist/workstation/connection/encryptedChannel.js +261 -0
  54. package/dist/workstation/connection/retryPolicy.js +14 -0
  55. package/dist/workstation/connection/supervisor.js +353 -0
  56. package/dist/workstation/crypto.js +63 -0
  57. package/dist/workstation/daemon.js +225 -0
  58. package/dist/workstation/daemonctl.js +95 -0
  59. package/dist/workstation/exec.js +112 -0
  60. package/dist/workstation/journal.js +257 -0
  61. package/dist/workstation/names.js +17 -0
  62. package/dist/workstation/oauth/browser.js +24 -0
  63. package/dist/workstation/oauth/deviceAuthorization.js +198 -0
  64. package/dist/workstation/osinfo.js +4 -0
  65. package/dist/workstation/protocol/agent.js +606 -0
  66. package/dist/workstation/protocol/common.js +136 -0
  67. package/dist/workstation/protocol/computer.js +31 -0
  68. package/dist/workstation/protocol/management.js +116 -0
  69. package/dist/workstation/register.js +42 -0
  70. package/dist/workstation/service/installer.js +362 -0
  71. package/dist/workstation/service/launchd.js +46 -0
  72. package/dist/workstation/service/loginShell.js +36 -0
  73. package/dist/workstation/service/systemd.js +48 -0
  74. package/dist/workstation/service/types.js +73 -0
  75. package/dist/workstation/supervision.js +52 -0
  76. package/dist/workstation/systemMetrics.js +271 -0
  77. package/dist/workstation/update/artifact.js +61 -0
  78. package/dist/workstation/update/coordinator.js +240 -0
  79. package/dist/workstation/update/npmInstaller.js +91 -0
  80. package/dist/workstation/update/redact.js +36 -0
  81. package/dist/workstation/update/runUpdate.js +215 -0
  82. package/dist/workstation/update/stateStore.js +87 -0
  83. package/dist/workstation/update/taskRegistry.js +142 -0
  84. package/dist/workstation/update/types.js +3 -0
  85. package/dist/workstation/update/watchdog.js +311 -0
  86. package/dist/workstation/version.js +12 -0
  87. package/package.json +39 -0
  88. package/skills/axom/SKILL.md +83 -0
  89. package/skills/axom-definition/SKILL.md +74 -0
  90. 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
+ }