@agentprojectcontext/apx 1.15.5 → 1.16.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/package.json +40 -5
- package/src/cli/commands/log.js +113 -0
- package/src/cli/commands/overlay.js +253 -0
- package/src/cli/commands/sys.js +88 -16
- package/src/cli/index.js +23 -1
- package/src/cli/terminal-chat/renderer.js +71 -56
- package/src/cli-ts/commands/agent.ts +173 -0
- package/src/cli-ts/commands/chat.ts +119 -0
- package/src/cli-ts/commands/daemon.ts +112 -0
- package/src/cli-ts/commands/exec.ts +109 -0
- package/src/cli-ts/commands/mcp.ts +235 -0
- package/src/cli-ts/commands/session.ts +224 -0
- package/src/cli-ts/commands/status.ts +61 -0
- package/src/cli-ts/http.ts +36 -0
- package/src/cli-ts/index.ts +73 -0
- package/src/cli-ts/ui.ts +107 -0
- package/src/core/logging.js +81 -0
- package/src/daemon/api.js +58 -0
- package/src/daemon/engines/anthropic.js +60 -1
- package/src/daemon/engines/index.js +2 -1
- package/src/daemon/engines/ollama.js +70 -3
- package/src/daemon/index.js +58 -0
- package/src/daemon/overlay-ws.js +40 -0
- package/src/daemon/plugins/index.js +2 -1
- package/src/daemon/plugins/overlay.js +177 -0
- package/src/daemon/plugins/telegram.js +15 -3
- package/src/daemon/super-agent.js +102 -19
- package/src/daemon/transcription.js +262 -59
- package/src/daemon/wakeup.js +14 -19
- package/src/daemon/whisper-server.py +57 -6
- package/src/overlay/index.html +44 -0
- package/src/overlay/main.js +480 -0
- package/src/overlay/package.json +3 -0
- package/src/overlay/preload.js +34 -0
- package/src/overlay/renderer.js +371 -0
- package/src/overlay/style.css +250 -0
- package/src/tui/_shims/cli-error.ts +6 -0
- package/src/tui/_shims/cli-logo.ts +18 -0
- package/src/tui/_shims/cli-ui.ts +1 -0
- package/src/tui/_shims/config-console-state.ts +7 -0
- package/src/tui/_shims/core-any.ts +30 -0
- package/src/tui/_shims/core-binary.ts +13 -0
- package/src/tui/_shims/core-flag.ts +3 -0
- package/src/tui/_shims/core-log.ts +14 -0
- package/src/tui/_shims/lsp-language.ts +1 -0
- package/src/tui/_shims/opencode-any.ts +135 -0
- package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
- package/src/tui/_shims/plugin-tui.ts +13 -0
- package/src/tui/_shims/provider-provider.ts +10 -0
- package/src/tui/_shims/session-retry.ts +1 -0
- package/src/tui/_shims/session-schema.ts +15 -0
- package/src/tui/_shims/session-session.ts +3 -0
- package/src/tui/_shims/snapshot.ts +4 -0
- package/src/tui/_shims/tool-any.ts +18 -0
- package/src/tui/_shims/util-error.ts +7 -0
- package/src/tui/_shims/util-filesystem.ts +79 -0
- package/src/tui/_shims/util-format.ts +7 -0
- package/src/tui/_shims/util-iife.ts +3 -0
- package/src/tui/_shims/util-locale.ts +10 -0
- package/src/tui/_shims/util-process.ts +38 -0
- package/src/tui/app.tsx +783 -0
- package/src/tui/asset/charge.wav +0 -0
- package/src/tui/asset/pulse-a.wav +0 -0
- package/src/tui/asset/pulse-b.wav +0 -0
- package/src/tui/asset/pulse-c.wav +0 -0
- package/src/tui/attach.ts +100 -0
- package/src/tui/component/bg-pulse-render.ts +436 -0
- package/src/tui/component/bg-pulse.tsx +99 -0
- package/src/tui/component/border.tsx +21 -0
- package/src/tui/component/dialog-agent.tsx +31 -0
- package/src/tui/component/dialog-console-org.tsx +103 -0
- package/src/tui/component/dialog-mcp.tsx +85 -0
- package/src/tui/component/dialog-model.tsx +175 -0
- package/src/tui/component/dialog-provider.tsx +456 -0
- package/src/tui/component/dialog-retry-action.tsx +160 -0
- package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
- package/src/tui/component/dialog-session-list.tsx +323 -0
- package/src/tui/component/dialog-session-rename.tsx +31 -0
- package/src/tui/component/dialog-skill.tsx +36 -0
- package/src/tui/component/dialog-stash.tsx +87 -0
- package/src/tui/component/dialog-status.tsx +168 -0
- package/src/tui/component/dialog-tag.tsx +44 -0
- package/src/tui/component/dialog-theme-list.tsx +50 -0
- package/src/tui/component/dialog-variant.tsx +39 -0
- package/src/tui/component/dialog-workspace-create.tsx +302 -0
- package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
- package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/tui/component/error-component.tsx +92 -0
- package/src/tui/component/logo.tsx +896 -0
- package/src/tui/component/plugin-route-missing.tsx +14 -0
- package/src/tui/component/prompt/autocomplete.tsx +869 -0
- package/src/tui/component/prompt/cwd.ts +0 -0
- package/src/tui/component/prompt/frecency.tsx +90 -0
- package/src/tui/component/prompt/history.tsx +108 -0
- package/src/tui/component/prompt/index.tsx +1809 -0
- package/src/tui/component/prompt/part.ts +16 -0
- package/src/tui/component/prompt/stash.tsx +101 -0
- package/src/tui/component/prompt/traits.ts +35 -0
- package/src/tui/component/spinner.tsx +24 -0
- package/src/tui/component/startup-loading.tsx +63 -0
- package/src/tui/component/todo-item.tsx +32 -0
- package/src/tui/component/use-connected.tsx +9 -0
- package/src/tui/component/workspace-label.tsx +19 -0
- package/src/tui/config/cwd.ts +5 -0
- package/src/tui/config/keybind.ts +432 -0
- package/src/tui/config/tui-migrate.ts +154 -0
- package/src/tui/config/tui-schema.ts +34 -0
- package/src/tui/config/tui.ts +46 -0
- package/src/tui/context/aggregate-failures.ts +34 -0
- package/src/tui/context/args.tsx +15 -0
- package/src/tui/context/command-palette.tsx +163 -0
- package/src/tui/context/directory.ts +15 -0
- package/src/tui/context/editor-zed.ts +283 -0
- package/src/tui/context/editor.ts +468 -0
- package/src/tui/context/event-apx.ts +22 -0
- package/src/tui/context/event.ts +6 -0
- package/src/tui/context/exit.tsx +60 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/kv.tsx +81 -0
- package/src/tui/context/local.tsx +608 -0
- package/src/tui/context/path-format.tsx +39 -0
- package/src/tui/context/project-apx.tsx +48 -0
- package/src/tui/context/project.tsx +7 -0
- package/src/tui/context/prompt.tsx +18 -0
- package/src/tui/context/route.tsx +52 -0
- package/src/tui/context/sdk-apx.tsx +185 -0
- package/src/tui/context/sdk.tsx +6 -0
- package/src/tui/context/sync-apx.tsx +178 -0
- package/src/tui/context/sync-v2.tsx +16 -0
- package/src/tui/context/sync.tsx +118 -0
- package/src/tui/context/theme/aura.json +69 -0
- package/src/tui/context/theme/ayu.json +80 -0
- package/src/tui/context/theme/carbonfox.json +248 -0
- package/src/tui/context/theme/catppuccin-frappe.json +230 -0
- package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
- package/src/tui/context/theme/catppuccin.json +112 -0
- package/src/tui/context/theme/cobalt2.json +225 -0
- package/src/tui/context/theme/cursor.json +249 -0
- package/src/tui/context/theme/dracula.json +219 -0
- package/src/tui/context/theme/everforest.json +241 -0
- package/src/tui/context/theme/flexoki.json +237 -0
- package/src/tui/context/theme/github.json +233 -0
- package/src/tui/context/theme/gruvbox.json +242 -0
- package/src/tui/context/theme/kanagawa.json +77 -0
- package/src/tui/context/theme/lucent-orng.json +234 -0
- package/src/tui/context/theme/material.json +235 -0
- package/src/tui/context/theme/matrix.json +77 -0
- package/src/tui/context/theme/mercury.json +252 -0
- package/src/tui/context/theme/monokai.json +221 -0
- package/src/tui/context/theme/nightowl.json +221 -0
- package/src/tui/context/theme/nord.json +223 -0
- package/src/tui/context/theme/one-dark.json +84 -0
- package/src/tui/context/theme/opencode.json +245 -0
- package/src/tui/context/theme/orng.json +249 -0
- package/src/tui/context/theme/osaka-jade.json +93 -0
- package/src/tui/context/theme/palenight.json +222 -0
- package/src/tui/context/theme/rosepine.json +234 -0
- package/src/tui/context/theme/solarized.json +223 -0
- package/src/tui/context/theme/synthwave84.json +226 -0
- package/src/tui/context/theme/tokyonight.json +243 -0
- package/src/tui/context/theme/vercel.json +245 -0
- package/src/tui/context/theme/vesper.json +218 -0
- package/src/tui/context/theme/zenburn.json +223 -0
- package/src/tui/context/theme.tsx +1247 -0
- package/src/tui/context/tui-config.tsx +9 -0
- package/src/tui/event.ts +16 -0
- package/src/tui/feature-plugins/home/footer.tsx +94 -0
- package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
- package/src/tui/feature-plugins/home/tips.tsx +59 -0
- package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
- package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
- package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/tui/feature-plugins/system/plugins.tsx +269 -0
- package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
- package/src/tui/feature-plugins/system/which-key.tsx +608 -0
- package/src/tui/keymap.tsx +166 -0
- package/src/tui/layer.ts +6 -0
- package/src/tui/plugin/api.tsx +381 -0
- package/src/tui/plugin/command-shim.ts +109 -0
- package/src/tui/plugin/internal.ts +33 -0
- package/src/tui/plugin/runtime.ts +1069 -0
- package/src/tui/plugin/slots.tsx +60 -0
- package/src/tui/routes/home.tsx +96 -0
- package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/tui/routes/session/dialog-message.tsx +108 -0
- package/src/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/tui/routes/session/footer.tsx +91 -0
- package/src/tui/routes/session/index.tsx +188 -0
- package/src/tui/routes/session/permission.tsx +722 -0
- package/src/tui/routes/session/question.tsx +490 -0
- package/src/tui/routes/session/sidebar.tsx +102 -0
- package/src/tui/routes/session/subagent-footer.tsx +133 -0
- package/src/tui/run.ts +84 -0
- package/src/tui/thread.ts +261 -0
- package/src/tui/tsconfig.json +40 -0
- package/src/tui/ui/dialog-alert.tsx +66 -0
- package/src/tui/ui/dialog-confirm.tsx +108 -0
- package/src/tui/ui/dialog-export-options.tsx +217 -0
- package/src/tui/ui/dialog-help.tsx +40 -0
- package/src/tui/ui/dialog-prompt.tsx +101 -0
- package/src/tui/ui/dialog-select.tsx +553 -0
- package/src/tui/ui/dialog.tsx +211 -0
- package/src/tui/ui/link.tsx +34 -0
- package/src/tui/ui/spinner.ts +368 -0
- package/src/tui/ui/toast.tsx +111 -0
- package/src/tui/util/clipboard.ts +217 -0
- package/src/tui/util/editor.ts +37 -0
- package/src/tui/util/model.ts +23 -0
- package/src/tui/util/provider-origin.ts +7 -0
- package/src/tui/util/revert-diff.ts +18 -0
- package/src/tui/util/scroll.ts +25 -0
- package/src/tui/util/selection.ts +65 -0
- package/src/tui/util/signal.ts +41 -0
- package/src/tui/util/sound.ts +156 -0
- package/src/tui/util/transcript.ts +112 -0
- package/src/tui/validate-session.ts +29 -0
- package/src/tui/win32.ts +130 -0
- package/src/tui/worker.ts +104 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { CommandModule, Argv, ArgumentsCamelCase } from "yargs";
|
|
2
|
+
import { getHttp, type StreamEvent } from "../http.js";
|
|
3
|
+
import { error } from "../ui.js";
|
|
4
|
+
|
|
5
|
+
interface GlobalArgs { project?: string }
|
|
6
|
+
|
|
7
|
+
async function resolveProjectId(project?: string): Promise<string> {
|
|
8
|
+
const http = await getHttp();
|
|
9
|
+
const projects = (await http.get("/projects")) as Array<{ id: string; name: string; path: string }>;
|
|
10
|
+
if (!projects?.length) throw new Error("No projects registered. Run: apx init");
|
|
11
|
+
if (project) {
|
|
12
|
+
const match = projects.find(
|
|
13
|
+
(p) => p.id === project || p.name === project || p.path === project || p.path?.endsWith("/" + project),
|
|
14
|
+
);
|
|
15
|
+
if (!match) throw new Error(`Project not found: ${project}`);
|
|
16
|
+
return match.id;
|
|
17
|
+
}
|
|
18
|
+
return projects[0]!.id;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const execCmd: CommandModule = {
|
|
22
|
+
command: "exec <agent> [prompt..]",
|
|
23
|
+
aliases: ["run"],
|
|
24
|
+
describe: "Run a one-shot prompt through an agent (non-interactive)",
|
|
25
|
+
builder: (yargs: Argv) =>
|
|
26
|
+
yargs
|
|
27
|
+
.positional("agent", { type: "string", demandOption: true, describe: "Agent slug" })
|
|
28
|
+
.positional("prompt", { type: "string", array: true, describe: "Prompt text" })
|
|
29
|
+
.option("model", { type: "string", describe: "Override model" })
|
|
30
|
+
.option("max-tokens", { type: "number", describe: "Max output tokens" })
|
|
31
|
+
.option("temperature", { type: "number", describe: "Sampling temperature (0–1)" })
|
|
32
|
+
.option("format", {
|
|
33
|
+
choices: ["text", "json"] as const,
|
|
34
|
+
default: "text" as const,
|
|
35
|
+
describe: "Output format",
|
|
36
|
+
})
|
|
37
|
+
.option("stream", {
|
|
38
|
+
type: "boolean",
|
|
39
|
+
default: true,
|
|
40
|
+
describe: "Stream output as it arrives",
|
|
41
|
+
}),
|
|
42
|
+
handler: async (
|
|
43
|
+
args: ArgumentsCamelCase<Record<string, unknown>>,
|
|
44
|
+
) => {
|
|
45
|
+
try {
|
|
46
|
+
const project = args.project as string | undefined;
|
|
47
|
+
const agent = args.agent as string;
|
|
48
|
+
const promptArgs = args.prompt as string[] | undefined;
|
|
49
|
+
const model = args.model as string | undefined;
|
|
50
|
+
const maxTokens = args.maxTokens as number | undefined;
|
|
51
|
+
const temperature = args.temperature as number | undefined;
|
|
52
|
+
const format = args.format as "text" | "json";
|
|
53
|
+
const stream = args.stream as boolean;
|
|
54
|
+
|
|
55
|
+
// Build prompt from args + stdin
|
|
56
|
+
let prompt = (promptArgs ?? []).join(" ");
|
|
57
|
+
if (!process.stdin.isTTY) {
|
|
58
|
+
const chunks: Buffer[] = [];
|
|
59
|
+
for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
|
|
60
|
+
const piped = Buffer.concat(chunks).toString().trim();
|
|
61
|
+
if (piped) prompt = prompt ? prompt + "\n\n" + piped : piped;
|
|
62
|
+
}
|
|
63
|
+
if (!prompt) throw new Error("No prompt provided. Pass text as argument or via stdin.");
|
|
64
|
+
|
|
65
|
+
const http = await getHttp();
|
|
66
|
+
const pid = await resolveProjectId(project);
|
|
67
|
+
const body = {
|
|
68
|
+
prompt,
|
|
69
|
+
model,
|
|
70
|
+
maxTokens,
|
|
71
|
+
temperature,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (stream) {
|
|
75
|
+
// Try streaming endpoint first
|
|
76
|
+
try {
|
|
77
|
+
const result = await http.streamPost(
|
|
78
|
+
`/projects/${pid}/super-agent/chat/stream`,
|
|
79
|
+
{ ...body, contextNote: `Agent: ${agent}` },
|
|
80
|
+
(ev: StreamEvent) => {
|
|
81
|
+
if (ev.type === "chunk" && typeof ev.chunk === "string") {
|
|
82
|
+
process.stdout.write(ev.chunk);
|
|
83
|
+
}
|
|
84
|
+
if (ev.type === "event" && ev.event === "assistant_text" && typeof (ev as Record<string, unknown>).text === "string") {
|
|
85
|
+
process.stdout.write((ev as Record<string, unknown>).text as string);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
) as { text?: string };
|
|
89
|
+
if (!result?.text) process.stdout.write("\n");
|
|
90
|
+
return;
|
|
91
|
+
} catch {
|
|
92
|
+
// Fall through to non-streaming
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = (await http.post(`/projects/${pid}/agents/${agent}/exec`, body)) as {
|
|
97
|
+
text: string; usage?: { input_tokens: number; output_tokens: number };
|
|
98
|
+
};
|
|
99
|
+
if (format === "json") {
|
|
100
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
101
|
+
} else {
|
|
102
|
+
process.stdout.write(result.text + "\n");
|
|
103
|
+
}
|
|
104
|
+
} catch (err: unknown) {
|
|
105
|
+
error(err instanceof Error ? err.message : String(err));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { CommandModule, Argv, ArgumentsCamelCase } from "yargs";
|
|
2
|
+
import { getHttp } from "../http.js";
|
|
3
|
+
import { println, error, success, dim, highlight, table } from "../ui.js";
|
|
4
|
+
|
|
5
|
+
interface GlobalArgs { project?: string }
|
|
6
|
+
|
|
7
|
+
async function resolveProjectId(project?: string): Promise<string> {
|
|
8
|
+
const http = await getHttp();
|
|
9
|
+
const projects = (await http.get("/projects")) as Array<{ id: string; name: string; path: string }>;
|
|
10
|
+
if (!projects?.length) throw new Error("No projects registered. Run: apx init");
|
|
11
|
+
if (project) {
|
|
12
|
+
const match = projects.find(
|
|
13
|
+
(p) => p.id === project || p.name === project || p.path === project || p.path?.endsWith("/" + project),
|
|
14
|
+
);
|
|
15
|
+
if (!match) throw new Error(`Project not found: ${project}`);
|
|
16
|
+
return match.id;
|
|
17
|
+
}
|
|
18
|
+
return projects[0]!.id;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---------- list ----------
|
|
22
|
+
|
|
23
|
+
const listCmd: CommandModule = {
|
|
24
|
+
command: "list",
|
|
25
|
+
aliases: ["ls"],
|
|
26
|
+
describe: "List registered MCP servers",
|
|
27
|
+
handler: async (args: ArgumentsCamelCase<GlobalArgs>) => {
|
|
28
|
+
try {
|
|
29
|
+
const http = await getHttp();
|
|
30
|
+
const pid = await resolveProjectId(args.project as string | undefined);
|
|
31
|
+
const mcps = (await http.get(`/projects/${pid}/mcps`)) as Array<{
|
|
32
|
+
name: string; transport?: string; enabled?: boolean; source?: string;
|
|
33
|
+
}>;
|
|
34
|
+
if (!mcps?.length) { println(dim("No MCP servers configured.")); return; }
|
|
35
|
+
table(
|
|
36
|
+
mcps.map((m) => ({
|
|
37
|
+
Name: m.name,
|
|
38
|
+
Transport: m.transport || "-",
|
|
39
|
+
Enabled: m.enabled ? "✓" : "✗",
|
|
40
|
+
Source: m.source || "-",
|
|
41
|
+
})),
|
|
42
|
+
["Name", "Transport", "Enabled", "Source"],
|
|
43
|
+
);
|
|
44
|
+
} catch (err: unknown) {
|
|
45
|
+
error(err instanceof Error ? err.message : String(err));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// ---------- add ----------
|
|
52
|
+
|
|
53
|
+
const addCmd: CommandModule = {
|
|
54
|
+
command: "add <name>",
|
|
55
|
+
describe: "Register an MCP server",
|
|
56
|
+
builder: (yargs: Argv) =>
|
|
57
|
+
yargs
|
|
58
|
+
.positional("name", { type: "string", demandOption: true, describe: "MCP server name" })
|
|
59
|
+
.option("command", { alias: "c", type: "string", describe: "Command to launch MCP server" })
|
|
60
|
+
.option("url", { type: "string", describe: "Remote MCP server URL (for HTTP transport)" })
|
|
61
|
+
.option("env", {
|
|
62
|
+
type: "array",
|
|
63
|
+
string: true,
|
|
64
|
+
describe: "Environment variables (KEY=VALUE)",
|
|
65
|
+
})
|
|
66
|
+
.option("enabled", { type: "boolean", default: true, describe: "Enable the server" }),
|
|
67
|
+
handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
|
|
68
|
+
try {
|
|
69
|
+
const project = args.project as string | undefined;
|
|
70
|
+
const name = args.name as string;
|
|
71
|
+
const command = args.command as string | undefined;
|
|
72
|
+
const url = args.url as string | undefined;
|
|
73
|
+
const env = args.env as string[] | undefined;
|
|
74
|
+
const enabled = args.enabled as boolean;
|
|
75
|
+
const http = await getHttp();
|
|
76
|
+
const pid = await resolveProjectId(project);
|
|
77
|
+
// Parse env KEY=VALUE pairs
|
|
78
|
+
const envRecord: Record<string, string> = {};
|
|
79
|
+
for (const e of env ?? []) {
|
|
80
|
+
const idx = e.indexOf("=");
|
|
81
|
+
if (idx > 0) envRecord[e.slice(0, idx)] = e.slice(idx + 1);
|
|
82
|
+
}
|
|
83
|
+
const body: Record<string, unknown> = {
|
|
84
|
+
name,
|
|
85
|
+
enabled,
|
|
86
|
+
};
|
|
87
|
+
if (command) body.command = command;
|
|
88
|
+
if (url) body.url = url;
|
|
89
|
+
if (Object.keys(envRecord).length) body.env = envRecord;
|
|
90
|
+
|
|
91
|
+
await http.post(`/projects/${pid}/mcps`, body);
|
|
92
|
+
success(`MCP server registered: ${highlight(name)}`);
|
|
93
|
+
} catch (err: unknown) {
|
|
94
|
+
error(err instanceof Error ? err.message : String(err));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// ---------- remove ----------
|
|
101
|
+
|
|
102
|
+
const removeCmd: CommandModule = {
|
|
103
|
+
command: "remove <name>",
|
|
104
|
+
aliases: ["rm"],
|
|
105
|
+
describe: "Remove an MCP server",
|
|
106
|
+
builder: (yargs: Argv) =>
|
|
107
|
+
yargs.positional("name", { type: "string", demandOption: true, describe: "MCP server name" }),
|
|
108
|
+
handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
|
|
109
|
+
try {
|
|
110
|
+
const project = args.project as string | undefined;
|
|
111
|
+
const name = args.name as string;
|
|
112
|
+
const http = await getHttp();
|
|
113
|
+
const pid = await resolveProjectId(project);
|
|
114
|
+
await http.delete(`/projects/${pid}/mcps/${name}`);
|
|
115
|
+
success(`MCP server removed: ${name}`);
|
|
116
|
+
} catch (err: unknown) {
|
|
117
|
+
error(err instanceof Error ? err.message : String(err));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ---------- enable / disable ----------
|
|
124
|
+
|
|
125
|
+
const enableCmd: CommandModule = {
|
|
126
|
+
command: "enable <name>",
|
|
127
|
+
describe: "Enable an MCP server",
|
|
128
|
+
builder: (yargs: Argv) =>
|
|
129
|
+
yargs.positional("name", { type: "string", demandOption: true }),
|
|
130
|
+
handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
|
|
131
|
+
try {
|
|
132
|
+
const project = args.project as string | undefined;
|
|
133
|
+
const name = args.name as string;
|
|
134
|
+
const http = await getHttp();
|
|
135
|
+
const pid = await resolveProjectId(project);
|
|
136
|
+
await http.post(`/projects/${pid}/mcps`, { name, enabled: true });
|
|
137
|
+
success(`Enabled: ${name}`);
|
|
138
|
+
} catch (err: unknown) { error(String(err)); process.exit(1); }
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const disableCmd: CommandModule = {
|
|
143
|
+
command: "disable <name>",
|
|
144
|
+
describe: "Disable an MCP server",
|
|
145
|
+
builder: (yargs: Argv) =>
|
|
146
|
+
yargs.positional("name", { type: "string", demandOption: true }),
|
|
147
|
+
handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
|
|
148
|
+
try {
|
|
149
|
+
const project = args.project as string | undefined;
|
|
150
|
+
const name = args.name as string;
|
|
151
|
+
const http = await getHttp();
|
|
152
|
+
const pid = await resolveProjectId(project);
|
|
153
|
+
await http.post(`/projects/${pid}/mcps`, { name, enabled: false });
|
|
154
|
+
success(`Disabled: ${name}`);
|
|
155
|
+
} catch (err: unknown) { error(String(err)); process.exit(1); }
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// ---------- tools ----------
|
|
160
|
+
|
|
161
|
+
const toolsCmd: CommandModule = {
|
|
162
|
+
command: "tools <name>",
|
|
163
|
+
describe: "List tools exposed by an MCP server",
|
|
164
|
+
builder: (yargs: Argv) =>
|
|
165
|
+
yargs.positional("name", { type: "string", demandOption: true }),
|
|
166
|
+
handler: async (args: ArgumentsCamelCase<Record<string, unknown>>) => {
|
|
167
|
+
try {
|
|
168
|
+
const project = args.project as string | undefined;
|
|
169
|
+
const name = args.name as string;
|
|
170
|
+
const http = await getHttp();
|
|
171
|
+
const pid = await resolveProjectId(project);
|
|
172
|
+
const result = (await http.post(`/mcp/run`, {
|
|
173
|
+
project: pid,
|
|
174
|
+
name,
|
|
175
|
+
tool: "list_tools",
|
|
176
|
+
params: {},
|
|
177
|
+
})) as { result?: { tools?: Array<{ name: string; description?: string }> } };
|
|
178
|
+
const tools = result?.result?.tools ?? [];
|
|
179
|
+
if (!tools.length) { println(dim("No tools found.")); return; }
|
|
180
|
+
table(
|
|
181
|
+
tools.map((t) => ({ Tool: t.name, Description: t.description?.slice(0, 60) || "-" })),
|
|
182
|
+
["Tool", "Description"],
|
|
183
|
+
);
|
|
184
|
+
} catch (err: unknown) {
|
|
185
|
+
error(err instanceof Error ? err.message : String(err));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// ---------- check ----------
|
|
192
|
+
|
|
193
|
+
const checkCmd: CommandModule = {
|
|
194
|
+
command: "check",
|
|
195
|
+
describe: "Validate MCP configuration",
|
|
196
|
+
handler: async (args: ArgumentsCamelCase<GlobalArgs>) => {
|
|
197
|
+
try {
|
|
198
|
+
const http = await getHttp();
|
|
199
|
+
const pid = await resolveProjectId(args.project as string | undefined);
|
|
200
|
+
const result = (await http.get(`/projects/${pid}/mcps/check`)) as {
|
|
201
|
+
conflicts?: string[];
|
|
202
|
+
entries?: Array<{ name: string; ok: boolean; error?: string }>;
|
|
203
|
+
};
|
|
204
|
+
if (result.conflicts?.length) {
|
|
205
|
+
println("\x1b[93m⚠ Conflicts:\x1b[0m");
|
|
206
|
+
result.conflicts.forEach((c) => println(" " + c));
|
|
207
|
+
}
|
|
208
|
+
result.entries?.forEach((e) => {
|
|
209
|
+
if (e.ok) success(e.name);
|
|
210
|
+
else error(`${e.name}: ${e.error}`);
|
|
211
|
+
});
|
|
212
|
+
} catch (err: unknown) {
|
|
213
|
+
error(err instanceof Error ? err.message : String(err));
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// ---------- parent ----------
|
|
220
|
+
|
|
221
|
+
export const mcpCmd: CommandModule = {
|
|
222
|
+
command: "mcp",
|
|
223
|
+
describe: "Manage MCP (Model Context Protocol) servers",
|
|
224
|
+
builder: (yargs: Argv) =>
|
|
225
|
+
yargs
|
|
226
|
+
.command(listCmd)
|
|
227
|
+
.command(addCmd)
|
|
228
|
+
.command(removeCmd)
|
|
229
|
+
.command(enableCmd)
|
|
230
|
+
.command(disableCmd)
|
|
231
|
+
.command(toolsCmd)
|
|
232
|
+
.command(checkCmd)
|
|
233
|
+
.demandCommand(1, "Specify an mcp subcommand"),
|
|
234
|
+
handler: () => {},
|
|
235
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { CommandModule, Argv, ArgumentsCamelCase } from "yargs";
|
|
2
|
+
import { getHttp } from "../http.js";
|
|
3
|
+
import { println, error, success, dim, bold, table, highlight } from "../ui.js";
|
|
4
|
+
|
|
5
|
+
interface Session {
|
|
6
|
+
filename?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
started_at?: string;
|
|
9
|
+
ended_at?: string;
|
|
10
|
+
agent?: string;
|
|
11
|
+
status?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface GlobalArgs {
|
|
15
|
+
project?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function resolveProjectId(project?: string): Promise<string> {
|
|
19
|
+
const http = await getHttp();
|
|
20
|
+
const projects = (await http.get("/projects")) as Array<{
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
path: string;
|
|
24
|
+
}>;
|
|
25
|
+
if (!projects?.length) throw new Error("No projects registered. Run: apx init");
|
|
26
|
+
|
|
27
|
+
if (project) {
|
|
28
|
+
const match = projects.find(
|
|
29
|
+
(p) =>
|
|
30
|
+
p.id === project ||
|
|
31
|
+
p.name === project ||
|
|
32
|
+
p.path === project ||
|
|
33
|
+
p.path?.endsWith("/" + project),
|
|
34
|
+
);
|
|
35
|
+
if (!match) throw new Error(`Project not found: ${project}`);
|
|
36
|
+
return match.id;
|
|
37
|
+
}
|
|
38
|
+
// Default to first project
|
|
39
|
+
return projects[0]!.id;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------- list ----------
|
|
43
|
+
|
|
44
|
+
const listCmd: CommandModule<GlobalArgs, GlobalArgs & { last: number; format: string }> = {
|
|
45
|
+
command: "list",
|
|
46
|
+
aliases: ["ls"],
|
|
47
|
+
describe: "List sessions for the current project",
|
|
48
|
+
builder: (yargs: Argv<GlobalArgs>) =>
|
|
49
|
+
yargs
|
|
50
|
+
.option("last", {
|
|
51
|
+
alias: "n",
|
|
52
|
+
type: "number",
|
|
53
|
+
default: 20,
|
|
54
|
+
describe: "Number of recent sessions to show",
|
|
55
|
+
})
|
|
56
|
+
.option("format", {
|
|
57
|
+
choices: ["table", "json"] as const,
|
|
58
|
+
default: "table" as const,
|
|
59
|
+
describe: "Output format",
|
|
60
|
+
}),
|
|
61
|
+
handler: async (args: ArgumentsCamelCase<GlobalArgs & { last: number; format: string }>) => {
|
|
62
|
+
try {
|
|
63
|
+
const http = await getHttp();
|
|
64
|
+
const pid = await resolveProjectId(args.project);
|
|
65
|
+
const sessions = (await http.get(`/projects/${pid}/sessions`)) as Session[];
|
|
66
|
+
if (!sessions?.length) {
|
|
67
|
+
println(dim("No sessions found."));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const slice = sessions.slice(-args.last);
|
|
71
|
+
if (args.format === "json") {
|
|
72
|
+
process.stdout.write(JSON.stringify(slice, null, 2) + "\n");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
table(
|
|
76
|
+
slice.map((s) => ({
|
|
77
|
+
Title: s.title || s.filename || "(no title)",
|
|
78
|
+
Agent: s.agent || "-",
|
|
79
|
+
Started: s.started_at ? new Date(s.started_at).toLocaleString() : "-",
|
|
80
|
+
Status: s.status || "open",
|
|
81
|
+
})),
|
|
82
|
+
["Title", "Agent", "Started", "Status"],
|
|
83
|
+
);
|
|
84
|
+
} catch (err: unknown) {
|
|
85
|
+
error(err instanceof Error ? err.message : String(err));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ---------- new ----------
|
|
92
|
+
|
|
93
|
+
const newCmd: CommandModule<GlobalArgs, GlobalArgs & { title?: string; body?: string; agent?: string }> = {
|
|
94
|
+
command: "new",
|
|
95
|
+
describe: "Create a new session",
|
|
96
|
+
builder: (yargs: Argv<GlobalArgs>) =>
|
|
97
|
+
yargs
|
|
98
|
+
.option("title", {
|
|
99
|
+
type: "string",
|
|
100
|
+
describe: "Session title",
|
|
101
|
+
})
|
|
102
|
+
.option("body", {
|
|
103
|
+
type: "string",
|
|
104
|
+
describe: "Initial session body / context",
|
|
105
|
+
})
|
|
106
|
+
.option("agent", {
|
|
107
|
+
type: "string",
|
|
108
|
+
describe: "Agent slug to associate the session with",
|
|
109
|
+
}),
|
|
110
|
+
handler: async (args: ArgumentsCamelCase<GlobalArgs & { title?: string; body?: string; agent?: string }>) => {
|
|
111
|
+
try {
|
|
112
|
+
const http = await getHttp();
|
|
113
|
+
const pid = await resolveProjectId(args.project);
|
|
114
|
+
const agentSlug = args.agent || "default";
|
|
115
|
+
const session = (await http.post(`/projects/${pid}/agents/${agentSlug}/sessions`, {
|
|
116
|
+
title: args.title || `Session ${new Date().toLocaleDateString()}`,
|
|
117
|
+
body: args.body,
|
|
118
|
+
})) as { filename: string; path: string };
|
|
119
|
+
success(`Session created: ${highlight(session.filename)}`);
|
|
120
|
+
if (session.path) println(dim(session.path));
|
|
121
|
+
} catch (err: unknown) {
|
|
122
|
+
error(err instanceof Error ? err.message : String(err));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// ---------- get / show ----------
|
|
129
|
+
|
|
130
|
+
const getCmd: CommandModule<GlobalArgs, GlobalArgs & { id: string; body: boolean }> = {
|
|
131
|
+
command: "get <id>",
|
|
132
|
+
aliases: ["show"],
|
|
133
|
+
describe: "Show a session by filename or ID",
|
|
134
|
+
builder: (yargs: Argv<GlobalArgs>) =>
|
|
135
|
+
yargs
|
|
136
|
+
.positional("id", { type: "string", demandOption: true, describe: "Session filename or ID" })
|
|
137
|
+
.option("body", {
|
|
138
|
+
type: "boolean",
|
|
139
|
+
default: false,
|
|
140
|
+
describe: "Print session body / markdown",
|
|
141
|
+
}),
|
|
142
|
+
handler: async (args: ArgumentsCamelCase<GlobalArgs & { id: string; body: boolean }>) => {
|
|
143
|
+
try {
|
|
144
|
+
const http = await getHttp();
|
|
145
|
+
const pid = await resolveProjectId(args.project);
|
|
146
|
+
const session = (await http.get(`/projects/${pid}/sessions/${args.id}`)) as Record<string, unknown>;
|
|
147
|
+
if (args.body) {
|
|
148
|
+
process.stdout.write(String(session.body_md || session.body || "") + "\n");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
process.stdout.write(JSON.stringify(session, null, 2) + "\n");
|
|
152
|
+
} catch (err: unknown) {
|
|
153
|
+
error(err instanceof Error ? err.message : String(err));
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// ---------- delete ----------
|
|
160
|
+
|
|
161
|
+
const deleteCmd: CommandModule<GlobalArgs, GlobalArgs & { id: string }> = {
|
|
162
|
+
command: "delete <id>",
|
|
163
|
+
aliases: ["rm"],
|
|
164
|
+
describe: "Delete a session",
|
|
165
|
+
builder: (yargs: Argv<GlobalArgs>) =>
|
|
166
|
+
yargs.positional("id", { type: "string", demandOption: true, describe: "Session filename or ID" }),
|
|
167
|
+
handler: async (args: ArgumentsCamelCase<GlobalArgs & { id: string }>) => {
|
|
168
|
+
try {
|
|
169
|
+
const http = await getHttp();
|
|
170
|
+
const pid = await resolveProjectId(args.project);
|
|
171
|
+
await http.delete(`/projects/${pid}/sessions/${args.id}`);
|
|
172
|
+
success(`Session deleted: ${args.id}`);
|
|
173
|
+
} catch (err: unknown) {
|
|
174
|
+
error(err instanceof Error ? err.message : String(err));
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// ---------- compact ----------
|
|
181
|
+
|
|
182
|
+
const compactCmd: CommandModule<GlobalArgs, GlobalArgs & { id?: string; model?: string }> = {
|
|
183
|
+
command: "compact [id]",
|
|
184
|
+
describe: "Summarize and compact a session's conversation history",
|
|
185
|
+
builder: (yargs: Argv<GlobalArgs>) =>
|
|
186
|
+
yargs
|
|
187
|
+
.positional("id", { type: "string", describe: "Session ID (defaults to latest)" })
|
|
188
|
+
.option("model", { type: "string", describe: "Model to use for summarization" }),
|
|
189
|
+
handler: async (args: ArgumentsCamelCase<GlobalArgs & { id?: string; model?: string }>) => {
|
|
190
|
+
try {
|
|
191
|
+
const http = await getHttp();
|
|
192
|
+
const pid = await resolveProjectId(args.project);
|
|
193
|
+
const path = args.id
|
|
194
|
+
? `/projects/${pid}/sessions/${args.id}/compact`
|
|
195
|
+
: `/sessions/${pid}/compact`;
|
|
196
|
+
const result = (await http.post(path, { model: args.model, project: pid })) as {
|
|
197
|
+
compacted_turns?: number;
|
|
198
|
+
summary?: string;
|
|
199
|
+
};
|
|
200
|
+
success(`Compacted ${result.compacted_turns ?? "?"} turns.`);
|
|
201
|
+
if (result.summary) println(dim(result.summary));
|
|
202
|
+
} catch (err: unknown) {
|
|
203
|
+
error(err instanceof Error ? err.message : String(err));
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// ---------- parent command ----------
|
|
210
|
+
|
|
211
|
+
export const sessionCmd: CommandModule<GlobalArgs, GlobalArgs> = {
|
|
212
|
+
command: "session",
|
|
213
|
+
aliases: ["sessions"],
|
|
214
|
+
describe: "Manage APC sessions",
|
|
215
|
+
builder: (yargs: Argv<GlobalArgs>) =>
|
|
216
|
+
yargs
|
|
217
|
+
.command(listCmd as CommandModule)
|
|
218
|
+
.command(newCmd as CommandModule)
|
|
219
|
+
.command(getCmd as CommandModule)
|
|
220
|
+
.command(deleteCmd as CommandModule)
|
|
221
|
+
.command(compactCmd as CommandModule)
|
|
222
|
+
.demandCommand(1, "Specify a session subcommand"),
|
|
223
|
+
handler: () => {},
|
|
224
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { CommandModule, ArgumentsCamelCase } from "yargs";
|
|
2
|
+
import { getHttp } from "../http.js";
|
|
3
|
+
import { println, error, success, dim, bold, highlight } from "../ui.js";
|
|
4
|
+
|
|
5
|
+
export const statusCmd: CommandModule = {
|
|
6
|
+
command: "status",
|
|
7
|
+
describe: "Show APX system health and project overview",
|
|
8
|
+
handler: async (args: ArgumentsCamelCase<{ project?: string }>) => {
|
|
9
|
+
try {
|
|
10
|
+
const http = await getHttp();
|
|
11
|
+
|
|
12
|
+
// Daemon health
|
|
13
|
+
let daemonOk = false;
|
|
14
|
+
try {
|
|
15
|
+
const health = (await http.get("/health")) as { version?: string; uptime_s?: number };
|
|
16
|
+
println(
|
|
17
|
+
"\x1b[92m●\x1b[0m Daemon " +
|
|
18
|
+
dim(`v${health.version ?? "?"}`) +
|
|
19
|
+
" uptime " + dim(`${health.uptime_s ?? "?"}s`),
|
|
20
|
+
);
|
|
21
|
+
daemonOk = true;
|
|
22
|
+
} catch {
|
|
23
|
+
println("\x1b[91m✗\x1b[0m Daemon " + dim("not running — run: apx daemon start"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!daemonOk) { process.exit(1); return; }
|
|
27
|
+
|
|
28
|
+
// Projects
|
|
29
|
+
const projects = (await http.get("/projects")) as Array<{
|
|
30
|
+
id: string; name: string; path: string;
|
|
31
|
+
}>;
|
|
32
|
+
println("");
|
|
33
|
+
println(bold("Projects") + " " + dim(`(${projects?.length ?? 0})`));
|
|
34
|
+
if (!projects?.length) {
|
|
35
|
+
println(dim(" No projects. Run: apx init <path>"));
|
|
36
|
+
} else {
|
|
37
|
+
for (const p of projects) {
|
|
38
|
+
const isActive = !args.project || p.name === args.project || p.id === args.project;
|
|
39
|
+
println(
|
|
40
|
+
(isActive ? "\x1b[94m▶\x1b[0m" : " ") +
|
|
41
|
+
" " + highlight(p.name ?? p.id) +
|
|
42
|
+
" " + dim(p.path),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Engines
|
|
48
|
+
try {
|
|
49
|
+
const eng = (await http.get("/engines")) as { engines: string[] };
|
|
50
|
+
if (eng?.engines?.length) {
|
|
51
|
+
println("");
|
|
52
|
+
println(bold("Engines") + " " + dim(`(${eng.engines.length})`));
|
|
53
|
+
println(dim(" " + eng.engines.slice(0, 6).join(" ") + (eng.engines.length > 6 ? " …" : "")));
|
|
54
|
+
}
|
|
55
|
+
} catch { /* engines endpoint optional */ }
|
|
56
|
+
} catch (err: unknown) {
|
|
57
|
+
error(err instanceof Error ? err.message : String(err));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Typed wrapper around the APX HTTP client (src/cli/http.js).
|
|
2
|
+
// We use dynamic import since the base client is plain JS ESM.
|
|
3
|
+
|
|
4
|
+
export interface StreamEvent {
|
|
5
|
+
type: "event" | "chunk" | "final" | "error";
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type HttpModule = {
|
|
10
|
+
get(path: string, opts?: RequestInit): Promise<unknown>;
|
|
11
|
+
post(path: string, body: unknown, opts?: RequestInit): Promise<unknown>;
|
|
12
|
+
put(path: string, body: unknown, opts?: RequestInit): Promise<unknown>;
|
|
13
|
+
patch(path: string, body: unknown, opts?: RequestInit): Promise<unknown>;
|
|
14
|
+
delete(path: string, opts?: RequestInit): Promise<unknown>;
|
|
15
|
+
streamPost(
|
|
16
|
+
path: string,
|
|
17
|
+
body: unknown,
|
|
18
|
+
onEvent: (ev: StreamEvent) => void,
|
|
19
|
+
opts?: RequestInit,
|
|
20
|
+
): Promise<unknown>;
|
|
21
|
+
baseUrl(): string;
|
|
22
|
+
ping(timeoutMs?: number): Promise<boolean>;
|
|
23
|
+
createAbortController(): AbortController;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let _http: HttpModule | null = null;
|
|
27
|
+
|
|
28
|
+
export async function getHttp(): Promise<HttpModule> {
|
|
29
|
+
if (!_http) {
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
31
|
+
// @ts-ignore – plain JS outside rootDir; HttpModule above provides typing
|
|
32
|
+
const m = (await import("../cli/http.js")) as { http: HttpModule };
|
|
33
|
+
_http = m.http;
|
|
34
|
+
}
|
|
35
|
+
return _http;
|
|
36
|
+
}
|