askshepherd 0.1.39 → 0.1.40
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/bin/shepherd-onboard.js +178 -21
- package/package.json +1 -1
package/bin/shepherd-onboard.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { constants as fsConstants, existsSync, mkdirSync, readFileSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
5
|
-
import { access, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { access, chmod, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
6
6
|
import { createServer } from "node:http";
|
|
7
7
|
import { homedir, platform } from "node:os";
|
|
8
8
|
import { basename, dirname, join } from "node:path";
|
|
@@ -12,7 +12,7 @@ import { fileURLToPath } from "node:url";
|
|
|
12
12
|
const DEFAULT_API_URL = "https://brain-api-customer-facing.up.railway.app";
|
|
13
13
|
const PACKAGE_NAME = "askshepherd";
|
|
14
14
|
const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
|
|
15
|
-
const PACKAGE_VERSION = "0.1.
|
|
15
|
+
const PACKAGE_VERSION = "0.1.40";
|
|
16
16
|
const MCP_SERVER_NAME = "shepherd";
|
|
17
17
|
const PACKAGE_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
18
18
|
const DEFAULT_AGENT_STATE_PATH = join(homedir(), ".shepherd", "raw-onboarding-agent.json");
|
|
@@ -133,6 +133,12 @@ async function dispatch() {
|
|
|
133
133
|
await runMessagesChatsCommand();
|
|
134
134
|
} else if (command === "messages-agent") {
|
|
135
135
|
await runMessagesAgent();
|
|
136
|
+
} else if (command === "write-agent-state") {
|
|
137
|
+
await runWriteAgentState();
|
|
138
|
+
} else if (command === "write-messages-config") {
|
|
139
|
+
await runWriteMessagesConfig();
|
|
140
|
+
} else if (command === "install-messages-agent") {
|
|
141
|
+
await runInstallMessagesAgent();
|
|
136
142
|
} else if (command === "coding-sessions-agent") {
|
|
137
143
|
await runCodingSessionsAgent();
|
|
138
144
|
} else if (command === "coding-sessions-status") {
|
|
@@ -923,9 +929,18 @@ async function writeMcpState(state) {
|
|
|
923
929
|
const path = mcpStatePath();
|
|
924
930
|
await mkdir(dirname(path), { recursive: true });
|
|
925
931
|
await writeFile(path, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
932
|
+
await chmod(path, 0o600);
|
|
926
933
|
return path;
|
|
927
934
|
}
|
|
928
935
|
|
|
936
|
+
function sanitizeUserFileId(userId) {
|
|
937
|
+
const safeId = String(userId ?? "").replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
938
|
+
if (!safeId || safeId === "." || safeId === ".." || /^\.+$/.test(safeId)) {
|
|
939
|
+
throw new Error("Onboarding returned an invalid user ID for local Messages config.");
|
|
940
|
+
}
|
|
941
|
+
return safeId;
|
|
942
|
+
}
|
|
943
|
+
|
|
929
944
|
function mcpStatePath() {
|
|
930
945
|
return expandHomePath(stringArg("state") ?? DEFAULT_MCP_STATE_PATH);
|
|
931
946
|
}
|
|
@@ -1345,6 +1360,96 @@ function renderLocalCodingSessionsStatus(status) {
|
|
|
1345
1360
|
return lines;
|
|
1346
1361
|
}
|
|
1347
1362
|
|
|
1363
|
+
async function runWriteAgentState() {
|
|
1364
|
+
const input = await readJsonInput();
|
|
1365
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
1366
|
+
throw new Error("write-agent-state expects a JSON object on stdin.");
|
|
1367
|
+
}
|
|
1368
|
+
const previous = await readOptionalAgentState();
|
|
1369
|
+
const statePath = await writeAgentState({ ...(previous ?? {}), ...input });
|
|
1370
|
+
console.log(JSON.stringify({ statePath }, null, 2));
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
async function runWriteMessagesConfig() {
|
|
1374
|
+
const input = await readJsonInput();
|
|
1375
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
1376
|
+
throw new Error("write-messages-config expects a JSON object on stdin.");
|
|
1377
|
+
}
|
|
1378
|
+
const configPath = await writeMessagesConfig({
|
|
1379
|
+
apiUrl: trimTrailingSlash(requiredConfigString(input.apiUrl, "apiUrl")),
|
|
1380
|
+
userId: requiredConfigString(input.userId, "userId"),
|
|
1381
|
+
agentToken: requiredConfigString(input.agentToken, "agentToken"),
|
|
1382
|
+
backfillDays: parseBackfillDays(input.backfillDays, null),
|
|
1383
|
+
allowedChatIds: input.allowedChatIds,
|
|
1384
|
+
selectedChats: Array.isArray(input.selectedChats) ? input.selectedChats : [],
|
|
1385
|
+
});
|
|
1386
|
+
console.log(JSON.stringify({ configPath }, null, 2));
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
async function runInstallMessagesAgent() {
|
|
1390
|
+
const configPath = stringArg("config");
|
|
1391
|
+
if (!configPath) throw new Error("install-messages-agent requires --config <path>");
|
|
1392
|
+
let config;
|
|
1393
|
+
try {
|
|
1394
|
+
config = JSON.parse(await readFile(configPath, "utf8"));
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
if (err && typeof err === "object" && "code" in err) throw err;
|
|
1397
|
+
throw new Error(`install-messages-agent: config file at ${configPath} does not contain valid JSON.`);
|
|
1398
|
+
}
|
|
1399
|
+
const userId = stringArg("user-id") ?? requiredConfigString(config.userId, "userId");
|
|
1400
|
+
const overrides = {
|
|
1401
|
+
programArguments: parseJsonArrayArg("program"),
|
|
1402
|
+
environment: parseJsonObjectArg("env"),
|
|
1403
|
+
};
|
|
1404
|
+
|
|
1405
|
+
if (args["dry-run"]) {
|
|
1406
|
+
console.log(JSON.stringify(buildMessagesAgentInstall(configPath, userId, overrides), null, 2));
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
const install = await installMessagesAgent(configPath, userId, overrides);
|
|
1411
|
+
console.log(JSON.stringify(install, null, 2));
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
async function readJsonInput() {
|
|
1415
|
+
const chunks = [];
|
|
1416
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1417
|
+
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
1418
|
+
if (!text) throw new Error("Expected JSON input on stdin.");
|
|
1419
|
+
return JSON.parse(text);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function parseJsonArrayArg(name) {
|
|
1423
|
+
const raw = stringArg(name);
|
|
1424
|
+
if (!raw) return undefined;
|
|
1425
|
+
let parsed;
|
|
1426
|
+
try {
|
|
1427
|
+
parsed = JSON.parse(raw);
|
|
1428
|
+
} catch {
|
|
1429
|
+
throw new Error(`--${name} must be a JSON array of strings.`);
|
|
1430
|
+
}
|
|
1431
|
+
if (!Array.isArray(parsed) || parsed.some((value) => typeof value !== "string")) {
|
|
1432
|
+
throw new Error(`--${name} must be a JSON array of strings.`);
|
|
1433
|
+
}
|
|
1434
|
+
return parsed;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function parseJsonObjectArg(name) {
|
|
1438
|
+
const raw = stringArg(name);
|
|
1439
|
+
if (!raw) return undefined;
|
|
1440
|
+
let parsed;
|
|
1441
|
+
try {
|
|
1442
|
+
parsed = JSON.parse(raw);
|
|
1443
|
+
} catch {
|
|
1444
|
+
throw new Error(`--${name} must be a JSON object of string values.`);
|
|
1445
|
+
}
|
|
1446
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)
|
|
1447
|
+
|| Object.values(parsed).some((value) => typeof value !== "string")) {
|
|
1448
|
+
throw new Error(`--${name} must be a JSON object of string values.`);
|
|
1449
|
+
}
|
|
1450
|
+
return parsed;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1348
1453
|
async function runMessagesChatsCommand() {
|
|
1349
1454
|
await ensureMessagesReadPermission({ noOpen: Boolean(args["no-open"]) });
|
|
1350
1455
|
const chats = await listRecentMessageChats({
|
|
@@ -1759,6 +1864,31 @@ Options:
|
|
|
1759
1864
|
return;
|
|
1760
1865
|
}
|
|
1761
1866
|
|
|
1867
|
+
if (which === "write-agent-state" || which === "write-messages-config" || which === "install-messages-agent") {
|
|
1868
|
+
console.log(`Shepherd onboarding engine commands
|
|
1869
|
+
|
|
1870
|
+
These non-interactive commands are used by GUI onboarding apps (for example the
|
|
1871
|
+
Shepherd macOS app) so that this CLI stays the single owner of Shepherd state
|
|
1872
|
+
file schemas and the launchd install flow.
|
|
1873
|
+
|
|
1874
|
+
Usage:
|
|
1875
|
+
shepherd-onboard write-agent-state JSON object on stdin is merged into ~/.shepherd/raw-onboarding-agent.json. Prints {statePath}.
|
|
1876
|
+
shepherd-onboard write-messages-config JSON object on stdin ({apiUrl, userId, agentToken, backfillDays?, allowedChatIds, selectedChats?}) is written to ~/.shepherd/raw-messages/<userId>.json. Prints {configPath}.
|
|
1877
|
+
shepherd-onboard install-messages-agent --config <path>
|
|
1878
|
+
Installs and verifies the Messages launchd agent for an existing config. Prints install metadata.
|
|
1879
|
+
|
|
1880
|
+
install-messages-agent options:
|
|
1881
|
+
--config <path> Messages agent config created by onboarding. Required.
|
|
1882
|
+
--user-id <id> Override the user ID. Defaults to the config's userId.
|
|
1883
|
+
--program <json_array> Replace the default npx launcher with custom ProgramArguments (e.g. a signed app binary). --config <path> is appended.
|
|
1884
|
+
--env <json_object> Extra EnvironmentVariables merged into the launchd plist (e.g. ELECTRON_RUN_AS_NODE).
|
|
1885
|
+
--dry-run Print the launchd plist and paths without writing or loading anything.
|
|
1886
|
+
--no-permission-prompt Fail instead of prompting when Full Disk Access is missing.
|
|
1887
|
+
--help Show this help.
|
|
1888
|
+
`);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1762
1892
|
if (which === "mcp") {
|
|
1763
1893
|
console.log(`Shepherd MCP stdio proxy
|
|
1764
1894
|
|
|
@@ -2149,6 +2279,9 @@ async function writeAgentState(state) {
|
|
|
2149
2279
|
const path = agentStatePath();
|
|
2150
2280
|
await mkdir(dirname(path), { recursive: true });
|
|
2151
2281
|
await writeFile(path, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
2282
|
+
// writeFile's mode only applies on creation; an existing file keeps its
|
|
2283
|
+
// permissions, so enforce them on every token write.
|
|
2284
|
+
await chmod(path, 0o600);
|
|
2152
2285
|
return path;
|
|
2153
2286
|
}
|
|
2154
2287
|
|
|
@@ -2660,7 +2793,11 @@ function headers(token) {
|
|
|
2660
2793
|
async function writeMessagesConfig(input) {
|
|
2661
2794
|
const dir = join(homedir(), ".shepherd", "raw-messages");
|
|
2662
2795
|
await mkdir(dir, { recursive: true });
|
|
2663
|
-
|
|
2796
|
+
// The userId reaches this path from the network (server-issued session id)
|
|
2797
|
+
// and, via write-messages-config, from stdin; sanitize it like the launchd
|
|
2798
|
+
// label does so it can never traverse outside the raw-messages directory.
|
|
2799
|
+
const safeId = sanitizeUserFileId(input.userId);
|
|
2800
|
+
const path = join(dir, `${safeId}.json`);
|
|
2664
2801
|
const allowedChatIds = parseAllowedChatIds(input.allowedChatIds);
|
|
2665
2802
|
const allChats = selectedChatIdsIncludeAll(allowedChatIds);
|
|
2666
2803
|
if (!allChats && allowedChatIds.length === 0) {
|
|
@@ -2681,25 +2818,34 @@ async function writeMessagesConfig(input) {
|
|
|
2681
2818
|
}, null, 2),
|
|
2682
2819
|
{ mode: 0o600 },
|
|
2683
2820
|
);
|
|
2821
|
+
await chmod(path, 0o600);
|
|
2684
2822
|
return path;
|
|
2685
2823
|
}
|
|
2686
2824
|
|
|
2687
|
-
|
|
2688
|
-
if (platform() !== "darwin") {
|
|
2689
|
-
throw new Error("automatic local Messages sync is only supported on macOS");
|
|
2690
|
-
}
|
|
2691
|
-
|
|
2825
|
+
function buildMessagesAgentInstall(configPath, userId, overrides = {}) {
|
|
2692
2826
|
const safeId = userId.replace(/[^a-zA-Z0-9.-]/g, "-");
|
|
2693
2827
|
const label = `ai.shepherd.raw-messages.${safeId}`;
|
|
2694
2828
|
const rawDir = join(homedir(), ".shepherd", "raw-messages");
|
|
2695
2829
|
const agentsDir = join(homedir(), "Library", "LaunchAgents");
|
|
2696
|
-
await mkdir(rawDir, { recursive: true });
|
|
2697
|
-
await mkdir(agentsDir, { recursive: true });
|
|
2698
|
-
|
|
2699
2830
|
const plistPath = join(agentsDir, `${label}.plist`);
|
|
2700
2831
|
const stdoutPath = join(rawDir, `${safeId}.out.log`);
|
|
2701
2832
|
const stderrPath = join(rawDir, `${safeId}.err.log`);
|
|
2702
|
-
|
|
2833
|
+
|
|
2834
|
+
const programPrefix = Array.isArray(overrides.programArguments) && overrides.programArguments.length > 0
|
|
2835
|
+
? overrides.programArguments
|
|
2836
|
+
: ["/usr/bin/env", "npx", "-y", PACKAGE_SPEC, "messages-agent"];
|
|
2837
|
+
const programArguments = [...programPrefix, "--config", configPath];
|
|
2838
|
+
const environment = {
|
|
2839
|
+
PATH: launchAgentPath(),
|
|
2840
|
+
...stringRecord(overrides.environment),
|
|
2841
|
+
};
|
|
2842
|
+
|
|
2843
|
+
const programArgumentsXml = programArguments
|
|
2844
|
+
.map((value) => ` <string>${xmlEscape(value)}</string>`)
|
|
2845
|
+
.join("\n");
|
|
2846
|
+
const environmentXml = Object.entries(environment)
|
|
2847
|
+
.map(([key, value]) => ` <key>${xmlEscape(key)}</key>\n <string>${xmlEscape(value)}</string>`)
|
|
2848
|
+
.join("\n");
|
|
2703
2849
|
|
|
2704
2850
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2705
2851
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -2709,32 +2855,43 @@ async function installMessagesAgent(configPath, userId) {
|
|
|
2709
2855
|
<string>${xmlEscape(label)}</string>
|
|
2710
2856
|
<key>ProgramArguments</key>
|
|
2711
2857
|
<array>
|
|
2712
|
-
|
|
2713
|
-
<string>npx</string>
|
|
2714
|
-
<string>-y</string>
|
|
2715
|
-
<string>${PACKAGE_SPEC}</string>
|
|
2716
|
-
<string>messages-agent</string>
|
|
2717
|
-
<string>--config</string>
|
|
2718
|
-
<string>${xmlEscape(configPath)}</string>
|
|
2858
|
+
${programArgumentsXml}
|
|
2719
2859
|
</array>
|
|
2720
2860
|
<key>KeepAlive</key>
|
|
2721
2861
|
<true/>
|
|
2722
2862
|
<key>RunAtLoad</key>
|
|
2723
2863
|
<true/>
|
|
2864
|
+
<key>ThrottleInterval</key>
|
|
2865
|
+
<integer>10</integer>
|
|
2866
|
+
<key>WorkingDirectory</key>
|
|
2867
|
+
<string>${xmlEscape(rawDir)}</string>
|
|
2724
2868
|
<key>StandardOutPath</key>
|
|
2725
2869
|
<string>${xmlEscape(stdoutPath)}</string>
|
|
2726
2870
|
<key>StandardErrorPath</key>
|
|
2727
2871
|
<string>${xmlEscape(stderrPath)}</string>
|
|
2728
2872
|
<key>EnvironmentVariables</key>
|
|
2729
2873
|
<dict>
|
|
2730
|
-
|
|
2731
|
-
<string>${xmlEscape(launchPath)}</string>
|
|
2874
|
+
${environmentXml}
|
|
2732
2875
|
</dict>
|
|
2733
2876
|
</dict>
|
|
2734
2877
|
</plist>
|
|
2735
2878
|
`;
|
|
2736
2879
|
|
|
2880
|
+
return { label, rawDir, agentsDir, plistPath, stdoutPath, stderrPath, programArguments, environment, plist };
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
async function installMessagesAgent(configPath, userId, overrides = {}) {
|
|
2884
|
+
if (platform() !== "darwin") {
|
|
2885
|
+
throw new Error("automatic local Messages sync is only supported on macOS");
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
const install = buildMessagesAgentInstall(configPath, userId, overrides);
|
|
2889
|
+
const { label, rawDir, agentsDir, plistPath, stdoutPath, stderrPath, plist } = install;
|
|
2890
|
+
await mkdir(rawDir, { recursive: true });
|
|
2891
|
+
await mkdir(agentsDir, { recursive: true });
|
|
2892
|
+
|
|
2737
2893
|
await writeFile(plistPath, plist, { mode: 0o600 });
|
|
2894
|
+
await chmod(plistPath, 0o600);
|
|
2738
2895
|
while (true) {
|
|
2739
2896
|
const stdoutOffset = await fileLength(stdoutPath);
|
|
2740
2897
|
const stderrOffset = await fileLength(stderrPath);
|