@ghl-ai/aw 0.1.35-beta.9 → 0.1.35

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/commands/init.mjs CHANGED
@@ -19,7 +19,6 @@ import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs
19
19
  import { setupMcp } from '../mcp.mjs';
20
20
  import { autoUpdate, promptUpdate } from '../update.mjs';
21
21
  import { installGlobalHooks } from '../hooks.mjs';
22
- import { installAwEcc } from '../ecc.mjs';
23
22
 
24
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
25
24
  const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
@@ -201,12 +200,11 @@ export async function initCommand(args) {
201
200
 
202
201
  // Re-link IDE dirs + hooks (idempotent)
203
202
  linkWorkspace(HOME);
204
- await installAwEcc(cwd, { targets: ["cursor"], silent });
205
203
  generateCommands(HOME);
206
204
  copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
207
205
  initAwDocs(HOME);
208
- setupMcp(HOME, freshCfg?.namespace || team) || [];
209
- if (cwd !== HOME) setupMcp(cwd, freshCfg?.namespace || team);
206
+ await setupMcp(HOME, freshCfg?.namespace || team, { silent });
207
+ if (cwd !== HOME) await setupMcp(cwd, freshCfg?.namespace || team, { silent });
210
208
  installGlobalHooks();
211
209
 
212
210
  // Link current project if needed
@@ -282,12 +280,11 @@ export async function initCommand(args) {
282
280
  // Step 3: Link IDE dirs + setup tasks
283
281
  fmt.logStep('Linking IDE symlinks...');
284
282
  linkWorkspace(HOME);
285
- await installAwEcc(cwd, { targets: ["cursor"], silent });
286
283
  generateCommands(HOME);
287
284
  const instructionFiles = copyInstructions(HOME, null, team) || [];
288
285
  initAwDocs(HOME);
289
- const mcpFiles = setupMcp(HOME, team) || [];
290
- if (cwd !== HOME) setupMcp(cwd, team);
286
+ const mcpFiles = await setupMcp(HOME, team) || [];
287
+ if (cwd !== HOME) await setupMcp(cwd, team);
291
288
  const hooksInstalled = installGlobalHooks();
292
289
  installIdeTasks();
293
290
 
package/mcp.mjs CHANGED
@@ -3,26 +3,21 @@
3
3
 
4
4
  import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs';
5
5
  import { execSync } from 'node:child_process';
6
- import { join, resolve } from 'node:path';
6
+ import { createInterface } from 'node:readline';
7
+ import { join } from 'node:path';
7
8
  import { homedir } from 'node:os';
8
9
  import * as fmt from './fmt.mjs';
9
10
 
10
11
  const HOME = homedir();
11
- const DEFAULT_MCP_URL = 'https://staging.services.leadconnectorhq.com/agentic-workspace/mcp';
12
+ const DEFAULT_MCP_URL = 'https://services.leadconnectorhq.com/agentic-workspace/mcp';
12
13
 
13
14
  /**
14
15
  * Auto-detect MCP server paths.
15
16
  */
16
17
  function detectPaths() {
17
- const gitJenkinsCandidates = [
18
- join(HOME, 'Documents', 'GitHub', 'git-jenkins-mcp', 'dist', 'index.js'),
19
- resolve('..', 'git-jenkins-mcp', 'dist', 'index.js'),
20
- ];
21
-
22
- const gitJenkinsPath = gitJenkinsCandidates.find(p => existsSync(p)) || null;
23
18
  const ghlMcpUrl = process.env.GHL_MCP_URL || DEFAULT_MCP_URL;
24
19
 
25
- return { gitJenkinsPath, ghlMcpUrl };
20
+ return { ghlMcpUrl };
26
21
  }
27
22
 
28
23
  /**
@@ -33,10 +28,10 @@ function detectPaths() {
33
28
  * 2. `gh auth token` (GitHub CLI — most devs have this)
34
29
  * 3. null (fall back to ${GITHUB_TOKEN} interpolation in config)
35
30
  */
36
- function resolveGitHubToken() {
31
+ function resolveGitHubToken(silent = false) {
37
32
  // 1. Environment variable
38
33
  if (process.env.GITHUB_TOKEN) {
39
- fmt.logStep('Using GITHUB_TOKEN from environment');
34
+ if (!silent) fmt.logStep('Using GITHUB_TOKEN from environment');
40
35
  return process.env.GITHUB_TOKEN;
41
36
  }
42
37
 
@@ -56,12 +51,14 @@ function resolveGitHubToken() {
56
51
  timeout: 5000,
57
52
  }).trim();
58
53
  if (token && (token.startsWith('ghp_') || token.startsWith('gho_') || token.startsWith('github_pat_'))) {
59
- fmt.logStep('Using GitHub token from gh CLI');
54
+ if (!silent) fmt.logStep('Using GitHub token from gh CLI');
60
55
  return token;
61
56
  }
62
57
  } catch { /* not authenticated yet */ }
63
58
 
64
- // 2b. Not authenticated — run gh auth login (opens browser)
59
+ // 2b. Not authenticated — skip browser login in silent mode (no terminal)
60
+ if (silent) return null;
61
+
65
62
  fmt.logStep('GitHub CLI found but not authenticated — launching login...');
66
63
  try {
67
64
  execSync('gh auth login --web --git-protocol https', {
@@ -111,45 +108,165 @@ function resolveGitHubUser(token) {
111
108
  }
112
109
  }
113
110
 
111
+ /**
112
+ * Look for an existing ClickUp token already saved in IDE config files.
113
+ * Returns the first token found, or null.
114
+ */
115
+ function findExistingClickUpToken(cwd) {
116
+ const candidates = [
117
+ join(HOME, '.claude', 'settings.json'),
118
+ join(HOME, '.claude', 'mcp.json'),
119
+ join(HOME, '.mcp.json'),
120
+ join(HOME, '.cursor', 'mcp.json'),
121
+ join(cwd, '.claude', 'mcp.json'),
122
+ join(cwd, '.mcp.json'),
123
+ join(cwd, '.cursor', 'mcp.json'),
124
+ ];
125
+
126
+ for (const filePath of candidates) {
127
+ try {
128
+ if (!existsSync(filePath)) continue;
129
+ const config = JSON.parse(readFileSync(filePath, 'utf8'));
130
+ const token = config?.mcpServers?.['ghl-ai']?.headers?.['X-ClickUp-Token'];
131
+ if (token && typeof token === 'string' && token.startsWith('pk_')) {
132
+ return { token, source: filePath };
133
+ }
134
+ } catch { /* corrupt file — skip */ }
135
+ }
136
+
137
+ return null;
138
+ }
139
+
140
+ /**
141
+ * Resolve ClickUp personal API token.
142
+ *
143
+ * Resolution chain:
144
+ * 1. $CLICKUP_API_TOKEN env var (already set)
145
+ * 2. Existing token in IDE config files (already configured)
146
+ * 3. Open browser + prompt user (one-time)
147
+ * 4. null (skip ClickUp — other MCP tools still work)
148
+ */
149
+ async function resolveClickUpToken(silent = false, cwd = process.cwd()) {
150
+ // 1. Environment variable
151
+ if (process.env.CLICKUP_API_TOKEN) {
152
+ fmt.logStep('Using CLICKUP_API_TOKEN from environment');
153
+ return process.env.CLICKUP_API_TOKEN;
154
+ }
155
+
156
+ // 2. Already saved in an IDE config file — reuse it
157
+ const existing = findExistingClickUpToken(cwd);
158
+ if (existing) {
159
+ fmt.logStep(`Using existing ClickUp token from ${fmt.chalk.cyan(existing.source)}`);
160
+ return existing.token;
161
+ }
162
+
163
+ // In silent mode (git hooks, auto-pull) there is no terminal — skip prompt
164
+ if (silent) return null;
165
+
166
+ // 3. Interactive prompt — open browser and ask user to paste
167
+ fmt.logStep('ClickUp API token needed for per-user task attribution');
168
+ fmt.logStep('Go to https://app.clickup.com/settings/apps to copy your API token');
169
+
170
+ try {
171
+ const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
172
+ execSync(`${openCmd} "https://app.clickup.com/settings/apps"`, { stdio: 'ignore', timeout: 5000 });
173
+ fmt.logStep('Opened ClickUp settings in your browser');
174
+ } catch {
175
+ fmt.logWarn('Could not open browser — navigate to the URL above manually');
176
+ }
177
+
178
+ // Drain any leftover stdin bytes from prior interactive flows (e.g. gh auth login)
179
+ if (process.stdin.readable) {
180
+ process.stdin.resume();
181
+ await new Promise((r) => setTimeout(r, 100));
182
+ process.stdin.pause();
183
+ while (process.stdin.read() !== null) { /* drain */ }
184
+ }
185
+
186
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
187
+ const rawInput = await new Promise((resolve) => {
188
+ rl.question(' Paste your ClickUp API Token (pk_...): ', (answer) => {
189
+ rl.close();
190
+ resolve(answer);
191
+ });
192
+ });
193
+
194
+ // Extract the pk_ token from the paste — handles accidental double-paste,
195
+ // surrounding whitespace, or trailing garbage from terminal paste buffers.
196
+ const match = typeof rawInput === 'string' ? rawInput.match(/\b(pk_\w+)\b/) : null;
197
+ const token = match ? match[1] : null;
198
+
199
+ if (!token) {
200
+ fmt.logWarn('No valid ClickUp token provided — skipping (ClickUp tools will use shared token)');
201
+ return null;
202
+ }
203
+
204
+ // Validate token against ClickUp API
205
+ try {
206
+ const res = execSync(`curl -s -H "Authorization: ${token}" "https://api.clickup.com/api/v2/user"`, {
207
+ encoding: 'utf8',
208
+ stdio: ['pipe', 'pipe', 'pipe'],
209
+ timeout: 10000,
210
+ maxBuffer: 1024 * 256,
211
+ });
212
+ const data = JSON.parse(res);
213
+ if (data.err) {
214
+ fmt.logWarn(`ClickUp token validation failed: ${data.err} — using it anyway`);
215
+ } else {
216
+ const username = data.user?.username || data.user?.email;
217
+ if (username) {
218
+ fmt.logSuccess(`ClickUp token valid — user: ${fmt.chalk.cyan(username)}`);
219
+ } else {
220
+ fmt.logWarn('ClickUp token accepted but no user found — using it anyway');
221
+ }
222
+ }
223
+ } catch (e) {
224
+ fmt.logWarn(`ClickUp token validation failed: ${e.message?.split('\n')[0]} — using it anyway`);
225
+ }
226
+
227
+ return token;
228
+ }
229
+
114
230
  /**
115
231
  * Setup MCP configs globally for Claude Code and Cursor.
116
232
  * Merges ghl-ai server into existing configs without overwriting other servers.
117
233
  * Returns list of file paths that were created or updated.
118
234
  */
119
- export function setupMcp(cwd, namespace) {
235
+ export async function setupMcp(cwd, namespace, { silent = false } = {}) {
120
236
  const paths = detectPaths();
121
237
  const updatedFiles = [];
122
238
 
123
239
  const mcpUrl = paths.ghlMcpUrl;
124
- const ghToken = resolveGitHubToken();
240
+ const ghToken = resolveGitHubToken(silent);
125
241
 
126
242
  // Track who initialized MCP
127
- if (ghToken) {
243
+ if (ghToken && !silent) {
128
244
  const ghUser = resolveGitHubUser(ghToken);
129
245
  if (ghUser) {
130
246
  fmt.logSuccess(`Authenticated as GitHub user: ${fmt.chalk.cyan(ghUser)}`);
131
247
  }
132
248
  }
133
249
 
134
- // Server config with resolved token for local IDE configs (not committed, safe)
250
+ // Resolve ClickUp token skipped in silent mode (no terminal)
251
+ const clickupToken = await resolveClickUpToken(silent, cwd);
252
+
253
+ // Server config with resolved tokens for local IDE configs (not committed, safe)
254
+ const headers = { Authorization: `Bearer ${ghToken || '${GITHUB_TOKEN}'}` };
255
+ if (clickupToken) {
256
+ headers['X-ClickUp-Token'] = clickupToken;
257
+ }
258
+
135
259
  const ghlAiServerLocal = {
136
260
  type: 'http',
137
261
  url: mcpUrl,
138
- headers: { Authorization: `Bearer ${ghToken || '${GITHUB_TOKEN}'}` },
262
+ headers,
139
263
  };
140
264
 
141
- const gitJenkinsServer = paths.gitJenkinsPath
142
- ? { command: 'node', args: [paths.gitJenkinsPath] }
143
- : null;
144
-
145
265
  // ── Claude Code: ~/.claude/settings.json (global, local) ──
146
266
  const claudeSettingsPath = join(HOME, '.claude', 'settings.json');
147
267
  if (mergeJsonMcpServer(claudeSettingsPath, 'ghl-ai', ghlAiServerLocal)) {
148
268
  updatedFiles.push(claudeSettingsPath);
149
269
  }
150
- if (gitJenkinsServer && mergeJsonMcpServer(claudeSettingsPath, 'git-jenkins', gitJenkinsServer)) {
151
- updatedFiles.push(claudeSettingsPath);
152
- }
153
270
 
154
271
  // ── Claude Code: .mcp.json (project root — committed, uses env var) ──
155
272
  const projectMcpPath = join(cwd, '.mcp.json');
@@ -162,29 +279,20 @@ export function setupMcp(cwd, namespace) {
162
279
  if (mergeJsonMcpServer(claudeWorkspaceMcpPath, 'ghl-ai', ghlAiServerLocal)) {
163
280
  updatedFiles.push(claudeWorkspaceMcpPath);
164
281
  }
165
- if (gitJenkinsServer && mergeJsonMcpServer(claudeWorkspaceMcpPath, 'git-jenkins', gitJenkinsServer)) {
166
- updatedFiles.push(claudeWorkspaceMcpPath);
167
- }
168
282
 
169
283
  // ── Cursor: project .cursor/mcp.json (workspace-level, local) ──
170
284
  const cursorProjectMcpPath = join(cwd, '.cursor', 'mcp.json');
171
285
  if (mergeJsonMcpServer(cursorProjectMcpPath, 'ghl-ai', ghlAiServerLocal)) {
172
286
  updatedFiles.push(cursorProjectMcpPath);
173
287
  }
174
- if (gitJenkinsServer && mergeJsonMcpServer(cursorProjectMcpPath, 'git-jenkins', gitJenkinsServer)) {
175
- updatedFiles.push(cursorProjectMcpPath);
176
- }
177
288
 
178
289
  // ── Cursor: ~/.cursor/mcp.json (global, local) ──
179
290
  const cursorMcpPath = join(HOME, '.cursor', 'mcp.json');
180
291
  if (mergeJsonMcpServer(cursorMcpPath, 'ghl-ai', ghlAiServerLocal)) {
181
292
  updatedFiles.push(cursorMcpPath);
182
293
  }
183
- if (gitJenkinsServer && mergeJsonMcpServer(cursorMcpPath, 'git-jenkins', gitJenkinsServer)) {
184
- updatedFiles.push(cursorMcpPath);
185
- }
186
294
 
187
- // Deduplicate (claude settings may be added twice if both servers written)
295
+ // Deduplicate
188
296
  const unique = [...new Set(updatedFiles)];
189
297
 
190
298
  if (unique.length > 0) {
@@ -193,10 +301,6 @@ export function setupMcp(cwd, namespace) {
193
301
  fmt.logInfo('MCP servers already configured — no changes needed');
194
302
  }
195
303
 
196
- if (!paths.gitJenkinsPath) {
197
- fmt.logWarn('git-jenkins MCP not found — skipped');
198
- }
199
-
200
304
  return unique;
201
305
  }
202
306
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.35-beta.9",
3
+ "version": "0.1.35",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,8 +24,7 @@
24
24
  "registry.mjs",
25
25
  "apply.mjs",
26
26
  "update.mjs",
27
- "hooks.mjs",
28
- "ecc.mjs"
27
+ "hooks.mjs"
29
28
  ],
30
29
  "engines": {
31
30
  "node": ">=18.0.0"
package/ecc.mjs DELETED
@@ -1,48 +0,0 @@
1
- import { execSync } from "node:child_process";
2
- import { existsSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
4
- import * as fmt from "./fmt.mjs";
5
-
6
- const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
7
- const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
8
- const AW_ECC_TAG = "v1.0.0";
9
- const TMP_DIR = "/tmp/aw-ecc";
10
-
11
- function cloneRepo(tag, dest, stdio) {
12
- try {
13
- execSync(`git clone --depth 1 --branch ${tag} ${AW_ECC_REPO_SSH} ${dest}`, {
14
- stdio,
15
- });
16
- } catch {
17
- execSync(
18
- `git clone --depth 1 --branch ${tag} ${AW_ECC_REPO_HTTPS} ${dest}`,
19
- { stdio },
20
- );
21
- }
22
- }
23
-
24
- export async function installAwEcc(
25
- cwd,
26
- { targets = ["cursor"], silent = false } = {},
27
- ) {
28
- if (!silent) fmt.logStep("Installing aw-ecc engine...");
29
-
30
- if (existsSync(TMP_DIR)) rmSync(TMP_DIR, { recursive: true, force: true });
31
-
32
- cloneRepo(AW_ECC_TAG, TMP_DIR, silent ? "pipe" : "inherit");
33
-
34
- execSync("npm install --no-audit --no-fund", {
35
- cwd: TMP_DIR,
36
- stdio: silent ? "pipe" : "inherit",
37
- });
38
-
39
- for (const target of targets) {
40
- execSync(
41
- `node ${join(TMP_DIR, "scripts/install-apply.js")} --target ${target} --profile full`,
42
- { cwd, stdio: silent ? "pipe" : "inherit" },
43
- );
44
- }
45
-
46
- rmSync(TMP_DIR, { recursive: true, force: true });
47
- if (!silent) fmt.logSuccess("aw-ecc engine installed");
48
- }