pmx-canvas 0.1.35 → 0.1.36
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/CHANGELOG.md +52 -0
- package/Readme.md +14 -2
- package/dist/canvas/index.js +82 -41
- package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -0
- package/dist/types/mcp/canvas-access.d.ts +20 -1
- package/dist/types/server/ax-context.d.ts +1 -1
- package/dist/types/server/ax-state.d.ts +28 -0
- package/dist/types/server/ax-wait.d.ts +23 -0
- package/dist/types/server/canvas-state.d.ts +55 -3
- package/dist/types/server/html-surface.d.ts +7 -0
- package/dist/types/server/index.d.ts +60 -2
- package/docs/ax-host-adapter-contract.md +65 -0
- package/docs/http-api.md +34 -2
- package/docs/mcp.md +5 -1
- package/docs/screenshot.png +0 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +50 -8
- package/skills/pmx-canvas/references/codex-app-adapter.md +15 -1
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +31 -0
- package/src/client/nodes/ExtAppFrame.tsx +73 -5
- package/src/client/nodes/HtmlNode.tsx +12 -3
- package/src/client/nodes/McpAppNode.tsx +12 -3
- package/src/json-render/renderer/index.tsx +3 -0
- package/src/mcp/canvas-access.ts +74 -5
- package/src/mcp/server.ts +94 -53
- package/src/server/ax-context.ts +7 -1
- package/src/server/ax-state.ts +87 -0
- package/src/server/ax-wait.ts +56 -0
- package/src/server/canvas-state.ts +131 -3
- package/src/server/html-surface.ts +49 -11
- package/src/server/index.ts +82 -2
- package/src/server/server.ts +189 -9
package/src/server/server.ts
CHANGED
|
@@ -78,9 +78,11 @@ import {
|
|
|
78
78
|
import { buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
79
79
|
import { buildAgentContextPreamble, serializeNodeForAgentContext } from './agent-context.js';
|
|
80
80
|
import { buildCanvasAxContext, buildCanvasAxSurfaceSnapshot } from './ax-context.js';
|
|
81
|
-
import { applyAxInteraction, resolveNodeAxCapabilities } from './ax-interaction.js';
|
|
82
|
-
import { isAxEventKind, isAxEvidenceKind } from './ax-state.js';
|
|
81
|
+
import { applyAxInteraction, resolveNodeAxCapabilities, normalizeNodeAxCapabilities } from './ax-interaction.js';
|
|
82
|
+
import { isAxEventKind, isAxEvidenceKind, isAxActivityKind } from './ax-state.js';
|
|
83
|
+
import { waitForAxResolution, AX_WAIT_MAX_MS } from './ax-wait.js';
|
|
83
84
|
import type {
|
|
85
|
+
PmxAxEvidenceKind,
|
|
84
86
|
PmxAxPolicy,
|
|
85
87
|
PmxAxReviewAnchorType,
|
|
86
88
|
PmxAxReviewKind,
|
|
@@ -88,6 +90,7 @@ import type {
|
|
|
88
90
|
PmxAxReviewSeverity,
|
|
89
91
|
PmxAxReviewStatus,
|
|
90
92
|
PmxAxSource,
|
|
93
|
+
PmxAxWorkItemStatus,
|
|
91
94
|
} from './ax-state.js';
|
|
92
95
|
import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
|
|
93
96
|
import { validateLocalImageFile } from './image-source.js';
|
|
@@ -1758,7 +1761,15 @@ async function createCanvasWebpageNode(body: Record<string, unknown>): Promise<R
|
|
|
1758
1761
|
async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
1759
1762
|
const body = await readJson(req);
|
|
1760
1763
|
const queryType = new URL(req.url).searchParams.get('type');
|
|
1761
|
-
|
|
1764
|
+
// Report #50: require a resolvable type rather than silently defaulting to a
|
|
1765
|
+
// markdown node — an empty / type-less body created a phantom node before.
|
|
1766
|
+
const type = typeof body.type === 'string' ? body.type : (queryType || '');
|
|
1767
|
+
if (!type) {
|
|
1768
|
+
return responseJson({
|
|
1769
|
+
ok: false,
|
|
1770
|
+
error: `node creation requires a 'type' — pass it in the JSON body ({ "type": "markdown", ... }) or as a ?type= query param. Valid types: ${[...VALID_NODE_TYPES].join(', ')} (json-render / graph / web-artifact have dedicated endpoints).`,
|
|
1771
|
+
}, 400);
|
|
1772
|
+
}
|
|
1762
1773
|
|
|
1763
1774
|
if (!VALID_NODE_TYPES.has(type)) {
|
|
1764
1775
|
if (type === 'json-render') {
|
|
@@ -1824,8 +1835,11 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1824
1835
|
const content = type === 'image' && typeof body.path === 'string' && typeof body.content !== 'string'
|
|
1825
1836
|
? body.path
|
|
1826
1837
|
: body.content;
|
|
1827
|
-
// For html nodes, accept top-level `html`
|
|
1828
|
-
// can POST { type: 'html', title, html } without nesting
|
|
1838
|
+
// For html nodes, accept top-level `html` AND `axCapabilities` and merge into data
|
|
1839
|
+
// so callers can POST { type: 'html', title, html, axCapabilities } without nesting
|
|
1840
|
+
// under `data` (report #53 — transport parity with MCP canvas_add_html_node). A
|
|
1841
|
+
// top-level value overrides the same key under `data` (mirrors the `html` precedence).
|
|
1842
|
+
const topAxCapabilities = type === 'html' ? normalizeNodeAxCapabilities(body.axCapabilities) : null;
|
|
1829
1843
|
const htmlMergedData = type === 'html'
|
|
1830
1844
|
? {
|
|
1831
1845
|
...(extraData ?? {}),
|
|
@@ -1837,6 +1851,7 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1837
1851
|
...(Array.isArray(body.slideTitles) ? { slideTitles: body.slideTitles } : {}),
|
|
1838
1852
|
...(Array.isArray(body.embeddedNodeIds) ? { embeddedNodeIds: body.embeddedNodeIds } : {}),
|
|
1839
1853
|
...(Array.isArray(body.embeddedUrls) ? { embeddedUrls: body.embeddedUrls } : {}),
|
|
1854
|
+
...(topAxCapabilities ? { axCapabilities: topAxCapabilities } : {}),
|
|
1840
1855
|
}
|
|
1841
1856
|
: extraData;
|
|
1842
1857
|
let added: ReturnType<typeof addCanvasNode>;
|
|
@@ -2105,7 +2120,8 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
|
|
|
2105
2120
|
body.data ||
|
|
2106
2121
|
typeof body.arrangeLocked === 'boolean' ||
|
|
2107
2122
|
typeof body.strictSize === 'boolean' ||
|
|
2108
|
-
(existing.type === 'trace' && hasTraceNodeDataFields(body))
|
|
2123
|
+
(existing.type === 'trace' && hasTraceNodeDataFields(body)) ||
|
|
2124
|
+
(existing.type === 'html' && (body.html !== undefined || body.axCapabilities !== undefined))
|
|
2109
2125
|
) {
|
|
2110
2126
|
const data = { ...existing.data };
|
|
2111
2127
|
if (body.title !== undefined) {
|
|
@@ -2121,6 +2137,18 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
|
|
|
2121
2137
|
if (body.data && typeof body.data === 'object' && !Array.isArray(body.data)) {
|
|
2122
2138
|
Object.assign(data, body.data as Record<string, unknown>);
|
|
2123
2139
|
}
|
|
2140
|
+
// Report #53: for html nodes, accept top-level `html` / `axCapabilities` on PATCH
|
|
2141
|
+
// too (top-level overrides the `data.*` merge above — matches POST + MCP parity).
|
|
2142
|
+
if (existing.type === 'html') {
|
|
2143
|
+
if (body.html !== undefined) {
|
|
2144
|
+
if (typeof body.html !== 'string') {
|
|
2145
|
+
return responseJson({ ok: false, error: 'HTML node field "html" must be a string.' }, 400);
|
|
2146
|
+
}
|
|
2147
|
+
data.html = resolveHtmlContent(body.html);
|
|
2148
|
+
}
|
|
2149
|
+
const patchAxCapabilities = normalizeNodeAxCapabilities(body.axCapabilities);
|
|
2150
|
+
if (patchAxCapabilities) data.axCapabilities = patchAxCapabilities;
|
|
2151
|
+
}
|
|
2124
2152
|
if (existing.type === 'webpage') {
|
|
2125
2153
|
const nextUrl = typeof body.url === 'string'
|
|
2126
2154
|
? body.url
|
|
@@ -3912,8 +3940,132 @@ function handleGetAxState(): Response {
|
|
|
3912
3940
|
return responseJson({ ok: true, state: canvasState.getAxState() });
|
|
3913
3941
|
}
|
|
3914
3942
|
|
|
3915
|
-
function handleGetAxContext(): Response {
|
|
3916
|
-
|
|
3943
|
+
function handleGetAxContext(url: URL): Response {
|
|
3944
|
+
// Optional ?consumer= filters the compact `delivery` lead block (loop-safe — a
|
|
3945
|
+
// consumer never sees steering/activity it originated), so a host adapter can
|
|
3946
|
+
// inject its own un-truncated pending block per turn (report #54 hardening).
|
|
3947
|
+
const consumer = url.searchParams.get('consumer') ?? undefined;
|
|
3948
|
+
return responseJson(buildCanvasAxContext(consumer));
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
// Clamp ?waitMs= to [0, AX_WAIT_MAX_MS]. 0 (or absent/NaN) = a plain single read.
|
|
3952
|
+
function parseAxWaitMs(url: URL): number {
|
|
3953
|
+
const raw = Number(url.searchParams.get('waitMs') ?? '');
|
|
3954
|
+
return Number.isFinite(raw) && raw > 0 ? Math.min(raw, AX_WAIT_MAX_MS) : 0;
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
function isReviewSeverity(v: unknown): v is PmxAxReviewSeverity {
|
|
3958
|
+
return v === 'info' || v === 'warning' || v === 'error';
|
|
3959
|
+
}
|
|
3960
|
+
function isReviewKind(v: unknown): v is PmxAxReviewKind {
|
|
3961
|
+
return v === 'comment' || v === 'finding';
|
|
3962
|
+
}
|
|
3963
|
+
function isReviewAnchor(v: unknown): v is PmxAxReviewAnchorType {
|
|
3964
|
+
return v === 'node' || v === 'file' || v === 'region';
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
// Validate untrusted activity `reactions` from an HTTP body into the typed override
|
|
3968
|
+
// shape ingestActivity expects. `false` suppresses a default reaction; an object
|
|
3969
|
+
// overrides its fields (invalid fields are dropped, not stored raw).
|
|
3970
|
+
function normalizeActivityReactions(input: Record<string, unknown>): {
|
|
3971
|
+
workItem?: false | { status?: PmxAxWorkItemStatus; detail?: string | null };
|
|
3972
|
+
evidence?: false | { kind?: PmxAxEvidenceKind; body?: string | null };
|
|
3973
|
+
review?: false | { severity?: PmxAxReviewSeverity; kind?: PmxAxReviewKind; anchorType?: PmxAxReviewAnchorType; nodeId?: string | null };
|
|
3974
|
+
} {
|
|
3975
|
+
const out: ReturnType<typeof normalizeActivityReactions> = {};
|
|
3976
|
+
if (input.workItem === false) out.workItem = false;
|
|
3977
|
+
else if (isRecord(input.workItem)) {
|
|
3978
|
+
const status = normalizeAxWorkItemStatus(input.workItem.status);
|
|
3979
|
+
out.workItem = {
|
|
3980
|
+
...(status ? { status } : {}),
|
|
3981
|
+
...(typeof input.workItem.detail === 'string' ? { detail: input.workItem.detail } : {}),
|
|
3982
|
+
};
|
|
3983
|
+
}
|
|
3984
|
+
if (input.evidence === false) out.evidence = false;
|
|
3985
|
+
else if (isRecord(input.evidence)) {
|
|
3986
|
+
out.evidence = {
|
|
3987
|
+
...(isAxEvidenceKind(input.evidence.kind) ? { kind: input.evidence.kind } : {}),
|
|
3988
|
+
...(typeof input.evidence.body === 'string' ? { body: input.evidence.body } : {}),
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
if (input.review === false) out.review = false;
|
|
3992
|
+
else if (isRecord(input.review)) {
|
|
3993
|
+
out.review = {
|
|
3994
|
+
...(isReviewSeverity(input.review.severity) ? { severity: input.review.severity } : {}),
|
|
3995
|
+
...(isReviewKind(input.review.kind) ? { kind: input.review.kind } : {}),
|
|
3996
|
+
...(isReviewAnchor(input.review.anchorType) ? { anchorType: input.review.anchorType } : {}),
|
|
3997
|
+
...(typeof input.review.nodeId === 'string' ? { nodeId: input.review.nodeId } : {}),
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
return out;
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
// Report primitive A: ingest a harness-forwarded agent activity; the board auto-reacts.
|
|
4004
|
+
async function handleAxActivityIngest(req: Request): Promise<Response> {
|
|
4005
|
+
const body = await readJson(req);
|
|
4006
|
+
if (!isAxActivityKind(body.kind)) {
|
|
4007
|
+
return responseJson({ ok: false, error: "activity requires a valid 'kind': one of tool-start, tool-result, failure, error, session-start, session-end, command, note." }, 400);
|
|
4008
|
+
}
|
|
4009
|
+
if (typeof body.title !== 'string' || !body.title.trim()) {
|
|
4010
|
+
return responseJson({ ok: false, error: 'activity requires a title.' }, 400);
|
|
4011
|
+
}
|
|
4012
|
+
const result = canvasState.ingestActivity(
|
|
4013
|
+
{
|
|
4014
|
+
kind: body.kind,
|
|
4015
|
+
title: body.title,
|
|
4016
|
+
...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
|
|
4017
|
+
...(body.outcome === 'success' || body.outcome === 'failure' ? { outcome: body.outcome } : {}),
|
|
4018
|
+
...(typeof body.ref === 'string' ? { ref: body.ref } : {}),
|
|
4019
|
+
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
4020
|
+
...(isRecord(body.data) ? { data: body.data } : {}),
|
|
4021
|
+
...(isRecord(body.reactions) ? { reactions: normalizeActivityReactions(body.reactions) } : {}),
|
|
4022
|
+
},
|
|
4023
|
+
{ source: normalizeAxSource(body.source, 'api') },
|
|
4024
|
+
);
|
|
4025
|
+
const meta = { sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() };
|
|
4026
|
+
broadcastWorkbenchEvent('ax-event-created', { event: result.event, ...meta });
|
|
4027
|
+
if (result.workItem) broadcastWorkbenchEvent('ax-state-changed', { workItem: result.workItem, ...meta });
|
|
4028
|
+
if (result.evidence) broadcastWorkbenchEvent('ax-event-created', { evidence: result.evidence, ...meta });
|
|
4029
|
+
if (result.review) broadcastWorkbenchEvent('ax-state-changed', { reviewAnnotation: result.review, ...meta });
|
|
4030
|
+
return responseJson({ ok: true, ...result });
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
// Report primitive D: single-item read of a gate, with optional ?waitMs= long-poll
|
|
4034
|
+
// that resolves when the human resolves it in the browser (gates that actually gate).
|
|
4035
|
+
async function handleAxApprovalGet(url: URL, id: string, req: Request): Promise<Response> {
|
|
4036
|
+
const waitMs = parseAxWaitMs(url);
|
|
4037
|
+
const { value, pending } = await waitForAxResolution({
|
|
4038
|
+
read: () => canvasState.getApproval(id),
|
|
4039
|
+
isResolved: (g) => g.status !== 'pending',
|
|
4040
|
+
timeoutMs: waitMs,
|
|
4041
|
+
signal: req.signal,
|
|
4042
|
+
});
|
|
4043
|
+
if (!value) return responseJson({ ok: false, error: 'approval gate not found.' }, 404);
|
|
4044
|
+
return responseJson({ ok: true, approvalGate: value, pending });
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
async function handleAxElicitationGet(url: URL, id: string, req: Request): Promise<Response> {
|
|
4048
|
+
const waitMs = parseAxWaitMs(url);
|
|
4049
|
+
const { value, pending } = await waitForAxResolution({
|
|
4050
|
+
read: () => canvasState.getElicitation(id),
|
|
4051
|
+
isResolved: (e) => e.status !== 'pending',
|
|
4052
|
+
timeoutMs: waitMs,
|
|
4053
|
+
signal: req.signal,
|
|
4054
|
+
});
|
|
4055
|
+
if (!value) return responseJson({ ok: false, error: 'elicitation not found.' }, 404);
|
|
4056
|
+
return responseJson({ ok: true, elicitation: value, pending });
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
async function handleAxModeGet(url: URL, id: string, req: Request): Promise<Response> {
|
|
4060
|
+
const waitMs = parseAxWaitMs(url);
|
|
4061
|
+
const { value, pending } = await waitForAxResolution({
|
|
4062
|
+
read: () => canvasState.getModeRequest(id),
|
|
4063
|
+
isResolved: (m) => m.status !== 'pending',
|
|
4064
|
+
timeoutMs: waitMs,
|
|
4065
|
+
signal: req.signal,
|
|
4066
|
+
});
|
|
4067
|
+
if (!value) return responseJson({ ok: false, error: 'mode request not found.' }, 404);
|
|
4068
|
+
return responseJson({ ok: true, modeRequest: value, pending });
|
|
3917
4069
|
}
|
|
3918
4070
|
|
|
3919
4071
|
// Compact AX state for surfaces (the same shape seeded into AX-enabled iframes).
|
|
@@ -4172,6 +4324,11 @@ async function handleAxWorkAdd(req: Request): Promise<Response> {
|
|
|
4172
4324
|
if (typeof body.title !== 'string' || !body.title.trim()) {
|
|
4173
4325
|
return responseJson({ ok: false, error: 'work item requires a title.' }, 400);
|
|
4174
4326
|
}
|
|
4327
|
+
// Report #56: reject an unknown status (e.g. "in_progress") instead of silently
|
|
4328
|
+
// dropping it — the accepted tokens use hyphens.
|
|
4329
|
+
if (body.status !== undefined && !normalizeAxWorkItemStatus(body.status)) {
|
|
4330
|
+
return responseJson({ ok: false, error: `invalid work item status "${String(body.status)}"; expected one of: todo, in-progress, blocked, done, cancelled.` }, 400);
|
|
4331
|
+
}
|
|
4175
4332
|
const status = normalizeAxWorkItemStatus(body.status);
|
|
4176
4333
|
const workItem = canvasState.addWorkItem(
|
|
4177
4334
|
{
|
|
@@ -4192,6 +4349,10 @@ async function handleAxWorkAdd(req: Request): Promise<Response> {
|
|
|
4192
4349
|
|
|
4193
4350
|
async function handleAxWorkUpdate(req: Request, id: string): Promise<Response> {
|
|
4194
4351
|
const body = await readJson(req);
|
|
4352
|
+
// Report #56: reject an unknown status instead of returning ok:true + no-op.
|
|
4353
|
+
if (body.status !== undefined && !normalizeAxWorkItemStatus(body.status)) {
|
|
4354
|
+
return responseJson({ ok: false, error: `invalid work item status "${String(body.status)}"; expected one of: todo, in-progress, blocked, done, cancelled.` }, 400);
|
|
4355
|
+
}
|
|
4195
4356
|
const status = normalizeAxWorkItemStatus(body.status);
|
|
4196
4357
|
const workItem = canvasState.updateWorkItem(
|
|
4197
4358
|
id,
|
|
@@ -5422,7 +5583,11 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5422
5583
|
}
|
|
5423
5584
|
|
|
5424
5585
|
if (url.pathname === '/api/canvas/ax/context' && req.method === 'GET') {
|
|
5425
|
-
return handleGetAxContext();
|
|
5586
|
+
return handleGetAxContext(url);
|
|
5587
|
+
}
|
|
5588
|
+
|
|
5589
|
+
if (url.pathname === '/api/canvas/ax/activity' && req.method === 'POST') {
|
|
5590
|
+
return handleAxActivityIngest(req);
|
|
5426
5591
|
}
|
|
5427
5592
|
|
|
5428
5593
|
if (url.pathname === '/api/canvas/ax/surface-snapshot' && req.method === 'GET') {
|
|
@@ -5477,6 +5642,11 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5477
5642
|
return handleAxApprovalResolve(req, approvalId);
|
|
5478
5643
|
}
|
|
5479
5644
|
|
|
5645
|
+
if (url.pathname.startsWith('/api/canvas/ax/approval/') && !url.pathname.endsWith('/resolve') && req.method === 'GET') {
|
|
5646
|
+
const approvalId = decodeURIComponent(url.pathname.slice('/api/canvas/ax/approval/'.length));
|
|
5647
|
+
return handleAxApprovalGet(url, approvalId, req);
|
|
5648
|
+
}
|
|
5649
|
+
|
|
5480
5650
|
if (url.pathname === '/api/canvas/ax/evidence' && req.method === 'POST') {
|
|
5481
5651
|
return handleAxEvidenceAdd(req);
|
|
5482
5652
|
}
|
|
@@ -5532,6 +5702,11 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5532
5702
|
return handleAxElicitationRespond(req, elicitationId);
|
|
5533
5703
|
}
|
|
5534
5704
|
|
|
5705
|
+
if (url.pathname.startsWith('/api/canvas/ax/elicitation/') && !url.pathname.endsWith('/respond') && req.method === 'GET') {
|
|
5706
|
+
const elicitationId = decodeURIComponent(url.pathname.slice('/api/canvas/ax/elicitation/'.length));
|
|
5707
|
+
return handleAxElicitationGet(url, elicitationId, req);
|
|
5708
|
+
}
|
|
5709
|
+
|
|
5535
5710
|
if (url.pathname === '/api/canvas/ax/mode' && req.method === 'GET') {
|
|
5536
5711
|
return handleAxModeList();
|
|
5537
5712
|
}
|
|
@@ -5547,6 +5722,11 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5547
5722
|
return handleAxModeResolve(req, modeId);
|
|
5548
5723
|
}
|
|
5549
5724
|
|
|
5725
|
+
if (url.pathname.startsWith('/api/canvas/ax/mode/') && !url.pathname.endsWith('/resolve') && req.method === 'GET') {
|
|
5726
|
+
const modeId = decodeURIComponent(url.pathname.slice('/api/canvas/ax/mode/'.length));
|
|
5727
|
+
return handleAxModeGet(url, modeId, req);
|
|
5728
|
+
}
|
|
5729
|
+
|
|
5550
5730
|
if (url.pathname === '/api/canvas/ax/command' && req.method === 'GET') {
|
|
5551
5731
|
return handleAxCommandList();
|
|
5552
5732
|
}
|