askshepherd 0.1.38 → 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/README.md +6 -4
- package/bin/shepherd-onboard.js +187 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,7 +75,8 @@ above; it performs the bounded local checks.
|
|
|
75
75
|
|
|
76
76
|
## Set Up Coding Agent Sessions
|
|
77
77
|
|
|
78
|
-
Use this when the user asks "Help me set up coding agent sessions"
|
|
78
|
+
Use this when the user asks "Help me set up coding agent sessions" or "Enable
|
|
79
|
+
coding agent sessions locally for Shepherd":
|
|
79
80
|
|
|
80
81
|
```sh
|
|
81
82
|
npx -y askshepherd@latest agent --login
|
|
@@ -120,9 +121,10 @@ The saved MCP state includes:
|
|
|
120
121
|
The installed MCP server is local npm first, remote brain second. For questions
|
|
121
122
|
like "what do I have set up on Shepherd?", "is Shepherd syncing?", or "help me
|
|
122
123
|
set up coding agent sessions", the MCP exposes local tools such as
|
|
123
|
-
`shepherd_status
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
`shepherd_status`, `shepherd_setup_coding_sessions`, and
|
|
125
|
+
`shepherd_enable_coding_sessions` that route agents to the local
|
|
126
|
+
`askshepherd status` / add-source flow. Production memory and wiki tools remain
|
|
127
|
+
remote Railway-backed tools for source recall and company-memory answers.
|
|
126
128
|
Those local MCP tools are also the permission boundary: an MCP client should not
|
|
127
129
|
use shell or file tools to inspect the user's folders or repositories for setup.
|
|
128
130
|
|
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") {
|
|
@@ -822,13 +828,20 @@ function localMcpTools() {
|
|
|
822
828
|
annotations: readOnlyAnnotations,
|
|
823
829
|
_meta: { provider: "local_npm", command: `${agentCommand()} agent --add-sources coding-sessions` },
|
|
824
830
|
},
|
|
831
|
+
{
|
|
832
|
+
name: "shepherd_enable_coding_sessions",
|
|
833
|
+
description: "Alias for shepherd_setup_coding_sessions. Use when the user asks to enable coding agent sessions locally for Shepherd. Return the local askshepherd npm setup commands; do not search, list, or read the user's folders or repos.",
|
|
834
|
+
inputSchema: emptyInputSchema,
|
|
835
|
+
annotations: readOnlyAnnotations,
|
|
836
|
+
_meta: { provider: "local_npm", command: `${agentCommand()} agent --add-sources coding-sessions` },
|
|
837
|
+
},
|
|
825
838
|
];
|
|
826
839
|
}
|
|
827
840
|
|
|
828
841
|
function localMcpInstructions(remoteInstructions, remoteConnectError) {
|
|
829
842
|
return [
|
|
830
843
|
"This MCP server is the local askshepherd npm wrapper plus production Shepherd memory/wiki tools.",
|
|
831
|
-
`For local setup/sync questions like "what do I have set up on Shepherd", "what have I enabled", "is Shepherd syncing", "help me set up coding agent sessions", or "enable coding sessions", use shepherd_status or
|
|
844
|
+
`For local setup/sync questions like "what do I have set up on Shepherd", "what have I enabled", "is Shepherd syncing", "help me set up coding agent sessions", "enable coding sessions", or "enable coding agent sessions locally for Shepherd", use shepherd_status, shepherd_setup_coding_sessions, or shepherd_enable_coding_sessions first. These local tools route to the local askshepherd npm status/setup flow. The askshepherd CLI is the only component that may perform bounded local checks of Shepherd state, LaunchAgents, and known Codex/Claude session locations.`,
|
|
832
845
|
"Hard boundary: do not use shell or filesystem tools such as ls, find, rg, grep, cat, Read, Glob, or Explore to inspect the user's home directory, repositories, ~/.codex, ~/.claude, or ~/.shepherd for Shepherd setup. If local status is needed, call shepherd_status or run the exact askshepherd status command.",
|
|
833
846
|
`If the user asks for raw local status outside MCP, tell them to run ${agentCommand()} status. For setup of coding agent sessions, ask consent, then use ${agentCommand()} agent --login if needed, ${agentCommand()} agent --add-sources coding-sessions --name "<full_name>" --org "<organization>", ${agentCommand()} agent --continue, then ${agentCommand()} status.`,
|
|
834
847
|
"Use production memory/wiki tools only for company-memory questions, source recall, wiki lookup, messages/meetings retrieval, or coding-session work history that has already synced to Shepherd.",
|
|
@@ -849,7 +862,7 @@ async function callLocalMcpTool(name) {
|
|
|
849
862
|
].join("\n\n"));
|
|
850
863
|
}
|
|
851
864
|
|
|
852
|
-
if (name === "shepherd_setup_coding_sessions") {
|
|
865
|
+
if (name === "shepherd_setup_coding_sessions" || name === "shepherd_enable_coding_sessions") {
|
|
853
866
|
const status = await collectShepherdStatus();
|
|
854
867
|
return localMcpTextResult(renderCodingSessionsSetupMcpResult(status));
|
|
855
868
|
}
|
|
@@ -916,9 +929,18 @@ async function writeMcpState(state) {
|
|
|
916
929
|
const path = mcpStatePath();
|
|
917
930
|
await mkdir(dirname(path), { recursive: true });
|
|
918
931
|
await writeFile(path, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
932
|
+
await chmod(path, 0o600);
|
|
919
933
|
return path;
|
|
920
934
|
}
|
|
921
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
|
+
|
|
922
944
|
function mcpStatePath() {
|
|
923
945
|
return expandHomePath(stringArg("state") ?? DEFAULT_MCP_STATE_PATH);
|
|
924
946
|
}
|
|
@@ -1338,6 +1360,96 @@ function renderLocalCodingSessionsStatus(status) {
|
|
|
1338
1360
|
return lines;
|
|
1339
1361
|
}
|
|
1340
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
|
+
|
|
1341
1453
|
async function runMessagesChatsCommand() {
|
|
1342
1454
|
await ensureMessagesReadPermission({ noOpen: Boolean(args["no-open"]) });
|
|
1343
1455
|
const chats = await listRecentMessageChats({
|
|
@@ -1752,6 +1864,31 @@ Options:
|
|
|
1752
1864
|
return;
|
|
1753
1865
|
}
|
|
1754
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
|
+
|
|
1755
1892
|
if (which === "mcp") {
|
|
1756
1893
|
console.log(`Shepherd MCP stdio proxy
|
|
1757
1894
|
|
|
@@ -2142,6 +2279,9 @@ async function writeAgentState(state) {
|
|
|
2142
2279
|
const path = agentStatePath();
|
|
2143
2280
|
await mkdir(dirname(path), { recursive: true });
|
|
2144
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);
|
|
2145
2285
|
return path;
|
|
2146
2286
|
}
|
|
2147
2287
|
|
|
@@ -2653,7 +2793,11 @@ function headers(token) {
|
|
|
2653
2793
|
async function writeMessagesConfig(input) {
|
|
2654
2794
|
const dir = join(homedir(), ".shepherd", "raw-messages");
|
|
2655
2795
|
await mkdir(dir, { recursive: true });
|
|
2656
|
-
|
|
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`);
|
|
2657
2801
|
const allowedChatIds = parseAllowedChatIds(input.allowedChatIds);
|
|
2658
2802
|
const allChats = selectedChatIdsIncludeAll(allowedChatIds);
|
|
2659
2803
|
if (!allChats && allowedChatIds.length === 0) {
|
|
@@ -2674,25 +2818,34 @@ async function writeMessagesConfig(input) {
|
|
|
2674
2818
|
}, null, 2),
|
|
2675
2819
|
{ mode: 0o600 },
|
|
2676
2820
|
);
|
|
2821
|
+
await chmod(path, 0o600);
|
|
2677
2822
|
return path;
|
|
2678
2823
|
}
|
|
2679
2824
|
|
|
2680
|
-
|
|
2681
|
-
if (platform() !== "darwin") {
|
|
2682
|
-
throw new Error("automatic local Messages sync is only supported on macOS");
|
|
2683
|
-
}
|
|
2684
|
-
|
|
2825
|
+
function buildMessagesAgentInstall(configPath, userId, overrides = {}) {
|
|
2685
2826
|
const safeId = userId.replace(/[^a-zA-Z0-9.-]/g, "-");
|
|
2686
2827
|
const label = `ai.shepherd.raw-messages.${safeId}`;
|
|
2687
2828
|
const rawDir = join(homedir(), ".shepherd", "raw-messages");
|
|
2688
2829
|
const agentsDir = join(homedir(), "Library", "LaunchAgents");
|
|
2689
|
-
await mkdir(rawDir, { recursive: true });
|
|
2690
|
-
await mkdir(agentsDir, { recursive: true });
|
|
2691
|
-
|
|
2692
2830
|
const plistPath = join(agentsDir, `${label}.plist`);
|
|
2693
2831
|
const stdoutPath = join(rawDir, `${safeId}.out.log`);
|
|
2694
2832
|
const stderrPath = join(rawDir, `${safeId}.err.log`);
|
|
2695
|
-
|
|
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");
|
|
2696
2849
|
|
|
2697
2850
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2698
2851
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -2702,32 +2855,43 @@ async function installMessagesAgent(configPath, userId) {
|
|
|
2702
2855
|
<string>${xmlEscape(label)}</string>
|
|
2703
2856
|
<key>ProgramArguments</key>
|
|
2704
2857
|
<array>
|
|
2705
|
-
|
|
2706
|
-
<string>npx</string>
|
|
2707
|
-
<string>-y</string>
|
|
2708
|
-
<string>${PACKAGE_SPEC}</string>
|
|
2709
|
-
<string>messages-agent</string>
|
|
2710
|
-
<string>--config</string>
|
|
2711
|
-
<string>${xmlEscape(configPath)}</string>
|
|
2858
|
+
${programArgumentsXml}
|
|
2712
2859
|
</array>
|
|
2713
2860
|
<key>KeepAlive</key>
|
|
2714
2861
|
<true/>
|
|
2715
2862
|
<key>RunAtLoad</key>
|
|
2716
2863
|
<true/>
|
|
2864
|
+
<key>ThrottleInterval</key>
|
|
2865
|
+
<integer>10</integer>
|
|
2866
|
+
<key>WorkingDirectory</key>
|
|
2867
|
+
<string>${xmlEscape(rawDir)}</string>
|
|
2717
2868
|
<key>StandardOutPath</key>
|
|
2718
2869
|
<string>${xmlEscape(stdoutPath)}</string>
|
|
2719
2870
|
<key>StandardErrorPath</key>
|
|
2720
2871
|
<string>${xmlEscape(stderrPath)}</string>
|
|
2721
2872
|
<key>EnvironmentVariables</key>
|
|
2722
2873
|
<dict>
|
|
2723
|
-
|
|
2724
|
-
<string>${xmlEscape(launchPath)}</string>
|
|
2874
|
+
${environmentXml}
|
|
2725
2875
|
</dict>
|
|
2726
2876
|
</dict>
|
|
2727
2877
|
</plist>
|
|
2728
2878
|
`;
|
|
2729
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
|
+
|
|
2730
2893
|
await writeFile(plistPath, plist, { mode: 0o600 });
|
|
2894
|
+
await chmod(plistPath, 0o600);
|
|
2731
2895
|
while (true) {
|
|
2732
2896
|
const stdoutOffset = await fileLength(stdoutPath);
|
|
2733
2897
|
const stderrOffset = await fileLength(stderrPath);
|