askshepherd 0.1.36 → 0.1.38
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 +44 -0
- package/bin/shepherd-onboard.js +504 -77
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,41 @@ The command:
|
|
|
54
54
|
|
|
55
55
|
The command does not expose Railway, database, Redis, or internal service details to the user.
|
|
56
56
|
|
|
57
|
+
## Check Sync Status
|
|
58
|
+
|
|
59
|
+
Use this when the user asks "Check what I've enabled for Shepherd?":
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npx -y askshepherd@latest status
|
|
63
|
+
npx -y askshepherd@latest status --json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It reports the saved Shepherd account, connected cloud sources, downstream
|
|
67
|
+
processing state, and local background sync health for Messages and Coding
|
|
68
|
+
Sessions.
|
|
69
|
+
|
|
70
|
+
Agents must not inspect the user's folders or repositories to answer setup
|
|
71
|
+
status. Do not run `ls`, `find`, `rg`, `grep`, `cat`, `Read`, `Glob`, or
|
|
72
|
+
`Explore` against the user's home directory, repositories, `~/.codex`,
|
|
73
|
+
`~/.claude`, or `~/.shepherd` for Shepherd setup. Use the npm status command
|
|
74
|
+
above; it performs the bounded local checks.
|
|
75
|
+
|
|
76
|
+
## Set Up Coding Agent Sessions
|
|
77
|
+
|
|
78
|
+
Use this when the user asks "Help me set up coding agent sessions":
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
npx -y askshepherd@latest agent --login
|
|
82
|
+
npx -y askshepherd@latest agent --add-sources coding-sessions --name "<name>" --org "<organization>"
|
|
83
|
+
npx -y askshepherd@latest agent --continue
|
|
84
|
+
npx -y askshepherd@latest status
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The coding agent should ask for consent before enabling this source. The local
|
|
88
|
+
collector syncs redacted Codex and Claude Code summaries, not full transcripts.
|
|
89
|
+
The agent should run only the commands above and should not search the local
|
|
90
|
+
filesystem for a Shepherd agent implementation.
|
|
91
|
+
|
|
57
92
|
## Customer MCP Login
|
|
58
93
|
|
|
59
94
|
After raw onboarding creates the Shepherd customer account, customers can
|
|
@@ -82,6 +117,15 @@ The saved MCP state includes:
|
|
|
82
117
|
- `authSource`: `local_onboarding` or `workos`
|
|
83
118
|
- `localAuth`: raw onboarding state reference when local auth was used
|
|
84
119
|
|
|
120
|
+
The installed MCP server is local npm first, remote brain second. For questions
|
|
121
|
+
like "what do I have set up on Shepherd?", "is Shepherd syncing?", or "help me
|
|
122
|
+
set up coding agent sessions", the MCP exposes local tools such as
|
|
123
|
+
`shepherd_status` and `shepherd_setup_coding_sessions` that route agents to the
|
|
124
|
+
local `askshepherd status` / add-source flow. Production memory and wiki tools
|
|
125
|
+
remain remote Railway-backed tools for source recall and company-memory answers.
|
|
126
|
+
Those local MCP tools are also the permission boundary: an MCP client should not
|
|
127
|
+
use shell or file tools to inspect the user's folders or repositories for setup.
|
|
128
|
+
|
|
85
129
|
Use `--json` when an agent or setup script needs machine-readable endpoint and
|
|
86
130
|
header details.
|
|
87
131
|
|
package/bin/shepherd-onboard.js
CHANGED
|
@@ -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.37";
|
|
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");
|
|
@@ -127,6 +127,8 @@ async function dispatch() {
|
|
|
127
127
|
await runMcpInstall();
|
|
128
128
|
} else if (command === "mcp") {
|
|
129
129
|
await runMcpProxy();
|
|
130
|
+
} else if (command === "status" || command === "sync-status" || command === "check") {
|
|
131
|
+
await runStatusCommand();
|
|
130
132
|
} else if (command === "messages-chats") {
|
|
131
133
|
await runMessagesChatsCommand();
|
|
132
134
|
} else if (command === "messages-agent") {
|
|
@@ -720,39 +722,172 @@ async function runMcpProxy() {
|
|
|
720
722
|
{ name: "askshepherd-mcp-proxy", version: PACKAGE_VERSION },
|
|
721
723
|
{ capabilities: {} },
|
|
722
724
|
);
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
725
|
+
let remoteConnected = false;
|
|
726
|
+
let remoteConnectError = null;
|
|
727
|
+
try {
|
|
728
|
+
await remote.connect(new StreamableHTTPClientTransport(new URL(mcpUrl), {
|
|
729
|
+
requestInit: {
|
|
730
|
+
headers: {
|
|
731
|
+
Authorization: `Bearer ${token}`,
|
|
732
|
+
},
|
|
727
733
|
},
|
|
728
|
-
}
|
|
729
|
-
|
|
734
|
+
}));
|
|
735
|
+
remoteConnected = true;
|
|
736
|
+
} catch (err) {
|
|
737
|
+
remoteConnectError = safeError(err);
|
|
738
|
+
}
|
|
730
739
|
|
|
731
740
|
const passthroughResultSchema = typeof ResultSchema.passthrough === "function"
|
|
732
741
|
? ResultSchema.passthrough()
|
|
733
742
|
: ResultSchema;
|
|
743
|
+
const remoteCapabilities = remoteConnected ? remote.getServerCapabilities() ?? {} : {};
|
|
744
|
+
const remoteInstructions = remoteConnected ? remote.getInstructions() ?? "" : "";
|
|
745
|
+
const localTools = localMcpTools();
|
|
746
|
+
const localToolNames = new Set(localTools.map((tool) => tool.name));
|
|
734
747
|
const local = new Server(
|
|
735
748
|
{ name: "askshepherd", version: PACKAGE_VERSION },
|
|
736
749
|
{
|
|
737
|
-
capabilities:
|
|
738
|
-
instructions:
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
750
|
+
capabilities: { ...remoteCapabilities, tools: { listChanged: false } },
|
|
751
|
+
instructions: localMcpInstructions(remoteInstructions, remoteConnectError),
|
|
752
|
+
...(remoteConnected
|
|
753
|
+
? {
|
|
754
|
+
fallbackRequestHandler: async (request, extra) => remote.request(
|
|
755
|
+
request,
|
|
756
|
+
passthroughResultSchema,
|
|
757
|
+
{ ...proxyRequestOptions, signal: extra.signal },
|
|
758
|
+
),
|
|
759
|
+
fallbackNotificationHandler: async (notification) => {
|
|
760
|
+
await remote.notification(notification);
|
|
761
|
+
},
|
|
762
|
+
}
|
|
763
|
+
: {}),
|
|
747
764
|
},
|
|
748
765
|
);
|
|
749
|
-
local.setRequestHandler(ListToolsRequestSchema, async (request, extra) =>
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
766
|
+
local.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
|
|
767
|
+
if (!remoteConnected) return { tools: localTools };
|
|
768
|
+
const remoteTools = await remote
|
|
769
|
+
.listTools(request.params, { ...proxyRequestOptions, signal: extra.signal })
|
|
770
|
+
.catch(() => ({ tools: [] }));
|
|
771
|
+
return {
|
|
772
|
+
...remoteTools,
|
|
773
|
+
tools: [
|
|
774
|
+
...localTools,
|
|
775
|
+
...(remoteTools.tools ?? []).filter((tool) => !localToolNames.has(tool.name)),
|
|
776
|
+
],
|
|
777
|
+
};
|
|
778
|
+
});
|
|
779
|
+
local.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
780
|
+
if (localToolNames.has(request.params.name)) {
|
|
781
|
+
return callLocalMcpTool(request.params.name);
|
|
782
|
+
}
|
|
783
|
+
if (!remoteConnected) {
|
|
784
|
+
return localMcpTextResult(`Production Shepherd MCP is unavailable (${remoteConnectError ?? "not connected"}). For local setup/sync status, use shepherd_status or run ${agentCommand()} status.`, true);
|
|
785
|
+
}
|
|
786
|
+
return remote.callTool(request.params, passthroughResultSchema, { ...proxyRequestOptions, signal: extra.signal });
|
|
787
|
+
});
|
|
753
788
|
await local.connect(new StdioServerTransport());
|
|
754
789
|
}
|
|
755
790
|
|
|
791
|
+
function localMcpTools() {
|
|
792
|
+
const emptyInputSchema = {
|
|
793
|
+
type: "object",
|
|
794
|
+
properties: {},
|
|
795
|
+
additionalProperties: false,
|
|
796
|
+
};
|
|
797
|
+
const readOnlyAnnotations = {
|
|
798
|
+
readOnlyHint: true,
|
|
799
|
+
destructiveHint: false,
|
|
800
|
+
openWorldHint: false,
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
return [
|
|
804
|
+
{
|
|
805
|
+
name: "shepherd_status",
|
|
806
|
+
description: "LOCAL Shepherd setup and sync status. Use this first when the user asks what they have enabled, what is connected, whether Shepherd is syncing, or why local Messages/Coding Sessions are not running. This is backed by the local askshepherd npm CLI; do not use production memory/wiki tools or shell/file exploration for local setup status.",
|
|
807
|
+
inputSchema: emptyInputSchema,
|
|
808
|
+
annotations: readOnlyAnnotations,
|
|
809
|
+
_meta: { provider: "local_npm", command: `${agentCommand()} status` },
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
name: "shepherd_local_status",
|
|
813
|
+
description: "Explicit local alias for shepherd_status. Returns the authoritative local askshepherd npm status path and current local setup/sync state. Do not inspect user folders or repositories yourself.",
|
|
814
|
+
inputSchema: emptyInputSchema,
|
|
815
|
+
annotations: readOnlyAnnotations,
|
|
816
|
+
_meta: { provider: "local_npm", command: `${agentCommand()} status` },
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
name: "shepherd_setup_coding_sessions",
|
|
820
|
+
description: "LOCAL setup guide for Codex and Claude Code coding-session sync. Use when the user asks to set up coding agent sessions. Ask for consent, then guide the local askshepherd npm login/add-sources/continue/status commands. Do not search, list, or read the user's folders or repos to discover setup.",
|
|
821
|
+
inputSchema: emptyInputSchema,
|
|
822
|
+
annotations: readOnlyAnnotations,
|
|
823
|
+
_meta: { provider: "local_npm", command: `${agentCommand()} agent --add-sources coding-sessions` },
|
|
824
|
+
},
|
|
825
|
+
];
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function localMcpInstructions(remoteInstructions, remoteConnectError) {
|
|
829
|
+
return [
|
|
830
|
+
"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 shepherd_setup_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
|
+
"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
|
+
`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
|
+
"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.",
|
|
835
|
+
"Important override: any production instruction saying not to use local shell commands applies only to production memory/wiki answers. It does not apply to local Shepherd setup, source enablement, or sync health.",
|
|
836
|
+
remoteConnectError ? `Production Shepherd MCP connection failed at startup: ${remoteConnectError}. Local setup/status tools are still available.` : "",
|
|
837
|
+
remoteInstructions ? `Production memory/wiki instructions: ${remoteInstructions}` : "",
|
|
838
|
+
].filter(Boolean).join(" ");
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async function callLocalMcpTool(name) {
|
|
842
|
+
if (name === "shepherd_status" || name === "shepherd_local_status") {
|
|
843
|
+
const status = await collectShepherdStatus();
|
|
844
|
+
return localMcpTextResult([
|
|
845
|
+
`Authoritative local status path: ${agentCommand()} status`,
|
|
846
|
+
"Use this result for setup/source/sync-health questions. Do not use production memory/wiki tools to answer what is enabled locally.",
|
|
847
|
+
"Do not inspect the user's folders or repositories yourself. Do not run ls/find/rg/grep/cat/Read/Glob/Explore against the user's home directory, repos, ~/.codex, ~/.claude, or ~/.shepherd for Shepherd setup.",
|
|
848
|
+
renderShepherdStatus(status),
|
|
849
|
+
].join("\n\n"));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (name === "shepherd_setup_coding_sessions") {
|
|
853
|
+
const status = await collectShepherdStatus();
|
|
854
|
+
return localMcpTextResult(renderCodingSessionsSetupMcpResult(status));
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return localMcpTextResult(`Unknown local Shepherd MCP tool: ${name}`, true);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function renderCodingSessionsSetupMcpResult(status) {
|
|
861
|
+
const command = status.commands.addCodingSessions;
|
|
862
|
+
const alreadyConfigured = Boolean(status.local.codingSessions.configPath);
|
|
863
|
+
return [
|
|
864
|
+
"Local Shepherd coding-session setup",
|
|
865
|
+
"",
|
|
866
|
+
"Use this when the user asks to set up coding agent sessions. Ask for explicit consent before enabling this source: Shepherd will read local Codex and Claude Code session logs, redact sensitive strings locally, and sync bounded summaries plus repo/command/file metadata, not full raw transcripts.",
|
|
867
|
+
"Do not inspect the user's folders or repositories to set this up. Do not run ls/find/rg/grep/cat/Read/Glob/Explore against the user's home directory, repos, ~/.codex, ~/.claude, or ~/.shepherd. Use only the Shepherd npm commands below and the status result they print.",
|
|
868
|
+
"",
|
|
869
|
+
alreadyConfigured
|
|
870
|
+
? "Current state: Coding Sessions already has a local config. Check whether the LaunchAgent is running and whether the last sync is healthy below."
|
|
871
|
+
: "Current state: Coding Sessions is not configured locally yet.",
|
|
872
|
+
"",
|
|
873
|
+
"Commands to run locally:",
|
|
874
|
+
`1. If there is no saved Shepherd login, run: ${status.commands.login}`,
|
|
875
|
+
`2. Add only this source: ${command}`,
|
|
876
|
+
`3. Finish/install the local agent: ${status.commands.continueSetup}`,
|
|
877
|
+
`4. Verify: ${status.commands.checkStatus}`,
|
|
878
|
+
"",
|
|
879
|
+
"Current local status:",
|
|
880
|
+
renderLocalCodingSessionsStatus(status.local.codingSessions).join("\n"),
|
|
881
|
+
].join("\n");
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function localMcpTextResult(text, isError = false) {
|
|
885
|
+
return {
|
|
886
|
+
content: [{ type: "text", text }],
|
|
887
|
+
isError,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
756
891
|
async function pollMcpLogin(apiUrl, started) {
|
|
757
892
|
const intervalMs = Math.max(1000, Number(started.intervalSeconds ?? 5) * 1000);
|
|
758
893
|
const expiresAt = Date.parse(started.expiresAt ?? "") || Date.now() + 600_000;
|
|
@@ -1021,20 +1156,186 @@ async function continueAgentOnboarding() {
|
|
|
1021
1156
|
}
|
|
1022
1157
|
|
|
1023
1158
|
async function printAgentStatus() {
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
);
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1159
|
+
await runStatusCommand();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
async function runStatusCommand() {
|
|
1163
|
+
const status = await collectShepherdStatus();
|
|
1164
|
+
if (args.json) {
|
|
1165
|
+
console.log(JSON.stringify(status, null, 2));
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
printShepherdStatus(status);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
async function collectShepherdStatus() {
|
|
1172
|
+
const statePath = agentStatePath();
|
|
1173
|
+
const state = await readOptionalAgentState();
|
|
1174
|
+
let production = null;
|
|
1175
|
+
let productionError = null;
|
|
1176
|
+
|
|
1177
|
+
if (state?.apiUrl && state?.sessionId && state?.sessionToken) {
|
|
1178
|
+
try {
|
|
1179
|
+
production = await getJson(
|
|
1180
|
+
`${trimTrailingSlash(state.apiUrl)}/onboarding/raw/session/${encodeURIComponent(state.sessionId)}/status`,
|
|
1181
|
+
{ token: state.sessionToken },
|
|
1182
|
+
);
|
|
1183
|
+
await updateAgentStateFromOnboardingResponse(state, production);
|
|
1184
|
+
} catch (err) {
|
|
1185
|
+
productionError = safeError(err);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const userId = production?.sessionId ?? state?.sessionId ?? null;
|
|
1190
|
+
const providers = production?.providers ?? state?.providers ?? {};
|
|
1191
|
+
const messagesLocal = await collectMessagesLocalStatus(userId);
|
|
1192
|
+
const codingSessionsLocal = await collectCodingSessionsLocalStatus(userId);
|
|
1193
|
+
|
|
1194
|
+
return {
|
|
1195
|
+
statePath,
|
|
1196
|
+
configured: Boolean(state),
|
|
1197
|
+
account: production?.account ?? state?.account ?? null,
|
|
1198
|
+
savedSources: state?.sources ?? {},
|
|
1199
|
+
providers,
|
|
1200
|
+
production: production
|
|
1201
|
+
? {
|
|
1202
|
+
status: production.status,
|
|
1203
|
+
providers,
|
|
1204
|
+
rawOnly: production.rawOnly === true,
|
|
1205
|
+
processingEnabled: production.processingEnabled === true,
|
|
1206
|
+
processing: production.processing,
|
|
1207
|
+
}
|
|
1208
|
+
: null,
|
|
1209
|
+
productionError,
|
|
1210
|
+
local: {
|
|
1211
|
+
messages: messagesLocal,
|
|
1212
|
+
codingSessions: codingSessionsLocal,
|
|
1213
|
+
},
|
|
1214
|
+
commands: {
|
|
1215
|
+
login: `${agentCommand()} agent --login`,
|
|
1216
|
+
checkStatus: `${agentCommand()} status`,
|
|
1217
|
+
addCodingSessions: `${agentCommand()} agent --add-sources coding-sessions --name "<full_name>" --org "<organization>"`,
|
|
1218
|
+
continueSetup: `${agentCommand()} agent --continue`,
|
|
1219
|
+
codingSessionsStatus: `${agentCommand()} coding-sessions-status`,
|
|
1220
|
+
messagesChats: `${agentCommand()} messages-chats`,
|
|
1221
|
+
},
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function printShepherdStatus(status) {
|
|
1226
|
+
console.log(renderShepherdStatus(status));
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function renderShepherdStatus(status) {
|
|
1230
|
+
const lines = ["", "Shepherd sync status", ""];
|
|
1231
|
+
|
|
1232
|
+
if (status.account) {
|
|
1233
|
+
const email = status.account.email ? ` <${status.account.email}>` : "";
|
|
1234
|
+
const org = status.account.organizationName ? ` / ${status.account.organizationName}` : "";
|
|
1235
|
+
lines.push(`Account: ${status.account.name ?? "unknown"}${email}${org}`);
|
|
1236
|
+
} else {
|
|
1237
|
+
lines.push("Account: no saved Shepherd onboarding session found");
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (status.productionError) {
|
|
1241
|
+
lines.push(`Production status: unavailable (${status.productionError})`);
|
|
1242
|
+
} else if (status.production) {
|
|
1243
|
+
lines.push(`Production status: ${status.production.status ?? "unknown"}`);
|
|
1244
|
+
lines.push(`Downstream processing: ${status.production.processingEnabled ? "enabled" : "not enabled"}`);
|
|
1245
|
+
} else {
|
|
1246
|
+
lines.push("Production status: not checked");
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
lines.push("", "Sources:");
|
|
1250
|
+
for (const source of statusSourceRows(status.providers, status.savedSources)) {
|
|
1251
|
+
const label = source.connected
|
|
1252
|
+
? "connected"
|
|
1253
|
+
: source.seen
|
|
1254
|
+
? "not connected"
|
|
1255
|
+
: source.selected
|
|
1256
|
+
? "selected in saved setup; connection unknown"
|
|
1257
|
+
: "not enabled in saved setup";
|
|
1258
|
+
lines.push(`- ${source.label}: ${label}`);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
lines.push("", "Local sync:");
|
|
1262
|
+
lines.push(...renderLocalMessagesStatus(status.local.messages));
|
|
1263
|
+
lines.push(...renderLocalCodingSessionsStatus(status.local.codingSessions));
|
|
1264
|
+
|
|
1265
|
+
lines.push("", "Useful commands:");
|
|
1266
|
+
if (!status.configured) lines.push(`- Sign in: ${status.commands.login}`);
|
|
1267
|
+
lines.push(`- Check again: ${status.commands.checkStatus}`);
|
|
1268
|
+
lines.push(`- Add coding sessions: ${status.commands.addCodingSessions}`);
|
|
1269
|
+
lines.push(`- Continue pending setup: ${status.commands.continueSetup}`);
|
|
1270
|
+
return lines.join("\n");
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function statusSourceRows(providers, savedSources = {}) {
|
|
1274
|
+
const definitions = [
|
|
1275
|
+
["google", "Google Workspace", "google"],
|
|
1276
|
+
["slack", "Slack", "slack"],
|
|
1277
|
+
["granola", "Granola", "granola"],
|
|
1278
|
+
["messages", "Messages", "messages"],
|
|
1279
|
+
["codingSessions", "Coding Sessions", "codingSessions"],
|
|
1280
|
+
];
|
|
1281
|
+
return definitions.map(([key, label, sourceKey]) => ({
|
|
1282
|
+
key,
|
|
1283
|
+
label,
|
|
1284
|
+
seen: Boolean(providers?.[key]),
|
|
1285
|
+
selected: savedSources?.[sourceKey] === true,
|
|
1286
|
+
connected: providers?.[key]?.connected === true,
|
|
1287
|
+
}));
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function printLocalMessagesStatus(status) {
|
|
1291
|
+
console.log(renderLocalMessagesStatus(status).join("\n"));
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function renderLocalMessagesStatus(status) {
|
|
1295
|
+
const lines = [];
|
|
1296
|
+
const prefix = "- Messages local agent";
|
|
1297
|
+
if (!status.configPath) {
|
|
1298
|
+
lines.push(`${prefix}: not configured`);
|
|
1299
|
+
} else {
|
|
1300
|
+
lines.push(`${prefix}: configured at ${status.configPath}`);
|
|
1301
|
+
}
|
|
1302
|
+
if (status.launch) {
|
|
1303
|
+
lines.push(` LaunchAgent: ${status.launch.label} ${status.launch.running ? "running" : "not running or unknown"}`);
|
|
1304
|
+
} else {
|
|
1305
|
+
lines.push(" LaunchAgent: not installed or unavailable");
|
|
1306
|
+
}
|
|
1307
|
+
lines.push(` Messages database: ${status.storage.readable ? "readable" : `not readable (${status.storage.reason})`}`);
|
|
1308
|
+
lines.push(` Queued unsent messages: ${status.queueDepth}`);
|
|
1309
|
+
return lines;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function printLocalCodingSessionsStatus(status) {
|
|
1313
|
+
console.log(renderLocalCodingSessionsStatus(status).join("\n"));
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function renderLocalCodingSessionsStatus(status) {
|
|
1317
|
+
const lines = [];
|
|
1318
|
+
const prefix = "- Coding Sessions local agent";
|
|
1319
|
+
if (!status.configPath) {
|
|
1320
|
+
lines.push(`${prefix}: not configured`);
|
|
1321
|
+
} else {
|
|
1322
|
+
lines.push(`${prefix}: configured at ${status.configPath}`);
|
|
1323
|
+
}
|
|
1324
|
+
if (status.launch) {
|
|
1325
|
+
lines.push(` LaunchAgent: ${status.launch.label} ${status.launch.running ? "running" : "not running or unknown"}`);
|
|
1326
|
+
} else {
|
|
1327
|
+
lines.push(" LaunchAgent: not installed or unavailable");
|
|
1328
|
+
}
|
|
1329
|
+
for (const probe of status.localFolders) {
|
|
1330
|
+
lines.push(` ${probe.provider}: ${probe.path} ${probe.readable ? "readable" : `not readable (${probe.reason})`}`);
|
|
1331
|
+
}
|
|
1332
|
+
if (status.lastSync) {
|
|
1333
|
+
lines.push(` Last sync: ${status.lastSync.finishedAt ?? "unknown"} (${status.lastSync.scanned ?? 0} scanned, ${status.lastSync.changed ?? 0} changed)`);
|
|
1334
|
+
} else {
|
|
1335
|
+
lines.push(" Last sync: none recorded");
|
|
1336
|
+
}
|
|
1337
|
+
lines.push(` Queued unsent sessions: ${status.queueDepth}`);
|
|
1338
|
+
return lines;
|
|
1038
1339
|
}
|
|
1039
1340
|
|
|
1040
1341
|
async function runMessagesChatsCommand() {
|
|
@@ -1173,55 +1474,85 @@ async function runCodingSessionsAgent() {
|
|
|
1173
1474
|
}
|
|
1174
1475
|
}
|
|
1175
1476
|
|
|
1176
|
-
async function
|
|
1177
|
-
const configPath =
|
|
1178
|
-
const config = configPath ?
|
|
1179
|
-
const userId = config?.userId ?? null;
|
|
1477
|
+
async function collectMessagesLocalStatus(preferredUserId = null) {
|
|
1478
|
+
const configPath = await messagesConfigPathForUser(preferredUserId) ?? await latestMessagesConfigPath();
|
|
1479
|
+
const config = configPath ? readJsonOptional(configPath) : null;
|
|
1480
|
+
const userId = config?.userId ?? preferredUserId ?? null;
|
|
1481
|
+
const safeId = userId ? safeFileId(userId) : null;
|
|
1482
|
+
const label = safeId ? `ai.shepherd.raw-messages.${safeId}` : null;
|
|
1483
|
+
const queue = safeId ? readJsonOptional(join(homedir(), ".shepherd", "raw-messages", `${safeId}-queue.json`)) : null;
|
|
1484
|
+
|
|
1485
|
+
return {
|
|
1486
|
+
configPath: configPath ?? null,
|
|
1487
|
+
userId,
|
|
1488
|
+
allChats: config?.allChats === true,
|
|
1489
|
+
selectedChatCount: Array.isArray(config?.allowedChatIds) ? config.allowedChatIds.length : 0,
|
|
1490
|
+
storage: await probePath("messages", MESSAGES_CHAT_DB_PATH),
|
|
1491
|
+
launch: localLaunchStatus(label),
|
|
1492
|
+
queueDepth: Array.isArray(queue) ? queue.length : 0,
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
async function collectCodingSessionsLocalStatus(preferredUserId = null, explicitConfigPath = null) {
|
|
1497
|
+
const configPath = explicitConfigPath ?? await codingSessionsConfigPathForUser(preferredUserId) ?? await latestCodingSessionsConfigPath();
|
|
1498
|
+
const config = configPath ? readJsonOptional(configPath) : null;
|
|
1499
|
+
const userId = config?.userId ?? preferredUserId ?? null;
|
|
1180
1500
|
const safeId = userId ? safeFileId(userId) : null;
|
|
1181
1501
|
const label = safeId ? `ai.shepherd.coding-sessions.${safeId}` : null;
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
const launch = label && platform() === "darwin"
|
|
1186
|
-
? {
|
|
1187
|
-
label,
|
|
1188
|
-
state: readLaunchctlPrint(`gui/${process.getuid?.() ?? 501}/${label}`),
|
|
1189
|
-
}
|
|
1190
|
-
: null;
|
|
1191
|
-
const production = await productionOnboardingStatusForCodingSessions(config).catch((err) => ({ error: safeError(err) }));
|
|
1192
|
-
const status = {
|
|
1502
|
+
const queue = safeId ? readJsonOptional(join(homedir(), ".shepherd", "coding-sessions", `${safeId}-queue.json`)) : null;
|
|
1503
|
+
|
|
1504
|
+
return {
|
|
1193
1505
|
configPath: configPath ?? null,
|
|
1194
1506
|
userId,
|
|
1195
|
-
localFolders:
|
|
1196
|
-
launch,
|
|
1197
|
-
lastSync,
|
|
1507
|
+
localFolders: await probeCodingSessionPaths(config ?? {}),
|
|
1508
|
+
launch: localLaunchStatus(label),
|
|
1509
|
+
lastSync: userId ? readJsonOptional(codingSessionsStatusFile(userId)) : null,
|
|
1198
1510
|
queueDepth: Array.isArray(queue) ? queue.length : 0,
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function localLaunchStatus(label) {
|
|
1515
|
+
if (!label || platform() !== "darwin") return null;
|
|
1516
|
+
const state = readLaunchctlPrint(`gui/${process.getuid?.() ?? 501}/${label}`);
|
|
1517
|
+
return {
|
|
1518
|
+
label,
|
|
1519
|
+
running: /state = running|job state = running/i.test(state),
|
|
1520
|
+
state,
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
async function runCodingSessionsStatus() {
|
|
1525
|
+
const configPath = stringArg("config") ?? await latestCodingSessionsConfigPath();
|
|
1526
|
+
const status = await collectCodingSessionsLocalStatus(null, configPath);
|
|
1527
|
+
const config = configPath ? readJsonOptional(configPath) : null;
|
|
1528
|
+
const production = await productionOnboardingStatusForCodingSessions(config).catch((err) => ({ error: safeError(err) }));
|
|
1529
|
+
const detailedStatus = {
|
|
1530
|
+
...status,
|
|
1199
1531
|
production,
|
|
1200
1532
|
};
|
|
1201
1533
|
|
|
1202
1534
|
if (args.json) {
|
|
1203
|
-
console.log(JSON.stringify(
|
|
1535
|
+
console.log(JSON.stringify(detailedStatus, null, 2));
|
|
1204
1536
|
return;
|
|
1205
1537
|
}
|
|
1206
1538
|
|
|
1207
1539
|
console.log("\nShepherd coding-session sync status\n");
|
|
1208
|
-
console.log(`Config: ${configPath ?? "not found"}`);
|
|
1209
|
-
if (userId) console.log(`User: ${userId}`);
|
|
1210
|
-
for (const probe of
|
|
1540
|
+
console.log(`Config: ${detailedStatus.configPath ?? "not found"}`);
|
|
1541
|
+
if (detailedStatus.userId) console.log(`User: ${detailedStatus.userId}`);
|
|
1542
|
+
for (const probe of detailedStatus.localFolders) {
|
|
1211
1543
|
console.log(`- ${probe.provider}: ${probe.path} ${probe.readable ? "readable" : `not readable (${probe.reason})`}`);
|
|
1212
1544
|
}
|
|
1213
|
-
if (launch) {
|
|
1214
|
-
|
|
1215
|
-
console.log(`LaunchAgent: ${launch.label} ${running ? "running" : "not running or unknown"}`);
|
|
1545
|
+
if (detailedStatus.launch) {
|
|
1546
|
+
console.log(`LaunchAgent: ${detailedStatus.launch.label} ${detailedStatus.launch.running ? "running" : "not running or unknown"}`);
|
|
1216
1547
|
} else {
|
|
1217
1548
|
console.log("LaunchAgent: not installed or unavailable");
|
|
1218
1549
|
}
|
|
1219
|
-
if (lastSync) {
|
|
1220
|
-
console.log(`Last sync: ${lastSync.finishedAt ?? "unknown"} (${lastSync.scanned ?? 0} scanned, ${lastSync.changed ?? 0} changed)`);
|
|
1550
|
+
if (detailedStatus.lastSync) {
|
|
1551
|
+
console.log(`Last sync: ${detailedStatus.lastSync.finishedAt ?? "unknown"} (${detailedStatus.lastSync.scanned ?? 0} scanned, ${detailedStatus.lastSync.changed ?? 0} changed)`);
|
|
1221
1552
|
} else {
|
|
1222
1553
|
console.log("Last sync: none recorded");
|
|
1223
1554
|
}
|
|
1224
|
-
console.log(`Queued unsent sessions: ${
|
|
1555
|
+
console.log(`Queued unsent sessions: ${detailedStatus.queueDepth}`);
|
|
1225
1556
|
if (production?.providers?.codingSessions) {
|
|
1226
1557
|
console.log(`Production provider: ${production.providers.codingSessions.connected ? "connected" : "not connected"}`);
|
|
1227
1558
|
} else if (production?.error) {
|
|
@@ -1264,6 +1595,7 @@ Usage:
|
|
|
1264
1595
|
npx -y ${PACKAGE_NAME}@latest agent --continue --granola-api-key <key> --messages-handle <value> --messages-chat-ids <ids|all>
|
|
1265
1596
|
npx -y ${PACKAGE_NAME}@latest agent --add-sources coding-sessions --name <name> --org <organization>
|
|
1266
1597
|
npx -y ${PACKAGE_NAME}@latest agent --status
|
|
1598
|
+
npx -y ${PACKAGE_NAME}@latest status
|
|
1267
1599
|
npx -y ${PACKAGE_NAME}@latest coding-sessions-status
|
|
1268
1600
|
npx -y ${PACKAGE_NAME}@latest messages-chats
|
|
1269
1601
|
npx -y ${PACKAGE_NAME}@latest granola-api-keys
|
|
@@ -1274,6 +1606,28 @@ The bare agent command is intended for coding-agent shells. For direct terminal
|
|
|
1274
1606
|
return;
|
|
1275
1607
|
}
|
|
1276
1608
|
|
|
1609
|
+
if (which === "status" || which === "sync-status" || which === "check") {
|
|
1610
|
+
console.log(`Shepherd sync status
|
|
1611
|
+
|
|
1612
|
+
Usage:
|
|
1613
|
+
npx -y ${PACKAGE_NAME}@latest status
|
|
1614
|
+
npx -y ${PACKAGE_NAME}@latest status --json
|
|
1615
|
+
|
|
1616
|
+
Shows the saved Shepherd account, connected cloud sources, production processing
|
|
1617
|
+
state, and local background sync health for Messages and Coding Sessions.
|
|
1618
|
+
|
|
1619
|
+
Aliases:
|
|
1620
|
+
sync-status
|
|
1621
|
+
check
|
|
1622
|
+
|
|
1623
|
+
Options:
|
|
1624
|
+
--json Print machine-readable status.
|
|
1625
|
+
--state <path> Local onboarding state file. Defaults to ~/.shepherd/raw-onboarding-agent.json.
|
|
1626
|
+
--help Show this help.
|
|
1627
|
+
`);
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1277
1631
|
if (which === "coding-sessions-agent") {
|
|
1278
1632
|
console.log(`Shepherd coding-session sync agent
|
|
1279
1633
|
|
|
@@ -1406,6 +1760,9 @@ Usage:
|
|
|
1406
1760
|
|
|
1407
1761
|
This command is installed into MCP clients. It reads ~/.shepherd/mcp.json and
|
|
1408
1762
|
proxies stdio MCP traffic to the authenticated production Shepherd MCP endpoint.
|
|
1763
|
+
It also exposes local npm-backed setup/status tools such as shepherd_status and
|
|
1764
|
+
shepherd_setup_coding_sessions so agents can answer what is enabled locally and
|
|
1765
|
+
guide coding-session setup.
|
|
1409
1766
|
|
|
1410
1767
|
Options:
|
|
1411
1768
|
--state <path> Token state file. Defaults to ~/.shepherd/mcp.json.
|
|
@@ -1420,6 +1777,7 @@ Options:
|
|
|
1420
1777
|
Usage:
|
|
1421
1778
|
npx -y ${PACKAGE_NAME}@latest
|
|
1422
1779
|
npx -y ${PACKAGE_NAME}@latest agent
|
|
1780
|
+
npx -y ${PACKAGE_NAME}@latest status
|
|
1423
1781
|
npx -y ${PACKAGE_NAME}@latest mcp-login
|
|
1424
1782
|
npx -y ${PACKAGE_NAME}@latest mcp-install
|
|
1425
1783
|
npx -y ${PACKAGE_NAME}@latest messages-chats
|
|
@@ -1479,6 +1837,16 @@ function printAgentContract() {
|
|
|
1479
1837
|
const payload = {
|
|
1480
1838
|
task: "Complete Shepherd customer-facing production raw-source onboarding for this user with the public customer setup flow.",
|
|
1481
1839
|
instruction: "Use this as a public onboarding checklist. Confirm required onboarding choices and fields directly with the user instead of assuming them from local repo context, shell identity, machine paths, or earlier conversations.",
|
|
1840
|
+
commonUserRequests: [
|
|
1841
|
+
{
|
|
1842
|
+
userSays: "Check what I've enabled for Shepherd.",
|
|
1843
|
+
agentShould: `Run ${command} status and summarize connected sources, local background agents, unreadable folders, queued items, and any next command it prints. Do not inspect local folders or repositories yourself.`,
|
|
1844
|
+
},
|
|
1845
|
+
{
|
|
1846
|
+
userSays: "Help me set up coding agent sessions.",
|
|
1847
|
+
agentShould: `Ask for consent to sync Codex and Claude Code summaries, then run ${command} agent --login if there is no saved Shepherd login, ${command} agent --add-sources coding-sessions --name "<full_name>" --org "<organization>", ${command} agent --continue, and finally ${command} status. Do not search the user's filesystem or repos for an agent implementation.`,
|
|
1848
|
+
},
|
|
1849
|
+
],
|
|
1482
1850
|
askStyle: [
|
|
1483
1851
|
"Ask in short interactive prompts, not as a pasted checklist.",
|
|
1484
1852
|
"Start with selection questions to determine what the user wants connected.",
|
|
@@ -1542,18 +1910,21 @@ function printAgentContract() {
|
|
|
1542
1910
|
"Do not ask the customer to create a Google service account or upload service account JSON for the default Shepherd-managed flow.",
|
|
1543
1911
|
"Do not use WorkOS Auth, WorkOS Pipes, or per-user Google OAuth for Google Workspace delegation.",
|
|
1544
1912
|
"Do not fill onboarding fields from local repository context, shell identity, machine paths, or earlier conversations; confirm them directly with the user.",
|
|
1913
|
+
"Do not use shell or file tools to list, search, or read the user's home directory, repositories, ~/.codex, ~/.claude, or ~/.shepherd for Shepherd setup. Only the askshepherd npm commands should perform local setup/status checks.",
|
|
1545
1914
|
],
|
|
1546
1915
|
loginCommand: `${command} agent --login`,
|
|
1547
1916
|
startCommand: `${command} agent --name "<full_name>" --org "<organization>"`,
|
|
1548
1917
|
addSourcesCommand: `${command} agent --add-sources coding-sessions --name "<full_name>" --org "<organization>"`,
|
|
1549
1918
|
continueCommand: `${command} agent --continue`,
|
|
1919
|
+
checkCommand: `${command} status`,
|
|
1550
1920
|
mcpLoginCommand: `${command} mcp-login`,
|
|
1551
1921
|
optionalContinueArgs: [
|
|
1552
1922
|
"--messages-handle \"<phone_or_apple_id>\" if local Messages is being connected",
|
|
1553
1923
|
"--messages-chat-ids \"<comma_separated_chat_ids>\" if local Messages is being connected, or --messages-chat-ids all when the user explicitly wants every current and future Messages chat watched",
|
|
1554
1924
|
"--granola-api-key \"<granola_key>\" if Granola is being connected",
|
|
1555
1925
|
],
|
|
1556
|
-
statusCommand: `${command}
|
|
1926
|
+
statusCommand: `${command} status`,
|
|
1927
|
+
agentStatusCommand: `${command} agent --status`,
|
|
1557
1928
|
messagesChatsCommand: `${command} messages-chats`,
|
|
1558
1929
|
messagesPermissions: {
|
|
1559
1930
|
macOS: "Local Messages raw sync needs Full Disk Access for the app running onboarding and for Node.js used by the background LaunchAgent. The Messages selector command validates local chat.db access, opens Full Disk Access settings if needed, and keeps checking until access works in interactive onboarding. Background sync install also checks that launchd can start the Messages agent. Contacts permission may also appear when resolving local contact names. The background Messages agent reloads Contacts on startup, watches AddressBook changes when available, and runs fallback contact sync so renamed contacts can hydrate prior ingested Messages rows for the token-bound customer account.",
|
|
@@ -1583,8 +1954,20 @@ Use this as a public checklist for coding-agent onboarding to Shepherd raw sync.
|
|
|
1583
1954
|
|
|
1584
1955
|
Confirm onboarding choices and fields directly with the user instead of assuming them from the local repo, shell username, machine paths, or earlier conversations.
|
|
1585
1956
|
|
|
1957
|
+
Do not inspect the user's folders or repositories for Shepherd setup. Do not run ls, find, rg, grep, cat, Read, Glob, or Explore against the user's home directory, repos, ~/.codex, ~/.claude, or ~/.shepherd. Use the askshepherd npm commands below; those commands perform the bounded local checks.
|
|
1958
|
+
|
|
1586
1959
|
Ask with short interactive prompts, not as one pasted checklist. Do not paste this whole checklist into the chat unless the user explicitly asks to see it.
|
|
1587
1960
|
|
|
1961
|
+
Common user requests:
|
|
1962
|
+
- If the user asks "Check what I've enabled for Shepherd?", run:
|
|
1963
|
+
${payload.checkCommand}
|
|
1964
|
+
Then summarize connected sources, local background agents, unreadable folders, queued items, and the next command to fix anything missing.
|
|
1965
|
+
- If the user asks "Help me set up coding agent sessions", ask for consent to sync Codex and Claude Code summaries, then run login if needed, add only the coding-sessions source, continue setup, and finish by checking status:
|
|
1966
|
+
${payload.loginCommand}
|
|
1967
|
+
${payload.addSourcesCommand}
|
|
1968
|
+
${payload.continueCommand}
|
|
1969
|
+
${payload.checkCommand}
|
|
1970
|
+
|
|
1588
1971
|
Start with selection questions to determine intent:
|
|
1589
1972
|
1. Organization: Join existing org, or Create new org.
|
|
1590
1973
|
2. Sources: Google Workspace (Gmail/Drive/Docs/Calendar/Sheets/Slides/Tasks/Contacts), Slack, Granola, Messages, Coding Sessions (Codex/Claude Code summaries). Allow multi-select if your interface supports it.
|
|
@@ -1661,6 +2044,7 @@ After the current modality is complete, run:
|
|
|
1661
2044
|
Omit either optional flag if that source is not being connected.
|
|
1662
2045
|
|
|
1663
2046
|
If Coding Sessions was selected, the continue step installs local Codex/Claude Code session summary sync. It probes ~/.codex and ~/.claude, redacts sensitive strings, and uploads bounded summaries and work metadata rather than full transcripts. It usually does not need Full Disk Access unless macOS denies access to those folders.
|
|
2047
|
+
The coding agent must not probe those folders directly; only the askshepherd CLI may perform that local check.
|
|
1664
2048
|
|
|
1665
2049
|
Check progress with:
|
|
1666
2050
|
${payload.statusCommand}
|
|
@@ -2509,6 +2893,7 @@ function readLaunchctlPrint(domainLabel) {
|
|
|
2509
2893
|
try {
|
|
2510
2894
|
return execFileSync("launchctl", ["print", domainLabel], {
|
|
2511
2895
|
encoding: "utf8",
|
|
2896
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2512
2897
|
timeout: 5_000,
|
|
2513
2898
|
});
|
|
2514
2899
|
} catch (err) {
|
|
@@ -4317,15 +4702,36 @@ function extractCodexCommands(payloads) {
|
|
|
4317
4702
|
function extractClaudeCommands(lines) {
|
|
4318
4703
|
const commands = [];
|
|
4319
4704
|
for (const line of lines) {
|
|
4320
|
-
const
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4705
|
+
for (const command of extractCommandStrings(line.toolUseResult ?? line.message?.content ?? line)) {
|
|
4706
|
+
if (isLikelyShellCommand(command)) {
|
|
4707
|
+
commands.push({ command: redactText(command, 600), exitCode: null, summary: null });
|
|
4708
|
+
}
|
|
4324
4709
|
}
|
|
4325
4710
|
}
|
|
4326
4711
|
return commands.slice(-100);
|
|
4327
4712
|
}
|
|
4328
4713
|
|
|
4714
|
+
function extractCommandStrings(value, depth = 0) {
|
|
4715
|
+
if (depth > 6 || value == null) return [];
|
|
4716
|
+
if (typeof value === "string") return [];
|
|
4717
|
+
if (Array.isArray(value)) return value.flatMap((item) => extractCommandStrings(item, depth + 1));
|
|
4718
|
+
if (typeof value !== "object") return [];
|
|
4719
|
+
|
|
4720
|
+
const commands = [];
|
|
4721
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
4722
|
+
if ((key === "command" || key === "input") && typeof nested === "string" && nested.trim()) {
|
|
4723
|
+
commands.push(nested);
|
|
4724
|
+
continue;
|
|
4725
|
+
}
|
|
4726
|
+
commands.push(...extractCommandStrings(nested, depth + 1));
|
|
4727
|
+
}
|
|
4728
|
+
return commands;
|
|
4729
|
+
}
|
|
4730
|
+
|
|
4731
|
+
function isLikelyShellCommand(value) {
|
|
4732
|
+
return /\b(?:git|npm|pnpm|yarn|bun|pytest|vitest|cargo|go|python|node|tsc|ruff|eslint|make)\b/.test(value);
|
|
4733
|
+
}
|
|
4734
|
+
|
|
4329
4735
|
async function repoMetadata(cwd) {
|
|
4330
4736
|
const base = { fullName: null, remote: null, branch: null, commit: null };
|
|
4331
4737
|
if (!cwd) return base;
|
|
@@ -4474,20 +4880,12 @@ function redactText(value, maxLength = 1000) {
|
|
|
4474
4880
|
.replace(/-----BEGIN [^-]+PRIVATE KEY-----[\s\S]*?-----END [^-]+PRIVATE KEY-----/g, "[redacted-private-key]")
|
|
4475
4881
|
.replace(/\b(?:sk|pk|rk|ghp|github_pat|xox[baprs])_[A-Za-z0-9_=-]{12,}\b/g, "[redacted-token]")
|
|
4476
4882
|
.replace(/\b[A-Za-z0-9._%+-]+:[A-Za-z0-9._%+-]+@/g, "[redacted-credentials]@")
|
|
4477
|
-
.replace(/\b(
|
|
4883
|
+
.replace(/\b(authorization|x-api-key|api[_-]?key|token|secret|password)(\s*[:=]\s*)['"]?[^'"\s]+/gi, "$1$2[redacted]")
|
|
4478
4884
|
.replace(/(OPENAI_API_KEY|FIREWORKS_API_KEY|ANTHROPIC_API_KEY|DATABASE_URL|REDIS_URL)=\S+/g, "$1=[redacted]")
|
|
4479
4885
|
.replace(new RegExp(homedir().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), "~")
|
|
4480
4886
|
.slice(0, maxLength);
|
|
4481
4887
|
}
|
|
4482
4888
|
|
|
4483
|
-
function unescapeJsonString(value) {
|
|
4484
|
-
try {
|
|
4485
|
-
return JSON.parse(`"${value.replace(/"/g, '\\"')}"`);
|
|
4486
|
-
} catch {
|
|
4487
|
-
return value;
|
|
4488
|
-
}
|
|
4489
|
-
}
|
|
4490
|
-
|
|
4491
4889
|
function hashString(value) {
|
|
4492
4890
|
return createHash("sha256").update(String(value ?? "")).digest("hex");
|
|
4493
4891
|
}
|
|
@@ -4515,6 +4913,35 @@ function codingSessionsStatusFile(userId) {
|
|
|
4515
4913
|
return path;
|
|
4516
4914
|
}
|
|
4517
4915
|
|
|
4916
|
+
async function messagesConfigPathForUser(userId) {
|
|
4917
|
+
if (!userId) return null;
|
|
4918
|
+
const path = join(homedir(), ".shepherd", "raw-messages", `${userId}.json`);
|
|
4919
|
+
return existsSync(path) ? path : null;
|
|
4920
|
+
}
|
|
4921
|
+
|
|
4922
|
+
async function codingSessionsConfigPathForUser(userId) {
|
|
4923
|
+
if (!userId) return null;
|
|
4924
|
+
const path = join(homedir(), ".shepherd", "coding-sessions", `${userId}.json`);
|
|
4925
|
+
return existsSync(path) ? path : null;
|
|
4926
|
+
}
|
|
4927
|
+
|
|
4928
|
+
async function latestMessagesConfigPath() {
|
|
4929
|
+
const dir = join(homedir(), ".shepherd", "raw-messages");
|
|
4930
|
+
try {
|
|
4931
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
4932
|
+
const files = [];
|
|
4933
|
+
for (const entry of entries) {
|
|
4934
|
+
if (!entry.isFile() || !entry.name.endsWith(".json") || entry.name.includes("-queue")) continue;
|
|
4935
|
+
const path = join(dir, entry.name);
|
|
4936
|
+
const info = await stat(path).catch(() => null);
|
|
4937
|
+
if (info) files.push({ path, mtimeMs: info.mtimeMs });
|
|
4938
|
+
}
|
|
4939
|
+
return files.sort((a, b) => b.mtimeMs - a.mtimeMs)[0]?.path ?? null;
|
|
4940
|
+
} catch {
|
|
4941
|
+
return null;
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4944
|
+
|
|
4518
4945
|
async function latestCodingSessionsConfigPath() {
|
|
4519
4946
|
const dir = join(homedir(), ".shepherd", "coding-sessions");
|
|
4520
4947
|
try {
|