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.
@@ -28,7 +28,15 @@ async function startMcpProxy(options) {
28
28
  writeEnabled = false;
29
29
  }
30
30
  const persist = async () => {
31
- await writeConfig(config);
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 });
@@ -1,10 +1,9 @@
1
1
  import * as readline from "readline";
2
- import { readConfig, writeConfig } from "../lib/config.js";
3
- import { getProfile, listProfiles } from "../lib/profile.js";
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
- const config = await readConfig();
101
- const profile = getProfile(config, alias);
102
- if (!profile) {
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
- // Proactively refresh token if needed (non-interactive)
107
- const tokensValid = await ensureValidTokens(profile.oauth, async () => writeConfig(config), profile.serverUrl);
108
- if (!tokensValid) {
109
- console.error(`Authentication expired. Run 'kontexted login --alias ${alias}' to re-authenticate.`);
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 config = await readConfig();
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 config = await readConfig();
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 config = await readConfig();
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(client, profile.workspace, options.name, options.displayName, options.parentId);
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(client, profile.workspace, options.name, options.title, options.folderId, options.content);
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(client, profile.workspace, options.noteId, options.content);
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 { findSyncDir, loadSyncConfig, loadSyncState, saveSyncState, validateProfile, } from "../../lib/sync/command-utils.js";
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: Determine profile to use
35
+ // Step 3: Authenticate and create API client
38
36
  const profileAlias = argv.alias || syncConfig.alias;
39
- // Step 4: Validate profile
40
- console.log("Validating profile...");
41
- const config = await readConfig();
42
- const profile = validateProfile(config, profileAlias);
43
- // Step 5: Warn about data loss if not forced
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: Create API client
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 { findSyncDir, loadSyncConfig, loadSyncState, validateProfile, } from "../../lib/sync/command-utils.js";
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: Determine profile to use
21
- const profileAlias = argv.alias || syncConfig.alias;
22
- // Step 4: Validate profile
23
- console.log("Validating profile...");
24
- const config = await readConfig();
25
- const profile = validateProfile(config, profileAlias);
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: Create API client
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, writeConfig } from "../../lib/config.js";
3
+ import { readConfig } from "../../lib/config.js";
4
4
  import { getProfile, profileExists } from "../../lib/profile.js";
5
- import { ApiClient } from "../../lib/api-client.js";
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 { logDebug } from "../../lib/logger.js";
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
- const config = await readConfig();
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
- const profile = getProfile(config, alias);
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
- await initializeQueueDatabase(queueDbPath);
84
+ initializeQueueDatabase(queueDbPath);
84
85
  // Step 6: Create API client and fetch workspace data
85
86
  console.log("Fetching workspace data from server...");
86
- const apiClient = new ApiClient(profile.serverUrl, profile.oauth, async () => {
87
- // The ApiClient already updates this.oauth.tokens which references the same object
88
- // as profile.oauth, so profile.oauth.tokens should have the new tokens.
89
- // We just need to persist the config with the updated oauth state.
90
- logDebug("[SYNC INIT] Persist callback called, saving tokens...");
91
- logDebug(`[SYNC INIT] Current access token: ${apiClient.getOAuth().tokens?.access_token?.substring(0, 20)}...`);
92
- await writeConfig(config);
93
- logDebug("[SYNC INIT] Tokens saved successfully");
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
- async function initializeQueueDatabase(dbPath) {
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 { findSyncDir, loadSyncConfig, validateProfile, daemonize, isDaemonChild, watchDaemonLog, clearDaemonPid, } from "../../lib/sync/command-utils.js";
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: Validate profile
112
- console.log("Validating profile...");
113
- const config = await readConfig();
114
- const profile = validateProfile(config, syncConfig.alias);
115
- // Step 4: Create API client
116
- const apiClient = new ApiClient(profile.serverUrl, profile.oauth, async () => {
117
- // Update config with refreshed tokens
118
- const updatedConfig = await readConfig();
119
- const updatedProfile = getProfile(updatedConfig, syncConfig.alias);
120
- if (updatedProfile) {
121
- updatedProfile.oauth = profile.oauth;
122
- await writeConfig(updatedConfig);
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 run 'kontexted login' to re-authenticate.");
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 { Database } from "bun:sqlite";
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.query("SELECT COUNT(*) as count FROM pending_changes").get();
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("0.1.0");
20
+ .version(packageJson.version);
15
21
  // Register subcommands
16
22
  registerLoginCommand(program);
17
23
  registerLogoutCommand(program);
@@ -29,7 +29,7 @@ export declare class ApiClient {
29
29
  * Refresh the access token using the refresh token
30
30
  * @returns true if refresh was successful, false otherwise
31
31
  */
32
- private refreshToken;
32
+ refreshToken(): Promise<boolean>;
33
33
  /**
34
34
  * Make a GET request
35
35
  */
@@ -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 = Bun.spawn([process.argv[0], ...process.argv.slice(1)], {
198
+ const child = spawn(process.argv[0], process.argv.slice(1), {
198
199
  detached: true,
199
- stdin: "ignore",
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",
@@ -2,7 +2,7 @@
2
2
  * Queue management for pending file changes
3
3
  * @packageDocumentation
4
4
  */
5
- import { Database } from "bun:sqlite";
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.run(`
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.query(`
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.run(`DELETE FROM pending_changes WHERE id = ?`, [id]);
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.run(`UPDATE pending_changes SET retry_count = retry_count + 1, last_error = ? WHERE id = ?`, [error, id]);
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.query(`SELECT COUNT(*) as count FROM pending_changes`).get();
78
+ const result = this.db.prepare(`SELECT COUNT(*) as count FROM pending_changes`).get();
79
79
  return result?.count ?? 0;
80
80
  }
81
81
  /**
@@ -29,6 +29,10 @@ export declare class RemoteListener {
29
29
  * Connect to SSE endpoint using fetch
30
30
  */
31
31
  private connect;
32
+ /**
33
+ * Handle SSE connection after successful response
34
+ */
35
+ private handleSSEConnection;
32
36
  /**
33
37
  * Process the SSE stream
34
38
  */
@@ -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 ${this.apiClient.getAccessToken()}`,
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.error("[RemoteListener] Authentication failed. Token may be expired.");
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
- if (!response.body) {
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.13",
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.13",
50
- "@kontexted/linux-x64": "0.1.13",
51
- "@kontexted/windows-x64": "0.1.13"
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
  }