driggsby 0.5.1 → 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/README.md +6 -0
- package/dist/dev/host-page.js +31 -12
- package/dist/device.js +127 -0
- package/dist/init/template-styles.js +42 -32
- package/dist/login/claim-client.js +10 -2
- package/dist/login/login.js +6 -2
- package/dist/mcp-setup/commands.js +7 -1
- package/dist/mcp-setup/setup.js +6 -1
- package/dist/mcp-setup/spawn-plan.js +6 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -22,6 +22,12 @@ npx driggsby@latest mcp setup other
|
|
|
22
22
|
After setup, authenticate Driggsby through your client — run `/mcp` in
|
|
23
23
|
Claude Code, or sign in through the browser window Codex opens.
|
|
24
24
|
|
|
25
|
+
So your Driggsby MCP page can tell your connections apart, `login` sends
|
|
26
|
+
Driggsby this computer's name and operating system version (for example
|
|
27
|
+
`mbp-studio` and `macOS 15.6`) with its sign-in, and
|
|
28
|
+
`mcp setup claude-code` saves them as headers on the Driggsby entry it
|
|
29
|
+
adds to Claude Code, which sends them with each request.
|
|
30
|
+
|
|
25
31
|
For another MCP client, choose Other. Driggsby currently supports only
|
|
26
32
|
OAuth-based remote MCP at:
|
|
27
33
|
|
package/dist/dev/host-page.js
CHANGED
|
@@ -23,9 +23,12 @@ export function hostPageHtml(slug, appOrigin, background) {
|
|
|
23
23
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
24
24
|
<title>${slug} — driggsby dev</title>
|
|
25
25
|
<style>
|
|
26
|
-
/* The strip above the frame is the console's title row
|
|
27
|
-
measured values: 46px tall on the black
|
|
28
|
-
|
|
26
|
+
/* The strip above the frame is the console's title row over a
|
|
27
|
+
dashboard, in its own measured values: 46px tall on the black
|
|
28
|
+
ground with no rule under it, the name centered in 14px ink, and
|
|
29
|
+
the side slot in the row's quiet 14px tertiary. Equal side tracks
|
|
30
|
+
keep the name centered; the slot ellipsizes only when the two
|
|
31
|
+
would otherwise meet. */
|
|
29
32
|
* { box-sizing: border-box; }
|
|
30
33
|
html { color-scheme: dark; }
|
|
31
34
|
body {
|
|
@@ -37,26 +40,42 @@ export function hostPageHtml(slug, appOrigin, background) {
|
|
|
37
40
|
color: #fafafa;
|
|
38
41
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
39
42
|
font-size: 14px;
|
|
40
|
-
line-height:
|
|
43
|
+
line-height: 1.5;
|
|
41
44
|
-webkit-font-smoothing: antialiased;
|
|
42
45
|
}
|
|
43
46
|
header {
|
|
44
|
-
display:
|
|
47
|
+
display: grid;
|
|
48
|
+
grid-template-columns: minmax(0, 1fr) fit-content(60%) minmax(0, 1fr);
|
|
49
|
+
column-gap: 16px;
|
|
45
50
|
align-items: center;
|
|
46
|
-
justify-content: space-between;
|
|
47
51
|
height: 46px;
|
|
48
|
-
padding: 0
|
|
49
|
-
border-bottom: 1px solid
|
|
52
|
+
padding: 0 20px;
|
|
53
|
+
border-bottom: 1px solid transparent;
|
|
54
|
+
}
|
|
55
|
+
header .slug,
|
|
56
|
+
header .mode {
|
|
57
|
+
grid-row: 1;
|
|
58
|
+
min-width: 0;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
text-overflow: ellipsis;
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
}
|
|
63
|
+
header .slug { grid-column: 2; }
|
|
64
|
+
header .mode {
|
|
65
|
+
grid-column: 3;
|
|
66
|
+
justify-self: end;
|
|
67
|
+
max-width: 100%;
|
|
68
|
+
color: #a1a1a1;
|
|
50
69
|
}
|
|
51
|
-
header .slug { font-weight: 500; }
|
|
52
|
-
header .mode { color: #a1a1a1; font-size: 13px; }
|
|
53
70
|
iframe {
|
|
54
71
|
flex: 1;
|
|
55
72
|
width: 100%;
|
|
56
73
|
border: none;
|
|
57
74
|
/* The app's declared background from driggsby.json, painted while
|
|
58
|
-
the app loads — the same surface the production host shows.
|
|
59
|
-
|
|
75
|
+
the app loads — the same surface the production host shows. With
|
|
76
|
+
none declared, Driggsby's own: its card surface on the black
|
|
77
|
+
page. */
|
|
78
|
+
background: ${background ?? "#0a0a0a"};
|
|
60
79
|
}
|
|
61
80
|
</style>
|
|
62
81
|
</head>
|
package/dist/device.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { hostname, platform, release, version } from "node:os";
|
|
4
|
+
// Load-bearing: these allowlists are what make the values safe as command
|
|
5
|
+
// arguments (spawn-plan.ts quotes them for cmd.exe) and as "Name: value"
|
|
6
|
+
// headers. They must never admit % ! ^ & | < > " \ : or a control
|
|
7
|
+
// character; device.test.ts checks each.
|
|
8
|
+
const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/;
|
|
9
|
+
const SYSTEM_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 ._/-]{0,63}$/;
|
|
10
|
+
const MAC_VERSION_FILE = "/System/Library/CoreServices/SystemVersion.plist";
|
|
11
|
+
// os-release(5): /etc first, then the vendor copy.
|
|
12
|
+
const LINUX_RELEASE_FILES = ["/etc/os-release", "/usr/lib/os-release"];
|
|
13
|
+
// Both files are a few hundred bytes; anything larger isn't one.
|
|
14
|
+
const MAX_FILE_BYTES = 64 * 1024;
|
|
15
|
+
// A PRETTY_NAME is a short line; a longer one is not parsed at all.
|
|
16
|
+
const MAX_PRETTY_NAME_CHARS = 256;
|
|
17
|
+
// Hostname labels that spell out an address ("ip-172-31-22-33",
|
|
18
|
+
// "c-73-162-12-34", "192-168-1-20") or are only digits name no computer.
|
|
19
|
+
const ADDRESS_LABEL = /^((ip|c)-)?\d{1,3}(-\d{1,3}){3}$|^\d+$/i;
|
|
20
|
+
// Windows 11 reports itself as NT 10.0; its builds start at 22000.
|
|
21
|
+
const WINDOWS_11_FIRST_BUILD = 22000;
|
|
22
|
+
export function describeDevice(facts) {
|
|
23
|
+
try {
|
|
24
|
+
const known = facts ?? realOsFacts();
|
|
25
|
+
return { name: deviceName(known.hostname), system: deviceSystem(known) };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// os.hostname() and os.release() can throw in unusual runtimes.
|
|
29
|
+
return { name: null, system: null };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// The known values alone, as the sign-in request carries them; null when
|
|
33
|
+
// nothing is known.
|
|
34
|
+
export function knownDevice(device) {
|
|
35
|
+
const known = {
|
|
36
|
+
...(device.name === null ? {} : { name: device.name }),
|
|
37
|
+
...(device.system === null ? {} : { system: device.system }),
|
|
38
|
+
};
|
|
39
|
+
return Object.keys(known).length === 0 ? null : known;
|
|
40
|
+
}
|
|
41
|
+
// The Driggsby headers `mcp setup` gives Claude Code, one per known value.
|
|
42
|
+
export function deviceHeaders(device) {
|
|
43
|
+
const headers = [];
|
|
44
|
+
if (device.name !== null) {
|
|
45
|
+
headers.push(`X-Driggsby-Device-Name: ${device.name}`);
|
|
46
|
+
}
|
|
47
|
+
if (device.system !== null) {
|
|
48
|
+
headers.push(`X-Driggsby-Device-System: ${device.system}`);
|
|
49
|
+
}
|
|
50
|
+
return headers;
|
|
51
|
+
}
|
|
52
|
+
// "mbp-studio.local" and "devbox-02.corp.example.test" are mbp-studio and
|
|
53
|
+
// devbox-02: the first label is the name people give their computer. An
|
|
54
|
+
// address or localhost names no computer.
|
|
55
|
+
function deviceName(raw) {
|
|
56
|
+
const trimmed = raw.trim();
|
|
57
|
+
if (isIP(trimmed) !== 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const first = trimmed.split(".")[0] ?? "";
|
|
61
|
+
if (first.toLowerCase() === "localhost" || ADDRESS_LABEL.test(first)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return NAME_PATTERN.test(first) ? first : null;
|
|
65
|
+
}
|
|
66
|
+
function deviceSystem(facts) {
|
|
67
|
+
switch (facts.platform) {
|
|
68
|
+
case "darwin":
|
|
69
|
+
return allowed(macVersion(facts)) ?? "macOS";
|
|
70
|
+
case "linux":
|
|
71
|
+
return allowed(linuxPrettyName(facts)) ?? "Linux";
|
|
72
|
+
case "win32":
|
|
73
|
+
return facts.version.includes("Server") ? "Windows Server" : windowsName(facts.release);
|
|
74
|
+
default:
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function macVersion(facts) {
|
|
79
|
+
const plist = facts.readFile(MAC_VERSION_FILE);
|
|
80
|
+
const version = plist?.match(/<key>ProductVersion<\/key>\s*<string>([0-9.]+)<\/string>/)?.[1];
|
|
81
|
+
return version === undefined ? null : `macOS ${version}`;
|
|
82
|
+
}
|
|
83
|
+
// PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" is Debian GNU/Linux 12.
|
|
84
|
+
function linuxPrettyName(facts) {
|
|
85
|
+
for (const path of LINUX_RELEASE_FILES) {
|
|
86
|
+
const osRelease = facts.readFile(path);
|
|
87
|
+
if (osRelease === null) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const line = osRelease.split(/\r?\n/).find((each) => each.startsWith("PRETTY_NAME="));
|
|
91
|
+
if (line === undefined || line.length > MAX_PRETTY_NAME_CHARS) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const value = line.slice("PRETTY_NAME=".length).trim().replace(/^["']|["']$/g, "");
|
|
95
|
+
return value.replace(/\s*\([^)]*\)\s*$/, "").trim();
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
// Windows 10 and 11 both report NT 10.0; anything else is just Windows.
|
|
100
|
+
function windowsName(nodeRelease) {
|
|
101
|
+
const [major, , buildText] = nodeRelease.split(".");
|
|
102
|
+
const build = Number(buildText);
|
|
103
|
+
if (major !== "10" || !Number.isInteger(build)) {
|
|
104
|
+
return "Windows";
|
|
105
|
+
}
|
|
106
|
+
return build >= WINDOWS_11_FIRST_BUILD ? "Windows 11" : "Windows 10";
|
|
107
|
+
}
|
|
108
|
+
function allowed(value) {
|
|
109
|
+
return value !== null && SYSTEM_PATTERN.test(value) ? value : null;
|
|
110
|
+
}
|
|
111
|
+
function realOsFacts() {
|
|
112
|
+
return { hostname: hostname(), platform: platform(), release: release(), version: version(), readFile: readTextFile };
|
|
113
|
+
}
|
|
114
|
+
// A regular file of a sane size only: never a FIFO or device node that
|
|
115
|
+
// would block, never something huge.
|
|
116
|
+
function readTextFile(path) {
|
|
117
|
+
try {
|
|
118
|
+
const stat = statSync(path);
|
|
119
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return readFileSync(path, "utf8");
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
// The scaffold's styles.css. A deployed app lives inside Driggsby's dark
|
|
2
2
|
// chrome, so the page is dark by default and its values are Driggsby's
|
|
3
|
-
// own: the console's
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// own: the console's palette, type, insets, and page column, measured off
|
|
4
|
+
// the console itself. The app is its own origin and cannot read the
|
|
5
|
+
// console's stylesheet, so each gray is written as the console paints it
|
|
6
|
+
// on its black page (the console's cards and fills are translucent over
|
|
7
|
+
// that ground) and each hue verbatim. The :root block is the same one
|
|
8
|
+
// Driggsby's own example dashboard carries. --bg-app must stay equal to
|
|
9
|
+
// driggsby.json's "background" (init-command.test.ts pins the two
|
|
10
|
+
// together): Driggsby paints that color while the app loads.
|
|
8
11
|
export const STYLES_CSS = `/* Driggsby's console palette, so this page reads as part of the app it
|
|
9
12
|
opens inside. Change these together if you re-theme; keep --bg-app equal
|
|
10
|
-
to the "background" in driggsby.json.
|
|
13
|
+
to the "background" in driggsby.json. The status tones are the
|
|
14
|
+
console's own, for gains, losses, warnings, and a live dot. */
|
|
11
15
|
:root {
|
|
12
16
|
color-scheme: dark;
|
|
13
17
|
--bg-app: #000000;
|
|
14
18
|
--bg-card: #0a0a0a;
|
|
15
19
|
--bg-mute: #262626;
|
|
16
20
|
--hairline: rgba(255, 255, 255, 0.14);
|
|
21
|
+
--hairline-strong: rgba(255, 255, 255, 0.22);
|
|
17
22
|
--text-primary: #fafafa;
|
|
18
23
|
--text-secondary: #c4c4c4;
|
|
19
24
|
--text-tertiary: #a1a1a1;
|
|
25
|
+
--status-success: oklch(0.72 0.14 160);
|
|
26
|
+
--text-success: oklch(0.82 0.11 160);
|
|
27
|
+
--text-warning: oklch(0.85 0.11 85);
|
|
28
|
+
--text-error: oklch(0.8 0.13 25);
|
|
20
29
|
--radius-card: 8px;
|
|
21
30
|
--radius-bar: 4px;
|
|
22
31
|
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
32
|
+
--ease: cubic-bezier(0.2, 0, 0, 1);
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
* {
|
|
@@ -32,21 +42,25 @@ body {
|
|
|
32
42
|
color: var(--text-primary);
|
|
33
43
|
font-family: var(--font-sans);
|
|
34
44
|
font-size: 14px;
|
|
35
|
-
line-height:
|
|
45
|
+
line-height: 1.5;
|
|
36
46
|
-webkit-font-smoothing: antialiased;
|
|
37
47
|
}
|
|
38
48
|
|
|
49
|
+
/* The console's page column. */
|
|
39
50
|
main {
|
|
40
|
-
max-width:
|
|
51
|
+
max-width: 75rem;
|
|
41
52
|
margin: 0 auto;
|
|
42
|
-
padding: 24px
|
|
53
|
+
padding: 24px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
main > header {
|
|
57
|
+
margin-bottom: 20px;
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
h1 {
|
|
46
61
|
margin: 0;
|
|
47
62
|
font-size: 24px;
|
|
48
63
|
font-weight: 600;
|
|
49
|
-
line-height: 36px;
|
|
50
64
|
letter-spacing: -0.02em;
|
|
51
65
|
}
|
|
52
66
|
|
|
@@ -54,24 +68,19 @@ h2 {
|
|
|
54
68
|
margin: 24px 0 12px;
|
|
55
69
|
font-size: 14px;
|
|
56
70
|
font-weight: 500;
|
|
57
|
-
line-height: 21px;
|
|
58
71
|
letter-spacing: -0.02em;
|
|
59
72
|
}
|
|
60
73
|
|
|
61
|
-
/*
|
|
62
|
-
|
|
74
|
+
/* The console's stat tiles: a card per figure, 16px apart, the row
|
|
75
|
+
capped at the console's 920px. */
|
|
63
76
|
.stat-grid {
|
|
64
77
|
display: grid;
|
|
65
78
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
66
|
-
gap:
|
|
67
|
-
|
|
68
|
-
border: 1px solid var(--hairline);
|
|
69
|
-
border-radius: var(--radius-card);
|
|
70
|
-
background: var(--hairline);
|
|
71
|
-
overflow: hidden;
|
|
79
|
+
gap: 16px;
|
|
80
|
+
max-width: 920px;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
|
-
@media (max-width:
|
|
83
|
+
@media (max-width: 900px) {
|
|
75
84
|
.stat-grid {
|
|
76
85
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
77
86
|
}
|
|
@@ -86,22 +95,22 @@ h2 {
|
|
|
86
95
|
.stat {
|
|
87
96
|
display: flex;
|
|
88
97
|
flex-direction: column;
|
|
89
|
-
padding: 16px;
|
|
98
|
+
padding: 14px 16px;
|
|
99
|
+
border: 1px solid var(--hairline);
|
|
100
|
+
border-radius: var(--radius-card);
|
|
90
101
|
background: var(--bg-card);
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
.stat-label {
|
|
94
105
|
font-size: 13px;
|
|
95
|
-
line-height: 20px;
|
|
96
106
|
color: var(--text-tertiary);
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
.stat-value {
|
|
100
|
-
margin-top:
|
|
110
|
+
margin-top: 2px;
|
|
101
111
|
font-size: 20px;
|
|
102
112
|
font-weight: 600;
|
|
103
|
-
|
|
104
|
-
letter-spacing: -0.02em;
|
|
113
|
+
letter-spacing: -0.01em;
|
|
105
114
|
font-variant-numeric: tabular-nums;
|
|
106
115
|
/* A currency string has no natural break, and a clipped balance is a
|
|
107
116
|
wrong number. If a value ever outgrows its cell, it wraps. */
|
|
@@ -165,6 +174,7 @@ h2 {
|
|
|
165
174
|
/* Holds its line even when empty, so every row measures the same as
|
|
166
175
|
the skeleton row it replaces. */
|
|
167
176
|
min-height: 21px;
|
|
177
|
+
margin-top: 1px;
|
|
168
178
|
color: var(--text-secondary);
|
|
169
179
|
overflow: hidden;
|
|
170
180
|
text-overflow: ellipsis;
|
|
@@ -179,10 +189,10 @@ h2 {
|
|
|
179
189
|
/* The skeleton ships in the HTML so the first paint is the final layout
|
|
180
190
|
as muted bars; the first data render replaces it in a dissolve (see the
|
|
181
191
|
"Settling in" section of app.js). Each bar's margins pad it to exactly
|
|
182
|
-
the height of the text it stands in for,
|
|
183
|
-
stacked bars are flex so those
|
|
184
|
-
|
|
185
|
-
lands. */
|
|
192
|
+
the height of the text it stands in for (the line, plus any margin the
|
|
193
|
+
text carries), and the columns holding stacked bars are flex so those
|
|
194
|
+
margins never collapse — the skeleton page and the data page measure
|
|
195
|
+
identical, and nothing shifts when data lands. */
|
|
186
196
|
.skeleton {
|
|
187
197
|
border-radius: var(--radius-bar);
|
|
188
198
|
background: var(--bg-mute);
|
|
@@ -192,13 +202,13 @@ h2 {
|
|
|
192
202
|
.skeleton-label {
|
|
193
203
|
width: 64px;
|
|
194
204
|
height: 12px;
|
|
195
|
-
margin:
|
|
205
|
+
margin: 3.75px 0;
|
|
196
206
|
}
|
|
197
207
|
|
|
198
208
|
.skeleton-value {
|
|
199
209
|
width: 96px;
|
|
200
210
|
height: 20px;
|
|
201
|
-
margin:
|
|
211
|
+
margin: 7px 0 5px;
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
.skeleton-glyph {
|
|
@@ -217,7 +227,7 @@ h2 {
|
|
|
217
227
|
.skeleton-institution {
|
|
218
228
|
width: 110px;
|
|
219
229
|
height: 12px;
|
|
220
|
-
margin:
|
|
230
|
+
margin: 5.5px 0 4.5px;
|
|
221
231
|
}
|
|
222
232
|
|
|
223
233
|
.skeleton-balance {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// token is returned by exactly one poll; callers must never poll again after
|
|
5
5
|
// an approval.
|
|
6
6
|
import { CliError } from "../cli-error.js";
|
|
7
|
+
import { knownDevice } from "../device.js";
|
|
7
8
|
import { sanitizeForTerminal } from "../terminal-text.js";
|
|
8
9
|
// The CLI's own sign-in: read, deploy, and the transaction-rule tools.
|
|
9
10
|
const CLAIM_SCOPE = "driggsby.cli";
|
|
@@ -23,11 +24,13 @@ export const SIGN_IN_START_FAILURE = "We weren't able to start a Driggsby sign-i
|
|
|
23
24
|
// Poll failures happen after the user may already have approved, so "start a
|
|
24
25
|
// sign-in" would be the wrong stage to describe.
|
|
25
26
|
const SIGN_IN_FINISH_FAILURE = "We weren't able to finish your Driggsby sign-in just now. Please try again\nin a minute.";
|
|
26
|
-
|
|
27
|
+
// device: this computer's name and system (device.ts), so the sign-in shows
|
|
28
|
+
// up on Driggsby's MCP page as the computer it is; only known values go.
|
|
29
|
+
export async function createClaimRequest(baseUrl, device = { name: null, system: null }) {
|
|
27
30
|
const response = await fetch(`${baseUrl}/app-tokens/claim-requests`, {
|
|
28
31
|
method: "POST",
|
|
29
32
|
headers: { "Content-Type": "application/json" },
|
|
30
|
-
body: JSON.stringify({ app_name: CLAIM_APP_NAME, scope: CLAIM_SCOPE }),
|
|
33
|
+
body: JSON.stringify({ app_name: CLAIM_APP_NAME, scope: CLAIM_SCOPE, ...deviceField(device) }),
|
|
31
34
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
32
35
|
// Never follow a redirect: a 307/308 would replay this claim flow —
|
|
33
36
|
// and, on the poll below, the poll secret — to whatever origin a
|
|
@@ -141,6 +144,11 @@ function stringField(record, field) {
|
|
|
141
144
|
const value = record?.[field];
|
|
142
145
|
return typeof value === "string" && value !== "" ? value : null;
|
|
143
146
|
}
|
|
147
|
+
// The claim body's device field: only the values this computer knows.
|
|
148
|
+
function deviceField(device) {
|
|
149
|
+
const known = knownDevice(device);
|
|
150
|
+
return known === null ? {} : { device: known };
|
|
151
|
+
}
|
|
144
152
|
// The server's user-facing error text, stripped of terminal control bytes
|
|
145
153
|
// like every other string this CLI did not author, and length-bounded.
|
|
146
154
|
function errorDescription(body) {
|
package/dist/login/login.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// page, poll until they approve, and store the token. The token is shown to
|
|
3
3
|
// no one — it goes straight from the poll response into the credential
|
|
4
4
|
// store.
|
|
5
|
-
import { apiBaseUrl, apiHost } from "../api/base-url.js";
|
|
5
|
+
import { apiBaseUrl, apiHost, baseUrlMayReceiveSavedSignIn } from "../api/base-url.js";
|
|
6
6
|
import { blockedNetworkError } from "../api/network-error.js";
|
|
7
7
|
import { CliError } from "../cli-error.js";
|
|
8
|
+
import { describeDevice } from "../device.js";
|
|
8
9
|
import { defaultCredentialEnvironment, describeStorageLocation, environmentToken, saveToken, } from "../credentials/store.js";
|
|
9
10
|
import { sanitizeForTerminal, wrapProse } from "../terminal-text.js";
|
|
10
11
|
import { createClaimRequest, pollClaimRequest, SIGN_IN_START_FAILURE, } from "./claim-client.js";
|
|
@@ -69,7 +70,10 @@ async function storeApprovedToken(appToken, environment, warn) {
|
|
|
69
70
|
}
|
|
70
71
|
async function createClaim(baseUrl) {
|
|
71
72
|
try {
|
|
72
|
-
|
|
73
|
+
// This computer's name goes only to Driggsby (or a local run), never
|
|
74
|
+
// to another host a base-URL override names.
|
|
75
|
+
const device = baseUrlMayReceiveSavedSignIn(baseUrl) ? describeDevice() : { name: null, system: null };
|
|
76
|
+
return await createClaimRequest(baseUrl, device);
|
|
73
77
|
}
|
|
74
78
|
catch (error) {
|
|
75
79
|
const blocked = blockedNetworkError(error, {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import {} from "./known-client.js";
|
|
2
2
|
export const DRIGGSBY_MCP_URL = "https://app.driggsby.com/mcp";
|
|
3
|
-
|
|
3
|
+
// headers: this computer's name and system for Driggsby's MCP page
|
|
4
|
+
// (device.ts builds them from allowlisted values only). Claude Code takes
|
|
5
|
+
// them after the URL (--header is variadic, so before the name it would
|
|
6
|
+
// swallow it); Codex has no header option, so it never gets any. The
|
|
7
|
+
// commands printed for a person to run carry none.
|
|
8
|
+
export function buildInstallerCommand(client, scope, headers = []) {
|
|
4
9
|
switch (client) {
|
|
5
10
|
case "claude-code":
|
|
6
11
|
return {
|
|
@@ -14,6 +19,7 @@ export function buildInstallerCommand(client, scope) {
|
|
|
14
19
|
scope ?? "user",
|
|
15
20
|
"driggsby",
|
|
16
21
|
DRIGGSBY_MCP_URL,
|
|
22
|
+
...headers.flatMap((header) => ["--header", header]),
|
|
17
23
|
],
|
|
18
24
|
};
|
|
19
25
|
case "codex":
|
package/dist/mcp-setup/setup.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { CliError } from "../cli-error.js";
|
|
3
|
+
import { describeDevice, deviceHeaders } from "../device.js";
|
|
3
4
|
import { trimLikeRust } from "../terminal-text.js";
|
|
4
5
|
import { runClientCommand } from "./client-command.js";
|
|
5
6
|
import { classifyExistingMcpConfig } from "./classify.js";
|
|
@@ -24,7 +25,11 @@ export async function runMcpSetup(options) {
|
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
write(`Adding Driggsby to ${displayName(client)} MCP config...\n`);
|
|
27
|
-
|
|
28
|
+
// The command it runs also names this computer (Claude Code only); the
|
|
29
|
+
// commands it prints for a person stay the plain, portable ones.
|
|
30
|
+
const headers = installClient === "claude-code" ? deviceHeaders(describeDevice()) : [];
|
|
31
|
+
const runInstaller = buildInstallerCommand(installClient, options.scope, headers);
|
|
32
|
+
const result = await runClientCommand(runInstaller, streamConfigOutput(client));
|
|
28
33
|
switch (result.kind) {
|
|
29
34
|
case "output":
|
|
30
35
|
if (result.output.succeeded) {
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// PATH + PATHEXT (never the current directory, which Windows shells would
|
|
6
6
|
// search first) and run shims through an explicit, absolute cmd.exe
|
|
7
7
|
// invocation with our own quoting. Real commands only ever carry fixed
|
|
8
|
-
// constant arguments (see commands.ts)
|
|
9
|
-
//
|
|
8
|
+
// constant arguments (see commands.ts) or this computer's name and system,
|
|
9
|
+
// allowlisted by device.ts to characters cmd.exe treats as plain; nothing
|
|
10
|
+
// user-controlled may ever flow into a spawn plan.
|
|
10
11
|
import { existsSync } from "node:fs";
|
|
11
12
|
import { isAbsolute, join } from "node:path";
|
|
12
13
|
import {} from "./commands.js";
|
|
@@ -80,8 +81,9 @@ export function resolveWindowsProgram(program, lookup = defaultLookup()) {
|
|
|
80
81
|
// (the cmd convention) and trailing backslashes doubled (so a path ending
|
|
81
82
|
// in \ cannot escape the closing quote for the child's own parser). The
|
|
82
83
|
// safe set covers every constant argument the CLI actually runs, including
|
|
83
|
-
// the MCP URL; only fixed constants
|
|
84
|
-
//
|
|
84
|
+
// the MCP URL; only fixed constants, PATH-resolved program paths, and the
|
|
85
|
+
// device values device.ts allowlists (no % ! ^ & | < > " \ or control
|
|
86
|
+
// characters) may reach this function.
|
|
85
87
|
export function quoteForCmd(value) {
|
|
86
88
|
if (/^[A-Za-z0-9_\-.:\\/=]+$/.test(value)) {
|
|
87
89
|
return value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "driggsby",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Set up and build on Driggsby, the MCP server for personal finance.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=18"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@driggsby/deploy": "0.
|
|
32
|
-
"@driggsby/sdk": "0.
|
|
31
|
+
"@driggsby/deploy": "0.6.0",
|
|
32
|
+
"@driggsby/sdk": "0.6.0"
|
|
33
33
|
}
|
|
34
34
|
}
|