patchcord 0.6.10 → 0.6.12
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/.claude-plugin/plugin.json +1 -1
- package/bin/patchcord.mjs +73 -33
- package/package.json +1 -1
- package/scripts/lib/resolve-project-bearer.mjs +200 -0
- package/scripts/subscribe.mjs +6 -25
package/bin/patchcord.mjs
CHANGED
|
@@ -10,6 +10,7 @@ const HOME = homedir();
|
|
|
10
10
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const pluginRoot = join(__dirname, "..");
|
|
13
|
+
import { resolveProjectBearer, harnessContext } from "../scripts/lib/resolve-project-bearer.mjs";
|
|
13
14
|
const cmd = process.argv[2];
|
|
14
15
|
|
|
15
16
|
if (cmd === "--version" || cmd === "-v") {
|
|
@@ -134,13 +135,7 @@ if (cmd === "plugin-path") {
|
|
|
134
135
|
// opencode (per-project) + windsurf, gemini, zed, openclaw, antigravity,
|
|
135
136
|
// cline, kimi (global). Each tool stores the bearer in its own shape.
|
|
136
137
|
function _kimiContextRequested(options = {}) {
|
|
137
|
-
|
|
138
|
-
return options.preferKimi === true
|
|
139
|
-
|| forceTool === "kimi"
|
|
140
|
-
|| Boolean(process.env.KIMI_CLI)
|
|
141
|
-
|| Boolean(process.env.KIMI_SESSION_ID)
|
|
142
|
-
|| Boolean(process.env.KIMI_CONFIG_FILE)
|
|
143
|
-
|| Boolean(process.env.KIMI_MCP_CONFIG_FILE);
|
|
138
|
+
return harnessContext(process.env, options).preferKimi;
|
|
144
139
|
}
|
|
145
140
|
|
|
146
141
|
// Strip legacy Python kimi-cli per-project artifacts. Kimi Code (Node) uses
|
|
@@ -182,7 +177,73 @@ function _purgeLegacyKimiProjectConfig(dir) {
|
|
|
182
177
|
} catch {}
|
|
183
178
|
}
|
|
184
179
|
|
|
185
|
-
//
|
|
180
|
+
// Cursor indexes skills from multiple trees. Leftover Codex globals
|
|
181
|
+
// (~/.agents/skills/patchcord*) and Claude colon-named plugin skills bundled
|
|
182
|
+
// inside cursor-agent's node_modules/patchcord both register duplicate slash
|
|
183
|
+
// commands (/patchcordinbox vs /patchcord-inbox). Keep only skills-cursor.
|
|
184
|
+
function _purgeCursorSkillDuplicates() {
|
|
185
|
+
const staleNames = [
|
|
186
|
+
"patchcord",
|
|
187
|
+
"patchcord-inbox",
|
|
188
|
+
"patchcord-wait",
|
|
189
|
+
"patchcord-subscribe",
|
|
190
|
+
"patchcordinbox",
|
|
191
|
+
"patchcordwait",
|
|
192
|
+
"patchcordsubscribe",
|
|
193
|
+
"patchcord:inbox",
|
|
194
|
+
"patchcord:wait",
|
|
195
|
+
"patchcord:subscribe",
|
|
196
|
+
];
|
|
197
|
+
for (const root of [
|
|
198
|
+
join(HOME, ".agents", "skills"),
|
|
199
|
+
join(HOME, ".cursor", "skills-cursor"),
|
|
200
|
+
]) {
|
|
201
|
+
for (const name of staleNames) {
|
|
202
|
+
const d = join(root, name);
|
|
203
|
+
if (!existsSync(d)) continue;
|
|
204
|
+
try { rmSync(d, { recursive: true, force: true }); } catch {}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// cursor-agent ships patchcord@* with Claude plugin skills/ — Cursor must not
|
|
208
|
+
// index those; only per-project skills-cursor copies are valid for Cursor CLI.
|
|
209
|
+
const cursorAgentRoot = join(HOME, ".local", "share", "cursor-agent");
|
|
210
|
+
if (existsSync(cursorAgentRoot)) {
|
|
211
|
+
const walk = (dir, depth = 0) => {
|
|
212
|
+
if (depth > 8) return;
|
|
213
|
+
let entries;
|
|
214
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
215
|
+
for (const ent of entries) {
|
|
216
|
+
const p = join(dir, ent.name);
|
|
217
|
+
if (ent.isDirectory()) {
|
|
218
|
+
if (ent.name === "patchcord" && dir.endsWith("node_modules")) {
|
|
219
|
+
for (const pluginSkill of ["inbox", "wait", "subscribe"]) {
|
|
220
|
+
const sd = join(p, "skills", pluginSkill);
|
|
221
|
+
if (existsSync(sd)) {
|
|
222
|
+
try { rmSync(sd, { recursive: true, force: true }); } catch {}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
walk(p, depth + 1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
walk(cursorAgentRoot);
|
|
232
|
+
}
|
|
233
|
+
const syncManifest = join(HOME, ".cursor", "skills-cursor", ".sync-manifest.json");
|
|
234
|
+
if (existsSync(syncManifest)) {
|
|
235
|
+
try {
|
|
236
|
+
const obj = JSON.parse(readFileSync(syncManifest, "utf-8"));
|
|
237
|
+
if (obj?.skills) {
|
|
238
|
+
for (const stale of ["patchcord", "patchcordinbox", "patchcordwait", "patchcordsubscribe", "patchcord:inbox", "patchcord:wait", "patchcord:subscribe"]) {
|
|
239
|
+
delete obj.skills[stale];
|
|
240
|
+
}
|
|
241
|
+
writeFileSync(syncManifest, JSON.stringify(obj, null, 2) + "\n");
|
|
242
|
+
}
|
|
243
|
+
} catch {}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
186
247
|
// preserving the rest of the user's YAML. Block-style only (what Hermes writes).
|
|
187
248
|
function upsertHermesConfig(existing, url, token) {
|
|
188
249
|
const block = [
|
|
@@ -268,7 +329,9 @@ function readGrokTomlShape(path) {
|
|
|
268
329
|
|
|
269
330
|
async function _resolveBearer(options = {}) {
|
|
270
331
|
const { readFileSync } = await import("fs");
|
|
271
|
-
|
|
332
|
+
|
|
333
|
+
const projectFound = resolveProjectBearer(process.cwd(), options);
|
|
334
|
+
if (projectFound) return projectFound;
|
|
272
335
|
|
|
273
336
|
// Strict JSON first; fall back to JSONC stripping for zed/gemini-style
|
|
274
337
|
// settings (which allow // /* */ trailing commas). The fallback strips
|
|
@@ -357,30 +420,6 @@ async function _resolveBearer(options = {}) {
|
|
|
357
420
|
} catch { return null; }
|
|
358
421
|
};
|
|
359
422
|
|
|
360
|
-
// Per-project (walk up from cwd). First win.
|
|
361
|
-
const kimiProjectReader = (cwd) => readJsonAt(join(cwd, ".kimi", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
|
|
362
|
-
const kimiCodeProjectReader = (cwd) => readJsonAt(join(cwd, ".kimi-code", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
|
|
363
|
-
const defaultProjectReaders = [
|
|
364
|
-
(cwd) => readJsonAt(join(cwd, ".mcp.json"), ["mcpServers", "patchcord"], "claude_code"),
|
|
365
|
-
(cwd) => readJsonAt(join(cwd, ".cursor", "mcp.json"), ["mcpServers", "patchcord"], "cursor"),
|
|
366
|
-
(cwd) => readJsonAt(join(cwd, ".vscode", "mcp.json"), ["servers", "patchcord"], "vscode"),
|
|
367
|
-
(cwd) => readJsonAt(join(cwd, "opencode.json"), ["mcp", "patchcord"], "opencode"),
|
|
368
|
-
(cwd) => readJsonAt(join(cwd, ".agents", "mcp_config.json"), ["mcpServers", "patchcord"], "antigravity"),
|
|
369
|
-
(cwd) => readGrokTomlShape(join(cwd, ".grok", "config.toml")),
|
|
370
|
-
(cwd) => readCodexTomlShape(join(cwd, ".codex", "config.toml")),
|
|
371
|
-
];
|
|
372
|
-
const projectReaders = preferKimi
|
|
373
|
-
? [kimiProjectReader, kimiCodeProjectReader, ...defaultProjectReaders]
|
|
374
|
-
: [...defaultProjectReaders, kimiProjectReader, kimiCodeProjectReader];
|
|
375
|
-
let dir = process.cwd();
|
|
376
|
-
while (dir && dir !== "/") {
|
|
377
|
-
for (const r of projectReaders) {
|
|
378
|
-
const found = r(dir);
|
|
379
|
-
if (found) { found.scope = "project"; return found; }
|
|
380
|
-
}
|
|
381
|
-
dir = dirname(dir);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
423
|
// Global fallbacks. Order by likelihood for current install base.
|
|
385
424
|
const zedPath = process.platform === "darwin"
|
|
386
425
|
? join(HOME, "Library", "Application Support", "Zed", "settings.json")
|
|
@@ -1809,6 +1848,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1809
1848
|
const cursorSkillsRoot = join(HOME, ".cursor", "skills-cursor");
|
|
1810
1849
|
const hasCursorAgent = run("which cursor-agent");
|
|
1811
1850
|
if (hasCursorAgent || existsSync(cursorSkillsRoot)) {
|
|
1851
|
+
_purgeCursorSkillDuplicates();
|
|
1812
1852
|
mkdirSync(cursorSkillsRoot, { recursive: true });
|
|
1813
1853
|
const cursorInboxDir = join(cursorSkillsRoot, "patchcord-inbox");
|
|
1814
1854
|
const cursorWaitDir = join(cursorSkillsRoot, "patchcord-wait");
|
package/package.json
CHANGED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Shared per-project bearer resolution for patchcord.mjs and subscribe.mjs.
|
|
2
|
+
// When multiple tool configs exist in one directory (common: .mcp.json from
|
|
3
|
+
// Claude Code + .cursor/mcp.json from Cursor), pick the config matching the
|
|
4
|
+
// invoking harness instead of always preferring .mcp.json first.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
|
|
9
|
+
function stripJsoncOutsideStrings(raw) {
|
|
10
|
+
let out = "";
|
|
11
|
+
let i = 0;
|
|
12
|
+
let inStr = false;
|
|
13
|
+
let strCh = "";
|
|
14
|
+
let prev = "";
|
|
15
|
+
while (i < raw.length) {
|
|
16
|
+
const c = raw[i];
|
|
17
|
+
if (inStr) {
|
|
18
|
+
out += c;
|
|
19
|
+
if (c === strCh && prev !== "\\") inStr = false;
|
|
20
|
+
prev = c;
|
|
21
|
+
i++;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (c === '"' || c === "'") {
|
|
25
|
+
inStr = true;
|
|
26
|
+
strCh = c;
|
|
27
|
+
out += c;
|
|
28
|
+
prev = c;
|
|
29
|
+
i++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (c === "/" && raw[i + 1] === "/") {
|
|
33
|
+
while (i < raw.length && raw[i] !== "\n") i++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (c === "/" && raw[i + 1] === "*") {
|
|
37
|
+
i += 2;
|
|
38
|
+
while (i < raw.length && !(raw[i] === "*" && raw[i + 1] === "/")) i++;
|
|
39
|
+
i += 2;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
out += c;
|
|
43
|
+
prev = c;
|
|
44
|
+
i++;
|
|
45
|
+
}
|
|
46
|
+
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseJsonc(raw) {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(raw);
|
|
52
|
+
} catch {}
|
|
53
|
+
return JSON.parse(stripJsoncOutsideStrings(raw));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extractBearer(entry) {
|
|
57
|
+
if (!entry) return null;
|
|
58
|
+
const auth = entry?.headers?.Authorization;
|
|
59
|
+
const url = entry.url || entry.httpUrl || entry.serverUrl;
|
|
60
|
+
if (!auth || !url) return null;
|
|
61
|
+
return {
|
|
62
|
+
token: auth.replace(/^Bearer\s+/i, ""),
|
|
63
|
+
baseUrl: url.replace(/\/mcp(\/bearer)?$/, ""),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readJsonAt(path, keyPath, tool) {
|
|
68
|
+
if (!existsSync(path)) return null;
|
|
69
|
+
try {
|
|
70
|
+
const obj = parseJsonc(readFileSync(path, "utf-8"));
|
|
71
|
+
const entry = keyPath.reduce((o, k) => o?.[k], obj);
|
|
72
|
+
const b = extractBearer(entry);
|
|
73
|
+
return b ? { ...b, configFile: path, tool } : null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readCodexTomlShape(path) {
|
|
80
|
+
if (!existsSync(path)) return null;
|
|
81
|
+
try {
|
|
82
|
+
const content = readFileSync(path, "utf-8");
|
|
83
|
+
const block = content.match(/\[mcp_servers\.patchcord[-\w]*\]([\s\S]*?)(?=\n\[|$)/);
|
|
84
|
+
if (!block) return null;
|
|
85
|
+
const urlMatch = block[1].match(/url\s*=\s*"([^"]+)"/);
|
|
86
|
+
const tokenMatch = block[1].match(/Bearer\s+([^\s"]+)/);
|
|
87
|
+
if (!urlMatch || !tokenMatch) return null;
|
|
88
|
+
return {
|
|
89
|
+
token: tokenMatch[1],
|
|
90
|
+
baseUrl: urlMatch[1].replace(/\/mcp(\/bearer)?$/, ""),
|
|
91
|
+
configFile: path,
|
|
92
|
+
tool: "codex",
|
|
93
|
+
};
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readGrokTomlShape(path) {
|
|
100
|
+
if (!existsSync(path)) return null;
|
|
101
|
+
try {
|
|
102
|
+
const content = readFileSync(path, "utf-8");
|
|
103
|
+
const header = content.match(/^\[mcp_servers\.patchcord(?:[-\w]*)?\]\s*$/m);
|
|
104
|
+
if (!header) return null;
|
|
105
|
+
const rest = content.slice(header.index + header[0].length);
|
|
106
|
+
const nextSection = rest.search(/^\[(?!mcp_servers\.patchcord(?:\.|[-\w]*\]))/m);
|
|
107
|
+
const block = nextSection === -1 ? rest : rest.slice(0, nextSection);
|
|
108
|
+
const urlMatch = block.match(/^\s*url\s*=\s*["']([^"']+)["']/m);
|
|
109
|
+
const tokenMatch = block.match(/["']?Authorization["']?\s*=\s*["']Bearer\s+([^"']+)["']/i);
|
|
110
|
+
if (!urlMatch || !tokenMatch) return null;
|
|
111
|
+
return {
|
|
112
|
+
token: tokenMatch[1],
|
|
113
|
+
baseUrl: urlMatch[1].replace(/\/mcp(\/bearer)?$/, ""),
|
|
114
|
+
configFile: path,
|
|
115
|
+
tool: "grok",
|
|
116
|
+
};
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const claudeReader = (cwd) => readJsonAt(join(cwd, ".mcp.json"), ["mcpServers", "patchcord"], "claude_code");
|
|
123
|
+
const cursorReader = (cwd) => readJsonAt(join(cwd, ".cursor", "mcp.json"), ["mcpServers", "patchcord"], "cursor");
|
|
124
|
+
const vscodeReader = (cwd) => readJsonAt(join(cwd, ".vscode", "mcp.json"), ["servers", "patchcord"], "vscode");
|
|
125
|
+
const opencodeReader = (cwd) => readJsonAt(join(cwd, "opencode.json"), ["mcp", "patchcord"], "opencode");
|
|
126
|
+
const antigravityReader = (cwd) => readJsonAt(join(cwd, ".agents", "mcp_config.json"), ["mcpServers", "patchcord"], "antigravity");
|
|
127
|
+
const grokReader = (cwd) => readGrokTomlShape(join(cwd, ".grok", "config.toml"));
|
|
128
|
+
const codexReader = (cwd) => readCodexTomlShape(join(cwd, ".codex", "config.toml"));
|
|
129
|
+
const kimiReader = (cwd) => readJsonAt(join(cwd, ".kimi", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
|
|
130
|
+
const kimiCodeReader = (cwd) => readJsonAt(join(cwd, ".kimi-code", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
|
|
131
|
+
|
|
132
|
+
/** Detect which harness launched this process (env + explicit overrides). */
|
|
133
|
+
export function harnessContext(env = process.env, options = {}) {
|
|
134
|
+
const forceTool = (env.PATCHCORD_FORCE_TOOL || env.PATCHCORD_TOOL || "").toLowerCase();
|
|
135
|
+
const preferKimi =
|
|
136
|
+
options.preferKimi === true
|
|
137
|
+
|| forceTool === "kimi"
|
|
138
|
+
|| Boolean(env.KIMI_CLI)
|
|
139
|
+
|| Boolean(env.KIMI_SESSION_ID)
|
|
140
|
+
|| Boolean(env.KIMI_CONFIG_FILE)
|
|
141
|
+
|| Boolean(env.KIMI_MCP_CONFIG_FILE);
|
|
142
|
+
const preferCursor =
|
|
143
|
+
options.preferCursor === true
|
|
144
|
+
|| forceTool === "cursor"
|
|
145
|
+
|| Boolean(env.CURSOR_AGENT);
|
|
146
|
+
return { forceTool, preferKimi, preferCursor };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Ordered per-project readers for the active harness. */
|
|
150
|
+
export function projectReadersForContext(ctx) {
|
|
151
|
+
const defaultReaders = [
|
|
152
|
+
claudeReader,
|
|
153
|
+
cursorReader,
|
|
154
|
+
vscodeReader,
|
|
155
|
+
opencodeReader,
|
|
156
|
+
antigravityReader,
|
|
157
|
+
grokReader,
|
|
158
|
+
codexReader,
|
|
159
|
+
];
|
|
160
|
+
const kimiReaders = [kimiReader, kimiCodeReader];
|
|
161
|
+
|
|
162
|
+
if (ctx.preferKimi) {
|
|
163
|
+
return [...kimiReaders, ...defaultReaders];
|
|
164
|
+
}
|
|
165
|
+
if (ctx.preferCursor) {
|
|
166
|
+
// Cursor CLI/IDE: .cursor/mcp.json must win over stale .mcp.json in the
|
|
167
|
+
// same repo (e.g. leftover Claude Code token from an old provision).
|
|
168
|
+
return [
|
|
169
|
+
cursorReader,
|
|
170
|
+
vscodeReader,
|
|
171
|
+
opencodeReader,
|
|
172
|
+
antigravityReader,
|
|
173
|
+
grokReader,
|
|
174
|
+
codexReader,
|
|
175
|
+
claudeReader,
|
|
176
|
+
...kimiReaders,
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
return [...defaultReaders, ...kimiReaders];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Walk up from startDir; return first matching project bearer or null. */
|
|
183
|
+
export function resolveProjectBearer(startDir, options = {}) {
|
|
184
|
+
const ctx = harnessContext(process.env, options);
|
|
185
|
+
const readers = projectReadersForContext(ctx);
|
|
186
|
+
let dir = startDir;
|
|
187
|
+
while (dir && dir !== "/") {
|
|
188
|
+
for (const r of readers) {
|
|
189
|
+
const found = r(dir);
|
|
190
|
+
if (found) {
|
|
191
|
+
found.scope = "project";
|
|
192
|
+
return found;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const parent = dirname(dir);
|
|
196
|
+
if (parent === dir) break;
|
|
197
|
+
dir = parent;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
package/scripts/subscribe.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { request as httpRequest } from "node:http";
|
|
|
13
13
|
import { URL } from "node:url";
|
|
14
14
|
import { dirname } from "node:path";
|
|
15
15
|
import { connect as wsConnect } from "./lib/ws.mjs";
|
|
16
|
+
import { resolveProjectBearer } from "./lib/resolve-project-bearer.mjs";
|
|
16
17
|
|
|
17
18
|
// --- Hermes webhook bridge mode -------------------------------------------
|
|
18
19
|
// Default mode writes "PATCHCORD: ..." lines to stdout for Claude Code's
|
|
@@ -70,35 +71,15 @@ function die(msg, code = 1) {
|
|
|
70
71
|
|
|
71
72
|
function readMcpConfig(cwd) {
|
|
72
73
|
// Prefer env vars injected by patchcord.mjs, which already ran _resolveBearer()
|
|
73
|
-
// and supports all
|
|
74
|
+
// and supports all tool configs with harness-aware ordering.
|
|
74
75
|
if (process.env.PATCHCORD_BASE_URL && process.env.PATCHCORD_BEARER_TOKEN) {
|
|
75
76
|
return { baseUrl: process.env.PATCHCORD_BASE_URL, token: process.env.PATCHCORD_BEARER_TOKEN };
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const path = `${dir}/.mcp.json`;
|
|
81
|
-
if (existsSync(path)) {
|
|
82
|
-
let json;
|
|
83
|
-
try {
|
|
84
|
-
json = JSON.parse(readFileSync(path, "utf8"));
|
|
85
|
-
} catch (e) {
|
|
86
|
-
die(`.mcp.json parse error: ${e.message}`);
|
|
87
|
-
}
|
|
88
|
-
const pc = json?.mcpServers?.patchcord;
|
|
89
|
-
if (!pc?.url || !pc?.headers?.Authorization) {
|
|
90
|
-
die(".mcp.json missing mcpServers.patchcord.url or Authorization");
|
|
91
|
-
}
|
|
92
|
-
let baseUrl = pc.url.replace(/\/mcp\/bearer$/, "").replace(/\/mcp$/, "");
|
|
93
|
-
const auth = pc.headers.Authorization;
|
|
94
|
-
const token = auth.startsWith("Bearer ") ? auth.slice(7) : auth;
|
|
95
|
-
return { baseUrl, token };
|
|
96
|
-
}
|
|
97
|
-
const parent = dirname(dir);
|
|
98
|
-
if (parent === dir) break;
|
|
99
|
-
dir = parent;
|
|
78
|
+
const found = resolveProjectBearer(cwd);
|
|
79
|
+
if (!found) {
|
|
80
|
+
die(`no patchcord config found — run from a project directory or use 'patchcord subscribe'`);
|
|
100
81
|
}
|
|
101
|
-
|
|
82
|
+
return { baseUrl: found.baseUrl, token: found.token };
|
|
102
83
|
}
|
|
103
84
|
|
|
104
85
|
function httpJson(urlStr, { method = "GET", headers = {}, body = null } = {}) {
|