@korso/shepherd 0.4.5 → 0.6.0
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/LICENSE +661 -0
- package/dist/index.js +526 -116
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -7,11 +7,18 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
7
7
|
// src/config.ts
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
var ConfigSchema = z.object({
|
|
10
|
-
// Hard-required:
|
|
10
|
+
// Hard-required: Hub endpoint.
|
|
11
11
|
HUB_URL: z.string().min(1, "HUB_URL is required"),
|
|
12
|
-
|
|
12
|
+
// Auth credentials. Exactly one form is needed (enforced by the refine below):
|
|
13
|
+
// - SHEPHERD_TOKEN: the hosted Hub credential (carries its own workspace).
|
|
14
|
+
// - TEAM_TOKEN: the self-host credential.
|
|
15
|
+
// SHEPHERD_TOKEN wins when both are present (see the derived `authToken`).
|
|
16
|
+
SHEPHERD_TOKEN: z.string().min(1).optional(),
|
|
17
|
+
TEAM_TOKEN: z.string().min(1).optional(),
|
|
13
18
|
// Optional overrides — resolveContext will apply defaults for any that are absent.
|
|
14
19
|
// WORKSPACE default is applied in resolveContext (auto-detected from cwd basename).
|
|
20
|
+
// NOTE: WORKSPACE is IGNORED by the hosted Hub — the SHEPHERD_TOKEN carries the
|
|
21
|
+
// workspace identity. It remains meaningful only for self-host (TEAM_TOKEN) setups.
|
|
15
22
|
WORKSPACE: z.string().min(1).optional(),
|
|
16
23
|
REPO: z.string().min(1).optional(),
|
|
17
24
|
BRANCH: z.string().min(1).optional(),
|
|
@@ -28,10 +35,14 @@ var ConfigSchema = z.object({
|
|
|
28
35
|
// work/sync/done/announce tool results as before. Both the MCP server and the
|
|
29
36
|
// hook must agree on this path.
|
|
30
37
|
SHEPHERD_INBOX_DIR: z.string().min(1).optional()
|
|
38
|
+
}).refine((c) => Boolean(c.SHEPHERD_TOKEN || c.TEAM_TOKEN), {
|
|
39
|
+
message: "Either SHEPHERD_TOKEN or TEAM_TOKEN is required",
|
|
40
|
+
path: ["SHEPHERD_TOKEN"]
|
|
31
41
|
});
|
|
32
42
|
function parseConfig(env) {
|
|
33
|
-
|
|
43
|
+
const parsed = ConfigSchema.parse({
|
|
34
44
|
HUB_URL: env["HUB_URL"],
|
|
45
|
+
SHEPHERD_TOKEN: env["SHEPHERD_TOKEN"],
|
|
35
46
|
TEAM_TOKEN: env["TEAM_TOKEN"],
|
|
36
47
|
WORKSPACE: env["WORKSPACE"],
|
|
37
48
|
REPO: env["REPO"],
|
|
@@ -43,6 +54,8 @@ function parseConfig(env) {
|
|
|
43
54
|
HEARTBEAT_INTERVAL_SECONDS: env["HEARTBEAT_INTERVAL_SECONDS"],
|
|
44
55
|
SHEPHERD_INBOX_DIR: env["SHEPHERD_INBOX_DIR"]
|
|
45
56
|
});
|
|
57
|
+
const authToken = parsed.SHEPHERD_TOKEN ?? parsed.TEAM_TOKEN;
|
|
58
|
+
return { ...parsed, authToken };
|
|
46
59
|
}
|
|
47
60
|
function loadConfig(env = process.env) {
|
|
48
61
|
try {
|
|
@@ -82,51 +95,63 @@ var HubRequestError = class extends Error {
|
|
|
82
95
|
};
|
|
83
96
|
function createHubClient({
|
|
84
97
|
hubUrl,
|
|
85
|
-
|
|
98
|
+
token,
|
|
86
99
|
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
87
100
|
}) {
|
|
88
101
|
const baseUrl = hubUrl.replace(/\/$/, "");
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
async function request(method, path3, body) {
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
105
|
+
const headers = {
|
|
106
|
+
"Authorization": `Bearer ${token}`
|
|
107
|
+
};
|
|
108
|
+
if (method === "POST") {
|
|
109
|
+
headers["Content-Type"] = "application/json";
|
|
110
|
+
}
|
|
111
|
+
let response;
|
|
112
|
+
try {
|
|
113
|
+
response = await fetch(`${baseUrl}${path3}`, {
|
|
114
|
+
method,
|
|
115
|
+
headers,
|
|
116
|
+
...method === "POST" ? { body: JSON.stringify(body) } : {},
|
|
117
|
+
signal: controller.signal
|
|
118
|
+
});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
const message = err instanceof DOMException && err.name === "AbortError" ? `Hub request timed out after ${timeoutMs}ms (${path3})` : `Hub unreachable at ${baseUrl}${path3}: ${String(err)}`;
|
|
122
|
+
throw new HubUnreachable(message, err);
|
|
123
|
+
} finally {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
}
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
let detail = "";
|
|
94
128
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"Authorization": `Bearer ${teamToken}`,
|
|
99
|
-
"Content-Type": "application/json"
|
|
100
|
-
},
|
|
101
|
-
body: JSON.stringify(body),
|
|
102
|
-
signal: controller.signal
|
|
103
|
-
});
|
|
104
|
-
} catch (err) {
|
|
105
|
-
clearTimeout(timer);
|
|
106
|
-
const message = err instanceof DOMException && err.name === "AbortError" ? `Hub request timed out after ${timeoutMs}ms (${path2})` : `Hub unreachable at ${baseUrl}${path2}: ${String(err)}`;
|
|
107
|
-
throw new HubUnreachable(message, err);
|
|
108
|
-
} finally {
|
|
109
|
-
clearTimeout(timer);
|
|
110
|
-
}
|
|
111
|
-
if (!response.ok) {
|
|
112
|
-
let detail = "";
|
|
113
|
-
try {
|
|
114
|
-
const data = await response.json();
|
|
115
|
-
if (data && typeof data === "object" && "error" in data && typeof data.error === "string") {
|
|
116
|
-
detail = `: ${data.error}`;
|
|
117
|
-
}
|
|
118
|
-
} catch {
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
if (data && typeof data === "object" && "error" in data && typeof data.error === "string") {
|
|
131
|
+
detail = `: ${data.error}`;
|
|
119
132
|
}
|
|
120
|
-
|
|
121
|
-
response.status,
|
|
122
|
-
`Hub returned HTTP ${response.status} for ${path2}${detail}`
|
|
123
|
-
);
|
|
133
|
+
} catch {
|
|
124
134
|
}
|
|
125
|
-
|
|
135
|
+
throw new HubRequestError(
|
|
136
|
+
response.status,
|
|
137
|
+
`Hub returned HTTP ${response.status} for ${path3}${detail}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return response.json();
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
post(path3, body) {
|
|
144
|
+
return request("POST", path3, body);
|
|
145
|
+
},
|
|
146
|
+
get(path3) {
|
|
147
|
+
return request("GET", path3);
|
|
126
148
|
}
|
|
127
149
|
};
|
|
128
150
|
}
|
|
129
151
|
|
|
152
|
+
// src/tools.ts
|
|
153
|
+
import { z as z3 } from "zod";
|
|
154
|
+
|
|
130
155
|
// ../shared/dist/names.js
|
|
131
156
|
var adjectives = [
|
|
132
157
|
"Able",
|
|
@@ -499,10 +524,199 @@ var LeaveRequest = z2.object({
|
|
|
499
524
|
var LeaveResponse = z2.object({
|
|
500
525
|
ok: z2.literal(true)
|
|
501
526
|
});
|
|
527
|
+
var Role = z2.enum(["admin", "member"]);
|
|
528
|
+
var WorkspaceSummary = z2.object({
|
|
529
|
+
id: z2.string(),
|
|
530
|
+
slug: z2.string(),
|
|
531
|
+
name: z2.string(),
|
|
532
|
+
role: Role
|
|
533
|
+
});
|
|
534
|
+
var CreateWorkspaceRequest = z2.object({
|
|
535
|
+
name: z2.string().min(1)
|
|
536
|
+
});
|
|
537
|
+
var ListWorkspacesResponse = z2.object({
|
|
538
|
+
workspaces: z2.array(WorkspaceSummary)
|
|
539
|
+
});
|
|
540
|
+
var MintTokenRequest = z2.object({
|
|
541
|
+
name: z2.string().min(1).optional()
|
|
542
|
+
});
|
|
543
|
+
var MintTokenResponse = z2.object({
|
|
544
|
+
// The raw shp_ token, shown once at creation and never returned again.
|
|
545
|
+
token: z2.string(),
|
|
546
|
+
id: z2.string()
|
|
547
|
+
});
|
|
548
|
+
var TokenSummary = z2.object({
|
|
549
|
+
id: z2.string(),
|
|
550
|
+
name: z2.string().nullable(),
|
|
551
|
+
// ISO timestamp string (see IsoTimestamp note above) or null when unused / not revoked.
|
|
552
|
+
lastUsedAt: IsoTimestamp.nullable(),
|
|
553
|
+
createdAt: IsoTimestamp,
|
|
554
|
+
revokedAt: IsoTimestamp.nullable()
|
|
555
|
+
});
|
|
556
|
+
var ListTokensResponse = z2.object({
|
|
557
|
+
tokens: z2.array(TokenSummary)
|
|
558
|
+
});
|
|
559
|
+
var CreateInviteRequest = z2.object({
|
|
560
|
+
expiresInDays: z2.number().int().positive().optional(),
|
|
561
|
+
// Omitted = unlimited, redeemable until explicitly revoked. Pass a positive
|
|
562
|
+
// integer to cap it instead.
|
|
563
|
+
maxUses: z2.number().int().positive().optional()
|
|
564
|
+
});
|
|
565
|
+
var InviteResponse = z2.object({
|
|
566
|
+
code: z2.string(),
|
|
567
|
+
// ISO timestamp string, or null when the invite never expires.
|
|
568
|
+
expiresAt: IsoTimestamp.nullable(),
|
|
569
|
+
// null = unlimited (redeemable until revoked).
|
|
570
|
+
maxUses: z2.number().int().positive().nullable(),
|
|
571
|
+
useCount: z2.number().int().nonnegative()
|
|
572
|
+
});
|
|
573
|
+
var InviteByEmailRequest = z2.object({
|
|
574
|
+
email: z2.string().email()
|
|
575
|
+
});
|
|
576
|
+
var InviteByEmailResponse = z2.object({
|
|
577
|
+
email: z2.string(),
|
|
578
|
+
sentAt: IsoTimestamp
|
|
579
|
+
});
|
|
580
|
+
var RedeemInviteResponse = z2.object({
|
|
581
|
+
// The workspace the caller just joined.
|
|
582
|
+
workspace: WorkspaceSummary
|
|
583
|
+
});
|
|
584
|
+
var MemberSummary = z2.object({
|
|
585
|
+
accountId: z2.string(),
|
|
586
|
+
displayName: z2.string().nullable(),
|
|
587
|
+
githubLogin: z2.string().nullable(),
|
|
588
|
+
avatarUrl: z2.string().nullable(),
|
|
589
|
+
role: Role
|
|
590
|
+
});
|
|
591
|
+
var ListMembersResponse = z2.object({
|
|
592
|
+
members: z2.array(MemberSummary)
|
|
593
|
+
});
|
|
594
|
+
var FeedbackType = z2.enum(["bug", "suggestion", "other"]);
|
|
595
|
+
var FeedbackRequest = z2.object({
|
|
596
|
+
type: FeedbackType,
|
|
597
|
+
body: z2.string().trim().min(1).max(4e3)
|
|
598
|
+
});
|
|
599
|
+
var FeedbackResponse = z2.object({
|
|
600
|
+
ok: z2.literal(true),
|
|
601
|
+
// uuid PK (the feedback table, like workspaces, uses gen_random_uuid()).
|
|
602
|
+
id: z2.string()
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// src/marker.ts
|
|
606
|
+
import * as fs from "fs";
|
|
607
|
+
import * as path from "path";
|
|
608
|
+
var MARKER_FILENAME = ".shepherd";
|
|
609
|
+
function findRepoRoot(cwd) {
|
|
610
|
+
let dir = path.resolve(cwd);
|
|
611
|
+
for (; ; ) {
|
|
612
|
+
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
613
|
+
const parent = path.dirname(dir);
|
|
614
|
+
if (parent === dir) return null;
|
|
615
|
+
dir = parent;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function markerPath(cwd) {
|
|
619
|
+
const root = findRepoRoot(cwd);
|
|
620
|
+
return root === null ? null : path.join(root, MARKER_FILENAME);
|
|
621
|
+
}
|
|
622
|
+
function readMarker(cwd = process.cwd()) {
|
|
623
|
+
const file = markerPath(cwd);
|
|
624
|
+
if (file === null) return null;
|
|
625
|
+
let raw;
|
|
626
|
+
try {
|
|
627
|
+
raw = fs.readFileSync(file, "utf8");
|
|
628
|
+
} catch {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
const parsed = JSON.parse(raw);
|
|
633
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.workspace === "string" && parsed.workspace.length > 0) {
|
|
634
|
+
return { workspace: parsed.workspace };
|
|
635
|
+
}
|
|
636
|
+
return null;
|
|
637
|
+
} catch {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function writeMarker(cwd = process.cwd(), slug) {
|
|
642
|
+
const file = markerPath(cwd);
|
|
643
|
+
if (file === null) {
|
|
644
|
+
throw new Error("not inside a git repository \u2014 cannot write .shepherd marker");
|
|
645
|
+
}
|
|
646
|
+
fs.writeFileSync(file, JSON.stringify({ workspace: slug }) + "\n", "utf8");
|
|
647
|
+
}
|
|
648
|
+
function removeMarker(cwd = process.cwd()) {
|
|
649
|
+
const file = markerPath(cwd);
|
|
650
|
+
if (file === null) return;
|
|
651
|
+
try {
|
|
652
|
+
fs.rmSync(file, { force: true });
|
|
653
|
+
} catch {
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/declined.ts
|
|
658
|
+
import { createHash } from "crypto";
|
|
659
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
660
|
+
import { homedir, tmpdir } from "os";
|
|
661
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
662
|
+
function defaultDeclinedDir() {
|
|
663
|
+
let base = "";
|
|
664
|
+
try {
|
|
665
|
+
base = homedir();
|
|
666
|
+
} catch {
|
|
667
|
+
base = "";
|
|
668
|
+
}
|
|
669
|
+
if (!base) base = tmpdir();
|
|
670
|
+
return join2(base, ".shepherd", "declined");
|
|
671
|
+
}
|
|
672
|
+
function declinedFilePath(repoRoot, dir = defaultDeclinedDir()) {
|
|
673
|
+
let normalized = resolve2(repoRoot);
|
|
674
|
+
if (process.platform === "win32") normalized = normalized.toLowerCase();
|
|
675
|
+
const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
676
|
+
return join2(dir, hash);
|
|
677
|
+
}
|
|
678
|
+
function isDeclined(repoRoot, dir = defaultDeclinedDir()) {
|
|
679
|
+
const file = declinedFilePath(repoRoot, dir);
|
|
680
|
+
let raw;
|
|
681
|
+
try {
|
|
682
|
+
raw = readFileSync2(file, "utf8");
|
|
683
|
+
} catch {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
const parsed = JSON.parse(raw);
|
|
688
|
+
return typeof parsed?.declinedAt === "string";
|
|
689
|
+
} catch {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function setDeclined(repoRoot, dir = defaultDeclinedDir()) {
|
|
694
|
+
const file = declinedFilePath(repoRoot, dir);
|
|
695
|
+
try {
|
|
696
|
+
mkdirSync(dirname2(file), { recursive: true });
|
|
697
|
+
const payload = JSON.stringify({ declinedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
698
|
+
writeFileSync2(file, payload + "\n", "utf8");
|
|
699
|
+
} catch (err) {
|
|
700
|
+
console.error(
|
|
701
|
+
`[shepherd] declined-state write failed: ${err instanceof Error ? err.message : String(err)}`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function clearDeclined(repoRoot, dir = defaultDeclinedDir()) {
|
|
706
|
+
const file = declinedFilePath(repoRoot, dir);
|
|
707
|
+
if (!existsSync2(file)) return;
|
|
708
|
+
try {
|
|
709
|
+
rmSync2(file, { force: true });
|
|
710
|
+
} catch (err) {
|
|
711
|
+
console.error(
|
|
712
|
+
`[shepherd] declined-state clear failed: ${err instanceof Error ? err.message : String(err)}`
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
502
716
|
|
|
503
717
|
// src/gitContext.ts
|
|
504
718
|
import { execFileSync } from "child_process";
|
|
505
|
-
import * as
|
|
719
|
+
import * as path2 from "path";
|
|
506
720
|
var GIT_TIMEOUT_MS = 2e3;
|
|
507
721
|
var MAX_COMMITS = 100;
|
|
508
722
|
var MAX_PATHS_PER_COMMIT = 500;
|
|
@@ -550,7 +764,7 @@ function detectRepo(cwd = process.cwd()) {
|
|
|
550
764
|
}
|
|
551
765
|
const top = runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
552
766
|
if (top) {
|
|
553
|
-
const base =
|
|
767
|
+
const base = path2.basename(top);
|
|
554
768
|
if (base) return base;
|
|
555
769
|
}
|
|
556
770
|
return null;
|
|
@@ -745,37 +959,37 @@ async function buildChangeReport(cwd, config) {
|
|
|
745
959
|
}
|
|
746
960
|
|
|
747
961
|
// src/inbox.ts
|
|
748
|
-
import { createHash } from "crypto";
|
|
962
|
+
import { createHash as createHash2 } from "crypto";
|
|
749
963
|
import {
|
|
750
964
|
appendFileSync,
|
|
751
|
-
mkdirSync,
|
|
752
|
-
readFileSync,
|
|
965
|
+
mkdirSync as mkdirSync2,
|
|
966
|
+
readFileSync as readFileSync3,
|
|
753
967
|
renameSync,
|
|
754
|
-
rmSync,
|
|
755
|
-
existsSync
|
|
968
|
+
rmSync as rmSync3,
|
|
969
|
+
existsSync as existsSync3
|
|
756
970
|
} from "fs";
|
|
757
|
-
import { homedir, tmpdir } from "os";
|
|
758
|
-
import { dirname, join, resolve } from "path";
|
|
971
|
+
import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
|
|
972
|
+
import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
|
|
759
973
|
function defaultInboxDir() {
|
|
760
974
|
let base = "";
|
|
761
975
|
try {
|
|
762
|
-
base =
|
|
976
|
+
base = homedir2();
|
|
763
977
|
} catch {
|
|
764
978
|
base = "";
|
|
765
979
|
}
|
|
766
|
-
if (!base) base =
|
|
767
|
-
return
|
|
980
|
+
if (!base) base = tmpdir2();
|
|
981
|
+
return join3(base, ".shepherd", "inbox");
|
|
768
982
|
}
|
|
769
983
|
function inboxFilePath(dir, cwd) {
|
|
770
|
-
let normalized =
|
|
984
|
+
let normalized = resolve3(cwd);
|
|
771
985
|
if (process.platform === "win32") normalized = normalized.toLowerCase();
|
|
772
|
-
const hash =
|
|
773
|
-
return
|
|
986
|
+
const hash = createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
987
|
+
return join3(dir, `${hash}.jsonl`);
|
|
774
988
|
}
|
|
775
989
|
function appendAnnouncements(filePath, announcements) {
|
|
776
990
|
if (!announcements || announcements.length === 0) return;
|
|
777
991
|
try {
|
|
778
|
-
|
|
992
|
+
mkdirSync2(dirname3(filePath), { recursive: true });
|
|
779
993
|
const payload = announcements.map((a) => JSON.stringify(a)).join("\n") + "\n";
|
|
780
994
|
appendFileSync(filePath, payload, "utf8");
|
|
781
995
|
} catch {
|
|
@@ -785,17 +999,17 @@ function drainInbox(filePath) {
|
|
|
785
999
|
const tmp = `${filePath}.draining`;
|
|
786
1000
|
let raw = "";
|
|
787
1001
|
try {
|
|
788
|
-
if (
|
|
789
|
-
raw +=
|
|
790
|
-
|
|
1002
|
+
if (existsSync3(tmp)) {
|
|
1003
|
+
raw += readFileSync3(tmp, "utf8");
|
|
1004
|
+
rmSync3(tmp, { force: true });
|
|
791
1005
|
}
|
|
792
1006
|
} catch {
|
|
793
1007
|
}
|
|
794
1008
|
try {
|
|
795
|
-
if (
|
|
1009
|
+
if (existsSync3(filePath)) {
|
|
796
1010
|
renameSync(filePath, tmp);
|
|
797
|
-
raw +=
|
|
798
|
-
|
|
1011
|
+
raw += readFileSync3(tmp, "utf8");
|
|
1012
|
+
rmSync3(tmp, { force: true });
|
|
799
1013
|
}
|
|
800
1014
|
} catch {
|
|
801
1015
|
}
|
|
@@ -836,6 +1050,12 @@ function classifyJoinFailure(err) {
|
|
|
836
1050
|
}
|
|
837
1051
|
return "unknown";
|
|
838
1052
|
}
|
|
1053
|
+
function classifyActivateFailure(err) {
|
|
1054
|
+
if (err instanceof HubRequestError && (err.status === 403 || err.status === 404)) {
|
|
1055
|
+
return "workspaceRejected";
|
|
1056
|
+
}
|
|
1057
|
+
return classifyJoinFailure(err);
|
|
1058
|
+
}
|
|
839
1059
|
function joinFailureCause(reason) {
|
|
840
1060
|
switch (reason) {
|
|
841
1061
|
case "unreachable":
|
|
@@ -956,8 +1176,11 @@ function formatChangeRecords(records, cwd = process.cwd()) {
|
|
|
956
1176
|
if (lines.length === 0) return "";
|
|
957
1177
|
return "Unlanded changes touching your area (awareness only \u2014 these are not blockers):\n" + lines.join("\n");
|
|
958
1178
|
}
|
|
1179
|
+
function hubErrorDetail(err) {
|
|
1180
|
+
return err instanceof HubUnreachable || err instanceof HubRequestError ? err.message : String(err);
|
|
1181
|
+
}
|
|
959
1182
|
function degradedResult(err) {
|
|
960
|
-
const detail =
|
|
1183
|
+
const detail = hubErrorDetail(err);
|
|
961
1184
|
return {
|
|
962
1185
|
content: [
|
|
963
1186
|
{
|
|
@@ -969,37 +1192,89 @@ function degradedResult(err) {
|
|
|
969
1192
|
}
|
|
970
1193
|
function registerTools(server, deps) {
|
|
971
1194
|
const { hubClient, config, context, heartbeat, inboxFile } = deps;
|
|
1195
|
+
const markerCwd = deps.cwd ?? process.cwd();
|
|
1196
|
+
const declinedDir = deps.declinedDir;
|
|
1197
|
+
const repoRoot = findRepoRoot(markerCwd);
|
|
972
1198
|
let sessionId = null;
|
|
973
1199
|
let agentName = null;
|
|
974
|
-
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
joinBody.model = context.model;
|
|
1200
|
+
const isHosted = Boolean(config.SHEPHERD_TOKEN);
|
|
1201
|
+
const selfHostMismatch = !isHosted && context.linked && config.WORKSPACE !== void 0 && config.WORKSPACE !== context.workspace;
|
|
1202
|
+
let hostedWorkspaceRejected = false;
|
|
1203
|
+
const dormant = !context.linked || selfHostMismatch;
|
|
1204
|
+
let linked = context.linked;
|
|
1205
|
+
let declined = context.declined;
|
|
1206
|
+
function rememberDecline() {
|
|
1207
|
+
if (repoRoot !== null) setDeclined(repoRoot, declinedDir);
|
|
1208
|
+
declined = true;
|
|
984
1209
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1210
|
+
function forgetDecline() {
|
|
1211
|
+
if (repoRoot !== null) clearDeclined(repoRoot, declinedDir);
|
|
1212
|
+
declined = false;
|
|
1213
|
+
}
|
|
1214
|
+
let joinFailure = null;
|
|
1215
|
+
let joinInFlight = Promise.resolve();
|
|
1216
|
+
async function activate(workspaceSlug) {
|
|
1217
|
+
const joinBody = {
|
|
1218
|
+
workspace: workspaceSlug,
|
|
1219
|
+
repo: context.repo,
|
|
1220
|
+
branch: context.branch,
|
|
1221
|
+
human: context.human,
|
|
1222
|
+
program: context.program
|
|
1223
|
+
};
|
|
1224
|
+
if (context.model !== void 0) {
|
|
1225
|
+
joinBody.model = context.model;
|
|
993
1226
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1227
|
+
const attempt = (async () => {
|
|
1228
|
+
try {
|
|
1229
|
+
const raw = await hubClient.post("/join", joinBody);
|
|
1230
|
+
const parsed = JoinResponse.safeParse(raw);
|
|
1231
|
+
if (!parsed.success || !parsed.data.sessionId) {
|
|
1232
|
+
joinFailure = "validation";
|
|
1233
|
+
console.error(
|
|
1234
|
+
"[shepherd] join failed (validation): hub returned a malformed join response (no usable sessionId)"
|
|
1235
|
+
);
|
|
1236
|
+
return { ok: false, reason: "validation" };
|
|
1237
|
+
}
|
|
1238
|
+
const newSessionId = parsed.data.sessionId;
|
|
1239
|
+
heartbeat.start(newSessionId);
|
|
1240
|
+
sessionId = newSessionId;
|
|
1241
|
+
agentName = parsed.data.agentName;
|
|
1242
|
+
linked = true;
|
|
1243
|
+
hostedWorkspaceRejected = false;
|
|
1244
|
+
joinFailure = null;
|
|
1245
|
+
return { ok: true };
|
|
1246
|
+
} catch (err) {
|
|
1247
|
+
const reason = classifyActivateFailure(err);
|
|
1248
|
+
joinFailure = classifyJoinFailure(err);
|
|
1249
|
+
if (reason === "workspaceRejected") {
|
|
1250
|
+
hostedWorkspaceRejected = true;
|
|
1251
|
+
console.error(
|
|
1252
|
+
`[shepherd] This repo is linked to workspace "${workspaceSlug}" but your configured token is for a different workspace \u2014 coordination disabled.`
|
|
1253
|
+
);
|
|
1254
|
+
} else {
|
|
1255
|
+
console.error(
|
|
1256
|
+
`[shepherd] join failed (${reason}): ${err instanceof Error ? err.message : String(err)}`
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
return { ok: false, reason };
|
|
1260
|
+
}
|
|
1261
|
+
})();
|
|
1262
|
+
joinInFlight = attempt.then(() => void 0);
|
|
1263
|
+
return attempt;
|
|
1264
|
+
}
|
|
1265
|
+
if (!context.linked) {
|
|
999
1266
|
console.error(
|
|
1000
|
-
|
|
1267
|
+
context.declined ? "[shepherd] This repo was declined \u2014 staying uncoordinated. Run `link` to change your mind." : "[shepherd] This repo isn't linked to a Shepherd workspace \u2014 staying uncoordinated. Run `link` to choose one."
|
|
1001
1268
|
);
|
|
1002
|
-
})
|
|
1269
|
+
} else if (selfHostMismatch) {
|
|
1270
|
+
console.error(
|
|
1271
|
+
`[shepherd] This repo is linked to workspace "${context.workspace}" but your configured token is for a different workspace \u2014 coordination disabled.`
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
if (!dormant) {
|
|
1275
|
+
forgetDecline();
|
|
1276
|
+
void activate(context.workspace);
|
|
1277
|
+
}
|
|
1003
1278
|
async function awaitJoin() {
|
|
1004
1279
|
await joinInFlight;
|
|
1005
1280
|
}
|
|
@@ -1013,6 +1288,27 @@ function registerTools(server, deps) {
|
|
|
1013
1288
|
]
|
|
1014
1289
|
};
|
|
1015
1290
|
}
|
|
1291
|
+
function notLinked() {
|
|
1292
|
+
const text = declined ? "Not coordinating this repo \u2014 you declined. Run `link` anytime to change your mind." : "This repo isn't linked to a Shepherd workspace \u2014 run `link` to choose one, or `decline` to stay uncoordinated and not be asked again.";
|
|
1293
|
+
return { content: [{ type: "text", text }] };
|
|
1294
|
+
}
|
|
1295
|
+
function workspaceMismatch() {
|
|
1296
|
+
return {
|
|
1297
|
+
content: [
|
|
1298
|
+
{
|
|
1299
|
+
type: "text",
|
|
1300
|
+
text: `This repo is linked to \`${context.workspace}\` but your configured token is for a different workspace \u2014 coordination disabled.`
|
|
1301
|
+
}
|
|
1302
|
+
]
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
async function coordinationGate() {
|
|
1306
|
+
await awaitJoin();
|
|
1307
|
+
if (!linked) return notLinked();
|
|
1308
|
+
if (selfHostMismatch || hostedWorkspaceRejected) return workspaceMismatch();
|
|
1309
|
+
if (sessionId === null) return sessionNotReady();
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1016
1312
|
function withIdentity(body) {
|
|
1017
1313
|
return agentName ? `You are ${agentName}.
|
|
1018
1314
|
|
|
@@ -1052,10 +1348,8 @@ ${section}` : body;
|
|
|
1052
1348
|
inputSchema: WorkAgentInput.shape
|
|
1053
1349
|
},
|
|
1054
1350
|
async (args) => {
|
|
1055
|
-
await
|
|
1056
|
-
if (
|
|
1057
|
-
return sessionNotReady();
|
|
1058
|
-
}
|
|
1351
|
+
const gated = await coordinationGate();
|
|
1352
|
+
if (gated) return gated;
|
|
1059
1353
|
try {
|
|
1060
1354
|
const changeReport = await changeReportForBody();
|
|
1061
1355
|
const body = { sessionId, ...args, ...changeReport ? { changeReport } : {} };
|
|
@@ -1091,10 +1385,8 @@ You hold this claim until you call done (workItemId: ${result.workItemId}) or it
|
|
|
1091
1385
|
inputSchema: DoneAgentInput.shape
|
|
1092
1386
|
},
|
|
1093
1387
|
async (args) => {
|
|
1094
|
-
await
|
|
1095
|
-
if (
|
|
1096
|
-
return sessionNotReady();
|
|
1097
|
-
}
|
|
1388
|
+
const gated = await coordinationGate();
|
|
1389
|
+
if (gated) return gated;
|
|
1098
1390
|
try {
|
|
1099
1391
|
const body = { sessionId, ...args };
|
|
1100
1392
|
const result = await hubClient.post("/done", body);
|
|
@@ -1125,10 +1417,8 @@ ${msgs}` : base }
|
|
|
1125
1417
|
inputSchema: AnnounceAgentInput.shape
|
|
1126
1418
|
},
|
|
1127
1419
|
async (args) => {
|
|
1128
|
-
await
|
|
1129
|
-
if (
|
|
1130
|
-
return sessionNotReady();
|
|
1131
|
-
}
|
|
1420
|
+
const gated = await coordinationGate();
|
|
1421
|
+
if (gated) return gated;
|
|
1132
1422
|
try {
|
|
1133
1423
|
const body = { sessionId, ...args };
|
|
1134
1424
|
const result = await hubClient.post("/announce", body);
|
|
@@ -1159,10 +1449,8 @@ ${msgs}` : base }
|
|
|
1159
1449
|
inputSchema: SyncAgentInput.shape
|
|
1160
1450
|
},
|
|
1161
1451
|
async (_args) => {
|
|
1162
|
-
await
|
|
1163
|
-
if (
|
|
1164
|
-
return sessionNotReady();
|
|
1165
|
-
}
|
|
1452
|
+
const gated = await coordinationGate();
|
|
1453
|
+
if (gated) return gated;
|
|
1166
1454
|
try {
|
|
1167
1455
|
const changeReport = await changeReportForBody();
|
|
1168
1456
|
const body = { sessionId, ...changeReport ? { changeReport } : {} };
|
|
@@ -1183,6 +1471,118 @@ ${msgs}` : base }
|
|
|
1183
1471
|
}
|
|
1184
1472
|
}
|
|
1185
1473
|
);
|
|
1474
|
+
function advisory(text) {
|
|
1475
|
+
return { content: [{ type: "text", text }] };
|
|
1476
|
+
}
|
|
1477
|
+
async function linkAndActivate(slug) {
|
|
1478
|
+
writeMarker(markerCwd, slug);
|
|
1479
|
+
forgetDecline();
|
|
1480
|
+
linked = true;
|
|
1481
|
+
const result = await activate(slug);
|
|
1482
|
+
if (result.ok) {
|
|
1483
|
+
return advisory(`Linked this repo to \`${slug}\` \u2014 coordinating in \`${slug}\` now.`);
|
|
1484
|
+
}
|
|
1485
|
+
return advisory(
|
|
1486
|
+
`Linked this repo to \`${slug}\`, but coordination couldn't start just now (${joinFailureCause(joinFailure)}). It'll connect on your next tool call or session.`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
server.registerTool(
|
|
1490
|
+
"link",
|
|
1491
|
+
{
|
|
1492
|
+
title: "Link this repo to a Shepherd workspace",
|
|
1493
|
+
description: "Opt this repository into Shepherd coordination by writing a committed `.shepherd` marker naming the workspace. Call with no argument: if you belong to exactly one workspace it is linked and coordination starts immediately; if you belong to several, the choices are listed for you to confirm one with the agent's user, then call `link` again with that `workspace`. You can only link to a workspace you are a member of. Takes effect immediately \u2014 no restart. Use `unlink` to opt out, or `decline` to stay uncoordinated without linking.",
|
|
1494
|
+
inputSchema: z3.object({
|
|
1495
|
+
workspace: z3.string().min(1).optional().describe("The workspace slug to link this repo to. Omit to auto-pick or list choices.")
|
|
1496
|
+
}).shape
|
|
1497
|
+
},
|
|
1498
|
+
async (args) => {
|
|
1499
|
+
const requested = args.workspace;
|
|
1500
|
+
if (!isHosted) {
|
|
1501
|
+
const allowed = config.WORKSPACE;
|
|
1502
|
+
if (!allowed) {
|
|
1503
|
+
return advisory(
|
|
1504
|
+
"Self-host mode has no configured workspace (WORKSPACE is unset) \u2014 cannot link."
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
if (requested !== void 0 && requested !== allowed) {
|
|
1508
|
+
return advisory(
|
|
1509
|
+
`This self-host deployment only serves the workspace \`${allowed}\`; you asked for \`${requested}\`. Choose: ${allowed}`
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
return linkAndActivate(allowed);
|
|
1513
|
+
}
|
|
1514
|
+
let slugs;
|
|
1515
|
+
try {
|
|
1516
|
+
const res = await hubClient.get("/workspaces");
|
|
1517
|
+
slugs = (res.workspaces ?? []).map((w) => w.slug).filter((s) => typeof s === "string" && s.length > 0);
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
const detail = hubErrorDetail(err);
|
|
1520
|
+
return advisory(
|
|
1521
|
+
`Couldn't reach the coordination hub to list your workspaces \u2014 link not changed. ${detail}`
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
if (slugs.length === 0) {
|
|
1525
|
+
return advisory(
|
|
1526
|
+
"Your account isn't a member of any workspaces yet \u2014 nothing to link to. Create one in the Shepherd dashboard, then run `link` again."
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
if (requested === void 0) {
|
|
1530
|
+
if (slugs.length === 1) {
|
|
1531
|
+
return linkAndActivate(slugs[0]);
|
|
1532
|
+
}
|
|
1533
|
+
return advisory(
|
|
1534
|
+
"You're a member of multiple Shepherd workspaces:\n" + slugs.map((s) => ` - ${s}`).join("\n") + "\n\nAsk the user which one to use for this repo, then call `link` again with it as the `workspace` argument."
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
if (!slugs.includes(requested)) {
|
|
1538
|
+
return advisory(
|
|
1539
|
+
`You're not a member of \`${requested}\`; choose one of: ${slugs.join(", ")}`
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
return linkAndActivate(requested);
|
|
1543
|
+
}
|
|
1544
|
+
);
|
|
1545
|
+
server.registerTool(
|
|
1546
|
+
"unlink",
|
|
1547
|
+
{
|
|
1548
|
+
title: "Unlink this repo from its Shepherd workspace",
|
|
1549
|
+
description: "Opt this repository OUT of Shepherd coordination by removing its `.shepherd` marker. Also records a local decline so you aren't re-prompted to link on the next session. The repo stays uncoordinated (no claims, no presence) until you `link` it again.",
|
|
1550
|
+
inputSchema: z3.object({}).shape
|
|
1551
|
+
},
|
|
1552
|
+
async () => {
|
|
1553
|
+
removeMarker(markerCwd);
|
|
1554
|
+
rememberDecline();
|
|
1555
|
+
linked = false;
|
|
1556
|
+
if (sessionId !== null) {
|
|
1557
|
+
heartbeat.stop();
|
|
1558
|
+
await leave();
|
|
1559
|
+
sessionId = null;
|
|
1560
|
+
agentName = null;
|
|
1561
|
+
}
|
|
1562
|
+
return advisory(
|
|
1563
|
+
"Unlinked \u2014 this repo will stay uncoordinated and won't ask again. Run `link` to re-enable."
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
);
|
|
1567
|
+
server.registerTool(
|
|
1568
|
+
"decline",
|
|
1569
|
+
{
|
|
1570
|
+
title: "Decline Shepherd coordination for this repo",
|
|
1571
|
+
description: "Opt out of Shepherd for this repo WITHOUT linking: records a local, per-user 'don't ask again' so this repo stays uncoordinated and you aren't prompted to link on future sessions. This is local only \u2014 it is never committed, so a teammate on the same repo can still link it. Run `link` anytime to change your mind.",
|
|
1572
|
+
inputSchema: z3.object({}).shape
|
|
1573
|
+
},
|
|
1574
|
+
async () => {
|
|
1575
|
+
if (linked) {
|
|
1576
|
+
return advisory(
|
|
1577
|
+
`Already coordinating \`${context.workspace}\` \u2014 run \`unlink\` to stop coordinating this repo.`
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
rememberDecline();
|
|
1581
|
+
return advisory(
|
|
1582
|
+
"Won't coordinate this repo or ask again. Run `link` anytime to change your mind."
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
);
|
|
1186
1586
|
async function leave() {
|
|
1187
1587
|
try {
|
|
1188
1588
|
await joinInFlight;
|
|
@@ -1198,23 +1598,23 @@ ${msgs}` : base }
|
|
|
1198
1598
|
}
|
|
1199
1599
|
|
|
1200
1600
|
// src/identityCache.ts
|
|
1201
|
-
import { mkdirSync as
|
|
1202
|
-
import { homedir as
|
|
1203
|
-
import { dirname as
|
|
1601
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
1602
|
+
import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
|
|
1603
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
1204
1604
|
function defaultIdentityCachePath() {
|
|
1205
1605
|
let base = "";
|
|
1206
1606
|
try {
|
|
1207
|
-
base =
|
|
1607
|
+
base = homedir3();
|
|
1208
1608
|
} catch {
|
|
1209
1609
|
base = "";
|
|
1210
1610
|
}
|
|
1211
|
-
if (!base) base =
|
|
1212
|
-
return
|
|
1611
|
+
if (!base) base = tmpdir3();
|
|
1612
|
+
return join4(base, ".shepherd", "identity.json");
|
|
1213
1613
|
}
|
|
1214
1614
|
function readCachedHuman(filePath = defaultIdentityCachePath()) {
|
|
1215
1615
|
let raw;
|
|
1216
1616
|
try {
|
|
1217
|
-
raw =
|
|
1617
|
+
raw = readFileSync4(filePath, "utf8");
|
|
1218
1618
|
} catch {
|
|
1219
1619
|
return null;
|
|
1220
1620
|
}
|
|
@@ -1229,9 +1629,9 @@ function readCachedHuman(filePath = defaultIdentityCachePath()) {
|
|
|
1229
1629
|
function writeCachedHuman(human, filePath = defaultIdentityCachePath()) {
|
|
1230
1630
|
if (typeof human !== "string" || human.trim().length === 0) return;
|
|
1231
1631
|
try {
|
|
1232
|
-
|
|
1632
|
+
mkdirSync3(dirname4(filePath), { recursive: true });
|
|
1233
1633
|
const payload = JSON.stringify({ human });
|
|
1234
|
-
|
|
1634
|
+
writeFileSync3(filePath, payload + "\n", "utf8");
|
|
1235
1635
|
} catch {
|
|
1236
1636
|
}
|
|
1237
1637
|
}
|
|
@@ -1241,6 +1641,9 @@ var defaultDeps = {
|
|
|
1241
1641
|
detectRepo,
|
|
1242
1642
|
detectBranch,
|
|
1243
1643
|
detectHuman,
|
|
1644
|
+
readMarker,
|
|
1645
|
+
findRepoRoot,
|
|
1646
|
+
isDeclined: (repoRoot) => isDeclined(repoRoot),
|
|
1244
1647
|
readCachedHuman,
|
|
1245
1648
|
writeCachedHuman
|
|
1246
1649
|
};
|
|
@@ -1253,8 +1656,13 @@ async function resolveContext(config, cwd = process.cwd(), deps = defaultDeps) {
|
|
|
1253
1656
|
const human = resolveHuman(config, cwd, deps);
|
|
1254
1657
|
const program = config.PROGRAM ?? "claude-code";
|
|
1255
1658
|
const model = config.MODEL ?? void 0;
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1659
|
+
const marker = deps.readMarker(cwd);
|
|
1660
|
+
const linked = marker !== null;
|
|
1661
|
+
const workspace = marker?.workspace ?? config.WORKSPACE ?? DEFAULT_WORKSPACE;
|
|
1662
|
+
const repoRoot = deps.findRepoRoot(cwd);
|
|
1663
|
+
const declined = repoRoot !== null ? deps.isDeclined(repoRoot) : false;
|
|
1664
|
+
const linkState = linked ? "linked" : declined ? "declined" : "unanswered";
|
|
1665
|
+
return { workspace, repo, branch, human, program, model, linked, declined, linkState };
|
|
1258
1666
|
}
|
|
1259
1667
|
function resolveHuman(config, cwd, deps) {
|
|
1260
1668
|
if (config.HUMAN) return config.HUMAN;
|
|
@@ -1326,9 +1734,11 @@ function createHeartbeat({
|
|
|
1326
1734
|
}
|
|
1327
1735
|
|
|
1328
1736
|
// src/instructions.ts
|
|
1329
|
-
var SHEPHERD_INSTRUCTIONS = `You are connected to Shepherd, the shared coordination hub for a team of agents (human and AI) working in the same repositories.
|
|
1737
|
+
var SHEPHERD_INSTRUCTIONS = `You are connected to Shepherd, the shared coordination hub for a team of agents (human and AI) working in the same repositories.
|
|
1738
|
+
|
|
1739
|
+
On your first coordination-relevant action in a repo, if it isn't linked and hasn't been declined, call \`link\` with no argument: it auto-starts coordination if you belong to exactly one workspace, or lists your workspaces \u2014 ask the user "Coordinate this repo with Shepherd? Which workspace?" then call \`link <workspace>\` with their answer, or \`decline\` if they say no. Ask at most once per repo: once linked or declined, do not ask again \u2014 the marker is committed and teammates inherit it, so coordination is automatic from then on.
|
|
1330
1740
|
|
|
1331
|
-
|
|
1741
|
+
Once linked, follow this procedure on every session, proactively and without being asked:
|
|
1332
1742
|
|
|
1333
1743
|
1. Before you start producing or changing files in an AREA of the codebase, call \`work\` ONCE. This includes authoring a plan or design doc: claim the doc's path (e.g. ["docs/plans/auth.md"], or the directory you'll write into) BEFORE you write it \u2014 a plan you're about to author counts as a unit of work, not exploration. Pass a one-line \`intent\` and the \`pathGlobs\` covering the files you expect to touch. Scope the globs as specifically as you reasonably can \u2014 tight enough to avoid colliding with unrelated work, broad enough to cover the task (e.g. ["src/auth/**"], not ["src/**"] and not a single file). Hold that one claim across all your edits in that area; do NOT re-claim per file. If it reports a conflict, coordinate or pick different work \u2014 never silently collide.
|
|
1334
1744
|
|
|
@@ -1347,7 +1757,7 @@ Commit work-in-progress as you go rather than sitting on a large dirty tree: com
|
|
|
1347
1757
|
// src/index.ts
|
|
1348
1758
|
async function main() {
|
|
1349
1759
|
const config = loadConfig();
|
|
1350
|
-
const hubClient = createHubClient({ hubUrl: config.HUB_URL,
|
|
1760
|
+
const hubClient = createHubClient({ hubUrl: config.HUB_URL, token: config.authToken });
|
|
1351
1761
|
const context = await resolveContext(config);
|
|
1352
1762
|
const inboxDir = config.SHEPHERD_INBOX_DIR ?? defaultInboxDir();
|
|
1353
1763
|
const inboxFile = inboxFilePath(inboxDir, process.cwd());
|