kontexted 0.1.14 → 0.1.16

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,11 @@
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
9
  import Database from "better-sqlite3";
10
10
  // ============ Yargs Command Module ============
11
11
  export const command = "init";
@@ -45,13 +45,13 @@ export const handler = async (argv) => {
45
45
  console.error("Error: --alias is required");
46
46
  process.exit(1);
47
47
  }
48
- const config = await readConfig();
48
+ let config = await readConfig();
49
49
  if (!profileExists(config, alias)) {
50
50
  console.error(`Error: Profile alias '${alias}' not found.`);
51
51
  console.error("Run 'kontexted login' to add a profile first.");
52
52
  process.exit(1);
53
53
  }
54
- const profile = getProfile(config, alias);
54
+ let profile = getProfile(config, alias);
55
55
  // Step 2: Determine workspace
56
56
  const workspaceSlug = argv.workspace || profile.workspace;
57
57
  if (!workspaceSlug) {
@@ -84,15 +84,22 @@ export const handler = async (argv) => {
84
84
  initializeQueueDatabase(queueDbPath);
85
85
  // Step 6: Create API client and fetch workspace data
86
86
  console.log("Fetching workspace data from server...");
87
- const apiClient = new ApiClient(profile.serverUrl, profile.oauth, async () => {
88
- // The ApiClient already updates this.oauth.tokens which references the same object
89
- // as profile.oauth, so profile.oauth.tokens should have the new tokens.
90
- // We just need to persist the config with the updated oauth state.
91
- logDebug("[SYNC INIT] Persist callback called, saving tokens...");
92
- logDebug(`[SYNC INIT] Current access token: ${apiClient.getOAuth().tokens?.access_token?.substring(0, 20)}...`);
93
- await writeConfig(config);
94
- logDebug("[SYNC INIT] Tokens saved successfully");
95
- });
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
+ }
96
103
  let pullResponse;
97
104
  try {
98
105
  const response = await apiClient.get(`/api/sync/pull?workspaceSlug=${encodeURIComponent(workspaceSlug)}`);
@@ -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();
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
  */
@@ -113,6 +113,10 @@ export class ApiClient {
113
113
  return false;
114
114
  }
115
115
  const tokens = (await response.json());
116
+ // Calculate absolute expiry time if not provided by server
117
+ if (tokens.expires_in && !tokens.expires_at) {
118
+ tokens.expires_at = Math.floor(Date.now() / 1000) + tokens.expires_in;
119
+ }
116
120
  this.oauth.tokens = tokens;
117
121
  await this.persist();
118
122
  logDebug("[API CLIENT] Token refresh successful");
@@ -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,47 @@
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
+ // Create a single persist callback to be reused
38
+ const persistCallback = createPersistCallback();
39
+ // Proactively validate/refresh tokens before creating the client
40
+ const tokensValid = await ensureValidTokens(profile.oauth, persistCallback, profile.serverUrl);
41
+ if (!tokensValid) {
42
+ throw new Error(`Authentication expired for ${profileAlias}. Run 'kontexted login --alias ${profileAlias}' to re-authenticate.`);
43
+ }
44
+ // Create the ApiClient with the same persist callback
45
+ const client = new ApiClient(profile.serverUrl, profile.oauth, persistCallback);
46
+ return { client, config, profile };
47
+ }
@@ -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.14",
3
+ "version": "0.1.16",
4
4
  "description": "CLI tool for Kontexted - MCP proxy, workspaces management, and local server",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,8 +48,8 @@
48
48
  "typescript": "^5.6.0"
49
49
  },
50
50
  "optionalDependencies": {
51
- "@kontexted/darwin-arm64": "0.1.14",
52
- "@kontexted/linux-x64": "0.1.14",
53
- "@kontexted/windows-x64": "0.1.14"
51
+ "@kontexted/darwin-arm64": "0.1.16",
52
+ "@kontexted/linux-x64": "0.1.16",
53
+ "@kontexted/windows-x64": "0.1.16"
54
54
  }
55
55
  }