askshepherd 0.1.44 → 0.1.45
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 +31 -6
- package/bin/shepherd-onboard.js +301 -41
- package/bin/shepherd.js +579 -32
- package/package.json +1 -1
- package/skills/shepherd/SKILL.md +37 -58
package/bin/shepherd-onboard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
3
|
-
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
4
|
import { constants as fsConstants, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
5
5
|
import { access, chmod, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
6
|
import { createServer } from "node:http";
|
|
@@ -37,7 +37,7 @@ const MCP_ENVIRONMENT_TARGETS = {
|
|
|
37
37
|
const DEFAULT_API_URL = CUSTOMER_DEPLOY_API_URL;
|
|
38
38
|
const PACKAGE_NAME = "askshepherd";
|
|
39
39
|
const PACKAGE_SPEC = `${PACKAGE_NAME}@latest`;
|
|
40
|
-
const PACKAGE_VERSION = "0.1.
|
|
40
|
+
const PACKAGE_VERSION = "0.1.45";
|
|
41
41
|
const DEFAULT_OFFICE_AUDIO_TRANSCRIPTION_BACKEND = "pyannote_full";
|
|
42
42
|
const OFFICE_AUDIO_TRANSCRIPTION_BACKENDS = new Set([
|
|
43
43
|
"pyannote_full",
|
|
@@ -341,6 +341,16 @@ async function runOnboarding() {
|
|
|
341
341
|
const finalizeBody = { sessionToken: session.sessionToken };
|
|
342
342
|
let selectedMessageChats = [];
|
|
343
343
|
|
|
344
|
+
if (sources.github) {
|
|
345
|
+
console.log("\nGitHub repository sync");
|
|
346
|
+
const githubToken = await valueOrPrompt("github-token", "GitHub fine-grained or classic PAT with repository webhook/API access", { secret: true, optional: true });
|
|
347
|
+
const githubRepoFullName = await valueOrPrompt("github-repo", "GitHub repository owner/name", { optional: true });
|
|
348
|
+
if (githubToken || githubRepoFullName) {
|
|
349
|
+
finalizeBody.githubToken = githubToken;
|
|
350
|
+
finalizeBody.githubRepoFullName = githubRepoFullName;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
344
354
|
if (sources.granola) {
|
|
345
355
|
if (session.authUrls?.granola) {
|
|
346
356
|
console.log("\nGranola authorization");
|
|
@@ -537,7 +547,7 @@ async function runAgentOnboarding() {
|
|
|
537
547
|
currentAction,
|
|
538
548
|
statePath,
|
|
539
549
|
messagesChatsCommand: sources.messages ? `${agentCommand()} messages-chats` : undefined,
|
|
540
|
-
nextCommand: `${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"`,
|
|
550
|
+
nextCommand: `${agentCommand()} agent --continue --github-token "<github_pat>" --github-repo "<owner/repo>" --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"`,
|
|
541
551
|
needsUserAction: agentNeedsUserAction(sources, currentAction),
|
|
542
552
|
}, null, 2));
|
|
543
553
|
return;
|
|
@@ -556,7 +566,7 @@ async function runAgentOnboarding() {
|
|
|
556
566
|
});
|
|
557
567
|
|
|
558
568
|
console.log("\nAfter that modality is complete, run:");
|
|
559
|
-
console.log(` ${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"`);
|
|
569
|
+
console.log(` ${agentCommand()} agent --continue --github-token "<github_pat>" --github-repo "<owner/repo>" --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"`);
|
|
560
570
|
console.log(" Omit either optional flag if that source is not being connected.");
|
|
561
571
|
}
|
|
562
572
|
|
|
@@ -1196,6 +1206,20 @@ function localMcpTools(opts = {}) {
|
|
|
1196
1206
|
};
|
|
1197
1207
|
|
|
1198
1208
|
const tools = [
|
|
1209
|
+
{
|
|
1210
|
+
name: "shepherd_onboarding_guide",
|
|
1211
|
+
description: "LOCAL agent-facing Shepherd onboarding workflow. Use when setting up Shepherd, choosing sources, deciding usage-skill/MCP install targets, or recovering the next onboarding step.",
|
|
1212
|
+
inputSchema: emptyInputSchema,
|
|
1213
|
+
annotations: readOnlyAnnotations,
|
|
1214
|
+
_meta: { provider: "local_npm", command: `${agentCommand()} guide` },
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
name: "shepherd_troubleshoot",
|
|
1218
|
+
description: "LOCAL state-aware Shepherd onboarding/setup help. Use when confused, blocked, auth expired, source setup is pending, local sync is unhealthy, or the wiki is still building.",
|
|
1219
|
+
inputSchema: emptyInputSchema,
|
|
1220
|
+
annotations: readOnlyAnnotations,
|
|
1221
|
+
_meta: { provider: "local_npm", command: `${agentCommand()} troubleshoot` },
|
|
1222
|
+
},
|
|
1199
1223
|
{
|
|
1200
1224
|
name: "shepherd_status",
|
|
1201
1225
|
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 Shepherd CLI; do not use production memory/wiki tools or shell/file exploration for local setup status.",
|
|
@@ -1274,7 +1298,7 @@ function localMcpInstructions(remoteInstructions, remoteConnectError, opts = {})
|
|
|
1274
1298
|
const environmentLabel = opts.environment ? mcpEnvironmentLabel(opts.environment, opts.mcpUrl) : "saved Shepherd MCP endpoint";
|
|
1275
1299
|
return [
|
|
1276
1300
|
"This MCP server is the local Shepherd CLI wrapper plus deployed Shepherd memory/wiki tools.",
|
|
1277
|
-
`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 Shepherd CLI status/setup flow. The Shepherd CLI is the only component that may perform bounded local checks of Shepherd state, LaunchAgents, and known Codex/Claude session locations.`,
|
|
1301
|
+
`For agent-led onboarding, source choice, usage-skill install targets, MCP install targets, or "what step is next", use shepherd_onboarding_guide first. If setup is blocked or confusing, use shepherd_troubleshoot. 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 Shepherd CLI status/setup flow. The Shepherd CLI is the only component that may perform bounded local checks of Shepherd state, LaunchAgents, and known Codex/Claude session locations.`,
|
|
1278
1302
|
"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 Shepherd CLI status command.",
|
|
1279
1303
|
`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()} login if needed, ${agentCommand()} onboard --add-sources coding-sessions --name "<full_name>" --org "<organization>", ${agentCommand()} continue, then ${agentCommand()} status.`,
|
|
1280
1304
|
"For memory/wiki questions when readiness is uncertain, call shepherd_wiki_status first. If it or any deployed Shepherd tool returns status: \"wiki_not_ready\", do not answer the underlying memory/wiki question yet; report the readiness progress/ETA and ask the user to retry when the initial build is ready.",
|
|
@@ -1289,6 +1313,16 @@ function localMcpInstructions(remoteInstructions, remoteConnectError, opts = {})
|
|
|
1289
1313
|
}
|
|
1290
1314
|
|
|
1291
1315
|
async function callLocalMcpTool(name, context = {}) {
|
|
1316
|
+
if (name === "shepherd_onboarding_guide") {
|
|
1317
|
+
const status = await collectShepherdStatus();
|
|
1318
|
+
return localMcpTextResult(renderOnboardingGuideMcpResult(status));
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
if (name === "shepherd_troubleshoot") {
|
|
1322
|
+
const status = await collectShepherdStatus();
|
|
1323
|
+
return localMcpTextResult(renderTroubleshootMcpResult(status));
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1292
1326
|
if (name === "shepherd_status" || name === "shepherd_local_status") {
|
|
1293
1327
|
const status = await collectShepherdStatus();
|
|
1294
1328
|
return localMcpTextResult([
|
|
@@ -1393,6 +1427,99 @@ function renderCodingSessionsSetupMcpResult(status) {
|
|
|
1393
1427
|
].join("\n");
|
|
1394
1428
|
}
|
|
1395
1429
|
|
|
1430
|
+
function renderOnboardingGuideMcpResult(status) {
|
|
1431
|
+
return [
|
|
1432
|
+
"Shepherd onboarding workflow",
|
|
1433
|
+
"",
|
|
1434
|
+
"Use the wrapper workflow as the source of truth for agent-led onboarding:",
|
|
1435
|
+
` ${agentCommand()} guide`,
|
|
1436
|
+
"",
|
|
1437
|
+
"High-level flow:",
|
|
1438
|
+
"1. Ask where to install the Shepherd usage skill: Codex, Claude Code, both, or skip.",
|
|
1439
|
+
` Command: ${agentCommand()} skill --install <codex|claude|all>`,
|
|
1440
|
+
"2. Ask source choices with a native multi-select UI/control when available.",
|
|
1441
|
+
" Offer only sources the guide says are available. Ask explicit consent before Messages or Coding Sessions.",
|
|
1442
|
+
`3. Authenticate the Shepherd account with WorkOS: ${agentCommand()} login`,
|
|
1443
|
+
`4. Start onboarding with confirmed full name, org, and selected sources: ${agentCommand()} onboard --name "<full_name>" --org "<organization>" --sources "<sources>"`,
|
|
1444
|
+
`5. After each browser/admin/PAT/local permission step, run: ${agentCommand()} continue`,
|
|
1445
|
+
`6. Verify local setup and readiness: ${agentCommand()} status, ${agentCommand()} shepherd_wiki_status, ${agentCommand()} tools --json`,
|
|
1446
|
+
`7. Ask where to install Shepherd MCP for querying: ${agentCommand()} mcp-login --install <codex|claude|all>`,
|
|
1447
|
+
"",
|
|
1448
|
+
"Messages guardrails: require explicit consent, Full Disk Access, and selected chats from messages-chats; never sync all chats by default.",
|
|
1449
|
+
"Coding Sessions guardrails: require explicit consent; Shepherd syncs bounded redacted Codex/Claude Code session metadata, not raw logs.",
|
|
1450
|
+
"Wiki readiness guardrail: if shepherd_wiki_status returns wiki_not_ready, say Shepherd is still learning and include progress/ETA instead of answering the memory question.",
|
|
1451
|
+
"",
|
|
1452
|
+
"Current local status:",
|
|
1453
|
+
renderShepherdStatus(status),
|
|
1454
|
+
].join("\n");
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function renderTroubleshootMcpResult(status) {
|
|
1458
|
+
const diagnostics = [];
|
|
1459
|
+
if (!status.configured || !status.account) {
|
|
1460
|
+
diagnostics.push([
|
|
1461
|
+
"No saved Shepherd onboarding session.",
|
|
1462
|
+
`Run ${agentCommand()} login, then ${agentCommand()} guide.`,
|
|
1463
|
+
]);
|
|
1464
|
+
}
|
|
1465
|
+
if (status.productionError) {
|
|
1466
|
+
diagnostics.push([
|
|
1467
|
+
`Backend status is unavailable: ${status.productionError}`,
|
|
1468
|
+
`Run ${agentCommand()} login, then ${agentCommand()} continue.`,
|
|
1469
|
+
]);
|
|
1470
|
+
}
|
|
1471
|
+
for (const source of statusSourceRows(status.providers, status.savedSources).filter((row) => row.selected && !row.connected)) {
|
|
1472
|
+
diagnostics.push([
|
|
1473
|
+
`${source.label} is selected but not connected.`,
|
|
1474
|
+
`Run ${agentCommand()} continue and complete the current modality it prints.`,
|
|
1475
|
+
]);
|
|
1476
|
+
}
|
|
1477
|
+
if (status.local.messages.configPath && status.local.messages.storage?.readable === false) {
|
|
1478
|
+
diagnostics.push([
|
|
1479
|
+
"Messages database is not readable.",
|
|
1480
|
+
`Grant Full Disk Access to the onboarding app and Node.js, then run ${agentCommand()} messages-chats.`,
|
|
1481
|
+
]);
|
|
1482
|
+
}
|
|
1483
|
+
if (status.local.messages.configPath && status.local.messages.launch?.running === false) {
|
|
1484
|
+
diagnostics.push([
|
|
1485
|
+
"Messages LaunchAgent is installed but not running.",
|
|
1486
|
+
`Run ${agentCommand()} status after fixing Full Disk Access; rerun continue if status asks for it.`,
|
|
1487
|
+
]);
|
|
1488
|
+
}
|
|
1489
|
+
if (status.local.codingSessions.configPath && status.local.codingSessions.launch?.running === false) {
|
|
1490
|
+
diagnostics.push([
|
|
1491
|
+
"Coding Sessions LaunchAgent is installed but not running.",
|
|
1492
|
+
`Run ${agentCommand()} coding-sessions-status, then ${agentCommand()} continue if status asks for setup repair.`,
|
|
1493
|
+
]);
|
|
1494
|
+
}
|
|
1495
|
+
const readiness = wikiReadinessPayloadFromStatus(status);
|
|
1496
|
+
if (readiness.status === "wiki_not_ready") {
|
|
1497
|
+
diagnostics.push([
|
|
1498
|
+
"Wiki is still building.",
|
|
1499
|
+
"This is not a setup failure. Tell the user Shepherd is still learning and include progress/ETA from shepherd_wiki_status.",
|
|
1500
|
+
]);
|
|
1501
|
+
}
|
|
1502
|
+
if (diagnostics.length === 0) {
|
|
1503
|
+
diagnostics.push([
|
|
1504
|
+
"No obvious local onboarding blocker found.",
|
|
1505
|
+
`Run ${agentCommand()} tools --json and use exact listed tool names.`,
|
|
1506
|
+
]);
|
|
1507
|
+
}
|
|
1508
|
+
return [
|
|
1509
|
+
"Shepherd troubleshooting",
|
|
1510
|
+
"",
|
|
1511
|
+
...diagnostics.flatMap(([symptom, fix], index) => [
|
|
1512
|
+
`${index + 1}. ${symptom}`,
|
|
1513
|
+
` Fix: ${fix}`,
|
|
1514
|
+
]),
|
|
1515
|
+
"",
|
|
1516
|
+
"Current local status:",
|
|
1517
|
+
renderShepherdStatus(status),
|
|
1518
|
+
"",
|
|
1519
|
+
`Full workflow: ${agentCommand()} guide`,
|
|
1520
|
+
].join("\n");
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1396
1523
|
function localMcpTextResult(text, isError = false) {
|
|
1397
1524
|
return {
|
|
1398
1525
|
content: [{ type: "text", text }],
|
|
@@ -1546,10 +1673,14 @@ function printMcpInstallResults(results) {
|
|
|
1546
1673
|
async function continueAgentOnboarding() {
|
|
1547
1674
|
let state = await readAgentState();
|
|
1548
1675
|
const body = { sessionToken: state.sessionToken };
|
|
1676
|
+
const githubToken = stringArg("github-token");
|
|
1677
|
+
const githubRepoFullName = stringArg("github-repo");
|
|
1549
1678
|
const granolaApiKey = stringArg("granola-api-key");
|
|
1550
1679
|
const messagesHandles = parseMessageHandleList(stringArg("messages-handle"));
|
|
1551
1680
|
const messagesHandle = messagesHandles[0] ?? null;
|
|
1552
1681
|
const selectedMessageChatIds = parseMessageChatIdsArg();
|
|
1682
|
+
if (githubToken) body.githubToken = githubToken;
|
|
1683
|
+
if (githubRepoFullName) body.githubRepoFullName = githubRepoFullName;
|
|
1553
1684
|
if (granolaApiKey) body.granolaApiKey = granolaApiKey;
|
|
1554
1685
|
if (messagesHandle) body.imessage = { handle: messagesHandle, handles: messagesHandles };
|
|
1555
1686
|
if (state.sources.messages && messagesHandle && selectedMessageChatIds.length === 0) {
|
|
@@ -1636,7 +1767,7 @@ async function continueAgentOnboarding() {
|
|
|
1636
1767
|
processing: finalized.processing,
|
|
1637
1768
|
errors: errors ? safeErrorRecord(errors) : undefined,
|
|
1638
1769
|
currentAction,
|
|
1639
|
-
nextCommand: errors ? `${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"` : undefined,
|
|
1770
|
+
nextCommand: errors ? `${agentCommand()} agent --continue --github-token "<github_pat>" --github-repo "<owner/repo>" --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"` : undefined,
|
|
1640
1771
|
mcpInstall: errors ? undefined : {
|
|
1641
1772
|
prompt: "Ask where to install Shepherd MCP for this customer: Codex, Claude Code, both, or none.",
|
|
1642
1773
|
targets: MCP_INSTALL_TARGETS,
|
|
@@ -1657,7 +1788,7 @@ async function continueAgentOnboarding() {
|
|
|
1657
1788
|
});
|
|
1658
1789
|
|
|
1659
1790
|
console.log("\nAfter that modality is complete, rerun:");
|
|
1660
|
-
console.log(` ${agentCommand()} agent --continue --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"`);
|
|
1791
|
+
console.log(` ${agentCommand()} agent --continue --github-token "<github_pat>" --github-repo "<owner/repo>" --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"`);
|
|
1661
1792
|
console.log(" Omit either optional flag if that source is not being connected.");
|
|
1662
1793
|
return;
|
|
1663
1794
|
}
|
|
@@ -1863,7 +1994,18 @@ function renderLocalCodingSessionsStatus(status) {
|
|
|
1863
1994
|
lines.push(` ${probe.provider}: ${probe.path} ${probe.readable ? "readable" : `not readable (${probe.reason})`}`);
|
|
1864
1995
|
}
|
|
1865
1996
|
if (status.lastSync) {
|
|
1866
|
-
|
|
1997
|
+
const uploadable = status.lastSync.uploadable ?? status.lastSync.scanned ?? 0;
|
|
1998
|
+
const ignored = status.lastSync.filesIgnored ?? null;
|
|
1999
|
+
const ignoredText = ignored == null ? "" : `, ${ignored} ignored`;
|
|
2000
|
+
lines.push(` Last sync: ${status.lastSync.finishedAt ?? "unknown"} (${uploadable} uploadable, ${status.lastSync.changed ?? 0} changed${ignoredText})`);
|
|
2001
|
+
const providerStatus = status.lastSync.providerStatus && typeof status.lastSync.providerStatus === "object" && !Array.isArray(status.lastSync.providerStatus)
|
|
2002
|
+
? status.lastSync.providerStatus
|
|
2003
|
+
: {};
|
|
2004
|
+
for (const provider of ["codex", "claude"]) {
|
|
2005
|
+
const info = providerStatus[provider];
|
|
2006
|
+
if (!info || typeof info !== "object" || Array.isArray(info)) continue;
|
|
2007
|
+
lines.push(` ${provider}: ${info.uploadable ?? info.scanned ?? 0} uploadable${info.filesIgnored == null ? "" : `, ${info.filesIgnored} ignored`}`);
|
|
2008
|
+
}
|
|
1867
2009
|
} else {
|
|
1868
2010
|
lines.push(" Last sync: none recorded");
|
|
1869
2011
|
}
|
|
@@ -2185,6 +2327,8 @@ async function runMessagesAgent() {
|
|
|
2185
2327
|
const userId = requiredConfigString(config.userId, "userId");
|
|
2186
2328
|
const agentToken = requiredConfigString(config.agentToken, "agentToken");
|
|
2187
2329
|
const channel = setRuntimeLaunchAgentChannel(config.channel);
|
|
2330
|
+
const deviceId = loadOrCreateShepherdDeviceId(channel);
|
|
2331
|
+
const agentMetadata = { deviceId, agentVersion: PACKAGE_VERSION, agentChannel: channel };
|
|
2188
2332
|
mergeShepherdOwnedMessageHandles(config.excludedMessageHandles);
|
|
2189
2333
|
const backfillOverride = args["backfill-days"] ?? process.env.SHEPHERD_BACKFILL_DAYS;
|
|
2190
2334
|
const backfillExplicit = backfillOverride !== undefined && backfillOverride !== null && backfillOverride !== "";
|
|
@@ -2199,9 +2343,9 @@ async function runMessagesAgent() {
|
|
|
2199
2343
|
|
|
2200
2344
|
const kit = await import("@photon-ai/imessage-kit");
|
|
2201
2345
|
const sdk = new kit.IMessageSDK({ debug: args.debug === true });
|
|
2202
|
-
const sender = new MessagesBatchSender(apiUrl, agentToken, userId, channel);
|
|
2346
|
+
const sender = new MessagesBatchSender(apiUrl, agentToken, userId, channel, agentMetadata);
|
|
2203
2347
|
const contactLookup = createMutableContactLookup(buildContactLookup({ userId, channel }));
|
|
2204
|
-
const serializer = createMessageSerializer(kit, contactLookup);
|
|
2348
|
+
const serializer = createMessageSerializer(kit, contactLookup, agentMetadata);
|
|
2205
2349
|
const stateCache = createMessageStateCache(userId, MESSAGES_STATE_CACHE_MAX, channel);
|
|
2206
2350
|
const backfillGate = createMessagesBackfillGate();
|
|
2207
2351
|
const contactSync = startMessagesContactSync(sender, contactLookup, {
|
|
@@ -2423,10 +2567,15 @@ async function runCodingSessionsAgent() {
|
|
|
2423
2567
|
startedAt,
|
|
2424
2568
|
finishedAt: new Date().toISOString(),
|
|
2425
2569
|
scanned: scan.sessions.length,
|
|
2570
|
+
uploadable: scan.sessions.length,
|
|
2571
|
+
filesDiscovered: scan.filesDiscovered ?? scan.sessions.length,
|
|
2572
|
+
filesSelected: scan.filesSelected ?? scan.sessions.length,
|
|
2573
|
+
filesIgnored: Math.max(0, Number(scan.filesDiscovered ?? scan.sessions.length) - scan.sessions.length),
|
|
2426
2574
|
changed: changed.length,
|
|
2427
2575
|
selected: selected.length,
|
|
2428
2576
|
sent: sendResult,
|
|
2429
2577
|
parserVersion: CODING_SESSION_PARSER_VERSION,
|
|
2578
|
+
providerStatus: codingSessionsProviderStatus(scan, previous, selected),
|
|
2430
2579
|
command: command ? {
|
|
2431
2580
|
id: command.id,
|
|
2432
2581
|
type: command.type,
|
|
@@ -6423,6 +6572,8 @@ Options:
|
|
|
6423
6572
|
--email <email> Advanced: must match the WorkOS-authenticated email.
|
|
6424
6573
|
--name <name> Full name.
|
|
6425
6574
|
--org <name> Organization name.
|
|
6575
|
+
--github-token <token> GitHub PAT for repository API and webhook setup.
|
|
6576
|
+
--github-repo <owner/repo> GitHub repository to sync with webhook coverage.
|
|
6426
6577
|
--granola-api-key <key> Legacy Granola API key fallback.
|
|
6427
6578
|
--messages-handle <value> Messages phone number or Apple ID email; comma-separate both if needed.
|
|
6428
6579
|
--messages-chat-ids <ids> Comma-separated local Messages chat IDs selected from messages-chats, or all to watch every current and future chat.
|
|
@@ -6435,7 +6586,7 @@ Options:
|
|
|
6435
6586
|
--no-open-granola Do not open Granola OAuth/API key setup.
|
|
6436
6587
|
--no-messages Skip local Messages.
|
|
6437
6588
|
--coding-sessions Opt in to local Codex/Claude Code session metadata sync.
|
|
6438
|
-
--sources <list> Exact sources to connect: google,notion,slack,granola,messages,coding-sessions,all.
|
|
6589
|
+
--sources <list> Exact sources to connect: google,notion,slack,github,granola,messages,coding-sessions,all.
|
|
6439
6590
|
--add-sources <list> Same as --sources, named for second-time onboarding.
|
|
6440
6591
|
--no-install-messages-agent
|
|
6441
6592
|
Save Messages credentials without starting launchd.
|
|
@@ -6560,6 +6711,7 @@ function printAgentContract() {
|
|
|
6560
6711
|
optionalContinueArgs: [
|
|
6561
6712
|
"--messages-handle \"<phone_or_apple_id[,apple_id]>\" if local Messages is being connected",
|
|
6562
6713
|
"--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",
|
|
6714
|
+
"--github-token \"<github_pat>\" and --github-repo \"<owner/repo>\" if GitHub is being connected",
|
|
6563
6715
|
"--granola-api-key \"<legacy_granola_key>\" only when Granola falls back to legacy API-key setup",
|
|
6564
6716
|
],
|
|
6565
6717
|
statusCommand: `${command} status`,
|
|
@@ -6609,7 +6761,7 @@ Common user requests:
|
|
|
6609
6761
|
|
|
6610
6762
|
Start with selection questions to determine intent:
|
|
6611
6763
|
1. Organization: Join existing org, or Create new org.
|
|
6612
|
-
2. Sources: Google Workspace (Gmail/Drive/Docs/Calendar/Sheets/Slides/Tasks/Contacts), Notion, Slack, Granola, Messages, Coding Sessions (Codex/Claude Code metadata). Allow multi-select if your interface supports it.
|
|
6764
|
+
2. Sources: Google Workspace (Gmail/Drive/Docs/Calendar/Sheets/Slides/Tasks/Contacts), Notion, Slack, GitHub, Granola, Messages, Coding Sessions (Codex/Claude Code metadata). Allow multi-select if your interface supports it.
|
|
6613
6765
|
3. Messages, if selected: Skip Messages, or Provide handle.
|
|
6614
6766
|
4. MCP install after onboarding completes: Codex, Claude Code, both, or none.
|
|
6615
6767
|
|
|
@@ -6651,7 +6803,7 @@ Add skip flags for sources the user did not select:
|
|
|
6651
6803
|
Or pass an exact source list, especially for adding sources later:
|
|
6652
6804
|
${command} agent --add-sources coding-sessions --name "<full_name>" --org "<organization>"
|
|
6653
6805
|
|
|
6654
|
-
That command creates/reuses the customer user and org, saves local state, and opens at most one source setup surface. It works one modality at a time after account setup: Google Workspace, then Notion, then Slack, then Granola. If Messages details are still missing, it prints the Messages selector command instead of opening another auth surface. Do not manually open later source setup surfaces until the command tells you that source is the current modality.
|
|
6806
|
+
That command creates/reuses the customer user and org, saves local state, and opens at most one source setup surface. It works one modality at a time after account setup: Google Workspace, then Notion, then Slack, then GitHub, then Granola. If Messages details are still missing, it prints the Messages selector command instead of opening another auth surface. Do not manually open later source setup surfaces until the command tells you that source is the current modality.
|
|
6655
6807
|
|
|
6656
6808
|
If Google Workspace is the current modality, the setup command opens the Admin Console domain-wide delegation page. Show this setup to the user and have their Google Workspace super admin authorize it:
|
|
6657
6809
|
|
|
@@ -6669,6 +6821,8 @@ Shepherd must still enforce selected users and groups internally before polling
|
|
|
6669
6821
|
|
|
6670
6822
|
If Slack is the current modality and your browser automation can complete that auth screen, do it. If it cannot click through OAuth screens, leave the opened browser tab for the user and ask them to complete Slack auth. Do not open Granola or Messages until Slack is complete and the continue command advances.
|
|
6671
6823
|
|
|
6824
|
+
If GitHub is the current modality, do not use browser OAuth as the primary setup. Ask the user for a GitHub fine-grained or classic PAT and exact owner/repo, then continue with --github-token and --github-repo. Shepherd uses this path to create the repository webhook; OAuth-only GitHub is tools-only and does not provide full event sync coverage.
|
|
6825
|
+
|
|
6672
6826
|
If Granola is the current modality, the command opens Granola browser authorization when OAuth is configured. Leave the browser tab for the user and ask them to complete authorization before continuing.
|
|
6673
6827
|
|
|
6674
6828
|
If the command reports the legacy Granola API-key fallback, it opens the Granola desktop app. If your local app automation can navigate it, go to:
|
|
@@ -6680,7 +6834,7 @@ If the legacy Granola API-key fallback is needed and Granola did not come forwar
|
|
|
6680
6834
|
That command opens Granola and tries to navigate to Settings -> Connectors -> API keys. If your tool cannot click inside Granola, leave Granola open and ask the user to go to that screen.
|
|
6681
6835
|
|
|
6682
6836
|
After the current modality is complete, run:
|
|
6683
|
-
${payload.continueCommand} --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"
|
|
6837
|
+
${payload.continueCommand} --github-token "<github_pat>" --github-repo "<owner/repo>" --messages-handle "<phone_or_apple_id[,apple_id]>" --messages-chat-ids "<comma_separated_chat_ids_or_all>" --granola-api-key "<legacy_granola_key>"
|
|
6684
6838
|
|
|
6685
6839
|
Omit any optional flag that is not being requested. Do not pass --granola-api-key after successful Granola browser authorization.
|
|
6686
6840
|
|
|
@@ -6867,6 +7021,32 @@ function channelRecord(channel) {
|
|
|
6867
7021
|
return channel === STABLE_CHANNEL ? {} : { channel };
|
|
6868
7022
|
}
|
|
6869
7023
|
|
|
7024
|
+
function shepherdDeviceIdFile(channel = cliChannel()) {
|
|
7025
|
+
const dir = join(homedir(), ".shepherd");
|
|
7026
|
+
mkdirSync(dir, { recursive: true });
|
|
7027
|
+
return join(dir, `${channelFilePrefix(normalizeLaunchAgentChannel(channel))}device-id`);
|
|
7028
|
+
}
|
|
7029
|
+
|
|
7030
|
+
function loadOrCreateShepherdDeviceId(channel = cliChannel()) {
|
|
7031
|
+
const path = shepherdDeviceIdFile(channel);
|
|
7032
|
+
try {
|
|
7033
|
+
const existing = readFileSync(path, "utf8").trim();
|
|
7034
|
+
if (isUuid(existing)) return existing.toLowerCase();
|
|
7035
|
+
} catch {
|
|
7036
|
+
// Missing or unreadable device id will be repaired below.
|
|
7037
|
+
}
|
|
7038
|
+
|
|
7039
|
+
const next = randomUUID();
|
|
7040
|
+
const tmpFile = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
7041
|
+
writeFileSync(tmpFile, `${next}\n`, { mode: 0o600 });
|
|
7042
|
+
renameSync(tmpFile, path);
|
|
7043
|
+
return next;
|
|
7044
|
+
}
|
|
7045
|
+
|
|
7046
|
+
function isUuid(value) {
|
|
7047
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(value ?? "").trim());
|
|
7048
|
+
}
|
|
7049
|
+
|
|
6870
7050
|
async function updateAgentStateFromOnboardingResponse(state, response) {
|
|
6871
7051
|
const authUrls = stringRecord(response?.authUrls);
|
|
6872
7052
|
const hasAuthUrls = Object.keys(authUrls).length > 0;
|
|
@@ -7012,17 +7192,13 @@ async function openNextAgentModality({ sources, authUrls = {}, noOpen = false, p
|
|
|
7012
7192
|
}
|
|
7013
7193
|
|
|
7014
7194
|
if (source === "github") {
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
};
|
|
7023
|
-
}
|
|
7024
|
-
await openOrPrint(url, { noOpen });
|
|
7025
|
-
return { source, label: "GitHub", opened: !noOpen, url };
|
|
7195
|
+
return {
|
|
7196
|
+
source,
|
|
7197
|
+
label: "GitHub",
|
|
7198
|
+
opened: false,
|
|
7199
|
+
patRequired: true,
|
|
7200
|
+
message: "Provide --github-token and --github-repo so Shepherd can create the repository webhook; OAuth alone is tools-only and does not provide full event sync coverage.",
|
|
7201
|
+
};
|
|
7026
7202
|
}
|
|
7027
7203
|
|
|
7028
7204
|
if (source === "granola") {
|
|
@@ -7104,14 +7280,8 @@ function printAgentCurrentAction(action, opts = {}) {
|
|
|
7104
7280
|
}
|
|
7105
7281
|
|
|
7106
7282
|
if (action.source === "github") {
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
} else if (action.url) {
|
|
7110
|
-
console.log(`GitHub authorization URL: ${action.url}`);
|
|
7111
|
-
} else if (action.message) {
|
|
7112
|
-
console.log(action.message);
|
|
7113
|
-
}
|
|
7114
|
-
console.log("Ask the user to complete GitHub authorization before opening another source.");
|
|
7283
|
+
console.log(action.message ?? "Provide a GitHub PAT and owner/repo for webhook-backed event sync.");
|
|
7284
|
+
console.log("Do not use browser OAuth as the primary GitHub setup path for event coverage.");
|
|
7115
7285
|
return;
|
|
7116
7286
|
}
|
|
7117
7287
|
|
|
@@ -7148,7 +7318,7 @@ function agentNeedsUserAction(sources, action) {
|
|
|
7148
7318
|
if (action.source === "google") return ["Have the customer's Google Workspace super admin authorize Shepherd's domain-wide delegation Client ID and scopes in Google Admin Console."];
|
|
7149
7319
|
if (action.source === "notion") return ["Complete Notion browser authorization."];
|
|
7150
7320
|
if (action.source === "slack") return ["Complete Slack browser authorization."];
|
|
7151
|
-
if (action.source === "github") return ["
|
|
7321
|
+
if (action.source === "github") return ["Provide --github-token and --github-repo so Shepherd can create the repository webhook; OAuth alone is tools-only and does not provide full event sync coverage."];
|
|
7152
7322
|
if (action.source === "granola") return [action.url ? "Complete Granola browser authorization." : "Create/copy a legacy Granola API key from the Granola Mac app."];
|
|
7153
7323
|
if (action.source === "messages") return ["Grant or confirm macOS Full Disk Access for the onboarding app and Node.js, run messages-chats, have the user select local Messages contacts/groups in the browser, then pass the printed chat IDs with the Messages handle."];
|
|
7154
7324
|
if (action.source === "codingSessions") return ["Run the continue command to install local Codex and Claude Code session metadata sync."];
|
|
@@ -9428,6 +9598,9 @@ function startMessagesCommandPoller(opts) {
|
|
|
9428
9598
|
allChats: opts.allChats,
|
|
9429
9599
|
allowedChatIds: opts.allowedChatIds,
|
|
9430
9600
|
queueDepth,
|
|
9601
|
+
deviceId: opts.sender.deviceId,
|
|
9602
|
+
agentVersion: opts.sender.agentVersion,
|
|
9603
|
+
agentChannel: opts.sender.agentChannel,
|
|
9431
9604
|
}),
|
|
9432
9605
|
});
|
|
9433
9606
|
clearMessagesAgentCommandState(opts.userId);
|
|
@@ -9534,6 +9707,9 @@ function messagesSourceSyncReceipt({
|
|
|
9534
9707
|
allChats,
|
|
9535
9708
|
allowedChatIds,
|
|
9536
9709
|
queueDepth,
|
|
9710
|
+
deviceId,
|
|
9711
|
+
agentVersion = PACKAGE_VERSION,
|
|
9712
|
+
agentChannel = cliChannel(),
|
|
9537
9713
|
}) {
|
|
9538
9714
|
const stored = countValue(result.totalStored);
|
|
9539
9715
|
const updated = countValue(result.totalUpdated);
|
|
@@ -9548,7 +9724,9 @@ function messagesSourceSyncReceipt({
|
|
|
9548
9724
|
sourceSyncRunId,
|
|
9549
9725
|
commandId: command.id,
|
|
9550
9726
|
provider: "imessage",
|
|
9551
|
-
|
|
9727
|
+
deviceId,
|
|
9728
|
+
agentVersion,
|
|
9729
|
+
agentChannel,
|
|
9552
9730
|
coverageFrom: coverageFrom ? coverageFrom.toISOString() : null,
|
|
9553
9731
|
coverageTo: coverageTo.toISOString(),
|
|
9554
9732
|
highWatermark: receiptWatermark(`${sourceSyncRunId}:imessage-rowid:${countValue(result.watermark)}`),
|
|
@@ -10246,11 +10424,20 @@ function snapshotContactMappings(mappings) {
|
|
|
10246
10424
|
return new Map(mappings.map((mapping) => [mapping.handle, mapping.name]));
|
|
10247
10425
|
}
|
|
10248
10426
|
|
|
10249
|
-
function createMessageSerializer(kit, contactLookup = emptyContactLookup()) {
|
|
10427
|
+
function createMessageSerializer(kit, contactLookup = emptyContactLookup(), agentMetadata = {}) {
|
|
10250
10428
|
const chatNames = new Map();
|
|
10251
10429
|
const isImageAttachment = kit.isImageAttachment ?? (() => false);
|
|
10252
10430
|
const isVideoAttachment = kit.isVideoAttachment ?? (() => false);
|
|
10253
10431
|
const isAudioAttachment = kit.isAudioAttachment ?? (() => false);
|
|
10432
|
+
const deviceId = typeof agentMetadata.deviceId === "string" && agentMetadata.deviceId.trim()
|
|
10433
|
+
? agentMetadata.deviceId.trim()
|
|
10434
|
+
: null;
|
|
10435
|
+
const agentVersion = typeof agentMetadata.agentVersion === "string" && agentMetadata.agentVersion.trim()
|
|
10436
|
+
? agentMetadata.agentVersion.trim()
|
|
10437
|
+
: PACKAGE_VERSION;
|
|
10438
|
+
const agentChannel = typeof agentMetadata.agentChannel === "string" && agentMetadata.agentChannel.trim()
|
|
10439
|
+
? normalizeLaunchAgentChannel(agentMetadata.agentChannel)
|
|
10440
|
+
: cliChannel();
|
|
10254
10441
|
|
|
10255
10442
|
return {
|
|
10256
10443
|
setChatName(chatId, name) {
|
|
@@ -10266,6 +10453,9 @@ function createMessageSerializer(kit, contactLookup = emptyContactLookup()) {
|
|
|
10266
10453
|
messageId: String(providerMessageGuid?.value ?? msg.messageId ?? msg.rowId),
|
|
10267
10454
|
providerMessageGuid: providerMessageGuid == null ? null : String(providerMessageGuid.value),
|
|
10268
10455
|
_provider_guid_explicit: providerMessageGuid == null ? null : providerMessageGuid.explicit,
|
|
10456
|
+
deviceId,
|
|
10457
|
+
agentVersion,
|
|
10458
|
+
agentChannel,
|
|
10269
10459
|
localRowId: Number(msg.rowId ?? 0),
|
|
10270
10460
|
rowId: Number(msg.rowId ?? 0),
|
|
10271
10461
|
text: msg.text ?? null,
|
|
@@ -10734,7 +10924,8 @@ function codingSessionsCommandScanOptions(command) {
|
|
|
10734
10924
|
if (type === "source_sync") {
|
|
10735
10925
|
return {
|
|
10736
10926
|
exhaustive: true,
|
|
10737
|
-
|
|
10927
|
+
maxFiles: maxFiles ?? DEFAULT_CODING_SESSION_FILES_PER_PROVIDER,
|
|
10928
|
+
...(providers.length ? { providers } : {}),
|
|
10738
10929
|
};
|
|
10739
10930
|
}
|
|
10740
10931
|
return {
|
|
@@ -10748,7 +10939,7 @@ function codingSessionsCommandPlan(command, scan, previousState) {
|
|
|
10748
10939
|
if (!command) return { sessions: [], force: false, exactSelection: false, result: base };
|
|
10749
10940
|
const payload = objectValue(command.payload) ?? {};
|
|
10750
10941
|
const type = stringValue(command.type);
|
|
10751
|
-
const maxFiles = positiveCommandInteger(payload.maxFiles) ??
|
|
10942
|
+
const maxFiles = positiveCommandInteger(payload.maxFiles) ?? DEFAULT_CODING_SESSION_FILES_PER_PROVIDER;
|
|
10752
10943
|
const force = type === "force_rescan" ? true : payload.force === false ? false : true;
|
|
10753
10944
|
|
|
10754
10945
|
if (type === "source_sync") {
|
|
@@ -10866,7 +11057,8 @@ function codingSessionsSourceSyncReceipt({
|
|
|
10866
11057
|
finishedAt,
|
|
10867
11058
|
coverageFrom: Object.prototype.hasOwnProperty.call(payload, "coverageFrom") ? stringValue(payload.coverageFrom) : null,
|
|
10868
11059
|
coverageTo: stringValue(payload.coverageTo) ?? finishedAt,
|
|
10869
|
-
fullInventory: true,
|
|
11060
|
+
fullInventory: scan.capped !== true,
|
|
11061
|
+
maxFilesPerProvider: positiveCommandInteger(payload.maxFiles) ?? DEFAULT_CODING_SESSION_FILES_PER_PROVIDER,
|
|
10870
11062
|
exhausted: failedCount === 0 && pendingUploads === 0,
|
|
10871
11063
|
capHit: scan.capped === true,
|
|
10872
11064
|
highWatermark,
|
|
@@ -10906,6 +11098,7 @@ function selectCodingSessionsPerProvider(sessions, maxFilesPerProvider) {
|
|
|
10906
11098
|
|
|
10907
11099
|
function codingSessionsCommandDiagnostics(scan, previousState) {
|
|
10908
11100
|
const providerCounts = {};
|
|
11101
|
+
const providerStatus = codingSessionsProviderStatus(scan, previousState, []);
|
|
10909
11102
|
let userMessages = 0;
|
|
10910
11103
|
let agentResponses = 0;
|
|
10911
11104
|
let sessionsWithVisibleTranscript = 0;
|
|
@@ -10922,7 +11115,10 @@ function codingSessionsCommandDiagnostics(scan, previousState) {
|
|
|
10922
11115
|
return {
|
|
10923
11116
|
parserVersion: CODING_SESSION_PARSER_VERSION,
|
|
10924
11117
|
scanned: scan.sessions.length,
|
|
11118
|
+
uploadable: scan.sessions.length,
|
|
10925
11119
|
filesDiscovered: scan.filesDiscovered ?? scan.sessions.length,
|
|
11120
|
+
filesSelected: scan.filesSelected ?? scan.sessions.length,
|
|
11121
|
+
filesIgnored: Math.max(0, Number(scan.filesDiscovered ?? scan.sessions.length) - scan.sessions.length),
|
|
10926
11122
|
filesParsed: scan.filesParsed ?? scan.sessions.length,
|
|
10927
11123
|
capped: scan.capped === true,
|
|
10928
11124
|
unchanged,
|
|
@@ -10931,11 +11127,42 @@ function codingSessionsCommandDiagnostics(scan, previousState) {
|
|
|
10931
11127
|
userMessages,
|
|
10932
11128
|
agentResponses,
|
|
10933
11129
|
providerCounts,
|
|
11130
|
+
providerStatus,
|
|
10934
11131
|
probes: sanitizeCodingSessionProbes(scan.probes),
|
|
10935
11132
|
errors: sanitizeCodingSessionErrors(scan.errors),
|
|
10936
11133
|
};
|
|
10937
11134
|
}
|
|
10938
11135
|
|
|
11136
|
+
function codingSessionsProviderStatus(scan, previousState, selectedSessions = []) {
|
|
11137
|
+
const status = {};
|
|
11138
|
+
const inventory = scan.providerStatus && typeof scan.providerStatus === "object" && !Array.isArray(scan.providerStatus)
|
|
11139
|
+
? scan.providerStatus
|
|
11140
|
+
: {};
|
|
11141
|
+
for (const provider of ["codex", "claude"]) {
|
|
11142
|
+
const providerSessions = scan.sessions.filter((session) => session.provider === provider);
|
|
11143
|
+
const selected = selectedSessions.filter((session) => session.provider === provider);
|
|
11144
|
+
const unchanged = providerSessions.filter((session) => previousState.hashes?.[session.sourcePathHash] === session.contentHash).length;
|
|
11145
|
+
const raw = inventory[provider] && typeof inventory[provider] === "object" && !Array.isArray(inventory[provider])
|
|
11146
|
+
? inventory[provider]
|
|
11147
|
+
: {};
|
|
11148
|
+
const filesDiscovered = countValue(raw.filesDiscovered);
|
|
11149
|
+
const filesSelected = countValue(raw.filesSelected);
|
|
11150
|
+
const uploadable = providerSessions.length;
|
|
11151
|
+
status[provider] = {
|
|
11152
|
+
filesDiscovered,
|
|
11153
|
+
filesSelected,
|
|
11154
|
+
filesIgnored: Math.max(0, filesDiscovered - uploadable),
|
|
11155
|
+
uploadable,
|
|
11156
|
+
scanned: uploadable,
|
|
11157
|
+
unchanged,
|
|
11158
|
+
changed: Math.max(0, uploadable - unchanged),
|
|
11159
|
+
selected: selected.length,
|
|
11160
|
+
capped: raw.capped === true,
|
|
11161
|
+
};
|
|
11162
|
+
}
|
|
11163
|
+
return status;
|
|
11164
|
+
}
|
|
11165
|
+
|
|
10939
11166
|
function sanitizeCodingSessionProbes(probes) {
|
|
10940
11167
|
if (!Array.isArray(probes)) return [];
|
|
10941
11168
|
return probes.map((probe) => ({
|
|
@@ -11008,7 +11235,9 @@ async function scanCodingSessions(config, previousState = { hashes: {} }, option
|
|
|
11008
11235
|
probes,
|
|
11009
11236
|
errors,
|
|
11010
11237
|
filesDiscovered: inventory.filesDiscovered,
|
|
11238
|
+
filesSelected: files.length,
|
|
11011
11239
|
filesParsed: sessions.length,
|
|
11240
|
+
providerStatus: inventory.providerStatus,
|
|
11012
11241
|
capped: inventory.capped,
|
|
11013
11242
|
sessions,
|
|
11014
11243
|
previousState,
|
|
@@ -11093,9 +11322,23 @@ async function codingSessionJsonlFiles({ codexDirs, claudeDir, errors, maxFilesP
|
|
|
11093
11322
|
...newestCodingSessionFiles(claudeFiles, maxFilesPerProvider),
|
|
11094
11323
|
].sort((a, b) => b.mtimeMs - a.mtimeMs || a.provider.localeCompare(b.provider) || a.path.localeCompare(b.path));
|
|
11095
11324
|
const filesDiscovered = codexFiles.length + claudeFiles.length;
|
|
11325
|
+
const providerStatus = {
|
|
11326
|
+
codex: {
|
|
11327
|
+
filesDiscovered: codexFiles.length,
|
|
11328
|
+
filesSelected: Math.min(codexFiles.length, maxFilesPerProvider),
|
|
11329
|
+
capped: codexFiles.length > maxFilesPerProvider,
|
|
11330
|
+
},
|
|
11331
|
+
claude: {
|
|
11332
|
+
filesDiscovered: claudeFiles.length,
|
|
11333
|
+
filesSelected: Math.min(claudeFiles.length, maxFilesPerProvider),
|
|
11334
|
+
capped: claudeFiles.length > maxFilesPerProvider,
|
|
11335
|
+
},
|
|
11336
|
+
};
|
|
11096
11337
|
return {
|
|
11097
11338
|
files,
|
|
11098
11339
|
filesDiscovered,
|
|
11340
|
+
filesSelected: files.length,
|
|
11341
|
+
providerStatus,
|
|
11099
11342
|
capped: files.length < filesDiscovered,
|
|
11100
11343
|
};
|
|
11101
11344
|
}
|
|
@@ -11749,6 +11992,11 @@ class MessagesBatchSender {
|
|
|
11749
11992
|
this.agentToken = agentToken;
|
|
11750
11993
|
this.userId = userId;
|
|
11751
11994
|
this.channel = normalizeLaunchAgentChannel(channel);
|
|
11995
|
+
this.deviceId = typeof options.deviceId === "string" && options.deviceId.trim() ? options.deviceId.trim() : null;
|
|
11996
|
+
this.agentVersion = typeof options.agentVersion === "string" && options.agentVersion.trim() ? options.agentVersion.trim() : PACKAGE_VERSION;
|
|
11997
|
+
this.agentChannel = typeof options.agentChannel === "string" && options.agentChannel.trim()
|
|
11998
|
+
? normalizeLaunchAgentChannel(options.agentChannel)
|
|
11999
|
+
: this.channel;
|
|
11752
12000
|
this.queueFile = join(homedir(), ".shepherd", "raw-messages", `${channelFilePrefix(this.channel)}${safeFileId(userId)}-queue.json`);
|
|
11753
12001
|
this.sendChain = Promise.resolve();
|
|
11754
12002
|
this.batchSize = clampInt(Number(options.batchSize ?? MESSAGES_UPLOAD_BATCH_SIZE), 1, 10_000);
|
|
@@ -11825,13 +12073,14 @@ class MessagesBatchSender {
|
|
|
11825
12073
|
}
|
|
11826
12074
|
|
|
11827
12075
|
async postBatch(messages) {
|
|
12076
|
+
const messagesWithMetadata = messages.map((message) => this.withAgentMetadata(message));
|
|
11828
12077
|
const res = await messagesAgentFetch(`${this.apiUrl}/api/imessage/ingest`, {
|
|
11829
12078
|
method: "POST",
|
|
11830
12079
|
headers: {
|
|
11831
12080
|
"Content-Type": "application/json",
|
|
11832
12081
|
"x-api-key": this.agentToken,
|
|
11833
12082
|
},
|
|
11834
|
-
body: JSON.stringify({ userId: this.userId, messages }),
|
|
12083
|
+
body: JSON.stringify({ userId: this.userId, messages: messagesWithMetadata }),
|
|
11835
12084
|
});
|
|
11836
12085
|
|
|
11837
12086
|
const json = await res.json().catch(() => ({}));
|
|
@@ -11882,13 +12131,14 @@ class MessagesBatchSender {
|
|
|
11882
12131
|
}
|
|
11883
12132
|
|
|
11884
12133
|
async postLocalStatus(status) {
|
|
12134
|
+
const statusWithMetadata = this.withAgentMetadata(publicMessagesBackfillStatus(status) ?? status);
|
|
11885
12135
|
const res = await messagesAgentFetch(`${this.apiUrl}/api/imessage/ingest/local-status`, {
|
|
11886
12136
|
method: "POST",
|
|
11887
12137
|
headers: {
|
|
11888
12138
|
"Content-Type": "application/json",
|
|
11889
12139
|
"x-api-key": this.agentToken,
|
|
11890
12140
|
},
|
|
11891
|
-
body: JSON.stringify({ status:
|
|
12141
|
+
body: JSON.stringify({ status: statusWithMetadata }),
|
|
11892
12142
|
});
|
|
11893
12143
|
|
|
11894
12144
|
const json = await res.json().catch(() => ({}));
|
|
@@ -11896,6 +12146,16 @@ class MessagesBatchSender {
|
|
|
11896
12146
|
return json;
|
|
11897
12147
|
}
|
|
11898
12148
|
|
|
12149
|
+
withAgentMetadata(value) {
|
|
12150
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
12151
|
+
return {
|
|
12152
|
+
...value,
|
|
12153
|
+
...(this.deviceId ? { deviceId: this.deviceId } : {}),
|
|
12154
|
+
agentVersion: this.agentVersion,
|
|
12155
|
+
agentChannel: this.agentChannel,
|
|
12156
|
+
};
|
|
12157
|
+
}
|
|
12158
|
+
|
|
11899
12159
|
async fetchCommand() {
|
|
11900
12160
|
const res = await messagesAgentFetch(`${this.apiUrl}/api/imessage/ingest/command`, {
|
|
11901
12161
|
headers: {
|