clementine-agent 1.10.1 → 1.10.3

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.
@@ -5217,11 +5217,16 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
5217
5217
  app.put('/api/settings/:key', async (req, res) => {
5218
5218
  try {
5219
5219
  const { key } = req.params;
5220
- const { value } = req.body;
5220
+ let { value } = req.body;
5221
5221
  if (typeof value !== 'string') {
5222
5222
  res.status(400).json({ error: 'value must be a string' });
5223
5223
  return;
5224
5224
  }
5225
+ // Strip whitespace + invisible chars that commonly hitchhike when users
5226
+ // paste from a website. Without this, Composio's API rejects the key as
5227
+ // 401 even though the visible characters look right. Trim covers
5228
+ // newlines, tabs, regular spaces, and zero-width chars.
5229
+ value = value.trim().replace(/[​-‍]/g, '');
5225
5230
  // Allow known keys + any valid env var name (A-Z, 0-9, _)
5226
5231
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
5227
5232
  res.status(400).json({ error: `Invalid key format: ${key}` });
package/dist/config.d.ts CHANGED
@@ -11,6 +11,12 @@ export declare const PKG_DIR: string;
11
11
  /** Data home — user data, vault, .env, logs, sessions. */
12
12
  export declare const BASE_DIR: string;
13
13
  import { shellEscape as _shellEscape } from './config/env-parser.js';
14
+ /**
15
+ * Look up a config value: local .env first, then process.env fallback.
16
+ * Keychain refs in either source are resolved lazily; failed resolution
17
+ * falls through to the fallback rather than returning the literal stub.
18
+ */
19
+ export declare function getEnv(key: string, fallback?: string): string;
14
20
  /** Merged view of process.env overlaid with .env. Use for classifyIntegrations / summarizeIntegrationStatus. */
15
21
  export declare function envSnapshot(): Record<string, string | undefined>;
16
22
  /** Test-only: clear the keychain ref cache so re-resolution can be tested. */
package/dist/config.js CHANGED
@@ -84,7 +84,7 @@ function maybeResolveRef(value) {
84
84
  * Keychain refs in either source are resolved lazily; failed resolution
85
85
  * falls through to the fallback rather than returning the literal stub.
86
86
  */
87
- function getEnv(key, fallback = '') {
87
+ export function getEnv(key, fallback = '') {
88
88
  const fromLocal = maybeResolveRef(env[key]);
89
89
  if (fromLocal !== undefined && fromLocal !== '')
90
90
  return fromLocal;
@@ -15,7 +15,18 @@
15
15
  import { Composio } from '@composio/core';
16
16
  import { ClaudeAgentSDKProvider } from '@composio/claude-agent-sdk';
17
17
  import pino from 'pino';
18
+ import { getEnv } from '../../config.js';
18
19
  const logger = pino({ name: 'clementine.composio' });
20
+ // `process.env` is intentionally NOT populated from .env (config.ts keeps
21
+ // secrets out of the SDK subprocess). Reading process.env.COMPOSIO_API_KEY
22
+ // directly works during the dashboard's hot-reload (PUT handler mutates
23
+ // process.env), but is empty after a fresh daemon restart even if the key
24
+ // is in .env. Use this helper everywhere we read Composio env vars: it
25
+ // prefers process.env (hot-reload from dashboard) and falls back to the
26
+ // .env file via getEnv (survives restarts).
27
+ function readComposioEnv(key) {
28
+ return process.env[key] || getEnv(key, '');
29
+ }
19
30
  // Curated set surfaced in the dashboard. Composio exposes 1000+ — rendering
20
31
  // them all is noisy. Users can still connect anything by editing this list.
21
32
  export const CURATED_TOOLKITS = [
@@ -50,7 +61,7 @@ let singleton = null;
50
61
  export function getComposio() {
51
62
  if (singleton)
52
63
  return singleton;
53
- const apiKey = process.env.COMPOSIO_API_KEY;
64
+ const apiKey = readComposioEnv('COMPOSIO_API_KEY');
54
65
  if (!apiKey)
55
66
  return null;
56
67
  singleton = new Composio({
@@ -60,7 +71,7 @@ export function getComposio() {
60
71
  return singleton;
61
72
  }
62
73
  export function isComposioEnabled() {
63
- return Boolean(process.env.COMPOSIO_API_KEY);
74
+ return Boolean(readComposioEnv('COMPOSIO_API_KEY'));
64
75
  }
65
76
  /**
66
77
  * Discard the cached client + identity cache so the next call to getComposio()
@@ -89,13 +100,14 @@ export async function getPreferredUserId() {
89
100
  // with the most existing connections, falling back to this constant.
90
101
  const DEFAULT_NEW_CONNECTION_USER_ID = 'default';
91
102
  export function clementineUserId() {
92
- return process.env.COMPOSIO_USER_ID ?? DEFAULT_NEW_CONNECTION_USER_ID;
103
+ return readComposioEnv('COMPOSIO_USER_ID') || DEFAULT_NEW_CONNECTION_USER_ID;
93
104
  }
94
105
  // Cached after first detection — avoids extra API calls per authorize.
95
106
  let detectedPreferredUserId = null;
96
107
  async function detectPreferredUserId(composio) {
97
- if (process.env.COMPOSIO_USER_ID)
98
- return process.env.COMPOSIO_USER_ID;
108
+ const explicit = readComposioEnv('COMPOSIO_USER_ID');
109
+ if (explicit)
110
+ return explicit;
99
111
  if (detectedPreferredUserId)
100
112
  return detectedPreferredUserId;
101
113
  try {
@@ -383,9 +395,17 @@ export async function authorizeToolkit(slug, opts) {
383
395
  // 1. Find or create an auth config. session.authorize() doesn't auto-create
384
396
  // so we have to pass authConfigId explicitly to connectedAccounts.initiate.
385
397
  let authConfigId;
386
- const existingConfig = (await composio.authConfigs.list({ toolkit: slug })).items[0];
398
+ let existingConfig;
399
+ try {
400
+ existingConfig = (await composio.authConfigs.list({ toolkit: slug })).items[0];
401
+ }
402
+ catch (err) {
403
+ logger.error({ err, slug, step: 'authConfigs.list' }, 'Composio authorize failed');
404
+ throw err;
405
+ }
387
406
  if (existingConfig) {
388
407
  authConfigId = existingConfig.id;
408
+ logger.debug({ slug, authConfigId }, 'Reusing existing auth config');
389
409
  }
390
410
  else {
391
411
  try {
@@ -394,12 +414,17 @@ export async function authorizeToolkit(slug, opts) {
394
414
  name: `${displayNameFor(slug)} Auth Config`,
395
415
  });
396
416
  authConfigId = created.id;
417
+ logger.debug({ slug, authConfigId }, 'Created managed auth config');
397
418
  }
398
419
  catch (err) {
399
- // 400 → Composio doesn't host a managed OAuth app for this toolkit;
400
- // user must register their own via the Composio dashboard.
401
420
  const status = err?.status;
402
- if (status === 400) {
421
+ logger.warn({ err, slug, status, step: 'authConfigs.create' }, 'authConfigs.create rejected — likely needs BYO setup');
422
+ // 400 → Composio doesn't host a managed OAuth app for this toolkit.
423
+ // 401/403 → key lacks permission to create managed auth configs (common
424
+ // for Google services where Composio's plan tier requires you to
425
+ // register your own Google OAuth project). Either way, the user fix
426
+ // is the same: set it up once in Composio's auth-configs dashboard.
427
+ if (status === 400 || status === 401 || status === 403) {
403
428
  throw new ComposioNeedsAuthConfigError(slug, String(err));
404
429
  }
405
430
  throw err;
@@ -412,12 +437,19 @@ export async function authorizeToolkit(slug, opts) {
412
437
  // freshly authorized Gmail lands next to the existing Outlook (etc.) under
413
438
  // the same user_id. Falls back to env override or "default".
414
439
  const userId = await detectPreferredUserId(composio);
415
- const conn = await composio.connectedAccounts.initiate(userId, authConfigId, {
416
- ...(existing.length > 0 ? { allowMultiple: true } : {}),
417
- ...(opts?.callbackUrl ? { callbackUrl: opts.callbackUrl } : {}),
418
- ...(opts?.alias ? { alias: opts.alias } : {}),
419
- });
420
- return { redirectUrl: conn.redirectUrl ?? null, connectionId: conn.id };
440
+ try {
441
+ const conn = await composio.connectedAccounts.initiate(userId, authConfigId, {
442
+ ...(existing.length > 0 ? { allowMultiple: true } : {}),
443
+ ...(opts?.callbackUrl ? { callbackUrl: opts.callbackUrl } : {}),
444
+ ...(opts?.alias ? { alias: opts.alias } : {}),
445
+ });
446
+ logger.debug({ slug, userId, connectionId: conn.id }, 'Initiated connection');
447
+ return { redirectUrl: conn.redirectUrl ?? null, connectionId: conn.id };
448
+ }
449
+ catch (err) {
450
+ logger.error({ err, slug, userId, authConfigId, step: 'connectedAccounts.initiate' }, 'Composio initiate failed');
451
+ throw err;
452
+ }
421
453
  }
422
454
  export async function disconnectToolkit(connectionId) {
423
455
  const composio = getComposio();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.10.1",
3
+ "version": "1.10.3",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",