@hasna/accounts 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -5
- package/dist/cli.js +458 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +376 -8
- package/dist/lib/supervisor.d.ts +69 -0
- package/dist/lib/supervisor.d.ts.map +1 -0
- package/dist/mcp.js +133 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4938,6 +4938,366 @@ function switchProfile(name, opts = {}) {
|
|
|
4938
4938
|
message
|
|
4939
4939
|
};
|
|
4940
4940
|
}
|
|
4941
|
+
// src/lib/supervisor.ts
|
|
4942
|
+
import { spawn } from "node:child_process";
|
|
4943
|
+
import { createHash } from "node:crypto";
|
|
4944
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync4, readdirSync, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4945
|
+
import { createConnection, createServer } from "node:net";
|
|
4946
|
+
import { basename, join as join9 } from "node:path";
|
|
4947
|
+
var STATE_SUFFIX = ".json";
|
|
4948
|
+
function supervisorDir() {
|
|
4949
|
+
return join9(accountsHome(), "supervisors");
|
|
4950
|
+
}
|
|
4951
|
+
function supervisorStatePath(toolId) {
|
|
4952
|
+
return join9(supervisorDir(), `${toolId}${STATE_SUFFIX}`);
|
|
4953
|
+
}
|
|
4954
|
+
function supervisorSocketPath(toolId) {
|
|
4955
|
+
if (process.platform === "win32") {
|
|
4956
|
+
const hash = createHash("sha1").update(accountsHome()).digest("hex").slice(0, 12);
|
|
4957
|
+
return `\\\\.\\pipe\\hasna-accounts-${hash}-${toolId}`;
|
|
4958
|
+
}
|
|
4959
|
+
return join9(supervisorDir(), `${toolId}.sock`);
|
|
4960
|
+
}
|
|
4961
|
+
function nowIso2() {
|
|
4962
|
+
return new Date().toISOString();
|
|
4963
|
+
}
|
|
4964
|
+
function parseState(raw) {
|
|
4965
|
+
const data = JSON.parse(raw);
|
|
4966
|
+
if (data.version !== 1 || typeof data.tool !== "string" || typeof data.profile !== "string" || typeof data.pid !== "number" || typeof data.socketPath !== "string" || !Array.isArray(data.command)) {
|
|
4967
|
+
return;
|
|
4968
|
+
}
|
|
4969
|
+
return data;
|
|
4970
|
+
}
|
|
4971
|
+
function readSupervisorState(toolId) {
|
|
4972
|
+
const path = supervisorStatePath(toolId);
|
|
4973
|
+
if (!existsSync8(path))
|
|
4974
|
+
return;
|
|
4975
|
+
try {
|
|
4976
|
+
return parseState(readFileSync4(path, "utf8"));
|
|
4977
|
+
} catch {
|
|
4978
|
+
return;
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
function listSupervisorStates() {
|
|
4982
|
+
const dir = supervisorDir();
|
|
4983
|
+
if (!existsSync8(dir))
|
|
4984
|
+
return [];
|
|
4985
|
+
return readdirSync(dir).filter((name) => name.endsWith(STATE_SUFFIX)).map((name) => basename(name, STATE_SUFFIX)).map((toolId) => readSupervisorState(toolId)).filter((state) => state !== undefined);
|
|
4986
|
+
}
|
|
4987
|
+
function writeSupervisorState(state) {
|
|
4988
|
+
mkdirSync6(supervisorDir(), { recursive: true });
|
|
4989
|
+
writeFileSync4(supervisorStatePath(state.tool), JSON.stringify(state, null, 2) + `
|
|
4990
|
+
`, { mode: 384 });
|
|
4991
|
+
}
|
|
4992
|
+
function removeSupervisorFiles(toolId) {
|
|
4993
|
+
rmSync2(supervisorStatePath(toolId), { force: true });
|
|
4994
|
+
if (process.platform !== "win32")
|
|
4995
|
+
rmSync2(supervisorSocketPath(toolId), { force: true });
|
|
4996
|
+
}
|
|
4997
|
+
function processAlive(pid) {
|
|
4998
|
+
try {
|
|
4999
|
+
process.kill(pid, 0);
|
|
5000
|
+
return true;
|
|
5001
|
+
} catch {
|
|
5002
|
+
return false;
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
function knownTool(id) {
|
|
5006
|
+
try {
|
|
5007
|
+
return getTool(id);
|
|
5008
|
+
} catch {
|
|
5009
|
+
return;
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
function resolveSupervisorLaunch(target, opts = {}) {
|
|
5013
|
+
const targetTool = knownTool(target);
|
|
5014
|
+
if (opts.profile) {
|
|
5015
|
+
const profile2 = getProfile(opts.profile, opts.tool ?? targetTool?.id);
|
|
5016
|
+
if (targetTool && profile2.tool !== targetTool.id) {
|
|
5017
|
+
throw new AccountsError(`profile "${profile2.name}" belongs to ${profile2.tool}, not ${targetTool.id}`);
|
|
5018
|
+
}
|
|
5019
|
+
return { profile: profile2, tool: getTool(profile2.tool), targetKind: targetTool ? "tool" : "profile" };
|
|
5020
|
+
}
|
|
5021
|
+
if (targetTool && !opts.tool) {
|
|
5022
|
+
const profile2 = currentProfile(targetTool.id) ?? appliedProfile(targetTool.id);
|
|
5023
|
+
if (!profile2) {
|
|
5024
|
+
throw new AccountsError(`no active ${targetTool.label} profile. Run \`accounts use <name> --tool ${targetTool.id}\` or pass --profile.`);
|
|
5025
|
+
}
|
|
5026
|
+
return { profile: profile2, tool: targetTool, targetKind: "tool" };
|
|
5027
|
+
}
|
|
5028
|
+
const profile = getProfile(target, opts.tool);
|
|
5029
|
+
return { profile, tool: getTool(profile.tool), targetKind: "profile" };
|
|
5030
|
+
}
|
|
5031
|
+
function exitCode(code, signal) {
|
|
5032
|
+
if (code !== null)
|
|
5033
|
+
return code;
|
|
5034
|
+
if (signal === "SIGINT")
|
|
5035
|
+
return 130;
|
|
5036
|
+
if (signal === "SIGTERM")
|
|
5037
|
+
return 143;
|
|
5038
|
+
return signal ? 1 : 0;
|
|
5039
|
+
}
|
|
5040
|
+
function killChildProcess(child, signal) {
|
|
5041
|
+
if (!child.pid)
|
|
5042
|
+
return;
|
|
5043
|
+
if (process.platform !== "win32") {
|
|
5044
|
+
try {
|
|
5045
|
+
process.kill(-child.pid, signal);
|
|
5046
|
+
return;
|
|
5047
|
+
} catch {}
|
|
5048
|
+
}
|
|
5049
|
+
child.kill(signal);
|
|
5050
|
+
}
|
|
5051
|
+
function wait(ms) {
|
|
5052
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5053
|
+
}
|
|
5054
|
+
async function listen(server, socketPath) {
|
|
5055
|
+
await new Promise((resolve3, reject) => {
|
|
5056
|
+
const onError = (err) => {
|
|
5057
|
+
server.off("listening", onListening);
|
|
5058
|
+
reject(err);
|
|
5059
|
+
};
|
|
5060
|
+
const onListening = () => {
|
|
5061
|
+
server.off("error", onError);
|
|
5062
|
+
resolve3();
|
|
5063
|
+
};
|
|
5064
|
+
server.once("error", onError);
|
|
5065
|
+
server.once("listening", onListening);
|
|
5066
|
+
server.listen(socketPath);
|
|
5067
|
+
});
|
|
5068
|
+
}
|
|
5069
|
+
async function sendSupervisorRequest(toolId, request, opts = {}) {
|
|
5070
|
+
const timeoutMs = opts.timeoutMs ?? 1500;
|
|
5071
|
+
const socketPath = supervisorSocketPath(toolId);
|
|
5072
|
+
return await new Promise((resolve3, reject) => {
|
|
5073
|
+
const socket = createConnection(socketPath);
|
|
5074
|
+
let buffer = "";
|
|
5075
|
+
let settled = false;
|
|
5076
|
+
const finish = (value) => {
|
|
5077
|
+
if (settled)
|
|
5078
|
+
return;
|
|
5079
|
+
settled = true;
|
|
5080
|
+
clearTimeout(timer);
|
|
5081
|
+
socket.destroy();
|
|
5082
|
+
resolve3(value);
|
|
5083
|
+
};
|
|
5084
|
+
const fail = (err) => {
|
|
5085
|
+
if (settled)
|
|
5086
|
+
return;
|
|
5087
|
+
settled = true;
|
|
5088
|
+
clearTimeout(timer);
|
|
5089
|
+
socket.destroy();
|
|
5090
|
+
if (opts.allowMissing && (err.code === "ENOENT" || err.code === "ECONNREFUSED")) {
|
|
5091
|
+
resolve3(undefined);
|
|
5092
|
+
} else {
|
|
5093
|
+
reject(new AccountsError(`could not contact accounts supervisor for ${toolId}: ${err.message}`));
|
|
5094
|
+
}
|
|
5095
|
+
};
|
|
5096
|
+
const timer = setTimeout(() => {
|
|
5097
|
+
fail(Object.assign(new Error(`timed out after ${timeoutMs}ms`), { code: "ETIMEDOUT" }));
|
|
5098
|
+
}, timeoutMs);
|
|
5099
|
+
socket.setEncoding("utf8");
|
|
5100
|
+
socket.once("connect", () => {
|
|
5101
|
+
socket.write(JSON.stringify(request) + `
|
|
5102
|
+
`);
|
|
5103
|
+
});
|
|
5104
|
+
socket.once("error", fail);
|
|
5105
|
+
socket.on("data", (chunk) => {
|
|
5106
|
+
buffer += chunk;
|
|
5107
|
+
const newline = buffer.indexOf(`
|
|
5108
|
+
`);
|
|
5109
|
+
if (newline === -1)
|
|
5110
|
+
return;
|
|
5111
|
+
try {
|
|
5112
|
+
finish(JSON.parse(buffer.slice(0, newline)));
|
|
5113
|
+
} catch (err) {
|
|
5114
|
+
fail(err);
|
|
5115
|
+
}
|
|
5116
|
+
});
|
|
5117
|
+
socket.once("end", () => {
|
|
5118
|
+
if (!settled)
|
|
5119
|
+
fail(new Error("connection closed without a response"));
|
|
5120
|
+
});
|
|
5121
|
+
});
|
|
5122
|
+
}
|
|
5123
|
+
async function runSupervisedTool(initialProfile, tool, initialArgs = [], opts = {}) {
|
|
5124
|
+
const socketPath = supervisorSocketPath(tool.id);
|
|
5125
|
+
const existing = readSupervisorState(tool.id);
|
|
5126
|
+
if (existing && processAlive(existing.pid)) {
|
|
5127
|
+
throw new AccountsError(`an accounts supervisor for ${tool.label} is already running (pid ${existing.pid})`);
|
|
5128
|
+
}
|
|
5129
|
+
removeSupervisorFiles(tool.id);
|
|
5130
|
+
mkdirSync6(supervisorDir(), { recursive: true });
|
|
5131
|
+
const startedAt = nowIso2();
|
|
5132
|
+
const restartDelayMs = opts.restartDelayMs ?? 350;
|
|
5133
|
+
const log = opts.log ?? (() => {
|
|
5134
|
+
return;
|
|
5135
|
+
});
|
|
5136
|
+
const server = createServer();
|
|
5137
|
+
let profile = initialProfile;
|
|
5138
|
+
let childArgs = initialArgs;
|
|
5139
|
+
let child;
|
|
5140
|
+
let stopping = false;
|
|
5141
|
+
let restarting = false;
|
|
5142
|
+
let settled = false;
|
|
5143
|
+
const state = () => ({
|
|
5144
|
+
version: 1,
|
|
5145
|
+
tool: tool.id,
|
|
5146
|
+
profile: profile.name,
|
|
5147
|
+
pid: process.pid,
|
|
5148
|
+
...child?.pid ? { childPid: child.pid } : {},
|
|
5149
|
+
socketPath,
|
|
5150
|
+
command: [tool.bin, ...childArgs],
|
|
5151
|
+
startedAt,
|
|
5152
|
+
updatedAt: nowIso2()
|
|
5153
|
+
});
|
|
5154
|
+
const persist = () => writeSupervisorState(state());
|
|
5155
|
+
const stopChild = async () => {
|
|
5156
|
+
const target = child;
|
|
5157
|
+
if (!target || target.exitCode !== null)
|
|
5158
|
+
return;
|
|
5159
|
+
await new Promise((resolve3) => {
|
|
5160
|
+
let done2 = false;
|
|
5161
|
+
const finish = () => {
|
|
5162
|
+
if (done2)
|
|
5163
|
+
return;
|
|
5164
|
+
done2 = true;
|
|
5165
|
+
clearTimeout(killTimer);
|
|
5166
|
+
resolve3();
|
|
5167
|
+
};
|
|
5168
|
+
const killTimer = setTimeout(() => {
|
|
5169
|
+
try {
|
|
5170
|
+
killChildProcess(target, "SIGKILL");
|
|
5171
|
+
} catch {
|
|
5172
|
+
finish();
|
|
5173
|
+
}
|
|
5174
|
+
}, 2500);
|
|
5175
|
+
target.once("exit", finish);
|
|
5176
|
+
try {
|
|
5177
|
+
killChildProcess(target, "SIGTERM");
|
|
5178
|
+
} catch {
|
|
5179
|
+
finish();
|
|
5180
|
+
}
|
|
5181
|
+
});
|
|
5182
|
+
};
|
|
5183
|
+
const cleanup = () => {
|
|
5184
|
+
server.close();
|
|
5185
|
+
removeSupervisorFiles(tool.id);
|
|
5186
|
+
process.off("SIGINT", onSigint);
|
|
5187
|
+
process.off("SIGTERM", onSigterm);
|
|
5188
|
+
};
|
|
5189
|
+
let resolveRun;
|
|
5190
|
+
const done = new Promise((resolve3) => {
|
|
5191
|
+
resolveRun = resolve3;
|
|
5192
|
+
});
|
|
5193
|
+
const finishRun = (code) => {
|
|
5194
|
+
if (settled)
|
|
5195
|
+
return;
|
|
5196
|
+
settled = true;
|
|
5197
|
+
cleanup();
|
|
5198
|
+
resolveRun(code);
|
|
5199
|
+
};
|
|
5200
|
+
const startChild = (nextProfile, nextArgs) => {
|
|
5201
|
+
profile = nextProfile;
|
|
5202
|
+
childArgs = nextArgs;
|
|
5203
|
+
useProfile(profile.name, tool.id);
|
|
5204
|
+
const env = profileEnv(profile, tool);
|
|
5205
|
+
log(`accounts supervisor: starting ${tool.bin} for ${profile.name}`);
|
|
5206
|
+
const proc = spawn(tool.bin, childArgs, {
|
|
5207
|
+
stdio: opts.stdio ?? "inherit",
|
|
5208
|
+
env: { ...process.env, ...env, ACCOUNTS_SUPERVISOR: "1", ACCOUNTS_ACTIVE: profile.name },
|
|
5209
|
+
detached: process.platform !== "win32"
|
|
5210
|
+
});
|
|
5211
|
+
child = proc;
|
|
5212
|
+
persist();
|
|
5213
|
+
proc.once("error", (err) => {
|
|
5214
|
+
log(`accounts supervisor: failed to start ${tool.bin}: ${err.message}`);
|
|
5215
|
+
if (!restarting && !stopping)
|
|
5216
|
+
finishRun(1);
|
|
5217
|
+
});
|
|
5218
|
+
proc.once("exit", (code, signal) => {
|
|
5219
|
+
if (child === proc)
|
|
5220
|
+
child = undefined;
|
|
5221
|
+
persist();
|
|
5222
|
+
if (restarting || stopping)
|
|
5223
|
+
return;
|
|
5224
|
+
finishRun(exitCode(code, signal));
|
|
5225
|
+
});
|
|
5226
|
+
};
|
|
5227
|
+
const restartWith = async (result) => {
|
|
5228
|
+
restarting = true;
|
|
5229
|
+
try {
|
|
5230
|
+
await wait(restartDelayMs);
|
|
5231
|
+
await stopChild();
|
|
5232
|
+
startChild(getProfile(result.profile.name, tool.id), result.command.slice(1));
|
|
5233
|
+
} finally {
|
|
5234
|
+
restarting = false;
|
|
5235
|
+
}
|
|
5236
|
+
};
|
|
5237
|
+
const shutdown = async (code) => {
|
|
5238
|
+
if (stopping)
|
|
5239
|
+
return;
|
|
5240
|
+
stopping = true;
|
|
5241
|
+
await stopChild();
|
|
5242
|
+
finishRun(code);
|
|
5243
|
+
};
|
|
5244
|
+
const handleRequest = async (request) => {
|
|
5245
|
+
if (request.type === "status")
|
|
5246
|
+
return { ok: true, state: state() };
|
|
5247
|
+
if (request.type === "stop") {
|
|
5248
|
+
setTimeout(() => void shutdown(0), 25);
|
|
5249
|
+
return { ok: true, stopping: true, state: state() };
|
|
5250
|
+
}
|
|
5251
|
+
if (request.type !== "switch_profile")
|
|
5252
|
+
return { ok: false, error: "unknown supervisor request" };
|
|
5253
|
+
if (request.tool && request.tool !== tool.id) {
|
|
5254
|
+
return { ok: false, error: `this supervisor runs ${tool.id}, not ${request.tool}` };
|
|
5255
|
+
}
|
|
5256
|
+
try {
|
|
5257
|
+
const result = switchProfile(request.name, {
|
|
5258
|
+
tool: tool.id,
|
|
5259
|
+
mode: request.mode ?? "auto",
|
|
5260
|
+
resume: request.resume ?? true,
|
|
5261
|
+
args: request.args ?? []
|
|
5262
|
+
});
|
|
5263
|
+
log(`accounts supervisor: switching ${tool.id} to ${result.profile.name}`);
|
|
5264
|
+
setTimeout(() => void restartWith(result), 0);
|
|
5265
|
+
return { ok: true, queued: true, result, state: state(), restartDelayMs };
|
|
5266
|
+
} catch (err) {
|
|
5267
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
5268
|
+
}
|
|
5269
|
+
};
|
|
5270
|
+
server.on("connection", (socket) => {
|
|
5271
|
+
socket.setEncoding("utf8");
|
|
5272
|
+
let buffer = "";
|
|
5273
|
+
socket.on("data", (chunk) => {
|
|
5274
|
+
buffer += chunk;
|
|
5275
|
+
const newline = buffer.indexOf(`
|
|
5276
|
+
`);
|
|
5277
|
+
if (newline === -1)
|
|
5278
|
+
return;
|
|
5279
|
+
const line = buffer.slice(0, newline);
|
|
5280
|
+
buffer = buffer.slice(newline + 1);
|
|
5281
|
+
(async () => {
|
|
5282
|
+
let response;
|
|
5283
|
+
try {
|
|
5284
|
+
response = await handleRequest(JSON.parse(line));
|
|
5285
|
+
} catch (err) {
|
|
5286
|
+
response = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
5287
|
+
}
|
|
5288
|
+
socket.end(JSON.stringify(response) + `
|
|
5289
|
+
`);
|
|
5290
|
+
})();
|
|
5291
|
+
});
|
|
5292
|
+
});
|
|
5293
|
+
const onSigint = () => void shutdown(130);
|
|
5294
|
+
const onSigterm = () => void shutdown(143);
|
|
5295
|
+
process.once("SIGINT", onSigint);
|
|
5296
|
+
process.once("SIGTERM", onSigterm);
|
|
5297
|
+
await listen(server, socketPath);
|
|
5298
|
+
startChild(profile, childArgs);
|
|
5299
|
+
return await done;
|
|
5300
|
+
}
|
|
4941
5301
|
// src/lib/pick.ts
|
|
4942
5302
|
import * as readline from "node:readline/promises";
|
|
4943
5303
|
import { stdin as input, stdout as output } from "node:process";
|
|
@@ -4971,8 +5331,8 @@ async function pickProfile(opts = {}) {
|
|
|
4971
5331
|
return { profile, mode };
|
|
4972
5332
|
}
|
|
4973
5333
|
// src/lib/hook.ts
|
|
4974
|
-
import { existsSync as
|
|
4975
|
-
import { join as
|
|
5334
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync5, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
5335
|
+
import { join as join10 } from "node:path";
|
|
4976
5336
|
var HOOK_FILE = "claude-hook.sh";
|
|
4977
5337
|
var MARKER = "# accounts-claude-hook";
|
|
4978
5338
|
var NAME_PATTERN = "^[a-z0-9][a-z0-9-]*$";
|
|
@@ -4980,7 +5340,7 @@ function shellQuotePath(path) {
|
|
|
4980
5340
|
return `'${path.replace(/'/g, `'\\''`)}'`;
|
|
4981
5341
|
}
|
|
4982
5342
|
function hookPath() {
|
|
4983
|
-
return
|
|
5343
|
+
return join10(accountsHome(), HOOK_FILE);
|
|
4984
5344
|
}
|
|
4985
5345
|
function hookScript() {
|
|
4986
5346
|
const quotedHook = shellQuotePath(hookPath());
|
|
@@ -5010,16 +5370,16 @@ claude() {
|
|
|
5010
5370
|
}
|
|
5011
5371
|
function installHook() {
|
|
5012
5372
|
const path = hookPath();
|
|
5013
|
-
|
|
5014
|
-
const created = !
|
|
5015
|
-
|
|
5373
|
+
mkdirSync7(accountsHome(), { recursive: true });
|
|
5374
|
+
const created = !existsSync9(path);
|
|
5375
|
+
writeFileSync5(path, hookScript(), { mode: 493 });
|
|
5016
5376
|
return { path, created };
|
|
5017
5377
|
}
|
|
5018
5378
|
function uninstallHook() {
|
|
5019
5379
|
const path = hookPath();
|
|
5020
|
-
if (!
|
|
5380
|
+
if (!existsSync9(path))
|
|
5021
5381
|
return false;
|
|
5022
|
-
const content =
|
|
5382
|
+
const content = readFileSync5(path, "utf8");
|
|
5023
5383
|
if (!content.includes(MARKER))
|
|
5024
5384
|
return false;
|
|
5025
5385
|
unlinkSync3(path);
|
|
@@ -5038,17 +5398,24 @@ export {
|
|
|
5038
5398
|
uninstallHook,
|
|
5039
5399
|
toolDefSchema,
|
|
5040
5400
|
switchProfile,
|
|
5401
|
+
supervisorStatePath,
|
|
5402
|
+
supervisorSocketPath,
|
|
5403
|
+
supervisorDir,
|
|
5041
5404
|
storeSchema,
|
|
5042
5405
|
storePath,
|
|
5043
5406
|
snapshotLiveAuthToProfile,
|
|
5044
5407
|
snapshotClaudeAuthToProfile,
|
|
5045
5408
|
shellSnippet,
|
|
5409
|
+
sendSupervisorRequest,
|
|
5046
5410
|
saveStore,
|
|
5411
|
+
runSupervisedTool,
|
|
5047
5412
|
restoreClaudeAuthFromProfile,
|
|
5413
|
+
resolveSupervisorLaunch,
|
|
5048
5414
|
renameProfile,
|
|
5049
5415
|
removeProfile,
|
|
5050
5416
|
removeCustomTool,
|
|
5051
5417
|
redetectEmail,
|
|
5418
|
+
readSupervisorState,
|
|
5052
5419
|
readClaudeKeychain,
|
|
5053
5420
|
profilesDir,
|
|
5054
5421
|
profileSchema,
|
|
@@ -5058,6 +5425,7 @@ export {
|
|
|
5058
5425
|
pickProfile,
|
|
5059
5426
|
loadStore,
|
|
5060
5427
|
listTools,
|
|
5428
|
+
listSupervisorStates,
|
|
5061
5429
|
listProfiles,
|
|
5062
5430
|
keychainSupported,
|
|
5063
5431
|
isSafeProfileName,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type StdioOptions } from "node:child_process";
|
|
2
|
+
import type { Profile, ToolDef } from "../types.js";
|
|
3
|
+
import { type SwitchMode, type SwitchResult } from "./switch.js";
|
|
4
|
+
export interface SupervisorState {
|
|
5
|
+
version: 1;
|
|
6
|
+
tool: string;
|
|
7
|
+
profile: string;
|
|
8
|
+
pid: number;
|
|
9
|
+
childPid?: number;
|
|
10
|
+
socketPath: string;
|
|
11
|
+
command: string[];
|
|
12
|
+
startedAt: string;
|
|
13
|
+
updatedAt: string;
|
|
14
|
+
}
|
|
15
|
+
export type SupervisorRequest = {
|
|
16
|
+
type: "status";
|
|
17
|
+
} | {
|
|
18
|
+
type: "switch_profile";
|
|
19
|
+
name: string;
|
|
20
|
+
tool?: string;
|
|
21
|
+
mode?: SwitchMode;
|
|
22
|
+
resume?: boolean;
|
|
23
|
+
args?: string[];
|
|
24
|
+
} | {
|
|
25
|
+
type: "stop";
|
|
26
|
+
};
|
|
27
|
+
export type SupervisorResponse = {
|
|
28
|
+
ok: true;
|
|
29
|
+
state: SupervisorState;
|
|
30
|
+
} | {
|
|
31
|
+
ok: true;
|
|
32
|
+
queued: true;
|
|
33
|
+
result: SwitchResult;
|
|
34
|
+
state: SupervisorState;
|
|
35
|
+
restartDelayMs: number;
|
|
36
|
+
} | {
|
|
37
|
+
ok: true;
|
|
38
|
+
stopping: true;
|
|
39
|
+
state: SupervisorState;
|
|
40
|
+
} | {
|
|
41
|
+
ok: false;
|
|
42
|
+
error: string;
|
|
43
|
+
};
|
|
44
|
+
export interface SupervisorLaunchPlan {
|
|
45
|
+
profile: Profile;
|
|
46
|
+
tool: ToolDef;
|
|
47
|
+
targetKind: "tool" | "profile";
|
|
48
|
+
}
|
|
49
|
+
export interface RunSupervisorOptions {
|
|
50
|
+
stdio?: StdioOptions;
|
|
51
|
+
restartDelayMs?: number;
|
|
52
|
+
log?: (message: string) => void;
|
|
53
|
+
}
|
|
54
|
+
export interface SupervisorClientOptions {
|
|
55
|
+
timeoutMs?: number;
|
|
56
|
+
allowMissing?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export declare function supervisorDir(): string;
|
|
59
|
+
export declare function supervisorStatePath(toolId: string): string;
|
|
60
|
+
export declare function supervisorSocketPath(toolId: string): string;
|
|
61
|
+
export declare function readSupervisorState(toolId: string): SupervisorState | undefined;
|
|
62
|
+
export declare function listSupervisorStates(): SupervisorState[];
|
|
63
|
+
export declare function resolveSupervisorLaunch(target: string, opts?: {
|
|
64
|
+
profile?: string;
|
|
65
|
+
tool?: string;
|
|
66
|
+
}): SupervisorLaunchPlan;
|
|
67
|
+
export declare function sendSupervisorRequest(toolId: string, request: SupervisorRequest, opts?: SupervisorClientOptions): Promise<SupervisorResponse | undefined>;
|
|
68
|
+
export declare function runSupervisedTool(initialProfile: Profile, tool: ToolDef, initialArgs?: string[], opts?: RunSupervisorOptions): Promise<number>;
|
|
69
|
+
//# sourceMappingURL=supervisor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../src/lib/supervisor.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp.js
CHANGED
|
@@ -17295,6 +17295,103 @@ function switchProfile(name, opts = {}) {
|
|
|
17295
17295
|
};
|
|
17296
17296
|
}
|
|
17297
17297
|
|
|
17298
|
+
// src/lib/supervisor.ts
|
|
17299
|
+
import { createHash } from "node:crypto";
|
|
17300
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync3, readdirSync, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
17301
|
+
import { createConnection, createServer } from "node:net";
|
|
17302
|
+
import { basename, join as join6 } from "node:path";
|
|
17303
|
+
var STATE_SUFFIX = ".json";
|
|
17304
|
+
function supervisorDir() {
|
|
17305
|
+
return join6(accountsHome(), "supervisors");
|
|
17306
|
+
}
|
|
17307
|
+
function supervisorStatePath(toolId) {
|
|
17308
|
+
return join6(supervisorDir(), `${toolId}${STATE_SUFFIX}`);
|
|
17309
|
+
}
|
|
17310
|
+
function supervisorSocketPath(toolId) {
|
|
17311
|
+
if (process.platform === "win32") {
|
|
17312
|
+
const hash = createHash("sha1").update(accountsHome()).digest("hex").slice(0, 12);
|
|
17313
|
+
return `\\\\.\\pipe\\hasna-accounts-${hash}-${toolId}`;
|
|
17314
|
+
}
|
|
17315
|
+
return join6(supervisorDir(), `${toolId}.sock`);
|
|
17316
|
+
}
|
|
17317
|
+
function parseState(raw) {
|
|
17318
|
+
const data = JSON.parse(raw);
|
|
17319
|
+
if (data.version !== 1 || typeof data.tool !== "string" || typeof data.profile !== "string" || typeof data.pid !== "number" || typeof data.socketPath !== "string" || !Array.isArray(data.command)) {
|
|
17320
|
+
return;
|
|
17321
|
+
}
|
|
17322
|
+
return data;
|
|
17323
|
+
}
|
|
17324
|
+
function readSupervisorState(toolId) {
|
|
17325
|
+
const path = supervisorStatePath(toolId);
|
|
17326
|
+
if (!existsSync5(path))
|
|
17327
|
+
return;
|
|
17328
|
+
try {
|
|
17329
|
+
return parseState(readFileSync3(path, "utf8"));
|
|
17330
|
+
} catch {
|
|
17331
|
+
return;
|
|
17332
|
+
}
|
|
17333
|
+
}
|
|
17334
|
+
function listSupervisorStates() {
|
|
17335
|
+
const dir = supervisorDir();
|
|
17336
|
+
if (!existsSync5(dir))
|
|
17337
|
+
return [];
|
|
17338
|
+
return readdirSync(dir).filter((name) => name.endsWith(STATE_SUFFIX)).map((name) => basename(name, STATE_SUFFIX)).map((toolId) => readSupervisorState(toolId)).filter((state) => state !== undefined);
|
|
17339
|
+
}
|
|
17340
|
+
async function sendSupervisorRequest(toolId, request, opts = {}) {
|
|
17341
|
+
const timeoutMs = opts.timeoutMs ?? 1500;
|
|
17342
|
+
const socketPath = supervisorSocketPath(toolId);
|
|
17343
|
+
return await new Promise((resolve2, reject) => {
|
|
17344
|
+
const socket = createConnection(socketPath);
|
|
17345
|
+
let buffer = "";
|
|
17346
|
+
let settled = false;
|
|
17347
|
+
const finish = (value) => {
|
|
17348
|
+
if (settled)
|
|
17349
|
+
return;
|
|
17350
|
+
settled = true;
|
|
17351
|
+
clearTimeout(timer);
|
|
17352
|
+
socket.destroy();
|
|
17353
|
+
resolve2(value);
|
|
17354
|
+
};
|
|
17355
|
+
const fail = (err) => {
|
|
17356
|
+
if (settled)
|
|
17357
|
+
return;
|
|
17358
|
+
settled = true;
|
|
17359
|
+
clearTimeout(timer);
|
|
17360
|
+
socket.destroy();
|
|
17361
|
+
if (opts.allowMissing && (err.code === "ENOENT" || err.code === "ECONNREFUSED")) {
|
|
17362
|
+
resolve2(undefined);
|
|
17363
|
+
} else {
|
|
17364
|
+
reject(new AccountsError(`could not contact accounts supervisor for ${toolId}: ${err.message}`));
|
|
17365
|
+
}
|
|
17366
|
+
};
|
|
17367
|
+
const timer = setTimeout(() => {
|
|
17368
|
+
fail(Object.assign(new Error(`timed out after ${timeoutMs}ms`), { code: "ETIMEDOUT" }));
|
|
17369
|
+
}, timeoutMs);
|
|
17370
|
+
socket.setEncoding("utf8");
|
|
17371
|
+
socket.once("connect", () => {
|
|
17372
|
+
socket.write(JSON.stringify(request) + `
|
|
17373
|
+
`);
|
|
17374
|
+
});
|
|
17375
|
+
socket.once("error", fail);
|
|
17376
|
+
socket.on("data", (chunk) => {
|
|
17377
|
+
buffer += chunk;
|
|
17378
|
+
const newline = buffer.indexOf(`
|
|
17379
|
+
`);
|
|
17380
|
+
if (newline === -1)
|
|
17381
|
+
return;
|
|
17382
|
+
try {
|
|
17383
|
+
finish(JSON.parse(buffer.slice(0, newline)));
|
|
17384
|
+
} catch (err) {
|
|
17385
|
+
fail(err);
|
|
17386
|
+
}
|
|
17387
|
+
});
|
|
17388
|
+
socket.once("end", () => {
|
|
17389
|
+
if (!settled)
|
|
17390
|
+
fail(new Error("connection closed without a response"));
|
|
17391
|
+
});
|
|
17392
|
+
});
|
|
17393
|
+
}
|
|
17394
|
+
|
|
17298
17395
|
// src/mcp.ts
|
|
17299
17396
|
function ok(data) {
|
|
17300
17397
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
@@ -17302,7 +17399,7 @@ function ok(data) {
|
|
|
17302
17399
|
function fail(message) {
|
|
17303
17400
|
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
|
|
17304
17401
|
}
|
|
17305
|
-
var server = new Server({ name: "accounts", version: "0.1.
|
|
17402
|
+
var server = new Server({ name: "accounts", version: "0.1.5" }, { capabilities: { tools: {} } });
|
|
17306
17403
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
17307
17404
|
tools: [
|
|
17308
17405
|
{
|
|
@@ -17320,9 +17417,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
17320
17417
|
description: "Show active and live/applied profile for a tool.",
|
|
17321
17418
|
inputSchema: { type: "object", properties: { tool: { type: "string" } }, required: ["tool"] }
|
|
17322
17419
|
},
|
|
17420
|
+
{
|
|
17421
|
+
name: "supervisor_status",
|
|
17422
|
+
description: "Show accounts-run supervisors that can restart an agent process after profile switches.",
|
|
17423
|
+
inputSchema: { type: "object", properties: { tool: { type: "string" } } }
|
|
17424
|
+
},
|
|
17323
17425
|
{
|
|
17324
17426
|
name: "switch_profile",
|
|
17325
|
-
description: "Switch to a profile.
|
|
17427
|
+
description: "Switch to a profile. If the current agent was started with accounts run, the supervisor restarts it under the new profile; otherwise this returns a restart/resume handoff command.",
|
|
17326
17428
|
inputSchema: {
|
|
17327
17429
|
type: "object",
|
|
17328
17430
|
properties: {
|
|
@@ -17351,17 +17453,43 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
17351
17453
|
return fail("tool is required");
|
|
17352
17454
|
return ok({ tool, active: currentProfile(tool) ?? null, applied: appliedProfile(tool) ?? null });
|
|
17353
17455
|
}
|
|
17456
|
+
case "supervisor_status": {
|
|
17457
|
+
const tool = args["tool"];
|
|
17458
|
+
const states = typeof tool === "string" ? listSupervisorStates().filter((state) => state.tool === tool) : listSupervisorStates();
|
|
17459
|
+
return ok({ supervisors: states });
|
|
17460
|
+
}
|
|
17354
17461
|
case "switch_profile": {
|
|
17355
17462
|
const name = args["name"];
|
|
17356
17463
|
if (typeof name !== "string")
|
|
17357
17464
|
return fail("name is required");
|
|
17465
|
+
const profile = getProfile(name, typeof args["tool"] === "string" ? args["tool"] : undefined);
|
|
17466
|
+
const resume = args["resume"] !== false;
|
|
17467
|
+
const switchArgs = Array.isArray(args["args"]) ? args["args"].filter((value) => typeof value === "string") : undefined;
|
|
17468
|
+
const supervisor = await sendSupervisorRequest(profile.tool, {
|
|
17469
|
+
type: "switch_profile",
|
|
17470
|
+
name: profile.name,
|
|
17471
|
+
tool: profile.tool,
|
|
17472
|
+
mode: typeof args["mode"] === "string" ? args["mode"] : "auto",
|
|
17473
|
+
resume,
|
|
17474
|
+
args: switchArgs
|
|
17475
|
+
}, { allowMissing: true });
|
|
17476
|
+
if (supervisor) {
|
|
17477
|
+
if (!supervisor.ok)
|
|
17478
|
+
return fail(supervisor.error);
|
|
17479
|
+
return ok({
|
|
17480
|
+
supervised: true,
|
|
17481
|
+
...supervisor,
|
|
17482
|
+
instruction: "Profile switch queued. The accounts supervisor will close this agent process and restart it under the selected profile."
|
|
17483
|
+
});
|
|
17484
|
+
}
|
|
17358
17485
|
const result = switchProfile(name, {
|
|
17359
|
-
tool:
|
|
17486
|
+
tool: profile.tool,
|
|
17360
17487
|
mode: typeof args["mode"] === "string" ? args["mode"] : "auto",
|
|
17361
|
-
resume
|
|
17362
|
-
args:
|
|
17488
|
+
resume,
|
|
17489
|
+
args: switchArgs
|
|
17363
17490
|
});
|
|
17364
17491
|
return ok({
|
|
17492
|
+
supervised: false,
|
|
17365
17493
|
...result,
|
|
17366
17494
|
instruction: result.restartRequired ? "Exit the current agent session and run commandLine to resume under the selected profile." : "Profile switched."
|
|
17367
17495
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/accounts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Manage and switch between multiple Claude Code (and other AI coding tool) profiles/accounts locally — isolated config dirs, per-account email, one-command switching.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|