noslopui 0.1.0 → 0.3.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 +58 -18
- package/SKILL.md +104 -39
- package/dist/adapters/claude-code.js +21 -121
- package/dist/adapters/codex.js +196 -0
- package/dist/adapters/index.js +16 -5
- package/dist/adapters/json-adapter.js +135 -0
- package/dist/adapters/json-agents.js +88 -0
- package/dist/adapters/types.js +4 -4
- package/dist/commands/account.js +49 -0
- package/dist/commands/catalog.js +214 -0
- package/dist/commands/doctor.js +39 -28
- package/dist/commands/init.js +69 -35
- package/dist/commands/login.js +31 -21
- package/dist/commands/remove.js +40 -5
- package/dist/commands/skill.js +42 -0
- package/dist/commands/update.js +17 -11
- package/dist/index.js +109 -29
- package/dist/lib/api.js +92 -15
- package/dist/lib/json-file.js +7 -2
- package/dist/lib/paths.js +41 -0
- package/dist/lib/session.js +49 -0
- package/dist/lib/skill-file.js +26 -0
- package/dist/lib/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// `noslopui skill` — install only the skill, for someone who connected an
|
|
2
|
+
// agent by hand (a connector sign-in, or a config they wrote). It never
|
|
3
|
+
// touches an agent's MCP config, so a sign-in stays a sign-in.
|
|
4
|
+
import { fetchSkill } from '../lib/api.js';
|
|
5
|
+
import { ADAPTERS, adapterById, detectAdapters } from '../adapters/index.js';
|
|
6
|
+
import { hint, ok, say, warn } from '../lib/ui.js';
|
|
7
|
+
export async function skill(options) {
|
|
8
|
+
say();
|
|
9
|
+
let targets;
|
|
10
|
+
if (options.agents?.length) {
|
|
11
|
+
targets = options.agents.map((id) => {
|
|
12
|
+
const a = adapterById(id);
|
|
13
|
+
if (!a)
|
|
14
|
+
throw new Error(`Unknown agent "${id}". Supported: ${ADAPTERS.map((x) => x.id).join(', ')}.`);
|
|
15
|
+
return a;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
targets = await detectAdapters();
|
|
20
|
+
if (!targets.length)
|
|
21
|
+
throw new Error(`No supported agent found. Pass one with --agent: ${ADAPTERS.map((a) => a.id).join(', ')}.`);
|
|
22
|
+
}
|
|
23
|
+
const download = await fetchSkill();
|
|
24
|
+
if (download.source === 'bundled')
|
|
25
|
+
warn('Using the copy in this package — the site was unreachable, so it may be behind.');
|
|
26
|
+
const written = new Map();
|
|
27
|
+
for (const adapter of targets) {
|
|
28
|
+
const path = adapter.skillPath();
|
|
29
|
+
if (!path) {
|
|
30
|
+
ok(`${adapter.label}: no skills folder; the rules reach it through the server`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!written.has(path))
|
|
34
|
+
written.set(path, (await adapter.installSkill(download.markdown))?.changed ?? false);
|
|
35
|
+
ok(`${adapter.label}: skill ${written.get(path) ? 'installed' : 'already current'}`);
|
|
36
|
+
hint(path);
|
|
37
|
+
}
|
|
38
|
+
say();
|
|
39
|
+
say('Restart your agent to pick it up.');
|
|
40
|
+
say();
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
package/dist/commands/update.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// In practice that means the skill: it's served from noslopui.com and changes
|
|
4
4
|
// as the workflow improves, while the config entry only changes if the server
|
|
5
5
|
// URL does. Nothing here needs a sign-in, so an expired trial or a revoked key
|
|
6
|
-
// doesn't block it.
|
|
6
|
+
// doesn't block it. Several agents share one skills folder, so each folder is
|
|
7
|
+
// written once.
|
|
7
8
|
import { fetchSkill, mcpUrl } from '../lib/api.js';
|
|
8
9
|
import { ADAPTERS } from '../adapters/index.js';
|
|
9
10
|
import { color, hint, ok, say, warn } from '../lib/ui.js';
|
|
@@ -11,6 +12,7 @@ export async function update() {
|
|
|
11
12
|
say();
|
|
12
13
|
let touched = false;
|
|
13
14
|
let markdown = null;
|
|
15
|
+
const written = new Map();
|
|
14
16
|
for (const adapter of ADAPTERS) {
|
|
15
17
|
if (!(await adapter.detect()))
|
|
16
18
|
continue;
|
|
@@ -21,22 +23,26 @@ export async function update() {
|
|
|
21
23
|
}
|
|
22
24
|
if (status.url !== mcpUrl()) {
|
|
23
25
|
warn(`${adapter.label}: configured for ${status.url}, not ${mcpUrl()}.`);
|
|
24
|
-
hint(
|
|
26
|
+
hint(`Run "npx noslopui init --agent ${adapter.id}" to move it — that needs a key, so it is not done here.`);
|
|
25
27
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
hint(skill.path);
|
|
31
|
-
touched = true;
|
|
28
|
+
const path = adapter.skillPath();
|
|
29
|
+
if (!path) {
|
|
30
|
+
ok(`${adapter.label}: no skills folder; the rules reach it through the server`);
|
|
31
|
+
continue;
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
if (!written.has(path)) {
|
|
34
|
+
markdown ??= (await fetchSkill()).markdown;
|
|
35
|
+
const skill = await adapter.installSkill(markdown);
|
|
36
|
+
written.set(path, skill?.changed ?? false);
|
|
37
|
+
if (skill?.changed)
|
|
38
|
+
touched = true;
|
|
35
39
|
}
|
|
40
|
+
ok(`${adapter.label}: skill ${written.get(path) ? 'updated' : 'already current'}`);
|
|
41
|
+
hint(path);
|
|
36
42
|
}
|
|
37
43
|
say();
|
|
38
44
|
if (touched)
|
|
39
|
-
say(
|
|
45
|
+
say('Restart your agents to pick up the new workflow.');
|
|
40
46
|
say();
|
|
41
47
|
return 0;
|
|
42
48
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// noslopui — connect a coding agent to noslopUI
|
|
2
|
+
// noslopui — connect a coding agent to noslopUI, and use the catalog from the
|
|
3
|
+
// terminal.
|
|
3
4
|
//
|
|
4
5
|
// Argument parsing is by hand and stays that way: a tool people run with
|
|
5
6
|
// `npx` should download in one tick, and a CLI-argument library is a
|
|
@@ -9,6 +10,10 @@ import { doctor } from './commands/doctor.js';
|
|
|
9
10
|
import { remove } from './commands/remove.js';
|
|
10
11
|
import { update } from './commands/update.js';
|
|
11
12
|
import { login } from './commands/login.js';
|
|
13
|
+
import { logout, whoami } from './commands/account.js';
|
|
14
|
+
import { skill } from './commands/skill.js';
|
|
15
|
+
import * as catalog from './commands/catalog.js';
|
|
16
|
+
import { ADAPTERS } from './adapters/index.js';
|
|
12
17
|
import { color, fail, say } from './lib/ui.js';
|
|
13
18
|
import { origin } from './lib/api.js';
|
|
14
19
|
import { CLI_VERSION as VERSION } from './lib/version.js';
|
|
@@ -19,52 +24,96 @@ ${color.bold('noslopui')} — hand-crafted UI components and design systems, in
|
|
|
19
24
|
${color.bold('Usage')}
|
|
20
25
|
npx noslopui <command> [options]
|
|
21
26
|
|
|
22
|
-
${color.bold('
|
|
23
|
-
init
|
|
24
|
-
doctor
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
remove
|
|
27
|
+
${color.bold('Set up')}
|
|
28
|
+
init Set up your agents: one sign-in, then the MCP server and the skill
|
|
29
|
+
doctor Check what is set up and what is wrong with it
|
|
30
|
+
update Re-download the skill for agents that are set up
|
|
31
|
+
skill Install only the skill (for an agent you connected by hand)
|
|
32
|
+
remove Take the noslopUI server back out (never touches other servers)
|
|
33
|
+
|
|
34
|
+
${color.bold('Account')}
|
|
35
|
+
login Sign this CLI in (for search and get); --agent <id> re-keys an agent
|
|
36
|
+
whoami Show the account and plan this CLI is signed in to
|
|
37
|
+
logout Forget this CLI's sign-in
|
|
38
|
+
|
|
39
|
+
${color.bold('Catalog')}
|
|
40
|
+
search <query> Search blocks and effects --section ui-blocks|motion-lab, --tag <facet:value>, --limit
|
|
41
|
+
get <id> Print a component's code --format tsx|html|vue, --write <file>
|
|
42
|
+
prompt <id> Print a component's AI-prompt version
|
|
43
|
+
design-systems [query] Search design systems --tag theme:dark …
|
|
44
|
+
design-system <id> Print a design system's DESIGN.md --write DESIGN.md
|
|
45
|
+
collections List your saved collections
|
|
46
|
+
collection <name|id> Show one collection
|
|
28
47
|
|
|
29
48
|
${color.bold('Options')}
|
|
30
|
-
--agent <id>
|
|
49
|
+
--agent <id> Only this agent (repeatable): ${ADAPTERS.map((a) => a.id).join(', ')}
|
|
31
50
|
--no-skill Do not install the skill (init)
|
|
32
51
|
--no-browser Do not open a browser; print the URL instead (init, login)
|
|
33
52
|
--skill Also remove the skill (remove)
|
|
34
53
|
--key <key> Use a key you already have instead of signing in (init, login)
|
|
54
|
+
--json Machine-readable output (catalog commands, whoami)
|
|
55
|
+
--force Overwrite the file --write points at
|
|
35
56
|
-y, --yes Take the default answer for every question
|
|
36
57
|
-h, --help Show this
|
|
37
58
|
-v, --version Show the version
|
|
38
59
|
|
|
39
|
-
${color.dim(`Server: ${origin()} ·
|
|
60
|
+
${color.dim(`Server: ${origin()} · NOSLOPUI_ORIGIN points somewhere else · NOSLOPUI_API_KEY signs in without a saved login.`)}
|
|
40
61
|
`);
|
|
41
62
|
}
|
|
42
63
|
function parse(argv) {
|
|
43
|
-
const parsed = { command: '', agents: [], skill: null, yes: false, browser: true, help: false, version: false };
|
|
64
|
+
const parsed = { command: '', args: [], agents: [], tags: [], skill: null, yes: false, browser: true, help: false, version: false, json: false, force: false };
|
|
65
|
+
const value = (i, name) => {
|
|
66
|
+
const v = argv[i];
|
|
67
|
+
if (v === undefined || v.startsWith('-'))
|
|
68
|
+
throw new Error(`${name} needs a value.`);
|
|
69
|
+
return v;
|
|
70
|
+
};
|
|
44
71
|
for (let i = 0; i < argv.length; i++) {
|
|
45
72
|
const arg = argv[i];
|
|
46
|
-
|
|
73
|
+
const [flag, inline] = arg.startsWith('--') && arg.includes('=') ? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [arg, undefined];
|
|
74
|
+
const take = () => inline ?? value(++i, flag);
|
|
75
|
+
if (flag === '--help' || flag === '-h')
|
|
47
76
|
parsed.help = true;
|
|
48
|
-
else if (
|
|
77
|
+
else if (flag === '--version' || flag === '-v')
|
|
49
78
|
parsed.version = true;
|
|
50
|
-
else if (
|
|
79
|
+
else if (flag === '--yes' || flag === '-y')
|
|
51
80
|
parsed.yes = true;
|
|
52
|
-
else if (
|
|
81
|
+
else if (flag === '--no-skill')
|
|
53
82
|
parsed.skill = false;
|
|
54
|
-
else if (
|
|
83
|
+
else if (flag === '--no-browser')
|
|
55
84
|
parsed.browser = false;
|
|
56
|
-
else if (
|
|
85
|
+
else if (flag === '--skill')
|
|
57
86
|
parsed.skill = true;
|
|
58
|
-
else if (
|
|
59
|
-
parsed.
|
|
60
|
-
else if (
|
|
61
|
-
parsed.
|
|
62
|
-
else if (
|
|
63
|
-
parsed.
|
|
64
|
-
else if (
|
|
65
|
-
parsed.
|
|
66
|
-
else if (
|
|
67
|
-
parsed.
|
|
87
|
+
else if (flag === '--json')
|
|
88
|
+
parsed.json = true;
|
|
89
|
+
else if (flag === '--force')
|
|
90
|
+
parsed.force = true;
|
|
91
|
+
else if (flag === '--agent')
|
|
92
|
+
parsed.agents.push(take());
|
|
93
|
+
else if (flag === '--tag')
|
|
94
|
+
parsed.tags.push(take());
|
|
95
|
+
else if (flag === '--key')
|
|
96
|
+
parsed.key = take();
|
|
97
|
+
else if (flag === '--format')
|
|
98
|
+
parsed.format = take();
|
|
99
|
+
else if (flag === '--framework')
|
|
100
|
+
parsed.framework = take();
|
|
101
|
+
else if (flag === '--section')
|
|
102
|
+
parsed.section = take();
|
|
103
|
+
else if (flag === '--write' || flag === '-o')
|
|
104
|
+
parsed.write = take();
|
|
105
|
+
else if (flag === '--limit') {
|
|
106
|
+
const n = Number(take());
|
|
107
|
+
if (!Number.isInteger(n) || n < 1)
|
|
108
|
+
throw new Error('--limit needs a whole number.');
|
|
109
|
+
parsed.limit = n;
|
|
110
|
+
}
|
|
111
|
+
else if (!arg.startsWith('-')) {
|
|
112
|
+
if (!parsed.command)
|
|
113
|
+
parsed.command = arg;
|
|
114
|
+
else
|
|
115
|
+
parsed.args.push(arg);
|
|
116
|
+
}
|
|
68
117
|
else
|
|
69
118
|
throw new Error(`Unknown option "${arg}". Run "npx noslopui --help".`);
|
|
70
119
|
}
|
|
@@ -81,6 +130,17 @@ async function main() {
|
|
|
81
130
|
usage();
|
|
82
131
|
return args.command ? 0 : 1;
|
|
83
132
|
}
|
|
133
|
+
const catalogOptions = {
|
|
134
|
+
args: args.args,
|
|
135
|
+
json: args.json,
|
|
136
|
+
tags: args.tags,
|
|
137
|
+
force: args.force,
|
|
138
|
+
...(args.limit ? { limit: args.limit } : {}),
|
|
139
|
+
...(args.format ? { format: args.format } : {}),
|
|
140
|
+
...(args.framework ? { framework: args.framework } : {}),
|
|
141
|
+
...(args.section ? { section: args.section } : {}),
|
|
142
|
+
...(args.write ? { write: args.write } : {}),
|
|
143
|
+
};
|
|
84
144
|
switch (args.command) {
|
|
85
145
|
case 'init':
|
|
86
146
|
return init({ agents: args.agents, skill: args.skill !== false, yes: args.yes, browser: args.browser, ...(args.key ? { key: args.key } : {}) });
|
|
@@ -88,10 +148,30 @@ async function main() {
|
|
|
88
148
|
return doctor();
|
|
89
149
|
case 'login':
|
|
90
150
|
return login({ agents: args.agents, yes: args.yes, browser: args.browser, ...(args.key ? { key: args.key } : {}) });
|
|
151
|
+
case 'logout':
|
|
152
|
+
return logout();
|
|
153
|
+
case 'whoami':
|
|
154
|
+
return whoami({ json: args.json });
|
|
91
155
|
case 'update':
|
|
92
156
|
return update();
|
|
157
|
+
case 'skill':
|
|
158
|
+
return skill({ agents: args.agents });
|
|
93
159
|
case 'remove':
|
|
94
|
-
return remove({ yes: args.yes, skill: args.skill === true });
|
|
160
|
+
return remove({ yes: args.yes, skill: args.skill === true, agents: args.agents });
|
|
161
|
+
case 'search':
|
|
162
|
+
return catalog.search(catalogOptions);
|
|
163
|
+
case 'get':
|
|
164
|
+
return catalog.get(catalogOptions);
|
|
165
|
+
case 'prompt':
|
|
166
|
+
return catalog.prompt(catalogOptions);
|
|
167
|
+
case 'design-systems':
|
|
168
|
+
return catalog.designSystems(catalogOptions);
|
|
169
|
+
case 'design-system':
|
|
170
|
+
return catalog.designSystem(catalogOptions);
|
|
171
|
+
case 'collections':
|
|
172
|
+
return catalog.collections(catalogOptions);
|
|
173
|
+
case 'collection':
|
|
174
|
+
return catalog.collection(catalogOptions);
|
|
95
175
|
default:
|
|
96
176
|
fail(`Unknown command "${args.command}".`);
|
|
97
177
|
usage();
|
|
@@ -108,8 +188,8 @@ main()
|
|
|
108
188
|
process.exitCode = code;
|
|
109
189
|
})
|
|
110
190
|
.catch((err) => {
|
|
111
|
-
|
|
191
|
+
process.stderr.write('\n');
|
|
112
192
|
fail(explain(err));
|
|
113
|
-
|
|
193
|
+
process.stderr.write('\n');
|
|
114
194
|
process.exitCode = 1;
|
|
115
195
|
});
|
package/dist/lib/api.js
CHANGED
|
@@ -76,19 +76,28 @@ export async function waitForApproval(started, onTick) {
|
|
|
76
76
|
}
|
|
77
77
|
throw new ApiError('The code expired before it was approved. Run the command again to get a new one.');
|
|
78
78
|
}
|
|
79
|
+
const MCP_HEADERS = {
|
|
80
|
+
'content-type': 'application/json',
|
|
81
|
+
accept: 'application/json, text/event-stream',
|
|
82
|
+
'mcp-protocol-version': '2025-06-18',
|
|
83
|
+
'user-agent': USER_AGENT,
|
|
84
|
+
};
|
|
85
|
+
/** The last JSON-RPC message in a reply, whether it came as plain JSON or SSE-framed. */
|
|
86
|
+
function rpcBody(text) {
|
|
87
|
+
const line = text.split(/\r?\n/).filter((l) => l.startsWith('data: ')).pop();
|
|
88
|
+
return JSON.parse(line ? line.slice(6) : text);
|
|
89
|
+
}
|
|
79
90
|
/**
|
|
80
91
|
* `doctor`'s probe: can we reach the MCP server, and does this key still work?
|
|
81
|
-
*
|
|
82
|
-
*
|
|
92
|
+
*
|
|
93
|
+
* Every MCP call needs a key or a sign-in, so without one the right answer is
|
|
94
|
+
* our own 401 pointing at the OAuth metadata — that's "reachable", and a
|
|
95
|
+
* challenge page or a proxy error is not. With a key it calls list_collections,
|
|
96
|
+
* the one tool that costs the account nothing: no trial clock, no fair-use count.
|
|
83
97
|
*/
|
|
84
98
|
export async function checkServer(key) {
|
|
85
99
|
let res;
|
|
86
|
-
const headers = {
|
|
87
|
-
'content-type': 'application/json',
|
|
88
|
-
accept: 'application/json, text/event-stream',
|
|
89
|
-
'mcp-protocol-version': '2025-06-18',
|
|
90
|
-
'user-agent': USER_AGENT,
|
|
91
|
-
};
|
|
100
|
+
const headers = { ...MCP_HEADERS };
|
|
92
101
|
if (key)
|
|
93
102
|
headers.authorization = `Bearer ${key}`;
|
|
94
103
|
try {
|
|
@@ -106,28 +115,96 @@ export async function checkServer(key) {
|
|
|
106
115
|
catch {
|
|
107
116
|
return { reachable: false, detail: `Could not reach ${mcpUrl()}.` };
|
|
108
117
|
}
|
|
118
|
+
const text = await res.text();
|
|
119
|
+
if (res.status === 401) {
|
|
120
|
+
const ours = /resource_metadata=/.test(res.headers.get('www-authenticate') ?? '');
|
|
121
|
+
if (!ours)
|
|
122
|
+
return { reachable: false, detail: `${mcpUrl()} answered 401, but not in the way noslopUI does — something in between is answering.` };
|
|
123
|
+
if (!key)
|
|
124
|
+
return { reachable: true, detail: 'Server reachable, and asking for sign-in as it should.' };
|
|
125
|
+
let message = 'The configured key was refused.';
|
|
126
|
+
try {
|
|
127
|
+
message = rpcBody(text)?.error?.message ?? message;
|
|
128
|
+
}
|
|
129
|
+
catch { }
|
|
130
|
+
return { reachable: true, detail: 'Server reachable.', keyState: 'rejected', keyDetail: message.split('. ')[0] };
|
|
131
|
+
}
|
|
109
132
|
if (!res.ok)
|
|
110
133
|
return { reachable: false, detail: `${mcpUrl()} answered HTTP ${res.status}.` };
|
|
111
|
-
|
|
112
|
-
const line = text.split(/\r?\n/).filter((l) => l.startsWith('data: ')).pop();
|
|
113
|
-
let json = null;
|
|
134
|
+
let json;
|
|
114
135
|
try {
|
|
115
|
-
json =
|
|
136
|
+
json = rpcBody(text);
|
|
116
137
|
}
|
|
117
138
|
catch {
|
|
118
139
|
return { reachable: false, detail: 'The server answered with something unreadable.' };
|
|
119
140
|
}
|
|
120
141
|
if (!key)
|
|
121
142
|
return { reachable: true, detail: `${(json?.result?.tools ?? []).length} tools available.` };
|
|
122
|
-
|
|
123
|
-
const body = json?.result?.content?.[0]?.text ?? '';
|
|
124
|
-
if (isError) {
|
|
143
|
+
if (json?.result?.isError === true) {
|
|
125
144
|
// The server writes these for an agent to relay; they're already the right
|
|
126
145
|
// words for a person, so pass them through rather than inventing our own.
|
|
146
|
+
const body = json?.result?.content?.[0]?.text ?? '';
|
|
127
147
|
return { reachable: true, detail: 'Server reachable.', keyState: 'rejected', keyDetail: body.split('. ')[0] };
|
|
128
148
|
}
|
|
129
149
|
return { reachable: true, detail: 'Server reachable.', keyState: 'ok', keyDetail: 'Key accepted.' };
|
|
130
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* One MCP tool call — what the terminal commands are built on, so the CLI and
|
|
153
|
+
* an agent can never see different things, and every gate (plan, trial,
|
|
154
|
+
* fair use) is the server's.
|
|
155
|
+
*/
|
|
156
|
+
export async function callTool(key, name, args = {}) {
|
|
157
|
+
let res;
|
|
158
|
+
try {
|
|
159
|
+
res = await fetch(mcpUrl(), {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: { ...MCP_HEADERS, authorization: `Bearer ${key}` },
|
|
162
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name, arguments: args } }),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
throw new ApiError(`Could not reach ${origin()}. Check your connection, then run the command again.`);
|
|
167
|
+
}
|
|
168
|
+
const text = await res.text();
|
|
169
|
+
let json;
|
|
170
|
+
try {
|
|
171
|
+
json = rpcBody(text);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
throw new ApiError(`${origin()} returned something that isn't JSON (HTTP ${res.status}).`, res.status);
|
|
175
|
+
}
|
|
176
|
+
if (res.status === 401) {
|
|
177
|
+
throw new ApiError(`${json?.error?.message ?? 'Not signed in.'}\n Run "npx noslopui login" to sign in again.`, 401);
|
|
178
|
+
}
|
|
179
|
+
if (json?.error)
|
|
180
|
+
throw new ApiError(json.error.message ?? `The server refused the call (${json.error.code}).`);
|
|
181
|
+
const out = json?.result?.content?.[0]?.text ?? '';
|
|
182
|
+
let data = null;
|
|
183
|
+
try {
|
|
184
|
+
data = JSON.parse(out);
|
|
185
|
+
}
|
|
186
|
+
catch { }
|
|
187
|
+
return { isError: json?.result?.isError === true, text: out, data };
|
|
188
|
+
}
|
|
189
|
+
export async function whoami(key) {
|
|
190
|
+
let res;
|
|
191
|
+
try {
|
|
192
|
+
res = await fetch(`${origin()}/api/cli/me`, { headers: { authorization: `Bearer ${key}`, 'user-agent': USER_AGENT } });
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
throw new ApiError(`Could not reach ${origin()}. Check your connection, then run the command again.`);
|
|
196
|
+
}
|
|
197
|
+
let json = null;
|
|
198
|
+
try {
|
|
199
|
+
json = await res.json();
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
throw new ApiError(`${origin()} returned something that isn't JSON (HTTP ${res.status}).`, res.status);
|
|
203
|
+
}
|
|
204
|
+
if (!res.ok)
|
|
205
|
+
throw new ApiError(json?.message ?? `HTTP ${res.status}`, res.status);
|
|
206
|
+
return json;
|
|
207
|
+
}
|
|
131
208
|
/**
|
|
132
209
|
* The workflow file. Fetched from the site, because the site is where it's
|
|
133
210
|
* assembled — the same text the MCP server sends as its instructions, so the
|
package/dist/lib/json-file.js
CHANGED
|
@@ -18,7 +18,8 @@ export class ConfigUnreadableError extends Error {
|
|
|
18
18
|
path;
|
|
19
19
|
cause_;
|
|
20
20
|
constructor(path, cause_) {
|
|
21
|
-
|
|
21
|
+
const why = cause_ instanceof Error && cause_.message ? ` (${cause_.message})` : '';
|
|
22
|
+
super(`${path} couldn't be read safely${why}, so it was left untouched.`);
|
|
22
23
|
this.path = path;
|
|
23
24
|
this.cause_ = cause_;
|
|
24
25
|
this.name = 'ConfigUnreadableError';
|
|
@@ -65,11 +66,15 @@ export function backupFile(path) {
|
|
|
65
66
|
}
|
|
66
67
|
/** Write via a temp file in the same directory, then rename over the target. */
|
|
67
68
|
export function writeJsonFile(path, value, { indent = 2 } = {}) {
|
|
69
|
+
writeTextFile(path, `${JSON.stringify(value, null, indent)}\n`);
|
|
70
|
+
}
|
|
71
|
+
/** The same atomic write for a file that isn't JSON (Codex's config.toml). */
|
|
72
|
+
export function writeTextFile(path, text) {
|
|
68
73
|
const dir = dirname(path);
|
|
69
74
|
mkdirSync(dir, { recursive: true });
|
|
70
75
|
const temp = join(dir, `.noslopui-write-${process.pid}-${Date.now()}.tmp`);
|
|
71
76
|
try {
|
|
72
|
-
writeFileSync(temp,
|
|
77
|
+
writeFileSync(temp, text, 'utf8');
|
|
73
78
|
renameSync(temp, path);
|
|
74
79
|
}
|
|
75
80
|
catch (err) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Where agents keep their files. Every path starts from the home directory or
|
|
2
|
+
// an environment variable the OS sets for it, never a hardcoded user folder, so
|
|
3
|
+
// the tests can point all of them at a throwaway directory.
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { delimiter, join } from 'node:path';
|
|
7
|
+
export const home = () => homedir();
|
|
8
|
+
/** `%APPDATA%` on Windows (Roaming), where Electron apps and Devin keep user config. */
|
|
9
|
+
export function appData() {
|
|
10
|
+
return process.env.APPDATA || join(homedir(), 'AppData', 'Roaming');
|
|
11
|
+
}
|
|
12
|
+
/** `$XDG_CONFIG_HOME`, or `~/.config` — the Linux/macOS home for CLI config. */
|
|
13
|
+
export function xdgConfig() {
|
|
14
|
+
return process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
|
|
15
|
+
}
|
|
16
|
+
/** Where a VS Code-family app keeps its user settings, per OS. */
|
|
17
|
+
export function vscodeUserDir(product = 'Code') {
|
|
18
|
+
if (process.platform === 'win32')
|
|
19
|
+
return join(appData(), product, 'User');
|
|
20
|
+
if (process.platform === 'darwin')
|
|
21
|
+
return join(homedir(), 'Library', 'Application Support', product, 'User');
|
|
22
|
+
return join(xdgConfig(), product, 'User');
|
|
23
|
+
}
|
|
24
|
+
/** The skills folder several agents share: Codex, Cursor, OpenCode, VS Code. */
|
|
25
|
+
export const sharedSkillPath = () => join(homedir(), '.agents', 'skills', 'noslopui', 'SKILL.md');
|
|
26
|
+
export function onPath(binary) {
|
|
27
|
+
const paths = (process.env.PATH ?? '').split(delimiter).filter(Boolean);
|
|
28
|
+
const suffixes = process.platform === 'win32' ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';') : [''];
|
|
29
|
+
for (const dir of paths) {
|
|
30
|
+
for (const suffix of suffixes) {
|
|
31
|
+
try {
|
|
32
|
+
if (existsSync(join(dir, binary + suffix)))
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// An unreadable PATH entry is not our problem; keep looking.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The CLI's own sign-in — what `noslopui search`, `get` and `whoami` use.
|
|
2
|
+
//
|
|
3
|
+
// It's an ordinary noslopUI API key, issued for the agent id `cli` by the same
|
|
4
|
+
// browser approval `init` uses, and named after this machine on the account
|
|
5
|
+
// page, where it can be revoked like any other. It's stored in the user's
|
|
6
|
+
// config folder with owner-only permissions, and never printed in full.
|
|
7
|
+
//
|
|
8
|
+
// Agent keys stay in each agent's own config; this file is only the CLI's.
|
|
9
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { appData, xdgConfig } from './paths.js';
|
|
12
|
+
export function sessionPath() {
|
|
13
|
+
const dir = process.platform === 'win32' ? join(appData(), 'noslopui') : join(xdgConfig(), 'noslopui');
|
|
14
|
+
return join(dir, 'credentials.json');
|
|
15
|
+
}
|
|
16
|
+
export function readSession() {
|
|
17
|
+
if (process.env.NOSLOPUI_API_KEY)
|
|
18
|
+
return { key: process.env.NOSLOPUI_API_KEY, savedAt: 'env' };
|
|
19
|
+
const path = sessionPath();
|
|
20
|
+
if (!existsSync(path))
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
24
|
+
return typeof parsed?.key === 'string' && parsed.key ? parsed : null;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function saveSession(key) {
|
|
31
|
+
const path = sessionPath();
|
|
32
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
33
|
+
writeFileSync(path, `${JSON.stringify({ key, savedAt: new Date().toISOString() }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
34
|
+
try {
|
|
35
|
+
// mode only applies when the file is created; tighten an existing one too.
|
|
36
|
+
chmodSync(path, 0o600);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Windows ignores POSIX modes; the file is under the user's own profile.
|
|
40
|
+
}
|
|
41
|
+
return path;
|
|
42
|
+
}
|
|
43
|
+
export function clearSession() {
|
|
44
|
+
const path = sessionPath();
|
|
45
|
+
if (!existsSync(path))
|
|
46
|
+
return false;
|
|
47
|
+
rmSync(path, { force: true });
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Writing and removing the skill file. Shared by every adapter, because several
|
|
2
|
+
// agents read the same folder (`~/.agents/skills/` is read by Codex, Cursor,
|
|
3
|
+
// OpenCode and VS Code alike) — one copy there serves all of them.
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
export function installSkillAt(path, markdown) {
|
|
7
|
+
if (existsSync(path)) {
|
|
8
|
+
try {
|
|
9
|
+
if (readFileSync(path, 'utf8') === markdown)
|
|
10
|
+
return { path, changed: false };
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// Unreadable but present: fall through and replace it.
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
17
|
+
writeFileSync(path, markdown, 'utf8');
|
|
18
|
+
return { path, changed: true };
|
|
19
|
+
}
|
|
20
|
+
export function removeSkillAt(path) {
|
|
21
|
+
if (!existsSync(path))
|
|
22
|
+
return false;
|
|
23
|
+
// Only our own folder (…/skills/noslopui/), and only because we created it.
|
|
24
|
+
rmSync(dirname(path), { recursive: true, force: true });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
package/dist/lib/version.js
CHANGED