kontexted 0.1.5
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 +75 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +48 -0
- package/dist/commands/logout.d.ts +5 -0
- package/dist/commands/logout.js +33 -0
- package/dist/commands/mcp.d.ts +15 -0
- package/dist/commands/mcp.js +65 -0
- package/dist/commands/server/doctor.d.ts +4 -0
- package/dist/commands/server/doctor.js +33 -0
- package/dist/commands/server/index.d.ts +6 -0
- package/dist/commands/server/index.js +125 -0
- package/dist/commands/server/init.d.ts +6 -0
- package/dist/commands/server/init.js +112 -0
- package/dist/commands/server/logs.d.ts +7 -0
- package/dist/commands/server/logs.js +39 -0
- package/dist/commands/server/migrate.d.ts +4 -0
- package/dist/commands/server/migrate.js +29 -0
- package/dist/commands/server/show-invite.d.ts +4 -0
- package/dist/commands/server/show-invite.js +29 -0
- package/dist/commands/server/start.d.ts +6 -0
- package/dist/commands/server/start.js +44 -0
- package/dist/commands/server/status.d.ts +4 -0
- package/dist/commands/server/status.js +23 -0
- package/dist/commands/server/stop.d.ts +6 -0
- package/dist/commands/server/stop.js +32 -0
- package/dist/commands/show-config.d.ts +5 -0
- package/dist/commands/show-config.js +19 -0
- package/dist/commands/skill.d.ts +5 -0
- package/dist/commands/skill.js +211 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25 -0
- package/dist/lib/api-client.d.ts +36 -0
- package/dist/lib/api-client.js +130 -0
- package/dist/lib/config.d.ts +17 -0
- package/dist/lib/config.js +46 -0
- package/dist/lib/index.d.ts +6 -0
- package/dist/lib/index.js +6 -0
- package/dist/lib/logger.d.ts +24 -0
- package/dist/lib/logger.js +76 -0
- package/dist/lib/mcp-client.d.ts +14 -0
- package/dist/lib/mcp-client.js +62 -0
- package/dist/lib/oauth.d.ts +42 -0
- package/dist/lib/oauth.js +383 -0
- package/dist/lib/profile.d.ts +37 -0
- package/dist/lib/profile.js +49 -0
- package/dist/lib/proxy-server.d.ts +12 -0
- package/dist/lib/proxy-server.js +131 -0
- package/dist/lib/server/binary.d.ts +32 -0
- package/dist/lib/server/binary.js +127 -0
- package/dist/lib/server/config.d.ts +57 -0
- package/dist/lib/server/config.js +136 -0
- package/dist/lib/server/constants.d.ts +29 -0
- package/dist/lib/server/constants.js +35 -0
- package/dist/lib/server/daemon.d.ts +34 -0
- package/dist/lib/server/daemon.js +199 -0
- package/dist/lib/server/index.d.ts +5 -0
- package/dist/lib/server/index.js +5 -0
- package/dist/lib/server/migrate.d.ts +9 -0
- package/dist/lib/server/migrate.js +51 -0
- package/dist/lib/server-url.d.ts +8 -0
- package/dist/lib/server-url.js +25 -0
- package/dist/skill-init/index.d.ts +3 -0
- package/dist/skill-init/index.js +3 -0
- package/dist/skill-init/providers/base.d.ts +26 -0
- package/dist/skill-init/providers/base.js +1 -0
- package/dist/skill-init/providers/index.d.ts +13 -0
- package/dist/skill-init/providers/index.js +17 -0
- package/dist/skill-init/providers/opencode.d.ts +5 -0
- package/dist/skill-init/providers/opencode.js +48 -0
- package/dist/skill-init/templates/index.d.ts +3 -0
- package/dist/skill-init/templates/index.js +3 -0
- package/dist/skill-init/templates/kontexted-cli.d.ts +2 -0
- package/dist/skill-init/templates/kontexted-cli.js +169 -0
- package/dist/skill-init/utils.d.ts +66 -0
- package/dist/skill-init/utils.js +122 -0
- package/dist/types/index.d.ts +67 -0
- package/dist/types/index.js +4 -0
- package/package.json +50 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isPlatformSupported, getPlatform, getBinaryPath, configExists, getServerStatus, startServer, } from '../../lib/server/index.js';
|
|
2
|
+
const DOCKER_URL = 'https://hub.docker.com/r/rabbyte-tech/kontexted';
|
|
3
|
+
function checkPrerequisites() {
|
|
4
|
+
if (!isPlatformSupported()) {
|
|
5
|
+
return { valid: false, error: `Platform not supported: ${getPlatform()}. Consider using Docker: ${DOCKER_URL}` };
|
|
6
|
+
}
|
|
7
|
+
if (!getBinaryPath()) {
|
|
8
|
+
return { valid: false, error: 'Server binary not found. Run `kontexted server install` first.' };
|
|
9
|
+
}
|
|
10
|
+
if (!configExists()) {
|
|
11
|
+
return { valid: false, error: 'Configuration not found. Run `kontexted server init` first.' };
|
|
12
|
+
}
|
|
13
|
+
const status = getServerStatus();
|
|
14
|
+
if (status.running && status.pid) {
|
|
15
|
+
return { valid: false, error: `Server is already running (PID: ${status.pid})` };
|
|
16
|
+
}
|
|
17
|
+
return { valid: true };
|
|
18
|
+
}
|
|
19
|
+
// ============ Yargs Command Module ============
|
|
20
|
+
export const command = 'start';
|
|
21
|
+
export const desc = 'Start the Kontexted server';
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
export const builder = (yargs) => {
|
|
24
|
+
return yargs.option('foreground', {
|
|
25
|
+
alias: 'f',
|
|
26
|
+
type: 'boolean',
|
|
27
|
+
description: 'Run server in foreground (blocking)',
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
export const handler = async (argv) => {
|
|
31
|
+
const check = checkPrerequisites();
|
|
32
|
+
if (!check.valid) {
|
|
33
|
+
console.error('Error:', check.error);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const pid = await startServer({ foreground: argv.foreground });
|
|
38
|
+
console.log(argv.foreground ? `Server running (PID: ${pid})` : `Server started (PID: ${pid})`);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error('Error:', error instanceof Error ? error.message : 'Unknown error');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getServerStatus, loadConfig } from '../../lib/server/index.js';
|
|
2
|
+
import { CONFIG_FILE, LOG_FILE } from '../../lib/server/constants.js';
|
|
3
|
+
// ============ Yargs Command Module ============
|
|
4
|
+
export const command = 'status';
|
|
5
|
+
export const desc = 'Show server status';
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
export const builder = (yargs) => yargs;
|
|
8
|
+
export const handler = async () => {
|
|
9
|
+
const status = getServerStatus();
|
|
10
|
+
if (!status.running) {
|
|
11
|
+
console.log('Server Status: Not running');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const config = loadConfig();
|
|
15
|
+
console.log('Server Status: Running');
|
|
16
|
+
console.log(` PID: ${status.pid}`);
|
|
17
|
+
if (config) {
|
|
18
|
+
console.log(` Port: ${config.server.port}`);
|
|
19
|
+
console.log(` Host: ${config.server.host}`);
|
|
20
|
+
}
|
|
21
|
+
console.log(` Config: ${CONFIG_FILE}`);
|
|
22
|
+
console.log(` Logs: ${LOG_FILE}`);
|
|
23
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getServerStatus, stopServer } from '../../lib/server/index.js';
|
|
2
|
+
// ============ Yargs Command Module ============
|
|
3
|
+
export const command = 'stop';
|
|
4
|
+
export const desc = 'Stop the Kontexted server';
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
export const builder = (yargs) => {
|
|
7
|
+
return yargs.option('force', {
|
|
8
|
+
type: 'boolean',
|
|
9
|
+
description: 'Force kill the server (SIGKILL)',
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
export const handler = async (argv) => {
|
|
13
|
+
const status = getServerStatus();
|
|
14
|
+
if (!status.running) {
|
|
15
|
+
console.log('Server is not running');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const stopped = await stopServer({ force: argv.force ?? false });
|
|
20
|
+
if (stopped) {
|
|
21
|
+
console.log('Server stopped');
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.error('Error: Failed to stop server');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error('Error:', error instanceof Error ? error.message : 'Unknown error');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readConfig, getConfigPath } from "../lib/config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Register the show-config command
|
|
4
|
+
*/
|
|
5
|
+
export function registerShowConfigCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command("show-config")
|
|
8
|
+
.description("Display all stored profiles")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
const config = await readConfig();
|
|
11
|
+
console.log(`Config file: ${getConfigPath()}`);
|
|
12
|
+
console.log("");
|
|
13
|
+
if (Object.keys(config.profiles).length === 0) {
|
|
14
|
+
console.log("No profiles configured.");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
console.log(JSON.stringify(config, null, 2));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import * as readline from "readline";
|
|
2
|
+
import { readConfig, writeConfig } from "../lib/config.js";
|
|
3
|
+
import { getProfile } from "../lib/profile.js";
|
|
4
|
+
import { ApiClient } from "../lib/api-client.js";
|
|
5
|
+
import { ensureValidTokens } from "../lib/oauth.js";
|
|
6
|
+
import { getProvider, allTemplates } from "../skill-init/index.js";
|
|
7
|
+
import { initSkill } from "../skill-init/utils.js";
|
|
8
|
+
/**
|
|
9
|
+
* Execute workspace-tree skill via the API
|
|
10
|
+
*/
|
|
11
|
+
async function executeWorkspaceTree(client, workspaceSlug) {
|
|
12
|
+
const response = await client.post("/api/skill/workspace-tree", { workspaceSlug });
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
const errorText = await response.text();
|
|
15
|
+
throw new Error(`Workspace tree skill failed: ${response.status} ${errorText}`);
|
|
16
|
+
}
|
|
17
|
+
return response.json();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Execute search-notes skill via the API
|
|
21
|
+
*/
|
|
22
|
+
async function executeSearchNotes(client, workspaceSlug, query, limit) {
|
|
23
|
+
const body = { workspaceSlug, query };
|
|
24
|
+
if (limit !== undefined) {
|
|
25
|
+
body.limit = limit;
|
|
26
|
+
}
|
|
27
|
+
const response = await client.post("/api/skill/search-notes", body);
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const errorText = await response.text();
|
|
30
|
+
throw new Error(`Search notes skill failed: ${response.status} ${errorText}`);
|
|
31
|
+
}
|
|
32
|
+
return response.json();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Execute note-by-id skill via the API
|
|
36
|
+
*/
|
|
37
|
+
async function executeNoteById(client, workspaceSlug, notePublicId) {
|
|
38
|
+
const response = await client.post("/api/skill/note-by-id", {
|
|
39
|
+
workspaceSlug,
|
|
40
|
+
notePublicId
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const errorText = await response.text();
|
|
44
|
+
throw new Error(`Note by ID skill failed: ${response.status} ${errorText}`);
|
|
45
|
+
}
|
|
46
|
+
return response.json();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Helper function to create an API client from a profile alias
|
|
50
|
+
*/
|
|
51
|
+
async function createApiClient(alias) {
|
|
52
|
+
const config = await readConfig();
|
|
53
|
+
const profile = getProfile(config, alias);
|
|
54
|
+
if (!profile) {
|
|
55
|
+
console.error(`Profile not found: ${alias}. Run 'kontexted login' first.`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
// Proactively refresh token if needed (non-interactive)
|
|
59
|
+
const tokensValid = await ensureValidTokens(profile.oauth, async () => writeConfig(config), profile.serverUrl);
|
|
60
|
+
if (!tokensValid) {
|
|
61
|
+
console.error(`Authentication expired. Run 'kontexted login --alias ${alias}' to re-authenticate.`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
return new ApiClient(profile.serverUrl, profile.oauth, async () => writeConfig(config));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Display results from a skill execution
|
|
68
|
+
*/
|
|
69
|
+
function displayResult(result) {
|
|
70
|
+
console.log(JSON.stringify(result, null, 2));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Register the skill command and its subcommands
|
|
74
|
+
*/
|
|
75
|
+
export function registerSkillCommand(program) {
|
|
76
|
+
const skillCommand = program
|
|
77
|
+
.command("skill")
|
|
78
|
+
.description("Invoke LLM skill via CLI");
|
|
79
|
+
skillCommand
|
|
80
|
+
.command("workspace-tree")
|
|
81
|
+
.description("Get the workspace tree structure")
|
|
82
|
+
.requiredOption("--alias <name>", "Profile alias to use")
|
|
83
|
+
.action(async (options) => {
|
|
84
|
+
try {
|
|
85
|
+
const client = await createApiClient(options.alias);
|
|
86
|
+
const config = await readConfig();
|
|
87
|
+
const profile = getProfile(config, options.alias);
|
|
88
|
+
if (!profile) {
|
|
89
|
+
console.error(`Profile not found: ${options.alias}. Run 'kontexted login' first.`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const result = await executeWorkspaceTree(client, profile.workspace);
|
|
93
|
+
displayResult(result);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
skillCommand
|
|
101
|
+
.command("search-notes")
|
|
102
|
+
.description("Search notes by query in a workspace")
|
|
103
|
+
.requiredOption("--alias <name>", "Profile alias to use")
|
|
104
|
+
.requiredOption("--query <text>", "Search query")
|
|
105
|
+
.option("--limit <number>", "Maximum number of results (default: 20, max: 50)", parseInt)
|
|
106
|
+
.action(async (options) => {
|
|
107
|
+
try {
|
|
108
|
+
const client = await createApiClient(options.alias);
|
|
109
|
+
const config = await readConfig();
|
|
110
|
+
const profile = getProfile(config, options.alias);
|
|
111
|
+
if (!profile) {
|
|
112
|
+
console.error(`Profile not found: ${options.alias}. Run 'kontexted login' first.`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const result = await executeSearchNotes(client, profile.workspace, options.query, options.limit);
|
|
116
|
+
displayResult(result);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
skillCommand
|
|
124
|
+
.command("note-by-id")
|
|
125
|
+
.description("Get a specific note by its public ID")
|
|
126
|
+
.requiredOption("--alias <name>", "Profile alias to use")
|
|
127
|
+
.requiredOption("--note-id <id>", "Public ID of the note")
|
|
128
|
+
.action(async (options) => {
|
|
129
|
+
try {
|
|
130
|
+
const client = await createApiClient(options.alias);
|
|
131
|
+
const config = await readConfig();
|
|
132
|
+
const profile = getProfile(config, options.alias);
|
|
133
|
+
if (!profile) {
|
|
134
|
+
console.error(`Profile not found: ${options.alias}. Run 'kontexted login' first.`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const result = await executeNoteById(client, profile.workspace, options.noteId);
|
|
138
|
+
displayResult(result);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
skillCommand
|
|
146
|
+
.command("init")
|
|
147
|
+
.description("Initialize AI agent skills for the current project")
|
|
148
|
+
.option("--provider <name>", "Provider to use (default: opencode)", "opencode")
|
|
149
|
+
.option("--all", "Generate all available skills without prompting", false)
|
|
150
|
+
.action(async (options) => {
|
|
151
|
+
try {
|
|
152
|
+
// Get the provider
|
|
153
|
+
let provider;
|
|
154
|
+
try {
|
|
155
|
+
provider = getProvider(options.provider);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
console.error(`Unknown skill provider: ${options.provider}`);
|
|
159
|
+
console.error(`Available providers: ${["opencode"].join(", ")}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
// Show what will be generated
|
|
163
|
+
console.log(`This will generate the following skills for ${provider.name}:`);
|
|
164
|
+
for (const template of allTemplates) {
|
|
165
|
+
const skillPath = provider.getSkillPath(template.name);
|
|
166
|
+
console.log(` - ${template.name} → ${skillPath}`);
|
|
167
|
+
}
|
|
168
|
+
// Check if we should prompt for confirmation
|
|
169
|
+
if (!options.all) {
|
|
170
|
+
const rl = readline.createInterface({
|
|
171
|
+
input: process.stdin,
|
|
172
|
+
output: process.stdout,
|
|
173
|
+
});
|
|
174
|
+
const answer = await new Promise((resolve) => {
|
|
175
|
+
rl.question("\nContinue? (y/N) ", (response) => {
|
|
176
|
+
rl.close();
|
|
177
|
+
resolve(response);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
if (answer.toLowerCase() !== "y") {
|
|
181
|
+
console.log("Aborted.");
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Generate all skills
|
|
186
|
+
const results = [];
|
|
187
|
+
for (const template of allTemplates) {
|
|
188
|
+
console.log(`Generating skill: ${template.name}`);
|
|
189
|
+
const result = await initSkill({
|
|
190
|
+
skill: template,
|
|
191
|
+
provider,
|
|
192
|
+
});
|
|
193
|
+
const status = result.created ? "Created" : "Updated";
|
|
194
|
+
console.log(`✓ ${status} ${result.path}`);
|
|
195
|
+
results.push({
|
|
196
|
+
name: result.name,
|
|
197
|
+
path: result.path,
|
|
198
|
+
created: result.created,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// Show summary
|
|
202
|
+
const created = results.filter((r) => r.created).length;
|
|
203
|
+
const updated = results.length - created;
|
|
204
|
+
console.log(`\nSummary: ${created} created, ${updated} updated`);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { registerLoginCommand } from "./commands/login.js";
|
|
4
|
+
import { registerLogoutCommand } from "./commands/logout.js";
|
|
5
|
+
import { registerShowConfigCommand } from "./commands/show-config.js";
|
|
6
|
+
import { registerMcpCommand } from "./commands/mcp.js";
|
|
7
|
+
import { registerSkillCommand } from "./commands/skill.js";
|
|
8
|
+
import { registerServerCommand } from "./commands/server/index.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name("kontexted")
|
|
12
|
+
.description("CLI tool for Kontexted - MCP proxy and workspace management")
|
|
13
|
+
.version("0.1.0");
|
|
14
|
+
// Register subcommands
|
|
15
|
+
registerLoginCommand(program);
|
|
16
|
+
registerLogoutCommand(program);
|
|
17
|
+
registerShowConfigCommand(program);
|
|
18
|
+
registerSkillCommand(program);
|
|
19
|
+
registerMcpCommand(program);
|
|
20
|
+
registerServerCommand(program);
|
|
21
|
+
// Parse arguments
|
|
22
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
23
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { OAuthState } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* API client for making authenticated HTTP requests with automatic token refresh
|
|
4
|
+
*/
|
|
5
|
+
export declare class ApiClient {
|
|
6
|
+
private serverUrl;
|
|
7
|
+
private oauth;
|
|
8
|
+
private persist;
|
|
9
|
+
constructor(serverUrl: string, oauth: OAuthState, persist: () => Promise<void>);
|
|
10
|
+
/**
|
|
11
|
+
* Make an authenticated HTTP request
|
|
12
|
+
* Automatically handles token refresh on 401 responses
|
|
13
|
+
*/
|
|
14
|
+
request(path: string, options?: RequestInit): Promise<Response>;
|
|
15
|
+
/**
|
|
16
|
+
* Refresh the access token using the refresh token
|
|
17
|
+
* @returns true if refresh was successful, false otherwise
|
|
18
|
+
*/
|
|
19
|
+
private refreshToken;
|
|
20
|
+
/**
|
|
21
|
+
* Make a GET request
|
|
22
|
+
*/
|
|
23
|
+
get(path: string): Promise<Response>;
|
|
24
|
+
/**
|
|
25
|
+
* Make a POST request with a JSON body
|
|
26
|
+
*/
|
|
27
|
+
post(path: string, body: unknown): Promise<Response>;
|
|
28
|
+
/**
|
|
29
|
+
* Make a PUT request with a JSON body
|
|
30
|
+
*/
|
|
31
|
+
put(path: string, body: unknown): Promise<Response>;
|
|
32
|
+
/**
|
|
33
|
+
* Make a DELETE request
|
|
34
|
+
*/
|
|
35
|
+
delete(path: string): Promise<Response>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { logDebug, logError } from "../lib/logger.js";
|
|
2
|
+
/**
|
|
3
|
+
* API client for making authenticated HTTP requests with automatic token refresh
|
|
4
|
+
*/
|
|
5
|
+
export class ApiClient {
|
|
6
|
+
serverUrl;
|
|
7
|
+
oauth;
|
|
8
|
+
persist;
|
|
9
|
+
constructor(serverUrl, oauth, persist) {
|
|
10
|
+
this.serverUrl = serverUrl;
|
|
11
|
+
this.oauth = oauth;
|
|
12
|
+
this.persist = persist;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Make an authenticated HTTP request
|
|
16
|
+
* Automatically handles token refresh on 401 responses
|
|
17
|
+
*/
|
|
18
|
+
async request(path, options = {}) {
|
|
19
|
+
const url = new URL(path, this.serverUrl);
|
|
20
|
+
// Prepare headers with Authorization
|
|
21
|
+
const headers = {
|
|
22
|
+
...options.headers,
|
|
23
|
+
Authorization: `Bearer ${this.oauth.tokens?.access_token}`,
|
|
24
|
+
};
|
|
25
|
+
// Set Content-Type to application/json by default for methods that have a body
|
|
26
|
+
if (options.body !== undefined && !headers["Content-Type"]) {
|
|
27
|
+
headers["Content-Type"] = "application/json";
|
|
28
|
+
}
|
|
29
|
+
const requestOptions = {
|
|
30
|
+
...options,
|
|
31
|
+
headers,
|
|
32
|
+
};
|
|
33
|
+
logDebug(`[API CLIENT] ${options.method ?? "GET"} ${path}`);
|
|
34
|
+
let response = await fetch(url.toString(), requestOptions);
|
|
35
|
+
logDebug(`[API CLIENT] Response status: ${response.status}`);
|
|
36
|
+
// Handle 401 Unauthorized - attempt to refresh token
|
|
37
|
+
if (response.status === 401 && this.oauth.tokens?.refresh_token) {
|
|
38
|
+
logDebug("[API CLIENT] Token expired, attempting to refresh...");
|
|
39
|
+
const refreshed = await this.refreshToken();
|
|
40
|
+
if (refreshed) {
|
|
41
|
+
logDebug("[API CLIENT] Token refreshed successfully, retrying request...");
|
|
42
|
+
// Update Authorization header with new token
|
|
43
|
+
headers.Authorization = `Bearer ${this.oauth.tokens?.access_token}`;
|
|
44
|
+
requestOptions.headers = headers;
|
|
45
|
+
// Retry the original request with new token
|
|
46
|
+
response = await fetch(url.toString(), requestOptions);
|
|
47
|
+
logDebug(`[API CLIENT] Retry response status: ${response.status}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
throw new Error("Failed to refresh access token. Please run 'kontexted login' to re-authenticate.");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// If still 401 after refresh attempt (or no refresh token available)
|
|
54
|
+
if (response.status === 401) {
|
|
55
|
+
throw new Error("Authentication failed. Please run 'kontexted login' to re-authenticate.");
|
|
56
|
+
}
|
|
57
|
+
return response;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Refresh the access token using the refresh token
|
|
61
|
+
* @returns true if refresh was successful, false otherwise
|
|
62
|
+
*/
|
|
63
|
+
async refreshToken() {
|
|
64
|
+
if (!this.oauth.tokens?.refresh_token) {
|
|
65
|
+
logError("[API CLIENT] No refresh token available");
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const tokenUrl = new URL("/api/auth/oauth2/token", new URL(this.serverUrl).origin);
|
|
70
|
+
logDebug(`[API CLIENT] Refreshing token - URL: ${tokenUrl.toString()}`);
|
|
71
|
+
const response = await fetch(tokenUrl.toString(), {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
75
|
+
},
|
|
76
|
+
body: new URLSearchParams({
|
|
77
|
+
grant_type: "refresh_token",
|
|
78
|
+
refresh_token: this.oauth.tokens.refresh_token,
|
|
79
|
+
client_id: this.oauth.clientInformation?.client_id ?? "",
|
|
80
|
+
client_secret: this.oauth.clientInformation?.client_secret ?? "",
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
logDebug(`[API CLIENT] Token refresh response status: ${response.status}`);
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const errorText = await response.text();
|
|
86
|
+
logError(`[API CLIENT] Failed to refresh access token - Status: ${response.status}, Error: ${errorText}`);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const tokens = (await response.json());
|
|
90
|
+
this.oauth.tokens = tokens;
|
|
91
|
+
await this.persist();
|
|
92
|
+
logDebug("[API CLIENT] Token refresh successful");
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
logError("[API CLIENT] Error refreshing access token:", error);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Make a GET request
|
|
102
|
+
*/
|
|
103
|
+
async get(path) {
|
|
104
|
+
return this.request(path, { method: "GET" });
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Make a POST request with a JSON body
|
|
108
|
+
*/
|
|
109
|
+
async post(path, body) {
|
|
110
|
+
return this.request(path, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
body: JSON.stringify(body),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Make a PUT request with a JSON body
|
|
117
|
+
*/
|
|
118
|
+
async put(path, body) {
|
|
119
|
+
return this.request(path, {
|
|
120
|
+
method: "PUT",
|
|
121
|
+
body: JSON.stringify(body),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Make a DELETE request
|
|
126
|
+
*/
|
|
127
|
+
async delete(path) {
|
|
128
|
+
return this.request(path, { method: "DELETE" });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Config } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Read the configuration file from disk
|
|
4
|
+
*/
|
|
5
|
+
export declare function readConfig(): Promise<Config>;
|
|
6
|
+
/**
|
|
7
|
+
* Write configuration to disk
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeConfig(config: Config): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Remove the entire config file
|
|
12
|
+
*/
|
|
13
|
+
export declare function removeConfig(): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Get the config file path (for display/debugging)
|
|
16
|
+
*/
|
|
17
|
+
export declare function getConfigPath(): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".kontexted");
|
|
5
|
+
const CONFIG_PATH = join(CONFIG_DIR, "profile.json");
|
|
6
|
+
/**
|
|
7
|
+
* Read the configuration file from disk
|
|
8
|
+
*/
|
|
9
|
+
export async function readConfig() {
|
|
10
|
+
try {
|
|
11
|
+
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
if (!parsed || typeof parsed !== "object") {
|
|
14
|
+
return { profiles: {} };
|
|
15
|
+
}
|
|
16
|
+
return parsed.profiles ? { profiles: parsed.profiles } : { profiles: {} };
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error &&
|
|
20
|
+
typeof error === "object" &&
|
|
21
|
+
"code" in error &&
|
|
22
|
+
error.code === "ENOENT") {
|
|
23
|
+
return { profiles: {} };
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Write configuration to disk
|
|
30
|
+
*/
|
|
31
|
+
export async function writeConfig(config) {
|
|
32
|
+
await mkdir(dirname(CONFIG_PATH), { recursive: true });
|
|
33
|
+
await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Remove the entire config file
|
|
37
|
+
*/
|
|
38
|
+
export async function removeConfig() {
|
|
39
|
+
await rm(CONFIG_PATH, { force: true });
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get the config file path (for display/debugging)
|
|
43
|
+
*/
|
|
44
|
+
export function getConfigPath() {
|
|
45
|
+
return CONFIG_PATH;
|
|
46
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set whether logging is enabled
|
|
3
|
+
*/
|
|
4
|
+
export declare function setLogEnabled(enabled: boolean): void;
|
|
5
|
+
/**
|
|
6
|
+
* Log an info message (file only)
|
|
7
|
+
*/
|
|
8
|
+
export declare function logInfo(message: string, data?: unknown): void;
|
|
9
|
+
/**
|
|
10
|
+
* Log a debug message (file only)
|
|
11
|
+
*/
|
|
12
|
+
export declare function logDebug(message: string, data?: unknown): void;
|
|
13
|
+
/**
|
|
14
|
+
* Log a warning message (file only)
|
|
15
|
+
*/
|
|
16
|
+
export declare function logWarn(message: string, data?: unknown): void;
|
|
17
|
+
/**
|
|
18
|
+
* Log an error message (file only)
|
|
19
|
+
*/
|
|
20
|
+
export declare function logError(message: string, data?: unknown): void;
|
|
21
|
+
/**
|
|
22
|
+
* Get the log file path
|
|
23
|
+
*/
|
|
24
|
+
export declare function getLogFilePath(): string;
|