kontexted 0.1.13 → 0.1.15
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/dist/commands/mcp.js +9 -1
- package/dist/commands/skill.js +25 -62
- package/dist/commands/sync/force-pull.js +22 -22
- package/dist/commands/sync/force-push.js +22 -23
- package/dist/commands/sync/init.js +24 -18
- package/dist/commands/sync/start.js +21 -18
- package/dist/commands/sync/status.js +4 -2
- package/dist/index.js +7 -1
- package/dist/lib/api-client.d.ts +1 -1
- package/dist/lib/sync/auth-utils.d.ts +16 -0
- package/dist/lib/sync/auth-utils.js +45 -0
- package/dist/lib/sync/command-utils.js +3 -4
- package/dist/lib/sync/queue.js +6 -6
- package/dist/lib/sync/remote-listener.d.ts +4 -0
- package/dist/lib/sync/remote-listener.js +46 -10
- package/package.json +6 -4
package/dist/commands/mcp.js
CHANGED
|
@@ -28,7 +28,15 @@ async function startMcpProxy(options) {
|
|
|
28
28
|
writeEnabled = false;
|
|
29
29
|
}
|
|
30
30
|
const persist = async () => {
|
|
31
|
-
await
|
|
31
|
+
const freshConfig = await readConfig();
|
|
32
|
+
const freshProfile = getProfile(freshConfig, profileKey);
|
|
33
|
+
if (freshProfile && profile.oauth.tokens) {
|
|
34
|
+
freshProfile.oauth = {
|
|
35
|
+
...profile.oauth,
|
|
36
|
+
tokens: { ...profile.oauth.tokens },
|
|
37
|
+
};
|
|
38
|
+
await writeConfig(freshConfig);
|
|
39
|
+
}
|
|
32
40
|
};
|
|
33
41
|
try {
|
|
34
42
|
const { client } = await connectRemoteClient(profile.serverUrl, profile.oauth, persist, { allowInteractive: false });
|
package/dist/commands/skill.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import * as readline from "readline";
|
|
2
|
-
import { readConfig
|
|
3
|
-
import {
|
|
4
|
-
import { ApiClient } from "../lib/api-client.js";
|
|
5
|
-
import { ensureValidTokens } from "../lib/oauth.js";
|
|
2
|
+
import { readConfig } from "../lib/config.js";
|
|
3
|
+
import { listProfiles } from "../lib/profile.js";
|
|
6
4
|
import { getProvider, allTemplates } from "../skill-init/index.js";
|
|
7
5
|
import { initSkill } from "../skill-init/utils.js";
|
|
6
|
+
import { createAuthenticatedClient } from "../lib/sync/auth-utils.js";
|
|
8
7
|
/**
|
|
9
8
|
* Execute workspace-tree skill via the API
|
|
10
9
|
*/
|
|
@@ -97,19 +96,19 @@ async function executeUpdateNoteContent(client, workspaceSlug, notePublicId, con
|
|
|
97
96
|
* Helper function to create an API client from a profile alias
|
|
98
97
|
*/
|
|
99
98
|
async function createApiClient(alias) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
console.error(`Profile not found: ${alias}. Run 'kontexted login' first.`);
|
|
104
|
-
process.exit(1);
|
|
99
|
+
try {
|
|
100
|
+
const auth = await createAuthenticatedClient(alias);
|
|
101
|
+
return { client: auth.client, profile: auth.profile };
|
|
105
102
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof Error) {
|
|
105
|
+
console.error(`\nError: ${error.message}`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.error("\nError: Failed to authenticate. Please run 'kontexted login'...");
|
|
109
|
+
}
|
|
110
110
|
process.exit(1);
|
|
111
111
|
}
|
|
112
|
-
return new ApiClient(profile.serverUrl, profile.oauth, async () => writeConfig(config));
|
|
113
112
|
}
|
|
114
113
|
/**
|
|
115
114
|
* Display results from a skill execution
|
|
@@ -175,14 +174,8 @@ export function registerSkillCommand(program) {
|
|
|
175
174
|
.requiredOption("--alias <name>", "Profile alias to use")
|
|
176
175
|
.action(async (options) => {
|
|
177
176
|
try {
|
|
178
|
-
const client = await createApiClient(options.alias);
|
|
179
|
-
const
|
|
180
|
-
const profile = getProfile(config, options.alias);
|
|
181
|
-
if (!profile) {
|
|
182
|
-
console.error(`Profile not found: ${options.alias}. Run 'kontexted login' first.`);
|
|
183
|
-
process.exit(1);
|
|
184
|
-
}
|
|
185
|
-
const result = await executeWorkspaceTree(client, profile.workspace);
|
|
177
|
+
const { client: apiClient, profile } = await createApiClient(options.alias);
|
|
178
|
+
const result = await executeWorkspaceTree(apiClient, profile.workspace);
|
|
186
179
|
displayResult(result);
|
|
187
180
|
}
|
|
188
181
|
catch (error) {
|
|
@@ -198,14 +191,8 @@ export function registerSkillCommand(program) {
|
|
|
198
191
|
.option("--limit <number>", "Maximum number of results (default: 20, max: 50)", parseInt)
|
|
199
192
|
.action(async (options) => {
|
|
200
193
|
try {
|
|
201
|
-
const client = await createApiClient(options.alias);
|
|
202
|
-
const
|
|
203
|
-
const profile = getProfile(config, options.alias);
|
|
204
|
-
if (!profile) {
|
|
205
|
-
console.error(`Profile not found: ${options.alias}. Run 'kontexted login' first.`);
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
const result = await executeSearchNotes(client, profile.workspace, options.query, options.limit);
|
|
194
|
+
const { client: apiClient, profile } = await createApiClient(options.alias);
|
|
195
|
+
const result = await executeSearchNotes(apiClient, profile.workspace, options.query, options.limit);
|
|
209
196
|
displayResult(result);
|
|
210
197
|
}
|
|
211
198
|
catch (error) {
|
|
@@ -220,14 +207,8 @@ export function registerSkillCommand(program) {
|
|
|
220
207
|
.requiredOption("--note-id <id>", "Public ID of the note")
|
|
221
208
|
.action(async (options) => {
|
|
222
209
|
try {
|
|
223
|
-
const client = await createApiClient(options.alias);
|
|
224
|
-
const
|
|
225
|
-
const profile = getProfile(config, options.alias);
|
|
226
|
-
if (!profile) {
|
|
227
|
-
console.error(`Profile not found: ${options.alias}. Run 'kontexted login' first.`);
|
|
228
|
-
process.exit(1);
|
|
229
|
-
}
|
|
230
|
-
const result = await executeNoteById(client, profile.workspace, options.noteId);
|
|
210
|
+
const { client: apiClient, profile } = await createApiClient(options.alias);
|
|
211
|
+
const result = await executeNoteById(apiClient, profile.workspace, options.noteId);
|
|
231
212
|
displayResult(result);
|
|
232
213
|
}
|
|
233
214
|
catch (error) {
|
|
@@ -244,18 +225,12 @@ export function registerSkillCommand(program) {
|
|
|
244
225
|
.option("--parent-id <parentPublicId>", "Public ID of parent folder (for nested folders)")
|
|
245
226
|
.action(async (options) => {
|
|
246
227
|
try {
|
|
247
|
-
const client = await createApiClient(options.alias);
|
|
248
|
-
const config = await readConfig();
|
|
249
|
-
const profile = getProfile(config, options.alias);
|
|
250
|
-
if (!profile) {
|
|
251
|
-
console.error(`Profile not found: ${options.alias}. Run 'kontexted login' first.`);
|
|
252
|
-
process.exit(1);
|
|
253
|
-
}
|
|
228
|
+
const { client: apiClient, profile } = await createApiClient(options.alias);
|
|
254
229
|
if (!profile.write) {
|
|
255
230
|
console.error("Error: Write operations not enabled for this profile. Re-login with 'kontexted login --alias <alias> --write' to enable write access.");
|
|
256
231
|
process.exit(1);
|
|
257
232
|
}
|
|
258
|
-
const result = await executeCreateFolder(
|
|
233
|
+
const result = await executeCreateFolder(apiClient, profile.workspace, options.name, options.displayName, options.parentId);
|
|
259
234
|
displayResult(result);
|
|
260
235
|
}
|
|
261
236
|
catch (error) {
|
|
@@ -273,18 +248,12 @@ export function registerSkillCommand(program) {
|
|
|
273
248
|
.option("--content <content>", "Initial content for the note")
|
|
274
249
|
.action(async (options) => {
|
|
275
250
|
try {
|
|
276
|
-
const client = await createApiClient(options.alias);
|
|
277
|
-
const config = await readConfig();
|
|
278
|
-
const profile = getProfile(config, options.alias);
|
|
279
|
-
if (!profile) {
|
|
280
|
-
console.error(`Profile not found: ${options.alias}. Run 'kontexted login' first.`);
|
|
281
|
-
process.exit(1);
|
|
282
|
-
}
|
|
251
|
+
const { client: apiClient, profile } = await createApiClient(options.alias);
|
|
283
252
|
if (!profile.write) {
|
|
284
253
|
console.error("Error: Write operations not enabled for this profile. Re-login with 'kontexted login --alias <alias> --write' to enable write access.");
|
|
285
254
|
process.exit(1);
|
|
286
255
|
}
|
|
287
|
-
const result = await executeCreateNote(
|
|
256
|
+
const result = await executeCreateNote(apiClient, profile.workspace, options.name, options.title, options.folderId, options.content);
|
|
288
257
|
displayResult(result);
|
|
289
258
|
}
|
|
290
259
|
catch (error) {
|
|
@@ -300,18 +269,12 @@ export function registerSkillCommand(program) {
|
|
|
300
269
|
.requiredOption("--content <content>", "New content for the note")
|
|
301
270
|
.action(async (options) => {
|
|
302
271
|
try {
|
|
303
|
-
const client = await createApiClient(options.alias);
|
|
304
|
-
const config = await readConfig();
|
|
305
|
-
const profile = getProfile(config, options.alias);
|
|
306
|
-
if (!profile) {
|
|
307
|
-
console.error(`Profile not found: ${options.alias}. Run 'kontexted login' first.`);
|
|
308
|
-
process.exit(1);
|
|
309
|
-
}
|
|
272
|
+
const { client: apiClient, profile } = await createApiClient(options.alias);
|
|
310
273
|
if (!profile.write) {
|
|
311
274
|
console.error("Error: Write operations not enabled for this profile. Re-login with 'kontexted login --alias <alias> --write' to enable write access.");
|
|
312
275
|
process.exit(1);
|
|
313
276
|
}
|
|
314
|
-
const result = await executeUpdateNoteContent(
|
|
277
|
+
const result = await executeUpdateNoteContent(apiClient, profile.workspace, options.noteId, options.content);
|
|
315
278
|
displayResult(result);
|
|
316
279
|
}
|
|
317
280
|
catch (error) {
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { readConfig, writeConfig } from "../../lib/config.js";
|
|
4
|
-
import { getProfile } from "../../lib/profile.js";
|
|
5
|
-
import { ApiClient } from "../../lib/api-client.js";
|
|
6
3
|
import { ensureDirectoryExists, formatMarkdown, computeFilePath } from "../../lib/sync/utils.js";
|
|
7
4
|
import { sha256 } from "../../lib/sync/crypto.js";
|
|
8
|
-
import {
|
|
5
|
+
import { createAuthenticatedClient } from "../../lib/sync/auth-utils.js";
|
|
6
|
+
import { findSyncDir, loadSyncConfig, loadSyncState, saveSyncState, } from "../../lib/sync/command-utils.js";
|
|
9
7
|
/**
|
|
10
8
|
* Prompt user for confirmation
|
|
11
9
|
*/
|
|
@@ -34,13 +32,26 @@ export async function handler(argv) {
|
|
|
34
32
|
// Step 2: Load sync config
|
|
35
33
|
console.log("Loading sync configuration...");
|
|
36
34
|
const syncConfig = await loadSyncConfig(syncDir);
|
|
37
|
-
// Step 3:
|
|
35
|
+
// Step 3: Authenticate and create API client
|
|
38
36
|
const profileAlias = argv.alias || syncConfig.alias;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
console.log(`Validating profile '${profileAlias}' and authenticating...`);
|
|
38
|
+
let apiClient;
|
|
39
|
+
let profile;
|
|
40
|
+
try {
|
|
41
|
+
const auth = await createAuthenticatedClient(profileAlias);
|
|
42
|
+
apiClient = auth.client;
|
|
43
|
+
profile = auth.profile;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof Error) {
|
|
47
|
+
console.error(`\nError: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.error("\nError: Failed to authenticate. Please run 'kontexted login'...");
|
|
51
|
+
}
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Step 4: Warn about data loss if not forced
|
|
44
55
|
if (!argv.force) {
|
|
45
56
|
console.log("\n⚠️ WARNING: This will overwrite ALL local files with remote versions.");
|
|
46
57
|
console.log(" Any local changes that don't exist on the server will be LOST.");
|
|
@@ -51,18 +62,7 @@ export async function handler(argv) {
|
|
|
51
62
|
return;
|
|
52
63
|
}
|
|
53
64
|
}
|
|
54
|
-
// Step 6:
|
|
55
|
-
console.log("Connecting to server...");
|
|
56
|
-
const apiClient = new ApiClient(profile.serverUrl, profile.oauth, async () => {
|
|
57
|
-
// Update config with refreshed tokens
|
|
58
|
-
const updatedConfig = await readConfig();
|
|
59
|
-
const updatedProfile = getProfile(updatedConfig, profileAlias);
|
|
60
|
-
if (updatedProfile) {
|
|
61
|
-
updatedProfile.oauth = profile.oauth;
|
|
62
|
-
await writeConfig(updatedConfig);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
// Step 7: Fetch all notes from server
|
|
65
|
+
// Step 6: Fetch all notes from server
|
|
66
66
|
console.log("Fetching remote notes...");
|
|
67
67
|
let response;
|
|
68
68
|
try {
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { readConfig, writeConfig } from "../../lib/config.js";
|
|
4
|
-
import { getProfile } from "../../lib/profile.js";
|
|
5
|
-
import { ApiClient } from "../../lib/api-client.js";
|
|
6
3
|
import { parseMarkdown } from "../../lib/sync/utils.js";
|
|
7
|
-
import {
|
|
4
|
+
import { createAuthenticatedClient } from "../../lib/sync/auth-utils.js";
|
|
5
|
+
import { findSyncDir, loadSyncConfig, loadSyncState, } from "../../lib/sync/command-utils.js";
|
|
8
6
|
/**
|
|
9
7
|
* Handler for the sync force-push command
|
|
10
8
|
*/
|
|
@@ -14,15 +12,27 @@ export async function handler(argv) {
|
|
|
14
12
|
console.log("Finding sync directory...");
|
|
15
13
|
const syncDir = await findSyncDir(cwd, argv.dir);
|
|
16
14
|
console.log(`Using sync directory: ${syncDir}`);
|
|
17
|
-
// Step 2: Load sync config
|
|
15
|
+
// Step 2: Load sync config (includes alias)
|
|
18
16
|
console.log("Loading sync configuration...");
|
|
19
17
|
const syncConfig = await loadSyncConfig(syncDir);
|
|
20
|
-
// Step 3:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
// Step 3: Authenticate and create API client
|
|
19
|
+
console.log("Validating profile and authenticating...");
|
|
20
|
+
let apiClient;
|
|
21
|
+
let profile;
|
|
22
|
+
try {
|
|
23
|
+
const auth = await createAuthenticatedClient(syncConfig.alias);
|
|
24
|
+
apiClient = auth.client;
|
|
25
|
+
profile = auth.profile;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof Error) {
|
|
29
|
+
console.error(`\nError: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.error("\nError: Failed to authenticate. Please run 'kontexted login'...");
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
26
36
|
// Step 5: Warn about data loss if not forced
|
|
27
37
|
if (!argv.force) {
|
|
28
38
|
console.log("\n⚠️ WARNING: This will overwrite ALL remote notes with local versions.");
|
|
@@ -34,18 +44,7 @@ export async function handler(argv) {
|
|
|
34
44
|
return;
|
|
35
45
|
}
|
|
36
46
|
}
|
|
37
|
-
// Step 6:
|
|
38
|
-
console.log("Connecting to server...");
|
|
39
|
-
const apiClient = new ApiClient(profile.serverUrl, profile.oauth, async () => {
|
|
40
|
-
// Update config with refreshed tokens
|
|
41
|
-
const updatedConfig = await readConfig();
|
|
42
|
-
const updatedProfile = getProfile(updatedConfig, profileAlias);
|
|
43
|
-
if (updatedProfile) {
|
|
44
|
-
updatedProfile.oauth = profile.oauth;
|
|
45
|
-
await writeConfig(updatedConfig);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
// Step 7: Load sync state
|
|
47
|
+
// Step 6: Load sync state
|
|
49
48
|
let state = await loadSyncState(syncDir);
|
|
50
49
|
if (state === null) {
|
|
51
50
|
state = { files: {}, folders: {}, lastFullSync: null, version: 1 };
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { readConfig
|
|
3
|
+
import { readConfig } from "../../lib/config.js";
|
|
4
4
|
import { getProfile, profileExists } from "../../lib/profile.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createAuthenticatedClient } from "../../lib/sync/auth-utils.js";
|
|
6
|
+
import { logDebug } from "../../lib/logger.js";
|
|
6
7
|
import { sha256 } from "../../lib/sync/crypto.js";
|
|
7
8
|
import { updateGitignore, formatMarkdown, ensureDirectoryExists } from "../../lib/sync/utils.js";
|
|
8
|
-
import
|
|
9
|
+
import Database from "better-sqlite3";
|
|
9
10
|
// ============ Yargs Command Module ============
|
|
10
11
|
export const command = "init";
|
|
11
12
|
export const desc = "Initialize sync in current directory";
|
|
@@ -44,13 +45,13 @@ export const handler = async (argv) => {
|
|
|
44
45
|
console.error("Error: --alias is required");
|
|
45
46
|
process.exit(1);
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
+
let config = await readConfig();
|
|
48
49
|
if (!profileExists(config, alias)) {
|
|
49
50
|
console.error(`Error: Profile alias '${alias}' not found.`);
|
|
50
51
|
console.error("Run 'kontexted login' to add a profile first.");
|
|
51
52
|
process.exit(1);
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
+
let profile = getProfile(config, alias);
|
|
54
55
|
// Step 2: Determine workspace
|
|
55
56
|
const workspaceSlug = argv.workspace || profile.workspace;
|
|
56
57
|
if (!workspaceSlug) {
|
|
@@ -80,18 +81,25 @@ export const handler = async (argv) => {
|
|
|
80
81
|
await ensureDirectoryExists(conflictsDir);
|
|
81
82
|
// Step 5: Initialize SQLite queue database
|
|
82
83
|
console.log("Initializing queue database...");
|
|
83
|
-
|
|
84
|
+
initializeQueueDatabase(queueDbPath);
|
|
84
85
|
// Step 6: Create API client and fetch workspace data
|
|
85
86
|
console.log("Fetching workspace data from server...");
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
let apiClient;
|
|
88
|
+
try {
|
|
89
|
+
const auth = await createAuthenticatedClient(alias);
|
|
90
|
+
apiClient = auth.client;
|
|
91
|
+
profile = auth.profile;
|
|
92
|
+
config = auth.config;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
console.error(`\nError: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.error("\nError: Failed to authenticate. Please run 'kontexted login'...");
|
|
100
|
+
}
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
95
103
|
let pullResponse;
|
|
96
104
|
try {
|
|
97
105
|
const response = await apiClient.get(`/api/sync/pull?workspaceSlug=${encodeURIComponent(workspaceSlug)}`);
|
|
@@ -211,9 +219,7 @@ export const handler = async (argv) => {
|
|
|
211
219
|
/**
|
|
212
220
|
* Initialize the SQLite queue database with the pending_changes table.
|
|
213
221
|
*/
|
|
214
|
-
|
|
215
|
-
// Use Bun's SQLite driver
|
|
216
|
-
const { Database } = await import("bun:sqlite");
|
|
222
|
+
function initializeQueueDatabase(dbPath) {
|
|
217
223
|
// Create database (this will also create the file)
|
|
218
224
|
const db = new Database(dbPath);
|
|
219
225
|
// Create pending_changes table
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { readConfig, writeConfig } from "../../lib/config.js";
|
|
4
|
-
import { getProfile } from "../../lib/profile.js";
|
|
5
|
-
import { ApiClient } from "../../lib/api-client.js";
|
|
6
3
|
import { SyncEngine } from "../../lib/sync/sync-engine.js";
|
|
7
|
-
import {
|
|
4
|
+
import { createAuthenticatedClient } from "../../lib/sync/auth-utils.js";
|
|
5
|
+
import { findSyncDir, loadSyncConfig, daemonize, isDaemonChild, watchDaemonLog, clearDaemonPid, } from "../../lib/sync/command-utils.js";
|
|
8
6
|
/**
|
|
9
7
|
* Setup console logging to file in daemon mode
|
|
10
8
|
*/
|
|
@@ -108,26 +106,31 @@ export async function handler(argv) {
|
|
|
108
106
|
// Step 2: Load sync config
|
|
109
107
|
console.log("Loading sync configuration...");
|
|
110
108
|
const syncConfig = await loadSyncConfig(syncDir);
|
|
111
|
-
// Step 3:
|
|
112
|
-
console.log("Validating profile...");
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
109
|
+
// Step 3: Authenticate and create API client
|
|
110
|
+
console.log("Validating profile and authenticating...");
|
|
111
|
+
let apiClient;
|
|
112
|
+
let profile;
|
|
113
|
+
try {
|
|
114
|
+
const auth = await createAuthenticatedClient(syncConfig.alias);
|
|
115
|
+
apiClient = auth.client;
|
|
116
|
+
profile = auth.profile;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error instanceof Error) {
|
|
120
|
+
console.error(`\nError: ${error.message}`);
|
|
123
121
|
}
|
|
124
|
-
|
|
122
|
+
else {
|
|
123
|
+
console.error("\nError: Failed to authenticate. Please run 'kontexted login'...");
|
|
124
|
+
}
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
125
127
|
// Test API connection
|
|
126
128
|
try {
|
|
127
129
|
const response = await apiClient.get(`/api/sync/pull?workspaceSlug=${encodeURIComponent(syncConfig.workspaceSlug)}`);
|
|
128
130
|
if (!response.ok) {
|
|
131
|
+
// 401 should not happen here since we already validated, but handle just in case
|
|
129
132
|
if (response.status === 401) {
|
|
130
|
-
console.error("\nError: Authentication failed. Please
|
|
133
|
+
console.error("\nError: Authentication failed unexpectedly. Please try 'kontexted login'...");
|
|
131
134
|
process.exit(1);
|
|
132
135
|
}
|
|
133
136
|
const errorText = await response.text();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { findSyncDir, loadSyncConfig, loadSyncState, isDaemonRunning, isPaused, } from "../../lib/sync/command-utils.js";
|
|
@@ -9,7 +9,9 @@ function countPendingChanges(syncDir) {
|
|
|
9
9
|
const queuePath = path.join(syncDir, ".sync", "queue.db");
|
|
10
10
|
try {
|
|
11
11
|
const db = new Database(queuePath, { readonly: true });
|
|
12
|
-
const result = db
|
|
12
|
+
const result = db
|
|
13
|
+
.prepare("SELECT COUNT(*) as count FROM pending_changes")
|
|
14
|
+
.get();
|
|
13
15
|
db.close();
|
|
14
16
|
return result?.count ?? 0;
|
|
15
17
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
2
5
|
import { Command } from "commander";
|
|
3
6
|
import { registerLoginCommand } from "./commands/login.js";
|
|
4
7
|
import { registerLogoutCommand } from "./commands/logout.js";
|
|
@@ -7,11 +10,14 @@ import { registerMcpCommand } from "./commands/mcp.js";
|
|
|
7
10
|
import { registerSkillCommand } from "./commands/skill.js";
|
|
8
11
|
import { registerServerCommand } from "./commands/server/index.js";
|
|
9
12
|
import { registerSyncCommand } from "./commands/sync/index.js";
|
|
13
|
+
// Read version from package.json
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
10
16
|
const program = new Command();
|
|
11
17
|
program
|
|
12
18
|
.name("kontexted")
|
|
13
19
|
.description("CLI tool for Kontexted - MCP proxy and workspace management")
|
|
14
|
-
.version(
|
|
20
|
+
.version(packageJson.version);
|
|
15
21
|
// Register subcommands
|
|
16
22
|
registerLoginCommand(program);
|
|
17
23
|
registerLogoutCommand(program);
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ApiClient } from "../../lib/api-client.js";
|
|
2
|
+
import type { Config, Profile } from "../../types/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Create an authenticated API client with proper token validation and persistence.
|
|
5
|
+
* This utility ensures tokens are validated/refreshed before creating the client
|
|
6
|
+
* and uses deep copy to avoid reference issues when persisting tokens.
|
|
7
|
+
*
|
|
8
|
+
* @param profileAlias - The profile alias to authenticate with
|
|
9
|
+
* @returns Object containing the authenticated ApiClient, config, and profile
|
|
10
|
+
* @throws Error if profile not found or authentication expired
|
|
11
|
+
*/
|
|
12
|
+
export declare function createAuthenticatedClient(profileAlias: string): Promise<{
|
|
13
|
+
client: ApiClient;
|
|
14
|
+
config: Config;
|
|
15
|
+
profile: Profile;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readConfig, writeConfig } from "../../lib/config.js";
|
|
2
|
+
import { getProfile } from "../../lib/profile.js";
|
|
3
|
+
import { ApiClient } from "../../lib/api-client.js";
|
|
4
|
+
import { ensureValidTokens } from "../../lib/oauth.js";
|
|
5
|
+
/**
|
|
6
|
+
* Create an authenticated API client with proper token validation and persistence.
|
|
7
|
+
* This utility ensures tokens are validated/refreshed before creating the client
|
|
8
|
+
* and uses deep copy to avoid reference issues when persisting tokens.
|
|
9
|
+
*
|
|
10
|
+
* @param profileAlias - The profile alias to authenticate with
|
|
11
|
+
* @returns Object containing the authenticated ApiClient, config, and profile
|
|
12
|
+
* @throws Error if profile not found or authentication expired
|
|
13
|
+
*/
|
|
14
|
+
export async function createAuthenticatedClient(profileAlias) {
|
|
15
|
+
// Read config and get profile
|
|
16
|
+
const config = await readConfig();
|
|
17
|
+
const profile = getProfile(config, profileAlias);
|
|
18
|
+
if (!profile) {
|
|
19
|
+
throw new Error(`Profile not found: ${profileAlias}`);
|
|
20
|
+
}
|
|
21
|
+
// Create a persist callback that uses deep copy to avoid reference issues
|
|
22
|
+
const createPersistCallback = () => {
|
|
23
|
+
return async () => {
|
|
24
|
+
const freshConfig = await readConfig();
|
|
25
|
+
const freshProfile = getProfile(freshConfig, profileAlias);
|
|
26
|
+
if (freshProfile) {
|
|
27
|
+
// DEEP COPY the tokens to avoid reference issues
|
|
28
|
+
// This is the fix for the persist callback bug
|
|
29
|
+
freshProfile.oauth = {
|
|
30
|
+
...profile.oauth,
|
|
31
|
+
tokens: profile.oauth.tokens ? { ...profile.oauth.tokens } : undefined,
|
|
32
|
+
};
|
|
33
|
+
await writeConfig(freshConfig);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
// Proactively validate/refresh tokens before creating the client
|
|
38
|
+
const tokensValid = await ensureValidTokens(profile.oauth, createPersistCallback(), profile.serverUrl);
|
|
39
|
+
if (!tokensValid) {
|
|
40
|
+
throw new Error(`Authentication expired for ${profileAlias}. Run 'kontexted login --alias ${profileAlias}' to re-authenticate.`);
|
|
41
|
+
}
|
|
42
|
+
// Create the ApiClient with the same deep-copy persist callback
|
|
43
|
+
const client = new ApiClient(profile.serverUrl, profile.oauth, createPersistCallback());
|
|
44
|
+
return { client, config, profile };
|
|
45
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
3
4
|
import { profileExists, getProfile } from "../../lib/profile.js";
|
|
4
5
|
/**
|
|
5
6
|
* Centralized utilities for sync command operations
|
|
@@ -194,11 +195,9 @@ export async function daemonize(syncDir) {
|
|
|
194
195
|
await logFile.write(`[${timestamp}] === Daemon starting ===\n`);
|
|
195
196
|
// Spawn child process with detached: true
|
|
196
197
|
// The child will have SYNC_DAEMON_CHILD=1 in its environment
|
|
197
|
-
const child =
|
|
198
|
+
const child = spawn(process.argv[0], process.argv.slice(1), {
|
|
198
199
|
detached: true,
|
|
199
|
-
|
|
200
|
-
stdout: logFile.fd,
|
|
201
|
-
stderr: logFile.fd,
|
|
200
|
+
stdio: ["ignore", logFile.fd, logFile.fd],
|
|
202
201
|
env: {
|
|
203
202
|
...process.env,
|
|
204
203
|
SYNC_DAEMON_CHILD: "1",
|
package/dist/lib/sync/queue.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Queue management for pending file changes
|
|
3
3
|
* @packageDocumentation
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
6
|
/**
|
|
7
7
|
* Queue for managing pending file changes to be synced
|
|
8
8
|
*/
|
|
@@ -13,7 +13,7 @@ export class Queue {
|
|
|
13
13
|
this.init();
|
|
14
14
|
}
|
|
15
15
|
init() {
|
|
16
|
-
this.db.
|
|
16
|
+
this.db.exec(`
|
|
17
17
|
CREATE TABLE IF NOT EXISTS pending_changes (
|
|
18
18
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
19
19
|
file_path TEXT NOT NULL,
|
|
@@ -42,7 +42,7 @@ export class Queue {
|
|
|
42
42
|
* @returns Array of pending changes ordered by detection time
|
|
43
43
|
*/
|
|
44
44
|
getAll() {
|
|
45
|
-
return this.db.
|
|
45
|
+
return this.db.prepare(`
|
|
46
46
|
SELECT
|
|
47
47
|
id,
|
|
48
48
|
file_path AS filePath,
|
|
@@ -60,7 +60,7 @@ export class Queue {
|
|
|
60
60
|
* @param id - The ID of the pending change to remove
|
|
61
61
|
*/
|
|
62
62
|
remove(id) {
|
|
63
|
-
this.db.
|
|
63
|
+
this.db.prepare(`DELETE FROM pending_changes WHERE id = ?`).run(id);
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* Increment the retry count for a pending change and record the error
|
|
@@ -68,14 +68,14 @@ export class Queue {
|
|
|
68
68
|
* @param error - The error message to record
|
|
69
69
|
*/
|
|
70
70
|
incrementRetry(id, error) {
|
|
71
|
-
this.db.
|
|
71
|
+
this.db.prepare(`UPDATE pending_changes SET retry_count = retry_count + 1, last_error = ? WHERE id = ?`).run(error, id);
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
74
|
* Get the count of pending changes in the queue
|
|
75
75
|
* @returns Number of pending changes
|
|
76
76
|
*/
|
|
77
77
|
getCount() {
|
|
78
|
-
const result = this.db.
|
|
78
|
+
const result = this.db.prepare(`SELECT COUNT(*) as count FROM pending_changes`).get();
|
|
79
79
|
return result?.count ?? 0;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
@@ -42,29 +42,52 @@ export class RemoteListener {
|
|
|
42
42
|
}
|
|
43
43
|
const url = `${this.apiClient.baseUrl}/api/sync/events?workspaceSlug=${encodeURIComponent(this.workspaceSlug)}`;
|
|
44
44
|
this.abortController = new AbortController();
|
|
45
|
+
// Get fresh token before connecting
|
|
46
|
+
let accessToken = this.apiClient.getAccessToken();
|
|
47
|
+
if (!accessToken) {
|
|
48
|
+
console.error("[RemoteListener] No access token available");
|
|
49
|
+
this.handleReconnect();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
45
52
|
try {
|
|
46
53
|
const response = await fetch(url, {
|
|
47
54
|
method: "GET",
|
|
48
55
|
headers: {
|
|
49
56
|
"Accept": "text/event-stream",
|
|
50
|
-
"Authorization": `Bearer ${
|
|
57
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
51
58
|
},
|
|
52
59
|
signal: this.abortController.signal,
|
|
53
60
|
});
|
|
54
61
|
if (!response.ok) {
|
|
55
62
|
if (response.status === 401) {
|
|
56
|
-
console.
|
|
63
|
+
console.warn("[RemoteListener] Token expired, attempting to refresh...");
|
|
64
|
+
// Attempt to refresh the token via ApiClient
|
|
65
|
+
const refreshed = await this.apiClient.refreshToken();
|
|
66
|
+
if (refreshed) {
|
|
67
|
+
// Retry the connection with the new token
|
|
68
|
+
const newToken = this.apiClient.getAccessToken();
|
|
69
|
+
if (newToken) {
|
|
70
|
+
console.log("[RemoteListener] Token refreshed, retrying connection...");
|
|
71
|
+
const retryResponse = await fetch(url, {
|
|
72
|
+
method: "GET",
|
|
73
|
+
headers: {
|
|
74
|
+
"Accept": "text/event-stream",
|
|
75
|
+
"Authorization": `Bearer ${newToken}`,
|
|
76
|
+
},
|
|
77
|
+
signal: this.abortController.signal,
|
|
78
|
+
});
|
|
79
|
+
if (retryResponse.ok) {
|
|
80
|
+
// Success on retry - handle SSE connection
|
|
81
|
+
await this.handleSSEConnection(retryResponse);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
console.error("[RemoteListener] Authentication failed after refresh attempt.");
|
|
57
87
|
}
|
|
58
88
|
throw new Error(`SSE connection failed: ${response.status}`);
|
|
59
89
|
}
|
|
60
|
-
|
|
61
|
-
throw new Error("No response body");
|
|
62
|
-
}
|
|
63
|
-
// Reset reconnect attempts on successful connection
|
|
64
|
-
this.reconnectAttempts = 0;
|
|
65
|
-
console.log("[RemoteListener] Connected to SSE");
|
|
66
|
-
// Process the stream
|
|
67
|
-
await this.processStream(response.body);
|
|
90
|
+
await this.handleSSEConnection(response);
|
|
68
91
|
}
|
|
69
92
|
catch (error) {
|
|
70
93
|
if (error instanceof Error && error.name === "AbortError") {
|
|
@@ -75,6 +98,19 @@ export class RemoteListener {
|
|
|
75
98
|
this.handleReconnect();
|
|
76
99
|
}
|
|
77
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Handle SSE connection after successful response
|
|
103
|
+
*/
|
|
104
|
+
async handleSSEConnection(response) {
|
|
105
|
+
if (!response.body) {
|
|
106
|
+
throw new Error("No response body");
|
|
107
|
+
}
|
|
108
|
+
// Reset reconnect attempts on successful connection
|
|
109
|
+
this.reconnectAttempts = 0;
|
|
110
|
+
console.log("[RemoteListener] Connected to SSE");
|
|
111
|
+
// Process the stream
|
|
112
|
+
await this.processStream(response.body);
|
|
113
|
+
}
|
|
78
114
|
/**
|
|
79
115
|
* Process the SSE stream
|
|
80
116
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kontexted",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "CLI tool for Kontexted - MCP proxy, workspaces management, and local server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
33
|
+
"better-sqlite3": "^11.0.0",
|
|
33
34
|
"chokidar": "^5.0.0",
|
|
34
35
|
"commander": "^12.1.0",
|
|
35
36
|
"eventsource": "^4.1.0",
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"zod": "^3.23.8"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
41
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
40
42
|
"@types/node": "^20.0.0",
|
|
41
43
|
"@types/yargs": "^17.0.35",
|
|
42
44
|
"bun-types": "^1.3.9",
|
|
@@ -46,8 +48,8 @@
|
|
|
46
48
|
"typescript": "^5.6.0"
|
|
47
49
|
},
|
|
48
50
|
"optionalDependencies": {
|
|
49
|
-
"@kontexted/darwin-arm64": "0.1.
|
|
50
|
-
"@kontexted/linux-x64": "0.1.
|
|
51
|
-
"@kontexted/windows-x64": "0.1.
|
|
51
|
+
"@kontexted/darwin-arm64": "0.1.15",
|
|
52
|
+
"@kontexted/linux-x64": "0.1.15",
|
|
53
|
+
"@kontexted/windows-x64": "0.1.15"
|
|
52
54
|
}
|
|
53
55
|
}
|