@messenger-agent/client 0.24.0-alpha.2
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/dist/args.d.ts +66 -0
- package/dist/args.js +303 -0
- package/dist/assets/skills/manage-coding-agent-client/SKILL.md +114 -0
- package/dist/auto-upgrade.d.ts +43 -0
- package/dist/auto-upgrade.js +184 -0
- package/dist/config-file.d.ts +22 -0
- package/dist/config-file.js +100 -0
- package/dist/control.d.ts +17 -0
- package/dist/control.js +152 -0
- package/dist/exec.d.ts +10 -0
- package/dist/exec.js +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +158 -0
- package/dist/install.d.ts +26 -0
- package/dist/install.js +142 -0
- package/dist/maintenance.d.ts +63 -0
- package/dist/maintenance.js +194 -0
- package/dist/paths.d.ts +8 -0
- package/dist/paths.js +20 -0
- package/dist/runtime.d.ts +34 -0
- package/dist/runtime.js +241 -0
- package/dist/service.d.ts +41 -0
- package/dist/service.js +208 -0
- package/dist/supervisor.d.ts +45 -0
- package/dist/supervisor.js +282 -0
- package/package.json +31 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseCliArgs, UsageError, usage } from "./args.js";
|
|
3
|
+
import { installClient } from "./install.js";
|
|
4
|
+
import { currentPackageVersion, installRuntime, normalizeUpgradeVersion } from "./runtime.js";
|
|
5
|
+
import { darwinCommands, linuxCommands, serviceName, uninstallService } from "./service.js";
|
|
6
|
+
import { runSupervisor } from "./supervisor.js";
|
|
7
|
+
import { runCommand } from "./exec.js";
|
|
8
|
+
import { defaultConfigPath } from "./paths.js";
|
|
9
|
+
import { writeUpgradeChannel } from "./config-file.js";
|
|
10
|
+
import { requestAgentRestart, requestClientActivity, requestMaintenanceCancel, requestMaintenanceStatus, requestScheduleMaintenance, } from "./control.js";
|
|
11
|
+
async function main() {
|
|
12
|
+
const args = parseCliArgs(process.argv.slice(2));
|
|
13
|
+
if (args.command === "version") {
|
|
14
|
+
console.log(`coding-agent ${await currentPackageVersion()}`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (args.command === "run-service") {
|
|
18
|
+
await runSupervisor(args.configPath ?? process.env.AGENT_CONFIG_PATH ?? defaultConfigPath);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (args.command === "install") {
|
|
22
|
+
const result = await installClient(args);
|
|
23
|
+
console.log(`Installed ${serviceName}`);
|
|
24
|
+
console.log(`Config: ${result.configPath}`);
|
|
25
|
+
console.log(`Data: ${result.dataDir}`);
|
|
26
|
+
console.log(`Runtime: ${result.runtime.currentLink}`);
|
|
27
|
+
console.log(`Command: ${result.runtime.cliWrapperPath}`);
|
|
28
|
+
console.log(`Service file: ${result.service.servicePath}`);
|
|
29
|
+
console.log(`Upgrade: ${result.runtime.cliWrapperPath} upgrade`);
|
|
30
|
+
console.log(`Restart: ${result.runtime.cliWrapperPath} restart`);
|
|
31
|
+
console.log(`Status: ${result.runtime.cliWrapperPath} status`);
|
|
32
|
+
console.log(`Logs: ${result.service.commands.logs}`);
|
|
33
|
+
for (const warning of result.service.warnings)
|
|
34
|
+
console.warn(`Warning: ${warning}`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (args.command === "upgrade") {
|
|
38
|
+
const { version, channel } = normalizeUpgradeVersion(args.version, args.channel);
|
|
39
|
+
if (channel) {
|
|
40
|
+
await writeUpgradeChannel(args.configPath, channel);
|
|
41
|
+
console.log(`Auto-upgrade channel: ${channel}`);
|
|
42
|
+
}
|
|
43
|
+
if (args.delaySeconds !== undefined) {
|
|
44
|
+
await scheduleMaintenance(args.configPath, { type: "upgrade", version }, args.delaySeconds, args.allowWaiting);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const runtime = await installRuntime({ version, configPath: args.configPath });
|
|
48
|
+
const commands = process.platform === "darwin" ? darwinCommands() : linuxCommands();
|
|
49
|
+
await runShellCommand(commands.restart);
|
|
50
|
+
console.log(`Upgraded runtime to ${runtime.version}`);
|
|
51
|
+
console.log(`Runtime: ${runtime.currentLink}`);
|
|
52
|
+
console.log(`Command: ${runtime.cliWrapperPath}`);
|
|
53
|
+
console.log(`Status: ${runtime.cliWrapperPath} status`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (args.command === "sessions") {
|
|
57
|
+
const activity = await requestClientActivity(args.configPath);
|
|
58
|
+
console.log(args.json ? JSON.stringify(activity) : formatActivity(activity));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (args.command === "maintenance") {
|
|
62
|
+
const task = args.action === "cancel"
|
|
63
|
+
? await requestMaintenanceCancel(args.configPath)
|
|
64
|
+
: await requestMaintenanceStatus(args.configPath);
|
|
65
|
+
console.log(args.json ? JSON.stringify(task ?? null) : formatMaintenanceTask(task));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (args.command === "restart" && args.agent) {
|
|
69
|
+
if (args.delaySeconds !== undefined) {
|
|
70
|
+
await scheduleMaintenance(process.env.AGENT_CONFIG_PATH ?? defaultConfigPath, { type: "restart", agent: args.agent }, args.delaySeconds, args.allowWaiting);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await requestAgentRestart(process.env.AGENT_CONFIG_PATH ?? defaultConfigPath, args.agent);
|
|
74
|
+
console.log(`Restarted ${args.agent} agent`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (args.command === "status" || args.command === "start" || args.command === "restart" || args.command === "stop") {
|
|
78
|
+
if ((args.command === "restart" || args.command === "stop") && args.delaySeconds !== undefined) {
|
|
79
|
+
await scheduleMaintenance(process.env.AGENT_CONFIG_PATH ?? defaultConfigPath, { type: args.command }, args.delaySeconds, args.allowWaiting);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const commands = process.platform === "darwin" ? darwinCommands() : linuxCommands();
|
|
83
|
+
const command = args.command === "status"
|
|
84
|
+
? commands.status
|
|
85
|
+
: args.command === "start"
|
|
86
|
+
? commands.start
|
|
87
|
+
: args.command === "restart"
|
|
88
|
+
? commands.restart
|
|
89
|
+
: commands.stop;
|
|
90
|
+
await runShellCommand(command);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (args.command === "uninstall") {
|
|
94
|
+
if (args.delaySeconds !== undefined) {
|
|
95
|
+
await scheduleMaintenance(process.env.AGENT_CONFIG_PATH ?? defaultConfigPath, { type: "uninstall" }, args.delaySeconds, args.allowWaiting);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await uninstallService();
|
|
99
|
+
console.log(`Uninstalled ${serviceName}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function scheduleMaintenance(configPath, operation, delaySeconds, allowWaiting) {
|
|
104
|
+
const task = await requestScheduleMaintenance(configPath, { operation, delaySeconds, allowWaiting });
|
|
105
|
+
console.log(`Scheduled maintenance task ${task.id}`);
|
|
106
|
+
console.log(`Operation: ${formatOperation(task.operation)}`);
|
|
107
|
+
console.log(`Not before: ${task.notBefore}`);
|
|
108
|
+
}
|
|
109
|
+
function formatActivity(activity) {
|
|
110
|
+
const lines = [`Active sessions: ${activity.active}`, `Waiting sessions: ${activity.waiting}`];
|
|
111
|
+
for (const name of ["codex", "claude"]) {
|
|
112
|
+
const status = activity.agents[name];
|
|
113
|
+
lines.push(status.available
|
|
114
|
+
? `${capitalize(name)}: active=${status.active}, waiting=${status.waiting}`
|
|
115
|
+
: `${capitalize(name)}: unavailable (${status.error})`);
|
|
116
|
+
}
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
}
|
|
119
|
+
function formatMaintenanceTask(task) {
|
|
120
|
+
if (!task)
|
|
121
|
+
return "No maintenance task has been scheduled";
|
|
122
|
+
return [
|
|
123
|
+
`Task: ${task.id}`,
|
|
124
|
+
`Operation: ${formatOperation(task.operation)}`,
|
|
125
|
+
`Status: ${task.status}`,
|
|
126
|
+
`Not before: ${task.notBefore}`,
|
|
127
|
+
...(task.message ? [`Message: ${task.message}`] : []),
|
|
128
|
+
].join("\n");
|
|
129
|
+
}
|
|
130
|
+
function formatOperation(operation) {
|
|
131
|
+
if (operation.type === "upgrade")
|
|
132
|
+
return `upgrade to ${operation.version}`;
|
|
133
|
+
if (operation.type === "restart")
|
|
134
|
+
return operation.agent ? `restart ${operation.agent}` : "restart client";
|
|
135
|
+
return operation.type;
|
|
136
|
+
}
|
|
137
|
+
function capitalize(value) {
|
|
138
|
+
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
139
|
+
}
|
|
140
|
+
async function runShellCommand(command) {
|
|
141
|
+
const result = await runCommand("/bin/sh", ["-lc", command], { allowFailure: true });
|
|
142
|
+
if (result.stdout)
|
|
143
|
+
process.stdout.write(result.stdout);
|
|
144
|
+
if (result.stderr)
|
|
145
|
+
process.stderr.write(result.stderr);
|
|
146
|
+
if (result.status !== 0)
|
|
147
|
+
process.exit(result.status ?? 1);
|
|
148
|
+
}
|
|
149
|
+
main().catch((err) => {
|
|
150
|
+
if (err instanceof UsageError) {
|
|
151
|
+
const message = err.message === usage() ? err.message : `${err.message}\n\n${usage()}`;
|
|
152
|
+
console.error(message);
|
|
153
|
+
process.exit(2);
|
|
154
|
+
}
|
|
155
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
156
|
+
console.error(message);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type RuntimeInstallResult } from "./runtime.js";
|
|
2
|
+
import { type ServiceInstallResult } from "./service.js";
|
|
3
|
+
import { type InstallArgs } from "./args.js";
|
|
4
|
+
export type InstallResult = {
|
|
5
|
+
configPath: string;
|
|
6
|
+
dataDir: string;
|
|
7
|
+
workspacePath: string;
|
|
8
|
+
runtime: RuntimeInstallResult;
|
|
9
|
+
service: ServiceInstallResult;
|
|
10
|
+
};
|
|
11
|
+
type ResolvedInstallArgs = Omit<InstallArgs, "tunnelId" | "token" | "workspace" | "workspaceId" | "workspaceName"> & {
|
|
12
|
+
tunnelId: string;
|
|
13
|
+
token: string;
|
|
14
|
+
workspace: string;
|
|
15
|
+
workspaceId: string;
|
|
16
|
+
workspaceName: string;
|
|
17
|
+
preserveWorkspaces: boolean;
|
|
18
|
+
createWorkspace: boolean;
|
|
19
|
+
};
|
|
20
|
+
export declare function assertSupportedPlatform(platform?: NodeJS.Platform): void;
|
|
21
|
+
export declare function assertNodeVersion(version?: string): void;
|
|
22
|
+
export declare function validateInstallArgs(args: InstallArgs): void;
|
|
23
|
+
export declare function resolveWorkspacePath(path: string, createIfMissing?: boolean): Promise<string>;
|
|
24
|
+
export declare function installClient(args: InstallArgs): Promise<InstallResult>;
|
|
25
|
+
export declare function resolveInstallArgs(args: InstallArgs): Promise<ResolvedInstallArgs>;
|
|
26
|
+
export {};
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { mkdir, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { readConfigYaml, updateClientConfig, writeUpgradeChannel } from "./config-file.js";
|
|
3
|
+
import { installRuntime } from "./runtime.js";
|
|
4
|
+
import { installService } from "./service.js";
|
|
5
|
+
import { UsageError } from "./args.js";
|
|
6
|
+
import { defaultWorkspacePath } from "./paths.js";
|
|
7
|
+
export function assertSupportedPlatform(platform = process.platform) {
|
|
8
|
+
if (platform !== "linux" && platform !== "darwin") {
|
|
9
|
+
throw new Error("Only Linux and macOS are supported");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function assertNodeVersion(version = process.versions.node) {
|
|
13
|
+
const major = Number.parseInt(version.split(".")[0] ?? "", 10);
|
|
14
|
+
if (!Number.isInteger(major) || major < 24) {
|
|
15
|
+
throw new Error("Node.js 24 or newer is required");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function validateInstallArgs(args) {
|
|
19
|
+
if (args.tunnelId && !/^[A-Za-z0-9._:-]+$/.test(args.tunnelId)) {
|
|
20
|
+
throw new Error("--tunnel-id may only contain letters, numbers, '.', '_', ':', and '-'");
|
|
21
|
+
}
|
|
22
|
+
if (args.workspaceId && !/^[A-Za-z0-9._:-]+$/.test(args.workspaceId)) {
|
|
23
|
+
throw new Error("--workspace-id may only contain letters, numbers, '.', '_', ':', and '-'");
|
|
24
|
+
}
|
|
25
|
+
if (args.workspaceName && (args.workspaceName.trim().length === 0 || args.workspaceName.trim().length > 128)) {
|
|
26
|
+
throw new Error("--workspace-name must be between 1 and 128 characters");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function resolveWorkspacePath(path, createIfMissing = false) {
|
|
30
|
+
if (createIfMissing)
|
|
31
|
+
await mkdir(path, { recursive: true });
|
|
32
|
+
const resolved = await realpath(path);
|
|
33
|
+
const stats = await stat(resolved);
|
|
34
|
+
if (!stats.isDirectory()) {
|
|
35
|
+
throw new Error("--workspace must point to an existing directory");
|
|
36
|
+
}
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
export async function installClient(args) {
|
|
40
|
+
assertSupportedPlatform();
|
|
41
|
+
assertNodeVersion();
|
|
42
|
+
const resolvedArgs = await resolveInstallArgs(args);
|
|
43
|
+
validateInstallArgs(resolvedArgs);
|
|
44
|
+
const workspacePath = await resolveWorkspacePath(resolvedArgs.workspace, resolvedArgs.createWorkspace);
|
|
45
|
+
await updateClientConfig({
|
|
46
|
+
configPath: resolvedArgs.configPath,
|
|
47
|
+
dataDir: resolvedArgs.dataDir,
|
|
48
|
+
workspacePath,
|
|
49
|
+
workspaceId: resolvedArgs.workspaceId,
|
|
50
|
+
workspaceName: resolvedArgs.workspaceName,
|
|
51
|
+
tunnelId: resolvedArgs.tunnelId,
|
|
52
|
+
token: resolvedArgs.token,
|
|
53
|
+
serverUrl: resolvedArgs.serverUrl,
|
|
54
|
+
preserveWorkspaces: resolvedArgs.preserveWorkspaces,
|
|
55
|
+
});
|
|
56
|
+
if (args.channel)
|
|
57
|
+
await writeUpgradeChannel(resolvedArgs.configPath, args.channel);
|
|
58
|
+
const runtime = await installRuntime({ version: "current", configPath: resolvedArgs.configPath });
|
|
59
|
+
const service = await installService({
|
|
60
|
+
configPath: resolvedArgs.configPath,
|
|
61
|
+
dataDir: resolvedArgs.dataDir,
|
|
62
|
+
workspacePath,
|
|
63
|
+
serviceCommandPath: runtime.wrapperPath,
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
configPath: resolvedArgs.configPath,
|
|
67
|
+
dataDir: resolvedArgs.dataDir,
|
|
68
|
+
workspacePath,
|
|
69
|
+
runtime,
|
|
70
|
+
service,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export async function resolveInstallArgs(args) {
|
|
74
|
+
const existing = await readConfigYaml(args.configPath);
|
|
75
|
+
const existingTunnel = objectAt(existing, "tunnel");
|
|
76
|
+
const tunnelId = args.tunnelId ?? stringAt(existingTunnel, "tunnel_id");
|
|
77
|
+
const token = args.token ?? stringAt(existingTunnel, "token");
|
|
78
|
+
const configuredWorkspace = findConfiguredWorkspace(existing, args.workspaceId, tunnelId);
|
|
79
|
+
const workspace = args.workspace ?? configuredWorkspace?.path ?? defaultWorkspacePath;
|
|
80
|
+
const workspaceId = args.workspaceId ?? configuredWorkspace?.id ?? (tunnelId ? `${tunnelId}--default` : undefined);
|
|
81
|
+
const workspaceName = args.workspaceName ?? configuredWorkspace?.name ?? (tunnelId ? `${tunnelId} Default` : undefined);
|
|
82
|
+
const usesDefaultWorkspace = workspace === defaultWorkspacePath;
|
|
83
|
+
if (!tunnelId)
|
|
84
|
+
throw new UsageError("Missing required option: --tunnel-id");
|
|
85
|
+
if (!token)
|
|
86
|
+
throw new UsageError("Missing required option: --token");
|
|
87
|
+
if (!workspaceId)
|
|
88
|
+
throw new UsageError("Missing required option: --workspace-id");
|
|
89
|
+
if (!workspaceName)
|
|
90
|
+
throw new UsageError("Missing required option: --workspace-name");
|
|
91
|
+
return {
|
|
92
|
+
...args,
|
|
93
|
+
tunnelId,
|
|
94
|
+
token,
|
|
95
|
+
workspace,
|
|
96
|
+
workspaceId,
|
|
97
|
+
workspaceName,
|
|
98
|
+
preserveWorkspaces: !args.workspace && !!configuredWorkspace,
|
|
99
|
+
createWorkspace: usesDefaultWorkspace,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function findConfiguredWorkspace(config, requestedWorkspaceId, tunnelId) {
|
|
103
|
+
const workspaces = config.workspaces;
|
|
104
|
+
if (!Array.isArray(workspaces))
|
|
105
|
+
return undefined;
|
|
106
|
+
const candidates = workspaces.filter((workspace) => !!workspace && typeof workspace === "object" && !Array.isArray(workspace) && typeof workspace.path === "string");
|
|
107
|
+
if (requestedWorkspaceId) {
|
|
108
|
+
const requested = candidates.find((workspace) => workspace.id === requestedWorkspaceId);
|
|
109
|
+
if (!requested || typeof requested.path !== "string")
|
|
110
|
+
return undefined;
|
|
111
|
+
return {
|
|
112
|
+
id: requestedWorkspaceId,
|
|
113
|
+
name: typeof requested.name === "string" && requested.name
|
|
114
|
+
? requested.name
|
|
115
|
+
: `${tunnelId ?? requestedWorkspaceId} Default`,
|
|
116
|
+
path: requested.path,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const preferredId = requestedWorkspaceId ?? (tunnelId ? `${tunnelId}--default` : undefined);
|
|
120
|
+
const configured = (preferredId ? candidates.find((workspace) => workspace.id === preferredId) : undefined) ??
|
|
121
|
+
candidates.find((workspace) => workspace.id === "default") ??
|
|
122
|
+
candidates[0];
|
|
123
|
+
if (!configured || typeof configured.path !== "string")
|
|
124
|
+
return undefined;
|
|
125
|
+
return {
|
|
126
|
+
id: typeof configured.id === "string" && configured.id ? configured.id : (preferredId ?? "default"),
|
|
127
|
+
name: typeof configured.name === "string" && configured.name
|
|
128
|
+
? configured.name
|
|
129
|
+
: `${tunnelId ?? preferredId ?? "default"} Default`,
|
|
130
|
+
path: configured.path,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function objectAt(config, key) {
|
|
134
|
+
const value = config[key];
|
|
135
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
136
|
+
return undefined;
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
function stringAt(config, key) {
|
|
140
|
+
const value = config?.[key];
|
|
141
|
+
return typeof value === "string" && value ? value : undefined;
|
|
142
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ClientActivityStatus, ManagedAgentName } from "./supervisor.js";
|
|
2
|
+
export type MaintenanceOperation = {
|
|
3
|
+
type: "upgrade";
|
|
4
|
+
version: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: "restart";
|
|
7
|
+
agent?: ManagedAgentName;
|
|
8
|
+
} | {
|
|
9
|
+
type: "stop";
|
|
10
|
+
} | {
|
|
11
|
+
type: "uninstall";
|
|
12
|
+
};
|
|
13
|
+
export type MaintenanceTaskStatus = "scheduled" | "waiting" | "running" | "restarting" | "stopping" | "completed" | "failed" | "cancelled";
|
|
14
|
+
export type MaintenanceTask = {
|
|
15
|
+
id: string;
|
|
16
|
+
operation: MaintenanceOperation;
|
|
17
|
+
status: MaintenanceTaskStatus;
|
|
18
|
+
allowWaiting: boolean;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
notBefore: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
completedAt?: string;
|
|
23
|
+
message?: string;
|
|
24
|
+
source?: "manual" | "automatic";
|
|
25
|
+
};
|
|
26
|
+
export type ScheduleMaintenanceOptions = {
|
|
27
|
+
operation: MaintenanceOperation;
|
|
28
|
+
delaySeconds: number;
|
|
29
|
+
allowWaiting: boolean;
|
|
30
|
+
source?: "manual" | "automatic";
|
|
31
|
+
};
|
|
32
|
+
type MaintenanceSchedulerOptions = {
|
|
33
|
+
statePath: string;
|
|
34
|
+
getActivity: () => Promise<ClientActivityStatus>;
|
|
35
|
+
execute: (operation: MaintenanceOperation, markServiceExit: (status: "restarting" | "stopping") => Promise<void>) => Promise<void>;
|
|
36
|
+
pollIntervalMs?: number;
|
|
37
|
+
maxWaitMs?: number;
|
|
38
|
+
now?: () => number;
|
|
39
|
+
};
|
|
40
|
+
export declare class MaintenanceScheduler {
|
|
41
|
+
private readonly options;
|
|
42
|
+
private task;
|
|
43
|
+
private timer;
|
|
44
|
+
private readonly pollIntervalMs;
|
|
45
|
+
private readonly maxWaitMs;
|
|
46
|
+
private readonly now;
|
|
47
|
+
constructor(options: MaintenanceSchedulerOptions);
|
|
48
|
+
start(): Promise<void>;
|
|
49
|
+
schedule(options: ScheduleMaintenanceOptions): Promise<MaintenanceTask>;
|
|
50
|
+
getTask(): MaintenanceTask | undefined;
|
|
51
|
+
cancel(): Promise<MaintenanceTask>;
|
|
52
|
+
stop(): void;
|
|
53
|
+
checkNow(): Promise<void>;
|
|
54
|
+
private arm;
|
|
55
|
+
private tick;
|
|
56
|
+
private updateWaiting;
|
|
57
|
+
private markServiceExit;
|
|
58
|
+
private finish;
|
|
59
|
+
private isoNow;
|
|
60
|
+
private readState;
|
|
61
|
+
private writeState;
|
|
62
|
+
}
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
const activeStatuses = new Set(["scheduled", "waiting", "running", "restarting", "stopping"]);
|
|
5
|
+
export class MaintenanceScheduler {
|
|
6
|
+
options;
|
|
7
|
+
task;
|
|
8
|
+
timer;
|
|
9
|
+
pollIntervalMs;
|
|
10
|
+
maxWaitMs;
|
|
11
|
+
now;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 1000;
|
|
15
|
+
this.maxWaitMs = options.maxWaitMs ?? 60 * 60 * 1000;
|
|
16
|
+
this.now = options.now ?? Date.now;
|
|
17
|
+
}
|
|
18
|
+
async start() {
|
|
19
|
+
this.task = await this.readState();
|
|
20
|
+
if (!this.task)
|
|
21
|
+
return;
|
|
22
|
+
if (this.task.status === "restarting" || this.task.status === "stopping") {
|
|
23
|
+
const now = this.isoNow();
|
|
24
|
+
this.task = {
|
|
25
|
+
...this.task,
|
|
26
|
+
status: "completed",
|
|
27
|
+
updatedAt: now,
|
|
28
|
+
completedAt: now,
|
|
29
|
+
message: "Client service completed the maintenance operation",
|
|
30
|
+
};
|
|
31
|
+
await this.writeState();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (this.task.status === "running") {
|
|
35
|
+
await this.finish("failed", "Client service exited before the maintenance operation completed");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (this.task.status === "scheduled" || this.task.status === "waiting")
|
|
39
|
+
this.arm();
|
|
40
|
+
}
|
|
41
|
+
async schedule(options) {
|
|
42
|
+
if (!Number.isInteger(options.delaySeconds) || options.delaySeconds < 1 || options.delaySeconds > 86_400) {
|
|
43
|
+
throw new Error("--delay must be an integer between 1 and 86400 seconds");
|
|
44
|
+
}
|
|
45
|
+
if (this.task &&
|
|
46
|
+
activeStatuses.has(this.task.status) &&
|
|
47
|
+
!(options.source !== "automatic" && this.task.source === "automatic" && isPending(this.task))) {
|
|
48
|
+
throw new Error(`Maintenance task ${this.task.id} is already ${this.task.status}`);
|
|
49
|
+
}
|
|
50
|
+
const nowMs = this.now();
|
|
51
|
+
const now = new Date(nowMs).toISOString();
|
|
52
|
+
this.task = {
|
|
53
|
+
id: randomUUID(),
|
|
54
|
+
operation: options.operation,
|
|
55
|
+
status: "scheduled",
|
|
56
|
+
allowWaiting: options.allowWaiting,
|
|
57
|
+
createdAt: now,
|
|
58
|
+
notBefore: new Date(nowMs + options.delaySeconds * 1000).toISOString(),
|
|
59
|
+
updatedAt: now,
|
|
60
|
+
source: options.source ?? "manual",
|
|
61
|
+
};
|
|
62
|
+
await this.writeState();
|
|
63
|
+
this.arm();
|
|
64
|
+
return this.task;
|
|
65
|
+
}
|
|
66
|
+
getTask() {
|
|
67
|
+
return this.task ? structuredClone(this.task) : undefined;
|
|
68
|
+
}
|
|
69
|
+
async cancel() {
|
|
70
|
+
if (!this.task || (this.task.status !== "scheduled" && this.task.status !== "waiting")) {
|
|
71
|
+
throw new Error("No cancellable maintenance task");
|
|
72
|
+
}
|
|
73
|
+
if (this.timer)
|
|
74
|
+
clearTimeout(this.timer);
|
|
75
|
+
const now = this.isoNow();
|
|
76
|
+
this.task = {
|
|
77
|
+
...this.task,
|
|
78
|
+
status: "cancelled",
|
|
79
|
+
updatedAt: now,
|
|
80
|
+
completedAt: now,
|
|
81
|
+
message: "Cancelled by user",
|
|
82
|
+
};
|
|
83
|
+
await this.writeState();
|
|
84
|
+
return this.task;
|
|
85
|
+
}
|
|
86
|
+
stop() {
|
|
87
|
+
if (this.timer)
|
|
88
|
+
clearTimeout(this.timer);
|
|
89
|
+
this.timer = undefined;
|
|
90
|
+
}
|
|
91
|
+
async checkNow() {
|
|
92
|
+
if (this.timer)
|
|
93
|
+
clearTimeout(this.timer);
|
|
94
|
+
this.timer = undefined;
|
|
95
|
+
await this.tick();
|
|
96
|
+
}
|
|
97
|
+
arm(delayMs) {
|
|
98
|
+
if (!this.task || (this.task.status !== "scheduled" && this.task.status !== "waiting"))
|
|
99
|
+
return;
|
|
100
|
+
if (this.timer)
|
|
101
|
+
clearTimeout(this.timer);
|
|
102
|
+
const untilNotBefore = Math.max(0, Date.parse(this.task.notBefore) - this.now());
|
|
103
|
+
this.timer = setTimeout(() => this.tick(), delayMs ?? untilNotBefore);
|
|
104
|
+
this.timer.unref();
|
|
105
|
+
}
|
|
106
|
+
async tick() {
|
|
107
|
+
this.timer = undefined;
|
|
108
|
+
const task = this.task;
|
|
109
|
+
if (!task || (task.status !== "scheduled" && task.status !== "waiting"))
|
|
110
|
+
return;
|
|
111
|
+
if (this.now() < Date.parse(task.notBefore)) {
|
|
112
|
+
this.arm();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const activity = await this.options.getActivity();
|
|
117
|
+
const unavailable = Object.entries(activity.agents)
|
|
118
|
+
.filter(([, status]) => !status.available)
|
|
119
|
+
.map(([name]) => name);
|
|
120
|
+
const blockers = [];
|
|
121
|
+
if (unavailable.length > 0)
|
|
122
|
+
blockers.push(`unavailable agents: ${unavailable.join(", ")}`);
|
|
123
|
+
if (activity.active > 0)
|
|
124
|
+
blockers.push(`${activity.active} active session(s)`);
|
|
125
|
+
if (!task.allowWaiting && activity.waiting > 0)
|
|
126
|
+
blockers.push(`${activity.waiting} waiting session(s)`);
|
|
127
|
+
if (blockers.length > 0) {
|
|
128
|
+
if (task.source !== "automatic" && this.now() - Date.parse(task.notBefore) >= this.maxWaitMs) {
|
|
129
|
+
await this.finish("failed", `Timed out waiting for an idle client: ${blockers.join("; ")}`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await this.updateWaiting(`Waiting for an idle client: ${blockers.join("; ")}`);
|
|
133
|
+
this.arm(this.pollIntervalMs);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const now = this.isoNow();
|
|
137
|
+
this.task = { ...task, status: "running", updatedAt: now, message: "Maintenance operation is running" };
|
|
138
|
+
await this.writeState();
|
|
139
|
+
await this.options.execute(task.operation, (status) => this.markServiceExit(status));
|
|
140
|
+
await this.finish("completed", "Maintenance operation completed");
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
await this.finish("failed", err instanceof Error ? err.message : String(err));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async updateWaiting(message) {
|
|
147
|
+
if (!this.task)
|
|
148
|
+
return;
|
|
149
|
+
this.task = { ...this.task, status: "waiting", updatedAt: this.isoNow(), message };
|
|
150
|
+
await this.writeState();
|
|
151
|
+
}
|
|
152
|
+
async markServiceExit(status) {
|
|
153
|
+
if (!this.task || this.task.status !== "running")
|
|
154
|
+
return;
|
|
155
|
+
this.task = {
|
|
156
|
+
...this.task,
|
|
157
|
+
status,
|
|
158
|
+
updatedAt: this.isoNow(),
|
|
159
|
+
message: `Maintenance operation is ${status === "restarting" ? "restarting" : "stopping"} the client service`,
|
|
160
|
+
};
|
|
161
|
+
await this.writeState();
|
|
162
|
+
}
|
|
163
|
+
async finish(status, message) {
|
|
164
|
+
if (!this.task)
|
|
165
|
+
return;
|
|
166
|
+
const now = this.isoNow();
|
|
167
|
+
this.task = { ...this.task, status, updatedAt: now, completedAt: now, message };
|
|
168
|
+
await this.writeState();
|
|
169
|
+
}
|
|
170
|
+
isoNow() {
|
|
171
|
+
return new Date(this.now()).toISOString();
|
|
172
|
+
}
|
|
173
|
+
async readState() {
|
|
174
|
+
try {
|
|
175
|
+
return JSON.parse(await readFile(this.options.statePath, "utf8"));
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
if (err.code === "ENOENT")
|
|
179
|
+
return undefined;
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async writeState() {
|
|
184
|
+
if (!this.task)
|
|
185
|
+
return;
|
|
186
|
+
await mkdir(dirname(this.options.statePath), { recursive: true, mode: 0o700 });
|
|
187
|
+
const temporaryPath = `${this.options.statePath}.tmp`;
|
|
188
|
+
await writeFile(temporaryPath, `${JSON.stringify(this.task, null, 2)}\n`, { mode: 0o600 });
|
|
189
|
+
await rename(temporaryPath, this.options.statePath);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function isPending(task) {
|
|
193
|
+
return task.status === "scheduled" || task.status === "waiting";
|
|
194
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const defaultConfigPath: string;
|
|
2
|
+
export declare const defaultDataDir: string;
|
|
3
|
+
export declare const defaultRuntimeDir: string;
|
|
4
|
+
export declare const defaultBinDir: string;
|
|
5
|
+
export declare const defaultWorkspacePath: string;
|
|
6
|
+
export declare function expandHome(path: string): string;
|
|
7
|
+
export declare function resolvePath(path: string): string;
|
|
8
|
+
export declare function configBaseDir(configPath: string): string;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
export const defaultConfigPath = join(homedir(), ".coding-agent", "config.yaml");
|
|
4
|
+
export const defaultDataDir = join(homedir(), ".coding-agent", "data");
|
|
5
|
+
export const defaultRuntimeDir = join(homedir(), ".coding-agent", "runtime");
|
|
6
|
+
export const defaultBinDir = join(homedir(), ".coding-agent", "bin");
|
|
7
|
+
export const defaultWorkspacePath = join(homedir(), "messenger-workspace");
|
|
8
|
+
export function expandHome(path) {
|
|
9
|
+
if (path === "~")
|
|
10
|
+
return homedir();
|
|
11
|
+
if (path.startsWith("~/"))
|
|
12
|
+
return join(homedir(), path.slice(2));
|
|
13
|
+
return path;
|
|
14
|
+
}
|
|
15
|
+
export function resolvePath(path) {
|
|
16
|
+
return resolve(expandHome(path));
|
|
17
|
+
}
|
|
18
|
+
export function configBaseDir(configPath) {
|
|
19
|
+
return dirname(configPath);
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type RuntimeInstallOptions = {
|
|
2
|
+
version: string;
|
|
3
|
+
configPath: string;
|
|
4
|
+
runtimeDir?: string;
|
|
5
|
+
binDir?: string;
|
|
6
|
+
skillsSourceDir?: string;
|
|
7
|
+
codexHome?: string;
|
|
8
|
+
claudeHome?: string;
|
|
9
|
+
};
|
|
10
|
+
export type RuntimeInstallResult = {
|
|
11
|
+
releaseDir: string;
|
|
12
|
+
currentLink: string;
|
|
13
|
+
wrapperPath: string;
|
|
14
|
+
cliWrapperPath: string;
|
|
15
|
+
version: string;
|
|
16
|
+
};
|
|
17
|
+
export type ReleaseChannel = "latest" | "beta" | "alpha";
|
|
18
|
+
export declare const releaseChannels: ReleaseChannel[];
|
|
19
|
+
export declare function normalizeReleaseChannel(value: unknown): ReleaseChannel | undefined;
|
|
20
|
+
export declare function parseReleaseChannel(value: unknown): ReleaseChannel | undefined;
|
|
21
|
+
export declare function normalizeUpgradeVersion(version: string, explicitChannel?: ReleaseChannel): {
|
|
22
|
+
version: string;
|
|
23
|
+
channel?: ReleaseChannel;
|
|
24
|
+
};
|
|
25
|
+
export declare function isPrereleaseVersion(version: string): boolean;
|
|
26
|
+
export declare function compareSemverVersions(left: string, right: string): number;
|
|
27
|
+
export declare function currentPackageVersion(): Promise<string>;
|
|
28
|
+
export declare function npmExecutablePath(nodeExecutable?: string): string;
|
|
29
|
+
export declare function npmProcessPath(nodeExecutable?: string, currentPath?: string | undefined): string;
|
|
30
|
+
export declare function currentBundledSkillsDir(moduleUrl?: string): string;
|
|
31
|
+
export declare function defaultAgentHomes(): [string, string];
|
|
32
|
+
export declare function installRuntime(options: RuntimeInstallOptions): Promise<RuntimeInstallResult>;
|
|
33
|
+
export declare function resolveChannelPackageVersion(channel: ReleaseChannel): Promise<string>;
|
|
34
|
+
export declare function syncBundledSkills(skillsSourceDir: string, agentHomes: string[]): Promise<void>;
|