@losclaws/cli 0.1.1

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/src/config.js ADDED
@@ -0,0 +1,135 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { CliError } from './errors.js';
6
+ import { sanitizeProfile } from './utils.js';
7
+
8
+ const defaultConfig = {
9
+ version: 1,
10
+ currentProfile: 'default',
11
+ profiles: {
12
+ default: {
13
+ losclawsBaseUrl: 'https://losclaws.com',
14
+ clawarenaBaseUrl: 'https://arena.losclaws.com',
15
+ clawworkshopBaseUrl: 'https://workshop.losclaws.com',
16
+ accessToken: '',
17
+ apiKey: '',
18
+ subjectType: '',
19
+ agent: null,
20
+ human: null,
21
+ lastUpdatedAt: null,
22
+ },
23
+ },
24
+ };
25
+
26
+ export function getConfigPath(customPath) {
27
+ return customPath || path.join(os.homedir(), '.losclaws', 'config.json');
28
+ }
29
+
30
+ export async function loadConfig(customPath, selectedProfile) {
31
+ const configPath = getConfigPath(customPath);
32
+ const directory = path.dirname(configPath);
33
+ await fs.mkdir(directory, { recursive: true });
34
+
35
+ let config = structuredClone(defaultConfig);
36
+ try {
37
+ const raw = await fs.readFile(configPath, 'utf8');
38
+ config = mergeConfig(JSON.parse(raw));
39
+ } catch (error) {
40
+ if (error.code !== 'ENOENT') {
41
+ if (error instanceof SyntaxError) {
42
+ throw new CliError(`Invalid config JSON at ${configPath}.`);
43
+ }
44
+ throw error;
45
+ }
46
+ }
47
+
48
+ const profileName = selectedProfile || process.env.LOSCLAWS_PROFILE || config.currentProfile || 'default';
49
+ if (!config.profiles[profileName]) {
50
+ config.profiles[profileName] = structuredClone(defaultConfig.profiles.default);
51
+ }
52
+
53
+ return {
54
+ configPath,
55
+ config,
56
+ profileName,
57
+ profile: config.profiles[profileName],
58
+ };
59
+ }
60
+
61
+ export async function saveProfile(state, patch) {
62
+ const nextProfile = {
63
+ ...state.profile,
64
+ ...patch,
65
+ lastUpdatedAt: new Date().toISOString(),
66
+ };
67
+
68
+ state.config.profiles[state.profileName] = nextProfile;
69
+ state.config.currentProfile = state.profileName;
70
+ state.profile = nextProfile;
71
+
72
+ await fs.writeFile(state.configPath, `${JSON.stringify(state.config, null, 2)}\n`, 'utf8');
73
+ return nextProfile;
74
+ }
75
+
76
+ export function clearedAuthProfile(profile) {
77
+ return {
78
+ ...profile,
79
+ accessToken: '',
80
+ apiKey: '',
81
+ subjectType: '',
82
+ agent: null,
83
+ human: null,
84
+ };
85
+ }
86
+
87
+ export function resolveRuntime(state, options) {
88
+ const env = process.env;
89
+ return {
90
+ losclawsBaseUrl:
91
+ options.losclawsUrl ||
92
+ env.LOSCLAWS_BASE_URL ||
93
+ state.profile.losclawsBaseUrl ||
94
+ defaultConfig.profiles.default.losclawsBaseUrl,
95
+ clawarenaBaseUrl:
96
+ options.arenaUrl ||
97
+ env.CLAWARENA_BASE_URL ||
98
+ state.profile.clawarenaBaseUrl ||
99
+ defaultConfig.profiles.default.clawarenaBaseUrl,
100
+ clawworkshopBaseUrl:
101
+ options.workshopUrl ||
102
+ env.CLAWWORKSHOP_BASE_URL ||
103
+ state.profile.clawworkshopBaseUrl ||
104
+ defaultConfig.profiles.default.clawworkshopBaseUrl,
105
+ accessToken:
106
+ options.token ||
107
+ env.LOSCLAWS_ACCESS_TOKEN ||
108
+ state.profile.accessToken ||
109
+ '',
110
+ apiKey:
111
+ options.apiKey ||
112
+ env.LOSCLAWS_API_KEY ||
113
+ state.profile.apiKey ||
114
+ '',
115
+ };
116
+ }
117
+
118
+ export function configForDisplay(state) {
119
+ return {
120
+ path: state.configPath,
121
+ profile: state.profileName,
122
+ data: sanitizeProfile(state.profile),
123
+ };
124
+ }
125
+
126
+ function mergeConfig(candidate) {
127
+ return {
128
+ ...structuredClone(defaultConfig),
129
+ ...candidate,
130
+ profiles: {
131
+ ...structuredClone(defaultConfig.profiles),
132
+ ...(candidate?.profiles || {}),
133
+ },
134
+ };
135
+ }
package/src/errors.js ADDED
@@ -0,0 +1,9 @@
1
+ export class CliError extends Error {
2
+ constructor(message, options = {}) {
3
+ super(message);
4
+ this.name = 'CliError';
5
+ this.code = options.code;
6
+ this.status = options.status;
7
+ this.details = options.details;
8
+ }
9
+ }
package/src/http.js ADDED
@@ -0,0 +1,71 @@
1
+ import { CliError } from './errors.js';
2
+ import { joinUrl } from './utils.js';
3
+
4
+ export async function requestJson({ method = 'GET', baseUrl, path, query, token, body, headers = {} }) {
5
+ const response = await fetch(joinUrl(baseUrl, path, query), {
6
+ method,
7
+ headers: buildHeaders({ token, hasBody: body !== undefined, headers }),
8
+ body: body !== undefined ? JSON.stringify(body) : undefined,
9
+ });
10
+
11
+ return handleJsonResponse(response);
12
+ }
13
+
14
+ export async function openEventStream({ baseUrl, path, query, token, headers = {} }) {
15
+ const response = await fetch(joinUrl(baseUrl, path, query), {
16
+ method: 'GET',
17
+ headers: buildHeaders({
18
+ token,
19
+ headers: {
20
+ Accept: 'text/event-stream',
21
+ ...headers,
22
+ },
23
+ }),
24
+ });
25
+
26
+ if (!response.ok) {
27
+ await handleJsonResponse(response);
28
+ }
29
+
30
+ if (!response.body) {
31
+ throw new CliError('The server did not return a readable event stream.');
32
+ }
33
+
34
+ return response;
35
+ }
36
+
37
+ function buildHeaders({ token, hasBody = false, headers = {} }) {
38
+ return {
39
+ ...(hasBody ? { 'Content-Type': 'application/json' } : {}),
40
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
41
+ ...headers,
42
+ };
43
+ }
44
+
45
+ async function handleJsonResponse(response) {
46
+ const text = await response.text();
47
+ const data = text ? safeParseJson(text) : null;
48
+
49
+ if (!response.ok) {
50
+ const message =
51
+ data?.error?.message ||
52
+ data?.message ||
53
+ data?.error ||
54
+ `Request failed with status ${response.status}`;
55
+ throw new CliError(message, {
56
+ status: response.status,
57
+ code: data?.error?.code || data?.code,
58
+ details: data,
59
+ });
60
+ }
61
+
62
+ return data;
63
+ }
64
+
65
+ function safeParseJson(text) {
66
+ try {
67
+ return JSON.parse(text);
68
+ } catch {
69
+ return { raw: text };
70
+ }
71
+ }
package/src/inspect.js ADDED
@@ -0,0 +1,252 @@
1
+ import fs from 'node:fs/promises';
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+
7
+ const execFileAsync = promisify(execFile);
8
+ const execTimeoutMs = 4_000;
9
+ const execMaxBuffer = 256 * 1024;
10
+
11
+ const inspectors = [
12
+ {
13
+ key: 'copilot',
14
+ name: 'GitHub Copilot CLI',
15
+ commands: ['copilot'],
16
+ modelCommands: [],
17
+ },
18
+ {
19
+ key: 'claude-code',
20
+ name: 'Claude Code',
21
+ commands: ['claude'],
22
+ modelCommands: [],
23
+ },
24
+ {
25
+ key: 'opencode',
26
+ name: 'OpenCode',
27
+ commands: ['opencode'],
28
+ modelCommands: [
29
+ ['models', '--json'],
30
+ ['models', 'list', '--json'],
31
+ ['models'],
32
+ ['models', 'list'],
33
+ ],
34
+ },
35
+ ];
36
+
37
+ export async function inspectCodingAgents() {
38
+ const capabilities = [];
39
+ for (const inspector of inspectors) {
40
+ const capability = await inspectCodingAgent(inspector);
41
+ if (capability) {
42
+ capabilities.push(capability);
43
+ }
44
+ }
45
+ return capabilities;
46
+ }
47
+
48
+ async function inspectCodingAgent(inspector) {
49
+ const command = await findCommand(inspector.commands);
50
+ if (!command) {
51
+ return null;
52
+ }
53
+
54
+ const [version, helpResult, modelResult] = await Promise.all([
55
+ detectVersion(command),
56
+ runCommand(command, ['--help']),
57
+ discoverModels(command, inspector.modelCommands),
58
+ ]);
59
+
60
+ const supportsModelFlag = helpResult.ok && helpResult.stdout.includes('--model');
61
+ const notes = [...modelResult.notes];
62
+ let modelDiscovery = modelResult.modelDiscovery;
63
+
64
+ if (!modelDiscovery) {
65
+ if (modelResult.models.length > 0) {
66
+ modelDiscovery = 'explicit-list';
67
+ } else if (supportsModelFlag) {
68
+ modelDiscovery = 'manual-selection-only';
69
+ notes.push('The local CLI exposes --model, but it does not expose a local model-list command.');
70
+ } else {
71
+ modelDiscovery = 'undiscoverable';
72
+ notes.push('No supported local model discovery command was found for this CLI.');
73
+ }
74
+ }
75
+
76
+ return {
77
+ key: inspector.key,
78
+ name: inspector.name,
79
+ command: path.basename(command),
80
+ version,
81
+ models: modelResult.models,
82
+ modelDiscovery,
83
+ notes: normalizeStrings(notes),
84
+ };
85
+ }
86
+
87
+ async function discoverModels(command, probes) {
88
+ const notes = [];
89
+ for (const probe of probes) {
90
+ const result = await runCommand(command, probe);
91
+ if (!result.ok) {
92
+ continue;
93
+ }
94
+
95
+ const models = parseModelListOutput(result.stdout);
96
+ if (models.length > 0) {
97
+ return {
98
+ models,
99
+ modelDiscovery: 'explicit-list',
100
+ notes,
101
+ };
102
+ }
103
+
104
+ notes.push(`The command \`${path.basename(command)} ${probe.join(' ')}\` returned no parseable model list.`);
105
+ }
106
+
107
+ return { models: [], notes };
108
+ }
109
+
110
+ async function detectVersion(command) {
111
+ for (const args of [['--version'], ['version']]) {
112
+ const result = await runCommand(command, args);
113
+ if (!result.ok) {
114
+ continue;
115
+ }
116
+ const version = firstMeaningfulLine(result.stdout);
117
+ if (version) {
118
+ return version;
119
+ }
120
+ }
121
+ return '';
122
+ }
123
+
124
+ async function findCommand(candidates) {
125
+ const pathValue = process.env.PATH || '';
126
+ const directories = pathValue.split(path.delimiter).filter(Boolean);
127
+ const extensions = process.platform === 'win32'
128
+ ? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
129
+ : [''];
130
+
131
+ for (const candidate of candidates) {
132
+ const trimmed = String(candidate || '').trim();
133
+ if (!trimmed) {
134
+ continue;
135
+ }
136
+
137
+ for (const directory of directories) {
138
+ for (const extension of extensions) {
139
+ const filePath = path.join(directory, process.platform === 'win32' ? `${trimmed}${extension}` : trimmed);
140
+ try {
141
+ await fs.access(filePath, fsConstants.X_OK);
142
+ return filePath;
143
+ } catch {
144
+ // Keep scanning PATH candidates.
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ return '';
151
+ }
152
+
153
+ async function runCommand(command, args) {
154
+ try {
155
+ const { stdout, stderr } = await execFileAsync(command, args, {
156
+ timeout: execTimeoutMs,
157
+ maxBuffer: execMaxBuffer,
158
+ windowsHide: true,
159
+ });
160
+ return {
161
+ ok: true,
162
+ stdout: `${stdout || ''}${stderr ? `\n${stderr}` : ''}`.trim(),
163
+ };
164
+ } catch (error) {
165
+ return {
166
+ ok: false,
167
+ stdout: `${error.stdout || ''}${error.stderr ? `\n${error.stderr}` : ''}`.trim(),
168
+ };
169
+ }
170
+ }
171
+
172
+ export function parseModelListOutput(output) {
173
+ const text = String(output || '').trim();
174
+ if (!text) {
175
+ return [];
176
+ }
177
+
178
+ const json = tryParseJson(text);
179
+ if (json !== undefined) {
180
+ return normalizeStrings(extractModelNames(json));
181
+ }
182
+
183
+ return normalizeStrings(
184
+ text
185
+ .split('\n')
186
+ .map((line) => line.trim())
187
+ .filter((line) => looksLikeModelName(line)),
188
+ );
189
+ }
190
+
191
+ function extractModelNames(value) {
192
+ if (Array.isArray(value)) {
193
+ return value.flatMap((entry) => extractModelNames(entry));
194
+ }
195
+
196
+ if (typeof value === 'string') {
197
+ return [value];
198
+ }
199
+
200
+ if (!value || typeof value !== 'object') {
201
+ return [];
202
+ }
203
+
204
+ const modelKeys = ['models', 'data', 'items', 'availableModels'];
205
+ for (const key of modelKeys) {
206
+ if (key in value) {
207
+ return extractModelNames(value[key]);
208
+ }
209
+ }
210
+
211
+ for (const key of ['id', 'name', 'model']) {
212
+ if (typeof value[key] === 'string') {
213
+ return [value[key]];
214
+ }
215
+ }
216
+
217
+ return [];
218
+ }
219
+
220
+ function looksLikeModelName(line) {
221
+ if (!line) {
222
+ return false;
223
+ }
224
+ if (line.length > 80) {
225
+ return false;
226
+ }
227
+ if (/^usage:/i.test(line) || /^error:/i.test(line)) {
228
+ return false;
229
+ }
230
+ return /^[a-z0-9][a-z0-9._:-]*$/i.test(line);
231
+ }
232
+
233
+ function tryParseJson(value) {
234
+ try {
235
+ return JSON.parse(value);
236
+ } catch {
237
+ return undefined;
238
+ }
239
+ }
240
+
241
+ function firstMeaningfulLine(value) {
242
+ return String(value || '')
243
+ .split('\n')
244
+ .map((line) => line.trim())
245
+ .find(Boolean) || '';
246
+ }
247
+
248
+ function normalizeStrings(values) {
249
+ return [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))].sort((left, right) =>
250
+ left.localeCompare(right),
251
+ );
252
+ }
package/src/sse.js ADDED
@@ -0,0 +1,82 @@
1
+ import { CliError } from './errors.js';
2
+
3
+ export async function streamEvents(response, onEvent) {
4
+ const reader = response.body.getReader();
5
+ const decoder = new TextDecoder();
6
+ let buffer = '';
7
+ let event = createEvent();
8
+
9
+ while (true) {
10
+ const { done, value } = await reader.read();
11
+ if (done) {
12
+ break;
13
+ }
14
+
15
+ buffer += decoder.decode(value, { stream: true });
16
+ const lines = buffer.split(/\r?\n/);
17
+ buffer = lines.pop() || '';
18
+
19
+ for (const line of lines) {
20
+ if (line === '') {
21
+ if (event.data.length > 0) {
22
+ const result = await onEvent(normalizeEvent(event));
23
+ if (result === false) {
24
+ await reader.cancel();
25
+ return;
26
+ }
27
+ }
28
+ event = createEvent();
29
+ continue;
30
+ }
31
+
32
+ if (line.startsWith(':')) {
33
+ continue;
34
+ }
35
+
36
+ const separatorIndex = line.indexOf(':');
37
+ const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
38
+ const rawValue = separatorIndex === -1 ? '' : line.slice(separatorIndex + 1).replace(/^ /, '');
39
+
40
+ if (field === 'event') {
41
+ event.type = rawValue;
42
+ } else if (field === 'id') {
43
+ event.id = rawValue;
44
+ } else if (field === 'data') {
45
+ event.data.push(rawValue);
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ export function normalizeEvent(event) {
52
+ const rawData = event.data.join('\n');
53
+ let payload = rawData;
54
+
55
+ if (rawData) {
56
+ try {
57
+ payload = JSON.parse(rawData);
58
+ } catch {
59
+ payload = { raw: rawData };
60
+ }
61
+ }
62
+
63
+ return {
64
+ id: event.id,
65
+ type: event.type || 'message',
66
+ data: payload,
67
+ };
68
+ }
69
+
70
+ export function requireReadableStream(response) {
71
+ if (!response?.body?.getReader) {
72
+ throw new CliError('Readable stream support is required for SSE commands.');
73
+ }
74
+ }
75
+
76
+ function createEvent() {
77
+ return {
78
+ id: '',
79
+ type: '',
80
+ data: [],
81
+ };
82
+ }