@ghl-ai/aw 0.1.85 → 0.1.88

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/cli.mjs CHANGED
@@ -183,8 +183,9 @@ function printHelp() {
183
183
  cmd('aw integrations add <key>', 'Install a specific tool (e.g. codex, caveman, context-mode)'),
184
184
  cmd('aw integrations remove <key>', 'Remove a tool'),
185
185
  cmd('aw integrations bundle <name>', 'Install a preset bundle'),
186
- cmd('aw clickup-sync <items.json> --list <id>', 'Sync JSON items into a ClickUp list (create new, comment recurring; input-validated, fail-closed)'),
186
+ cmd('aw clickup-sync <items.json> --list <id>', 'Sync JSON items into a ClickUp list via the ghl-ai MCP proxy using your GitHub PAT (no CLICKUP_API_TOKEN needed; create new, comment recurring; optional per-item tags + priority 1-4; input-validated, fail-closed)'),
187
187
  cmd('aw clickup-sync <items.json> --list <id> --dry-run', 'Preview the create/comment plan without any writes'),
188
+ cmd('aw clickup-sync <items.json> --list <id> --direct', 'Bypass the proxy and call ClickUp directly with a local CLICKUP_API_TOKEN'),
188
189
  cmd('aw drop <path>', 'Stop syncing or delete local content'),
189
190
  cmd('aw nuke', 'Remove entire .aw_registry/ & start fresh'),
190
191
  cmd('aw daemon install', 'Auto-pull on a schedule (macOS launchd / Linux cron)'),
package/clickup-sync.mjs CHANGED
@@ -25,7 +25,14 @@ export const LEGACY_NAME_KEY_RE = /key:(\S+)\s*$/;
25
25
  export const TITLE_MAX = 255;
26
26
  export const DESC_MAX = 8000;
27
27
 
28
- const ALLOWED_FIELDS = new Set(['key', 'title', 'description']);
28
+ // Tier 1 enrichment bounds. Tags are ClickUp tag names (attached at create);
29
+ // priority is ClickUp's fixed 1..4 scale (1=Urgent, 2=High, 3=Normal, 4=Low).
30
+ export const TAG_MAX = 100;
31
+ export const TAGS_MAX = 20;
32
+ export const PRIORITY_MIN = 1;
33
+ export const PRIORITY_MAX = 4;
34
+
35
+ const ALLOWED_FIELDS = new Set(['key', 'title', 'description', 'tags', 'priority']);
29
36
 
30
37
  /**
31
38
  * Validate untrusted, already-parsed JSON into a normalized item list.
@@ -78,7 +85,43 @@ export function parseAndValidateItems(raw) {
78
85
  throw new Error(`Invalid item ${index}: "description" exceeds ${DESC_MAX} characters.`);
79
86
  }
80
87
 
81
- return { key, title, description };
88
+ const normalized = { key, title, description };
89
+
90
+ // Optional tags: array of non-empty tag-name strings, deduped, bounded.
91
+ if (item.tags !== undefined) {
92
+ if (!Array.isArray(item.tags)) {
93
+ throw new Error(`Invalid item ${index}: "tags" must be an array of strings when present.`);
94
+ }
95
+ const tags = [];
96
+ const seenTags = new Set();
97
+ for (const tag of item.tags) {
98
+ if (typeof tag !== 'string' || tag.trim().length === 0) {
99
+ throw new Error(`Invalid item ${index}: every "tag" must be a non-empty string.`);
100
+ }
101
+ if (tag.length > TAG_MAX) {
102
+ throw new Error(`Invalid item ${index}: a "tag" exceeds ${TAG_MAX} characters.`);
103
+ }
104
+ if (seenTags.has(tag)) continue;
105
+ seenTags.add(tag);
106
+ tags.push(tag);
107
+ }
108
+ if (tags.length > TAGS_MAX) {
109
+ throw new Error(`Invalid item ${index}: "tags" exceeds ${TAGS_MAX} entries.`);
110
+ }
111
+ normalized.tags = tags;
112
+ }
113
+
114
+ // Optional priority: integer on ClickUp's 1..4 scale.
115
+ if (item.priority !== undefined) {
116
+ const { priority } = item;
117
+ if (typeof priority !== 'number' || !Number.isInteger(priority)
118
+ || priority < PRIORITY_MIN || priority > PRIORITY_MAX) {
119
+ throw new Error(`Invalid item ${index}: "priority" must be an integer ${PRIORITY_MIN}..${PRIORITY_MAX} (1=Urgent, 2=High, 3=Normal, 4=Low).`);
120
+ }
121
+ normalized.priority = priority;
122
+ }
123
+
124
+ return normalized;
82
125
  });
83
126
 
84
127
  return items;
@@ -11,14 +11,14 @@
11
11
  import { readFileSync } from 'node:fs';
12
12
  import * as fmt from '../fmt.mjs';
13
13
  import { chalk } from '../fmt.mjs';
14
- import { createClickUpClient } from '../integrations/clickup.mjs';
14
+ import { resolveClickUpClient } from '../integrations/clickup.mjs';
15
15
  import { parseAndValidateItems, embedSentinel, computeSyncPlan } from '../clickup-sync.mjs';
16
16
 
17
17
  const RECURRING_COMMENT = 'Recurred via `aw clickup-sync` — item seen again in the source list.';
18
18
 
19
19
  export async function clickupSyncCommand(args, deps = {}) {
20
20
  const readFile = deps.readFile ?? ((path) => readFileSync(path, 'utf8'));
21
- const makeClient = deps.createClient ?? (() => createClickUpClient());
21
+ const makeClient = deps.createClient ?? (() => resolveClickUpClient(args));
22
22
 
23
23
  fmt.configureInteractionMode(args);
24
24
  fmt.intro('aw clickup-sync');
@@ -91,10 +91,15 @@ export async function clickupSyncCommand(args, deps = {}) {
91
91
  let created = 0;
92
92
  for (const item of toCreate) {
93
93
  try {
94
- await client.createTask(listId, {
94
+ const payload = {
95
95
  name: item.title,
96
96
  description: embedSentinel(item.description, item.key),
97
- });
97
+ };
98
+ // Enrichment fields are create-time only and included solely when present,
99
+ // so items without them behave exactly as before (no empty tags / undefined priority).
100
+ if (item.tags !== undefined) payload.tags = item.tags;
101
+ if (item.priority !== undefined) payload.priority = item.priority;
102
+ await client.createTask(listId, payload);
98
103
  created++;
99
104
  } catch (err) {
100
105
  fmt.cancel(`Failed creating task for "${item.key}" after ${created} created (re-run to resume): ${errMessage(err)}`);
package/commands/push.mjs CHANGED
@@ -550,6 +550,20 @@ async function getGitStatus(repoDir) {
550
550
  return stdout;
551
551
  }
552
552
 
553
+ /**
554
+ * Hard-restore a reused docs cache clone to a clean tree. The clone is a
555
+ * disposable publish staging area (re-checked-out from origin and re-populated
556
+ * on every publish), so discarding local working-tree changes is safe and
557
+ * prevents a single aborted publish from permanently wedging the cache.
558
+ * Best-effort: swallow git errors so recovery never itself blocks a publish.
559
+ */
560
+ async function recoverDirtyDocsCache(cloneDir) {
561
+ await execFile('git', ['reset', '--hard'], { cwd: cloneDir, encoding: 'utf8' })
562
+ .catch((e) => fmt.logWarn(`AW docs cache reset failed: ${(e?.stderr || e?.message || '').toString().trim()}`));
563
+ await execFile('git', ['clean', '-fd'], { cwd: cloneDir, encoding: 'utf8' })
564
+ .catch((e) => fmt.logWarn(`AW docs cache clean failed: ${(e?.stderr || e?.message || '').toString().trim()}`));
565
+ }
566
+
553
567
  function parseGitHubRepo(remoteUrl) {
554
568
  const match = remoteUrl.trim().match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/);
555
569
  if (!match) return null;
@@ -654,8 +668,21 @@ async function ensureAwDocsRepoClone(home, publishConfig, sparseIncludePaths = [
654
668
  status = await getGitStatus(cloneDir);
655
669
  }
656
670
  if (status.trim()) {
671
+ // The cached docs clone is a disposable staging area — below we re-checkout
672
+ // the publish branch from origin and write fresh content, so any remaining
673
+ // local dirt is safe to discard. This is most often leftover staged/modified
674
+ // files from a PRIOR aborted publish; without recovery that dirt would
675
+ // permanently wedge every subsequent publish (isOnlyManagedAwCacheDirtiness
676
+ // only self-heals pure `.aw` dirt). Recover instead of hard-failing.
677
+ fmt.logWarn(`AW docs cache had leftover changes; recovering ${cloneDir}`);
678
+ await recoverDirtyDocsCache(cloneDir);
679
+ status = await getGitStatus(cloneDir);
680
+ }
681
+ if (status.trim()) {
682
+ // Still dirty after a hard reset+clean → genuinely unexpected (permissions,
683
+ // index corruption). Surface it rather than push from an unknown state.
657
684
  throw new Error([
658
- `AW docs repo worktree is dirty: ${cloneDir}`,
685
+ `AW docs repo worktree is dirty and could not be auto-recovered: ${cloneDir}`,
659
686
  status.trim(),
660
687
  'Clean the cached docs repo or set AW_DOCS_WORKTREE to a clean clone before publishing.',
661
688
  ].join('\n'));
@@ -0,0 +1,63 @@
1
+ // integrations/clickup-proxy.mjs — ClickUp client that proxies through the
2
+ // deployed ghl-ai MCP instead of calling api.clickup.com directly.
3
+ //
4
+ // Exposes the same interface as integrations/clickup.mjs
5
+ // ({ listAllTasks, createTask, addComment }) so commands/clickup-sync.mjs can
6
+ // use either client behind one seam. Auth is the caller's GitHub PAT (Bearer);
7
+ // the shared ClickUp token lives server-side, so no CLICKUP_API_TOKEN is needed
8
+ // locally.
9
+ //
10
+ // Retry discipline mirrors the direct client: get-tasks (a read) is retryable;
11
+ // create-task and add-comment (writes) are never auto-retried, so a transport
12
+ // failure after a server-side success cannot duplicate a task or comment. The
13
+ // sync is cross-run resumable via the description sentinel.
14
+
15
+ import { createMcpRpcClient } from './mcp-rpc.mjs';
16
+
17
+ // Matches integrations/clickup.mjs MAX_PAGES — a hard cap so a list that never
18
+ // reports last_page aborts instead of paging forever.
19
+ export const MAX_PAGES = 200;
20
+
21
+ /**
22
+ * @param {{ mcpUrl: string, token: string, rpc?: { callTool: Function } }} opts
23
+ */
24
+ export function createClickUpProxyClient(opts = {}) {
25
+ const { mcpUrl, token } = opts;
26
+ const rpc = opts.rpc ?? createMcpRpcClient({ url: mcpUrl, token });
27
+
28
+ // Page through every task in a list via the proxy tool. include_closed so
29
+ // completed prior tasks are not recreated; subtasks so nested tasks match too.
30
+ // This is an idempotent read → retryable.
31
+ async function listAllTasks(listId) {
32
+ const tasks = [];
33
+ for (let page = 0; page < MAX_PAGES; page++) {
34
+ const body = await rpc.callTool(
35
+ 'clickup_get-tasks',
36
+ { list_id: listId, page, include_closed: true, subtasks: true },
37
+ { retryable: true },
38
+ );
39
+ const pageTasks = Array.isArray(body?.tasks) ? body.tasks : [];
40
+ if (pageTasks.length === 0) return tasks;
41
+ tasks.push(...pageTasks);
42
+ if (body?.last_page === true) return tasks;
43
+ }
44
+ throw new Error(`ClickUp list ${listId} exceeded the ${MAX_PAGES}-page cap; aborting to avoid duplicate creates.`);
45
+ }
46
+
47
+ // Write — never retried (a retried POST could duplicate the task).
48
+ // tags/priority are create-time enrichment, forwarded only when present so
49
+ // the payload stays identical to the direct client for the same item.
50
+ async function createTask(listId, { name, description, tags, priority }) {
51
+ const args = { list_id: listId, name, description };
52
+ if (tags !== undefined) args.tags = tags;
53
+ if (priority !== undefined) args.priority = priority;
54
+ return rpc.callTool('clickup_create-task', args);
55
+ }
56
+
57
+ // Write — never retried (a retried POST could duplicate the comment).
58
+ async function addComment(taskId, commentText) {
59
+ return rpc.callTool('clickup_add-comment', { task_id: taskId, comment_text: commentText });
60
+ }
61
+
62
+ return { listAllTasks, createTask, addComment };
63
+ }
@@ -1,3 +1,6 @@
1
+ import { resolveGitHubToken, DEFAULT_MCP_URL } from '../mcp.mjs';
2
+ import { createClickUpProxyClient } from './clickup-proxy.mjs';
3
+
1
4
  // integrations/clickup.mjs — ClickUp v2 HTTP client for the aw CLI.
2
5
  //
3
6
  // Faithful .mjs port of apps/api/src/mcp/integrations/clickup.handler.ts, which
@@ -119,10 +122,15 @@ export function createClickUpClient(opts = {}) {
119
122
  throw new Error(`ClickUp list ${listId} exceeded the ${MAX_PAGES}-page cap; aborting to avoid duplicate creates.`);
120
123
  }
121
124
 
122
- async function createTask(listId, { name, description }) {
125
+ async function createTask(listId, { name, description, tags, priority }) {
126
+ // tags/priority are create-time enrichment, included only when present so the
127
+ // body matches the proxy client for the same item (identical outcome invariant).
128
+ const body = { name, description };
129
+ if (tags !== undefined) body.tags = tags;
130
+ if (priority !== undefined) body.priority = priority;
123
131
  return clickupFetch(`/list/${encodeURIComponent(listId)}/task`, {
124
132
  method: 'POST',
125
- body: JSON.stringify({ name, description }),
133
+ body: JSON.stringify(body),
126
134
  });
127
135
  }
128
136
 
@@ -135,3 +143,35 @@ export function createClickUpClient(opts = {}) {
135
143
 
136
144
  return { clickupFetch, listAllTasks, createTask, addComment };
137
145
  }
146
+
147
+ /**
148
+ * Pick the ClickUp client for `aw clickup-sync`. Proxy-by-default: route through
149
+ * the deployed ghl-ai MCP using the developer's GitHub PAT (no local ClickUp
150
+ * token). `--direct` or AW_CLICKUP_DIRECT=1 opts back into the direct
151
+ * api.clickup.com client (which requires CLICKUP_API_TOKEN).
152
+ *
153
+ * deps are injectable for tests; the CLI passes none.
154
+ * @param {Record<string, unknown>} [args] parsed CLI args
155
+ * @param {{ env?: NodeJS.ProcessEnv, resolveGitHubToken?: Function,
156
+ * createClickUpClient?: Function, createClickUpProxyClient?: Function }} [deps]
157
+ */
158
+ export function resolveClickUpClient(args = {}, deps = {}) {
159
+ const env = deps.env ?? process.env;
160
+ const resolveToken = deps.resolveGitHubToken ?? resolveGitHubToken;
161
+ const makeDirect = deps.createClickUpClient ?? (() => createClickUpClient());
162
+ const makeProxy = deps.createClickUpProxyClient ?? ((cfg) => createClickUpProxyClient(cfg));
163
+
164
+ const wantsDirect = args['--direct'] === true || env.AW_CLICKUP_DIRECT === '1';
165
+ if (wantsDirect) {
166
+ return makeDirect();
167
+ }
168
+
169
+ const token = resolveToken(true);
170
+ if (!token) {
171
+ throw new Error(
172
+ 'No GitHub PAT for the ghl-ai MCP proxy. Run `gh auth login` or set GITHUB_TOKEN, or use --direct with CLICKUP_API_TOKEN.',
173
+ );
174
+ }
175
+ const mcpUrl = env.GHL_MCP_URL || DEFAULT_MCP_URL;
176
+ return makeProxy({ mcpUrl, token });
177
+ }
@@ -0,0 +1,218 @@
1
+ // integrations/mcp-rpc.mjs — minimal MCP JSON-RPC session client for the aw CLI.
2
+ //
3
+ // A standalone-ESM port of the subset of apps/api/src/mcp/integrations/
4
+ // remote-mcp.client.ts that the CLI needs: open a session (initialize ->
5
+ // notifications/initialized), then call tools (tools/call) over Streamable
6
+ // HTTP, carrying Mcp-Session-Id and unwrapping the MCP content envelope.
7
+ //
8
+ // Retry policy is the load-bearing safety property: callTool does NOT retry by
9
+ // default. Only reads that pass { retryable: true } are retried on transport
10
+ // timeouts/5xx. Writes (create-task, add-comment) must never auto-retry — a
11
+ // transport failure after a server-side success would duplicate the write; the
12
+ // caller is expected to be resumable instead.
13
+
14
+ const DEFAULT_PROTOCOL_VERSION = '2025-03-26';
15
+ const DEFAULT_CLIENT_INFO = { name: 'ghl-ai-aw-cli', version: '1.0.0' };
16
+ const DEFAULT_TIMEOUT_MS = 30_000;
17
+
18
+ export class McpRpcError extends Error {
19
+ constructor(message, { status, timeout = false, transport = false } = {}) {
20
+ super(message);
21
+ this.name = 'McpRpcError';
22
+ this.status = status;
23
+ this.timeout = timeout;
24
+ this.transport = transport;
25
+ }
26
+ }
27
+
28
+ function errMessage(err) {
29
+ return err instanceof Error ? err.message : String(err);
30
+ }
31
+
32
+ // Parse an MCP HTTP body as either a JSON-RPC object or the last data: frame of
33
+ // an SSE stream (Streamable HTTP fallback). Mirrors remote-mcp.client parseRpcPayload.
34
+ function parseRpcPayload(raw) {
35
+ const trimmed = (raw ?? '').trim();
36
+ if (!trimmed) return {};
37
+
38
+ try {
39
+ return JSON.parse(trimmed);
40
+ } catch {
41
+ // Fall through to SSE frame parsing.
42
+ }
43
+
44
+ const lines = trimmed.split(/\r?\n/);
45
+ for (let i = lines.length - 1; i >= 0; i--) {
46
+ const line = lines[i]?.trim();
47
+ if (!line?.startsWith('data:')) continue;
48
+ const payload = line.slice(5).trim();
49
+ if (!payload || payload === '[DONE]') continue;
50
+ try {
51
+ return JSON.parse(payload);
52
+ } catch {
53
+ // Try an earlier frame.
54
+ }
55
+ }
56
+
57
+ throw new McpRpcError(`Unable to parse upstream MCP response: ${trimmed.slice(0, 300)}`);
58
+ }
59
+
60
+ // Unwrap an MCP tools/call `result` content envelope into a usable value.
61
+ // A single text block is JSON-parsed (falling back to the raw text); an error
62
+ // envelope throws. Mirrors remote-mcp.client unwrapMcpTextContent.
63
+ export function unwrapMcpContent(payload) {
64
+ if (!payload || typeof payload !== 'object') return payload;
65
+
66
+ const result = payload;
67
+ if (result.isError === true) {
68
+ const errorText = Array.isArray(result.content)
69
+ ? result.content.find((item) => item?.type === 'text')?.text
70
+ : undefined;
71
+ throw new McpRpcError(typeof errorText === 'string' ? errorText : 'Upstream MCP tool returned an error');
72
+ }
73
+
74
+ if (!Array.isArray(result.content) || result.content.length === 0) {
75
+ return payload;
76
+ }
77
+
78
+ if (result.content.length === 1 && result.content[0]?.type === 'text') {
79
+ const text = result.content[0]?.text;
80
+ if (typeof text !== 'string') return payload;
81
+ try {
82
+ return JSON.parse(text);
83
+ } catch {
84
+ return text;
85
+ }
86
+ }
87
+
88
+ return result.content;
89
+ }
90
+
91
+ function isStaleSessionError(err) {
92
+ if (!(err instanceof McpRpcError)) return false;
93
+ if (err.status === 404 || err.status === 410) return true;
94
+ const body = errMessage(err).toLowerCase();
95
+ return body.includes('session') && (body.includes('not found') || body.includes('expired') || body.includes('invalid'));
96
+ }
97
+
98
+ function isRetryableError(err) {
99
+ if (err instanceof McpRpcError) {
100
+ if (err.timeout || err.transport) return true;
101
+ return err.status === 502 || err.status === 503 || err.status === 504;
102
+ }
103
+ return err instanceof Error;
104
+ }
105
+
106
+ /**
107
+ * @param {{ url: string, token: string, fetchImpl?: typeof fetch, timeoutMs?: number,
108
+ * clientInfo?: { name: string, version: string }, protocolVersion?: string }} config
109
+ */
110
+ export function createMcpRpcClient(config = {}) {
111
+ const { url, token } = config;
112
+ if (!url || typeof url !== 'string') throw new Error('createMcpRpcClient: url is required');
113
+ if (!token || typeof token !== 'string') throw new Error('createMcpRpcClient: token is required');
114
+
115
+ const doFetch = config.fetchImpl ?? globalThis.fetch;
116
+ if (typeof doFetch !== 'function') throw new Error('createMcpRpcClient: fetch is unavailable');
117
+ const timeoutMs = Number.isFinite(config.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
118
+ const clientInfo = config.clientInfo ?? DEFAULT_CLIENT_INFO;
119
+ const protocolVersion = config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION;
120
+
121
+ let sessionId;
122
+ let initialized = false;
123
+ let rpcId = 1;
124
+
125
+ async function sendRpc(payload, { parseBody = true } = {}) {
126
+ const controller = new AbortController();
127
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
128
+ let response;
129
+ try {
130
+ response = await doFetch(url, {
131
+ method: 'POST',
132
+ headers: {
133
+ Accept: 'application/json, text/event-stream',
134
+ 'Content-Type': 'application/json',
135
+ Authorization: `Bearer ${token}`,
136
+ ...(sessionId ? { 'Mcp-Session-Id': sessionId } : {}),
137
+ },
138
+ body: JSON.stringify(payload),
139
+ signal: controller.signal,
140
+ });
141
+ } catch (err) {
142
+ if (err instanceof Error && err.name === 'AbortError') {
143
+ throw new McpRpcError(`MCP request timed out after ${timeoutMs}ms`, { timeout: true });
144
+ }
145
+ throw new McpRpcError(`MCP endpoint unreachable: ${errMessage(err)}`, { transport: true });
146
+ } finally {
147
+ clearTimeout(timer);
148
+ }
149
+
150
+ const raw = parseBody ? await response.text() : '';
151
+ const sid = response.headers.get('mcp-session-id');
152
+ if (sid) sessionId = sid;
153
+
154
+ if (!response.ok) {
155
+ throw new McpRpcError(`MCP ${response.status}: ${raw.slice(0, 500)}`, { status: response.status });
156
+ }
157
+ return parseBody ? parseRpcPayload(raw) : undefined;
158
+ }
159
+
160
+ function invalidateSession() {
161
+ sessionId = undefined;
162
+ initialized = false;
163
+ }
164
+
165
+ async function ensureSession() {
166
+ if (initialized && sessionId) return;
167
+ await sendRpc({
168
+ jsonrpc: '2.0',
169
+ id: rpcId++,
170
+ method: 'initialize',
171
+ params: { protocolVersion, capabilities: {}, clientInfo },
172
+ });
173
+ await sendRpc({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }, { parseBody: false });
174
+ initialized = true;
175
+ }
176
+
177
+ function unwrapEnvelope(payload) {
178
+ if (!payload || typeof payload !== 'object') return payload;
179
+ if (payload.error) {
180
+ throw new McpRpcError(payload.error.message ?? 'MCP returned an error');
181
+ }
182
+ return 'result' in payload ? payload.result : payload;
183
+ }
184
+
185
+ /**
186
+ * Call an MCP tool and return its unwrapped content.
187
+ * @param {string} name
188
+ * @param {Record<string, unknown>} args
189
+ * @param {{ retryable?: boolean }} [options] retryable=true only for idempotent reads.
190
+ */
191
+ async function callTool(name, args = {}, { retryable = false } = {}) {
192
+ const invoke = async () => {
193
+ await ensureSession();
194
+ const parsed = await sendRpc({
195
+ jsonrpc: '2.0',
196
+ id: rpcId++,
197
+ method: 'tools/call',
198
+ params: { name, arguments: args },
199
+ });
200
+ return unwrapMcpContent(unwrapEnvelope(parsed));
201
+ };
202
+
203
+ try {
204
+ return await invoke();
205
+ } catch (err) {
206
+ if (isStaleSessionError(err)) {
207
+ invalidateSession();
208
+ return invoke();
209
+ }
210
+ if (retryable && isRetryableError(err)) {
211
+ return invoke();
212
+ }
213
+ throw err;
214
+ }
215
+ }
216
+
217
+ return { callTool };
218
+ }
package/mcp.mjs CHANGED
@@ -11,7 +11,7 @@ import * as p from '@clack/prompts';
11
11
  import * as fmt from './fmt.mjs';
12
12
 
13
13
  const HOME = homedir();
14
- const DEFAULT_MCP_URL = 'https://services.leadconnectorhq.com/agentic-workspace/mcp';
14
+ export const DEFAULT_MCP_URL = 'https://services.leadconnectorhq.com/agentic-workspace/mcp';
15
15
  const MCP_PREFS_FILENAME = 'mcp-preferences.json';
16
16
  const ENABLED_MODE = 'enabled';
17
17
  const DISABLED_MODE = 'disabled';
@@ -121,7 +121,7 @@ function warnIfCodexBearerEnvMissing(envVar, silent = false) {
121
121
  * 2. `gh auth token` (GitHub CLI — most devs have this)
122
122
  * 3. null (fall back to ${GITHUB_TOKEN} interpolation in config)
123
123
  */
124
- function resolveGitHubToken(silent = false) {
124
+ export function resolveGitHubToken(silent = false) {
125
125
  // 1. Environment variable
126
126
  if (process.env.GITHUB_TOKEN) {
127
127
  if (!silent) fmt.logStep('Using GITHUB_TOKEN from environment');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.85",
3
+ "version": "0.1.88",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {