invokora 0.1.0

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.
Files changed (41) hide show
  1. package/dist/cli/app.d.ts +55 -0
  2. package/dist/cli/app.js +1087 -0
  3. package/dist/cli/config.d.ts +12 -0
  4. package/dist/cli/config.js +73 -0
  5. package/dist/cli/constants.d.ts +24 -0
  6. package/dist/cli/constants.js +52 -0
  7. package/dist/cli/http.d.ts +2 -0
  8. package/dist/cli/http.js +23 -0
  9. package/dist/cli/index.d.ts +6 -0
  10. package/dist/cli/index.js +11 -0
  11. package/dist/cli/mcp/app.d.ts +12 -0
  12. package/dist/cli/mcp/app.js +85 -0
  13. package/dist/cli/mcp/backend_client.d.ts +10 -0
  14. package/dist/cli/mcp/backend_client.js +91 -0
  15. package/dist/cli/mcp/errors.d.ts +28 -0
  16. package/dist/cli/mcp/errors.js +139 -0
  17. package/dist/cli/mcp/progress.d.ts +12 -0
  18. package/dist/cli/mcp/progress.js +49 -0
  19. package/dist/cli/mcp/responses_session.d.ts +21 -0
  20. package/dist/cli/mcp/responses_session.js +233 -0
  21. package/dist/cli/mcp/schemas.d.ts +99 -0
  22. package/dist/cli/mcp/schemas.js +66 -0
  23. package/dist/cli/mcp/server.d.ts +4 -0
  24. package/dist/cli/mcp/server.js +3 -0
  25. package/dist/cli/mcp/session_store.d.ts +32 -0
  26. package/dist/cli/mcp/session_store.js +58 -0
  27. package/dist/cli/mcp/tool_handlers.d.ts +3 -0
  28. package/dist/cli/mcp/tool_handlers.js +26 -0
  29. package/dist/cli/mcp_setup.d.ts +33 -0
  30. package/dist/cli/mcp_setup.js +225 -0
  31. package/dist/cli/oauth.d.ts +45 -0
  32. package/dist/cli/oauth.js +594 -0
  33. package/dist/cli/prompts.d.ts +23 -0
  34. package/dist/cli/prompts.js +175 -0
  35. package/dist/cli/release.d.ts +3 -0
  36. package/dist/cli/release.js +3 -0
  37. package/dist/cli/skills.d.ts +43 -0
  38. package/dist/cli/skills.js +443 -0
  39. package/dist/cli/types.d.ts +183 -0
  40. package/dist/cli/types.js +1 -0
  41. package/package.json +29 -0
@@ -0,0 +1,1087 @@
1
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { RELEASE_VERSION } from './release.js';
5
+ import { CONFIG_PATH, DEFAULT_BACKEND_URL, LANGUAGE_PATH, SYNC_SCOPE_ALIASES, SYNC_TARGET_ALIASES } from './constants.js';
6
+ import { ConfigStore } from './config.js';
7
+ import { McpConfigService } from './mcp_setup.js';
8
+ import { OAuthBrowserLoginFlow, parseLoginCommandOptions } from './oauth.js';
9
+ import { CliPrompts } from './prompts.js';
10
+ import { InvokoraApiClient, SkillShellManager } from './skills.js';
11
+ import { getErrorMessage } from './mcp/errors.js';
12
+ import { startStdioServer } from './mcp/app.js';
13
+ const USAGE_TEXT_EN = `
14
+ invokora CLI
15
+
16
+ Commands:
17
+ -h, --help Print CLI help
18
+ -v, --version Print CLI version
19
+ login [--timeout <ms>] [--no-open]
20
+ Authenticate via browser OAuth loopback callback
21
+ language [en|zh] Show or set CLI language
22
+ sync [--prune] Sync local skills interactively
23
+ --scope <scope> project | user
24
+ --target <client> Repeatable: codex | claude | claudecode | cursor
25
+ --yes Non-interactive mode
26
+ skill add Add or refresh local skills
27
+ --scope <scope> project | user
28
+ --target <client> Repeatable: codex | claude | claudecode | cursor
29
+ --yes Add all syncable skills without prompt
30
+ skill prune Sync and remove stale Invokora-managed local skills
31
+ --scope <scope> project | user
32
+ --target <client> Repeatable: codex | claude | claudecode | cursor
33
+ --yes Non-interactive mode
34
+ skill remove Remove Invokora-managed local skills
35
+ --scope <scope> project | user
36
+ --target <client> Repeatable: codex | claude | claudecode | cursor
37
+ --yes Remove all managed skills without prompt
38
+ workflow add Add or refresh local skills from selected workflows
39
+ --scope <scope> project | user
40
+ --target <client> Repeatable: codex | claude | claudecode | cursor
41
+ --yes Add all syncable workflows without prompt
42
+ mcp serve Start the stdio MCP server
43
+ mcp setup Setup MCP config in Claude/Cursor/Codex
44
+ --target <client> Repeatable: claude | cursor | codex
45
+ --path <file> Optional explicit config path (repeat with target order)
46
+ --yes Non-interactive mode
47
+ mcp defaults Show or set MCP default model/reason
48
+ --model <model> Default model for new MCP chat sessions
49
+ --reason <effort> Default reasoning effort: low | medium | high | xhigh
50
+ --clear-model Clear default model
51
+ --clear-reason Clear default reasoning effort
52
+
53
+ Examples:
54
+ invokora login # Open browser login and save local token
55
+ invokora --version # Print CLI version
56
+ invokora language zh # Show CLI help in Chinese
57
+ invokora login --no-open # Print login URL without opening browser
58
+ invokora login --timeout 180000 # Wait up to 180 seconds for login callback
59
+ invokora sync # Choose scope and clients interactively
60
+ invokora sync --scope project --target codex --yes
61
+ # Sync into project Codex skills without prompts
62
+ invokora skill add # Choose local skills to add or refresh
63
+ invokora skill prune --scope project --target cursor --yes
64
+ # Sync Cursor skills and prune stale managed skills
65
+ invokora skill remove --scope project --target codex --yes
66
+ # Remove project Codex Invokora-managed skills
67
+ invokora workflow add # Choose workflows to add or refresh
68
+ invokora mcp serve # Start stdio MCP server
69
+ invokora mcp setup # Setup MCP config interactively
70
+ invokora mcp setup --target claude --yes # Setup Claude MCP config without prompts
71
+ invokora mcp defaults --model gpt-5.4 --reason high
72
+ # Set MCP default model and reasoning effort
73
+ `.trim();
74
+ const USAGE_TEXT_ZH = `
75
+ invokora CLI
76
+
77
+ 命令:
78
+ -h, --help 输出 CLI help
79
+ -v, --version 输出 CLI 版本
80
+ login [--timeout <ms>] [--no-open]
81
+ 通过浏览器 OAuth 回调登录
82
+ language [en|zh] 查看或设置 CLI 语言
83
+ sync [--prune] 交互式同步可本地使用的 skills
84
+ --scope <scope> project | user
85
+ --target <client> 可重复:codex | claude | claudecode | cursor
86
+ --yes 非交互模式
87
+ skill add 新增或刷新本地 skills
88
+ --scope <scope> project | user
89
+ --target <client> 可重复:codex | claude | claudecode | cursor
90
+ --yes 免选择新增全部可同步 skills
91
+ skill prune 同步并清理失效的 Invokora 托管本地 skills
92
+ --scope <scope> project | user
93
+ --target <client> 可重复:codex | claude | claudecode | cursor
94
+ --yes 非交互模式
95
+ skill remove 删除 Invokora 托管本地 skills
96
+ --scope <scope> project | user
97
+ --target <client> 可重复:codex | claude | claudecode | cursor
98
+ --yes 免选择删除全部托管 skills
99
+ workflow add 新增或刷新所选 workflow 的本地 skills
100
+ --scope <scope> project | user
101
+ --target <client> 可重复:codex | claude | claudecode | cursor
102
+ --yes 免选择新增全部可同步 workflows
103
+ mcp serve 启动 stdio MCP server
104
+ mcp setup 写入 Claude/Cursor/Codex MCP 配置
105
+ --target <client> 可重复:claude | cursor | codex
106
+ --path <file> 可选配置路径,顺序对应 target
107
+ --yes 非交互模式
108
+ mcp defaults 查看或设置 MCP 默认模型 / 思考深度
109
+ --model <model> 新 MCP 会话默认模型
110
+ --reason <effort> 默认思考深度:low | medium | high | xhigh
111
+ --clear-model 清除默认模型
112
+ --clear-reason 清除默认思考深度
113
+
114
+ 示例:
115
+ invokora login # 打开浏览器登录并保存本地 token
116
+ invokora --version # 输出 CLI 版本
117
+ invokora language en # 使用英文显示 CLI help
118
+ invokora login --no-open # 只打印登录 URL,不自动打开浏览器
119
+ invokora login --timeout 180000 # 等待登录回调最多 180 秒
120
+ invokora sync # 交互式选择同步范围和客户端
121
+ invokora sync --scope project --target codex --yes
122
+ # 免交互同步到项目级 Codex skills
123
+ invokora skill add # 选择要新增或刷新的本地 skills
124
+ invokora skill prune --scope project --target cursor --yes
125
+ # 同步 Cursor skills 并清理失效托管目录
126
+ invokora skill remove --scope project --target codex --yes
127
+ # 删除项目级 Codex 的 Invokora 托管 skills
128
+ invokora workflow add # 选择要新增或刷新的 workflows
129
+ invokora mcp serve # 启动 stdio MCP server
130
+ invokora mcp setup # 交互式写入 MCP 配置
131
+ invokora mcp setup --target claude --yes # 免确认写入 Claude MCP 配置
132
+ invokora mcp defaults --model gpt-5.4 --reason high
133
+ # 设置 MCP 默认模型和思考深度
134
+ `.trim();
135
+ const HELP_TEXT_EN = {
136
+ root: USAGE_TEXT_EN,
137
+ login: `
138
+ Usage: invokora login [--timeout <ms>] [--no-open]
139
+
140
+ Authenticate via browser OAuth loopback callback.
141
+
142
+ Options:
143
+ -h, --help Print login help
144
+ --timeout <ms> Wait for login callback; default 120000
145
+ --no-open Print login URL without opening browser
146
+ `.trim(),
147
+ language: `
148
+ Usage: invokora language [en|zh]
149
+
150
+ Show or set CLI language.
151
+
152
+ Options:
153
+ -h, --help Print language help
154
+ `.trim(),
155
+ sync: `
156
+ Usage: invokora sync [--prune] [--scope <scope>] [--target <client>] [--yes]
157
+
158
+ Sync local skills interactively.
159
+
160
+ Options:
161
+ -h, --help Print sync help
162
+ --prune Remove stale Invokora-managed local skills
163
+ --scope <scope> project | user
164
+ --target <client> Repeatable: codex | claude | claudecode | cursor
165
+ --yes Non-interactive mode
166
+ `.trim(),
167
+ skill: `
168
+ Skill Commands:
169
+ invokora skill add Add or refresh local skills
170
+ invokora skill prune Sync and remove stale managed skills
171
+ invokora skill remove Remove managed local skills
172
+
173
+ Options:
174
+ -h, --help Print skill help
175
+ `.trim(),
176
+ 'skill-add': `
177
+ Usage: invokora skill add [--scope <scope>] [--target <client>] [--yes]
178
+
179
+ Add or refresh local skills.
180
+
181
+ Options:
182
+ -h, --help Print skill add help
183
+ --scope <scope> project | user
184
+ --target <client> Repeatable: codex | claude | claudecode | cursor
185
+ --yes Add all syncable skills without prompt
186
+ `.trim(),
187
+ 'skill-prune': `
188
+ Usage: invokora skill prune [--scope <scope>] [--target <client>] [--yes]
189
+
190
+ Sync and remove stale Invokora-managed local skills.
191
+
192
+ Options:
193
+ -h, --help Print skill prune help
194
+ --scope <scope> project | user
195
+ --target <client> Repeatable: codex | claude | claudecode | cursor
196
+ --yes Non-interactive mode
197
+ `.trim(),
198
+ 'skill-remove': `
199
+ Usage: invokora skill remove [--scope <scope>] [--target <client>] [--yes]
200
+
201
+ Remove Invokora-managed local skills.
202
+
203
+ Options:
204
+ -h, --help Print skill remove help
205
+ --scope <scope> project | user
206
+ --target <client> Repeatable: codex | claude | claudecode | cursor
207
+ --yes Remove all managed skills without prompt
208
+ `.trim(),
209
+ workflow: `
210
+ Workflow Commands:
211
+ invokora workflow add Add or refresh local skills from selected workflows
212
+
213
+ Options:
214
+ -h, --help Print workflow help
215
+ `.trim(),
216
+ 'workflow-add': `
217
+ Usage: invokora workflow add [--scope <scope>] [--target <client>] [--yes]
218
+
219
+ Add or refresh local skills from selected workflows.
220
+
221
+ Options:
222
+ -h, --help Print workflow add help
223
+ --scope <scope> project | user
224
+ --target <client> Repeatable: codex | claude | claudecode | cursor
225
+ --yes Add all syncable workflows without prompt
226
+ `.trim(),
227
+ mcp: `
228
+ MCP Commands:
229
+ invokora mcp serve Start the stdio MCP server
230
+ invokora mcp setup Setup MCP config in Claude/Cursor/Codex
231
+ invokora mcp defaults Show or set MCP default model/reason
232
+
233
+ Options:
234
+ -h, --help Print MCP help
235
+ `.trim(),
236
+ 'mcp-serve': `
237
+ Usage: invokora mcp serve
238
+
239
+ Start the stdio MCP server.
240
+
241
+ Options:
242
+ -h, --help Print MCP serve help
243
+ `.trim(),
244
+ 'mcp-setup': `
245
+ Usage: invokora mcp setup [--target <client>] [--path <file>] [--yes]
246
+
247
+ Setup MCP config in Claude/Cursor/Codex.
248
+
249
+ Options:
250
+ -h, --help Print MCP setup help
251
+ --target <client> Repeatable: claude | cursor | codex
252
+ --path <file> Optional explicit config path; repeat with target order
253
+ --yes Non-interactive mode
254
+ `.trim(),
255
+ 'mcp-defaults': `
256
+ Usage: invokora mcp defaults [--model <model>] [--reason <effort>] [--clear-model] [--clear-reason]
257
+
258
+ Show or set MCP default model/reason.
259
+
260
+ Options:
261
+ -h, --help Print MCP defaults help
262
+ --model <model> Default model for new MCP chat sessions
263
+ --reason <effort> Default reasoning effort: low | medium | high | xhigh
264
+ --clear-model Clear default model
265
+ --clear-reason Clear default reasoning effort
266
+ `.trim(),
267
+ };
268
+ const HELP_TEXT_ZH = {
269
+ root: USAGE_TEXT_ZH,
270
+ login: `
271
+ 用法:invokora login [--timeout <ms>] [--no-open]
272
+
273
+ 通过浏览器 OAuth 回调登录。
274
+
275
+ 选项:
276
+ -h, --help 输出 login help
277
+ --timeout <ms> 等待登录回调,默认 120000
278
+ --no-open 只打印登录 URL,不自动打开浏览器
279
+ `.trim(),
280
+ language: `
281
+ 用法:invokora language [en|zh]
282
+
283
+ 查看或设置 CLI 语言。
284
+
285
+ 选项:
286
+ -h, --help 输出 language help
287
+ `.trim(),
288
+ sync: `
289
+ 用法:invokora sync [--prune] [--scope <scope>] [--target <client>] [--yes]
290
+
291
+ 交互式同步可本地使用的 skills。
292
+
293
+ 选项:
294
+ -h, --help 输出 sync help
295
+ --prune 清理失效的 Invokora 托管本地 skills
296
+ --scope <scope> project | user
297
+ --target <client> 可重复:codex | claude | claudecode | cursor
298
+ --yes 非交互模式
299
+ `.trim(),
300
+ skill: `
301
+ Skill 命令:
302
+ invokora skill add 新增或刷新本地 skills
303
+ invokora skill prune 同步并清理失效托管 skills
304
+ invokora skill remove 删除托管本地 skills
305
+
306
+ 选项:
307
+ -h, --help 输出 skill help
308
+ `.trim(),
309
+ 'skill-add': `
310
+ 用法:invokora skill add [--scope <scope>] [--target <client>] [--yes]
311
+
312
+ 新增或刷新本地 skills。
313
+
314
+ 选项:
315
+ -h, --help 输出 skill add help
316
+ --scope <scope> project | user
317
+ --target <client> 可重复:codex | claude | claudecode | cursor
318
+ --yes 免选择新增全部可同步 skills
319
+ `.trim(),
320
+ 'skill-prune': `
321
+ 用法:invokora skill prune [--scope <scope>] [--target <client>] [--yes]
322
+
323
+ 同步并清理失效的 Invokora 托管本地 skills。
324
+
325
+ 选项:
326
+ -h, --help 输出 skill prune help
327
+ --scope <scope> project | user
328
+ --target <client> 可重复:codex | claude | claudecode | cursor
329
+ --yes 非交互模式
330
+ `.trim(),
331
+ 'skill-remove': `
332
+ 用法:invokora skill remove [--scope <scope>] [--target <client>] [--yes]
333
+
334
+ 删除 Invokora 托管本地 skills。
335
+
336
+ 选项:
337
+ -h, --help 输出 skill remove help
338
+ --scope <scope> project | user
339
+ --target <client> 可重复:codex | claude | claudecode | cursor
340
+ --yes 免选择删除全部托管 skills
341
+ `.trim(),
342
+ workflow: `
343
+ Workflow 命令:
344
+ invokora workflow add 新增或刷新所选 workflow 的本地 skills
345
+
346
+ 选项:
347
+ -h, --help 输出 workflow help
348
+ `.trim(),
349
+ 'workflow-add': `
350
+ 用法:invokora workflow add [--scope <scope>] [--target <client>] [--yes]
351
+
352
+ 新增或刷新所选 workflow 的本地 skills。
353
+
354
+ 选项:
355
+ -h, --help 输出 workflow add help
356
+ --scope <scope> project | user
357
+ --target <client> 可重复:codex | claude | claudecode | cursor
358
+ --yes 免选择新增全部可同步 workflows
359
+ `.trim(),
360
+ mcp: `
361
+ MCP 命令:
362
+ invokora mcp serve 启动 stdio MCP server
363
+ invokora mcp setup 写入 Claude/Cursor/Codex MCP 配置
364
+ invokora mcp defaults 查看或设置 MCP 默认模型 / 思考深度
365
+
366
+ 选项:
367
+ -h, --help 输出 MCP help
368
+ `.trim(),
369
+ 'mcp-serve': `
370
+ 用法:invokora mcp serve
371
+
372
+ 启动 stdio MCP server。
373
+
374
+ 选项:
375
+ -h, --help 输出 MCP serve help
376
+ `.trim(),
377
+ 'mcp-setup': `
378
+ 用法:invokora mcp setup [--target <client>] [--path <file>] [--yes]
379
+
380
+ 写入 Claude/Cursor/Codex MCP 配置。
381
+
382
+ 选项:
383
+ -h, --help 输出 MCP setup help
384
+ --target <client> 可重复:claude | cursor | codex
385
+ --path <file> 可选配置路径,顺序对应 target
386
+ --yes 非交互模式
387
+ `.trim(),
388
+ 'mcp-defaults': `
389
+ 用法:invokora mcp defaults [--model <model>] [--reason <effort>] [--clear-model] [--clear-reason]
390
+
391
+ 查看或设置 MCP 默认模型 / 思考深度。
392
+
393
+ 选项:
394
+ -h, --help 输出 MCP defaults help
395
+ --model <model> 新 MCP 会话默认模型
396
+ --reason <effort> 默认思考深度:low | medium | high | xhigh
397
+ --clear-model 清除默认模型
398
+ --clear-reason 清除默认思考深度
399
+ `.trim(),
400
+ };
401
+ export function parseCliLanguage(raw) {
402
+ const value = raw?.trim().toLowerCase().replace(/_/g, '-');
403
+ if (!value)
404
+ return null;
405
+ if (value === 'en' || value === 'en-us' || value === 'english')
406
+ return 'en';
407
+ if (value === 'zh' || value === 'zh-cn' || value === 'zh-hans' || value === 'chinese' || value === '中文')
408
+ return 'zh';
409
+ return null;
410
+ }
411
+ export function loadCliLanguage(languagePath = LANGUAGE_PATH) {
412
+ if (!existsSync(languagePath))
413
+ return 'en';
414
+ try {
415
+ return parseCliLanguage(readFileSync(languagePath, 'utf-8')) ?? 'en';
416
+ }
417
+ catch {
418
+ return 'en';
419
+ }
420
+ }
421
+ export function saveCliLanguage(language, languagePath = LANGUAGE_PATH) {
422
+ mkdirSync(dirname(languagePath), { recursive: true });
423
+ writeFileSync(languagePath, `${language}\n`, 'utf-8');
424
+ }
425
+ export function getUsageText(language = loadCliLanguage()) {
426
+ return language === 'zh' ? USAGE_TEXT_ZH : USAGE_TEXT_EN;
427
+ }
428
+ export function getCommandHelpText(args, language = loadCliLanguage()) {
429
+ const topic = resolveHelpTopic(args);
430
+ return language === 'zh' ? HELP_TEXT_ZH[topic] : HELP_TEXT_EN[topic];
431
+ }
432
+ export class InvokoraCliApp {
433
+ prompts;
434
+ configStore;
435
+ shellManager;
436
+ mcpService;
437
+ constructor(params) {
438
+ this.prompts = params?.prompts ?? new CliPrompts();
439
+ this.configStore = params?.configStore ?? new ConfigStore();
440
+ this.shellManager = params?.shellManager ?? new SkillShellManager();
441
+ this.mcpService = new McpConfigService(this.prompts);
442
+ }
443
+ async run(argv = process.argv) {
444
+ const [, , cmd, ...args] = argv;
445
+ if (isHelpRequest(cmd, args)) {
446
+ this.printUsage([cmd, ...args]);
447
+ return;
448
+ }
449
+ switch (cmd) {
450
+ case '-v':
451
+ case '--version':
452
+ return this.cmdVersion();
453
+ case 'login':
454
+ return this.cmdLogin(args);
455
+ case 'language':
456
+ return this.cmdLanguage(args);
457
+ case 'sync': {
458
+ let parsed;
459
+ try {
460
+ parsed = this.parseSyncArgs(args);
461
+ }
462
+ catch (error) {
463
+ console.error(error instanceof Error ? error.message : String(error));
464
+ process.exit(1);
465
+ }
466
+ let destinations;
467
+ try {
468
+ destinations = await this.resolveSyncDestinations(parsed);
469
+ }
470
+ catch (error) {
471
+ console.error(error instanceof Error ? error.message : String(error));
472
+ process.exit(1);
473
+ }
474
+ await this.syncSkills({ prune: parsed.prune }, undefined, destinations);
475
+ return;
476
+ }
477
+ case 'mcp':
478
+ if (args[0] === 'setup') {
479
+ return this.cmdMcpSetup(args.slice(1));
480
+ }
481
+ if (args[0] === 'serve') {
482
+ if (args.length > 1) {
483
+ console.error(`Unknown mcp serve options: ${args.slice(1).join(' ')}`);
484
+ process.exit(1);
485
+ }
486
+ return this.cmdMcpServe();
487
+ }
488
+ if (args[0] === 'defaults') {
489
+ return this.cmdMcpDefaults(args.slice(1));
490
+ }
491
+ console.error(`Unknown mcp subcommand/options: ${args.join(' ')}`);
492
+ process.exit(1);
493
+ case 'skill':
494
+ return this.cmdSkill(args);
495
+ case 'workflow':
496
+ return this.cmdWorkflow(args);
497
+ default:
498
+ this.printUsage();
499
+ }
500
+ }
501
+ async cmdLogin(rawArgs) {
502
+ let options;
503
+ try {
504
+ options = parseLoginCommandOptions(rawArgs);
505
+ }
506
+ catch (error) {
507
+ console.error(error instanceof Error ? error.message : String(error));
508
+ process.exit(1);
509
+ }
510
+ try {
511
+ const backendUrl = process.env.SKILZ_BACKEND_URL || DEFAULT_BACKEND_URL;
512
+ const response = await new OAuthBrowserLoginFlow({
513
+ backendUrl,
514
+ timeoutMs: options.timeoutMs,
515
+ noOpen: options.noOpen,
516
+ }).run();
517
+ this.configStore.save({
518
+ backend_url: backendUrl,
519
+ token: response.access_token,
520
+ refresh_token: response.refresh_token,
521
+ });
522
+ const email = response.user?.email;
523
+ console.log(`✓ Logged in${email ? ` as ${email}` : ''}`);
524
+ console.log(` Config saved to ${CONFIG_PATH}`);
525
+ }
526
+ catch (error) {
527
+ console.error('Login failed:', error instanceof Error ? error.message : String(error));
528
+ process.exit(1);
529
+ }
530
+ }
531
+ cmdVersion() {
532
+ console.log(`invokora ${RELEASE_VERSION}`);
533
+ }
534
+ cmdLanguage(rawArgs) {
535
+ if (rawArgs.length === 0) {
536
+ console.log(`Language: ${loadCliLanguage()} (${LANGUAGE_PATH})`);
537
+ return;
538
+ }
539
+ if (rawArgs.length > 1) {
540
+ console.error('Usage: invokora language [en|zh]');
541
+ process.exit(1);
542
+ }
543
+ const language = parseCliLanguage(rawArgs[0]);
544
+ if (!language) {
545
+ console.error('Unsupported language. Use: en or zh');
546
+ process.exit(1);
547
+ }
548
+ saveCliLanguage(language);
549
+ console.log(`Language set to ${language}`);
550
+ }
551
+ async cmdMcpServe() {
552
+ try {
553
+ await startStdioServer();
554
+ }
555
+ catch (error) {
556
+ console.error(getErrorMessage(error));
557
+ const config = this.configStore.load();
558
+ if (config) {
559
+ console.error(`Support: ${this.supportUrl(config, { category: 'cannot_invoke' })}`);
560
+ }
561
+ process.exit(1);
562
+ }
563
+ }
564
+ cmdMcpDefaults(rawArgs) {
565
+ let parsed;
566
+ try {
567
+ parsed = parseMcpDefaultsArgs(rawArgs);
568
+ }
569
+ catch (error) {
570
+ console.error(error instanceof Error ? error.message : String(error));
571
+ process.exit(1);
572
+ }
573
+ const config = this.requireConfig();
574
+ if (!parsed.changed) {
575
+ this.printMcpDefaults(config);
576
+ return;
577
+ }
578
+ const next = { ...config };
579
+ if (parsed.clearModel) {
580
+ delete next.default_model;
581
+ }
582
+ if (parsed.model !== undefined) {
583
+ next.default_model = parsed.model;
584
+ }
585
+ if (parsed.clearReason) {
586
+ delete next.default_reason;
587
+ }
588
+ if (parsed.reason !== undefined) {
589
+ next.default_reason = parsed.reason;
590
+ }
591
+ this.configStore.save(next);
592
+ console.log(`✓ MCP defaults saved to ${CONFIG_PATH}`);
593
+ this.printMcpDefaults(next);
594
+ }
595
+ async cmdSkill(args) {
596
+ const [subcommand, ...rawArgs] = args;
597
+ switch (subcommand) {
598
+ case 'add':
599
+ return this.cmdSkillAdd(rawArgs);
600
+ case 'prune':
601
+ return this.cmdSkillPrune(rawArgs);
602
+ case 'remove':
603
+ return this.cmdSkillRemove(rawArgs);
604
+ default:
605
+ console.error(`Unknown skill subcommand/options: ${args.join(' ')}`);
606
+ process.exit(1);
607
+ }
608
+ }
609
+ async cmdSkillAdd(rawArgs) {
610
+ const parsed = this.parseLocalSkillArgs(rawArgs);
611
+ const destinations = await this.resolveSyncDestinations(parsed);
612
+ const config = this.requireConfig();
613
+ const skills = await this.loadActiveSyncableSkills(config);
614
+ const selectedSkills = parsed.yes ? skills : await this.pickSkillsForAdd(skills);
615
+ if (selectedSkills.length === 0) {
616
+ console.log('No skill selected. Nothing changed.');
617
+ return;
618
+ }
619
+ await this.syncSkills({ prune: false }, config, destinations, selectedSkills);
620
+ }
621
+ async cmdSkillPrune(rawArgs) {
622
+ const parsed = this.parseLocalSkillArgs(rawArgs, { allowPruneFlag: true });
623
+ const destinations = await this.resolveSyncDestinations(parsed);
624
+ await this.syncSkills({ prune: true }, undefined, destinations);
625
+ }
626
+ async cmdSkillRemove(rawArgs) {
627
+ const parsed = this.parseLocalSkillArgs(rawArgs);
628
+ const destinations = await this.resolveSyncDestinations(parsed);
629
+ const selectedByDestination = parsed.yes
630
+ ? new Map(destinations.map((destination) => [destination.label, undefined]))
631
+ : await this.pickLocalManagedSkills(destinations);
632
+ if (selectedByDestination.size === 0) {
633
+ console.log('No skill selected. Nothing changed.');
634
+ return;
635
+ }
636
+ if (!parsed.yes) {
637
+ const selectedCount = [...selectedByDestination.values()].reduce((sum, slugs) => sum + (slugs?.length ?? 0), 0);
638
+ const confirmed = await this.prompts.yesNo(`Remove ${selectedCount} selected Invokora-managed local skill(s)?`, false);
639
+ if (!confirmed) {
640
+ console.log('Skipped.');
641
+ return;
642
+ }
643
+ }
644
+ let removed = 0;
645
+ for (const destination of destinations) {
646
+ if (!selectedByDestination.has(destination.label))
647
+ continue;
648
+ const result = this.shellManager.removeManagedSkills(destination.rootDir, selectedByDestination.get(destination.label));
649
+ removed += result.removed;
650
+ console.log(`✓ ${destination.label}: removed ${result.removed} Invokora-managed skill(s) from ${destination.rootDir}`);
651
+ }
652
+ console.log(`Remove complete for ${destinations.length} destination(s), ${removed} skill(s) removed.`);
653
+ }
654
+ async cmdWorkflow(args) {
655
+ const [subcommand, ...rawArgs] = args;
656
+ switch (subcommand) {
657
+ case 'add':
658
+ return this.cmdWorkflowAdd(rawArgs);
659
+ default:
660
+ console.error(`Unknown workflow subcommand/options: ${args.join(' ')}`);
661
+ process.exit(1);
662
+ }
663
+ }
664
+ async cmdWorkflowAdd(rawArgs) {
665
+ const parsed = this.parseLocalSkillArgs(rawArgs);
666
+ const destinations = await this.resolveSyncDestinations(parsed);
667
+ const config = this.requireConfig();
668
+ const client = this.apiClient(config);
669
+ const skills = await this.loadActiveSyncableSkills(config, client);
670
+ const workflows = this.filterWorkflowsWithSyncableSkills(await this.loadSyncableWorkflows(client), skills);
671
+ if (workflows.length === 0) {
672
+ console.log('No workflow available to add. Nothing changed.');
673
+ return;
674
+ }
675
+ const selectedWorkflows = parsed.yes ? workflows : await this.pickSyncableWorkflows(workflows);
676
+ if (selectedWorkflows.length === 0) {
677
+ console.log('No workflow selected. Nothing changed.');
678
+ return;
679
+ }
680
+ const selectedSkillIDs = new Set(selectedWorkflows.flatMap((workflow) => workflow.skill_ids));
681
+ const selectedSkills = skills.filter((skill) => selectedSkillIDs.has(skill.id));
682
+ if (selectedSkills.length === 0) {
683
+ console.log('No syncable skill found for selected workflow(s). Nothing changed.');
684
+ return;
685
+ }
686
+ await this.syncSkills({ prune: false }, config, destinations, selectedSkills);
687
+ }
688
+ async cmdMcpSetup(rawArgs) {
689
+ let parsed;
690
+ try {
691
+ parsed = this.mcpService.parseSetupArgs(rawArgs);
692
+ }
693
+ catch (error) {
694
+ console.error(error instanceof Error ? error.message : String(error));
695
+ process.exit(1);
696
+ }
697
+ let targets;
698
+ try {
699
+ targets = await this.mcpService.resolveTargets(parsed);
700
+ }
701
+ catch (error) {
702
+ console.error(error instanceof Error ? error.message : String(error));
703
+ process.exit(1);
704
+ }
705
+ if (targets.length === 0) {
706
+ console.log('No valid target selected. Nothing changed.');
707
+ return;
708
+ }
709
+ let explicitPaths;
710
+ try {
711
+ explicitPaths = this.mcpService.mapPathsToTargets(targets, parsed.paths);
712
+ }
713
+ catch (error) {
714
+ console.error(error instanceof Error ? error.message : String(error));
715
+ process.exit(1);
716
+ }
717
+ const config = this.requireConfig();
718
+ const serverConfig = this.mcpService.buildServerConfig(config);
719
+ let updatedCount = 0;
720
+ for (const target of targets) {
721
+ const explicitPath = explicitPaths.get(target);
722
+ try {
723
+ const result = await this.mcpService.setupTargetConfig({
724
+ target,
725
+ explicitPath,
726
+ serverConfig,
727
+ yes: parsed.yes,
728
+ });
729
+ if (result.status === 'updated') {
730
+ updatedCount++;
731
+ console.log(`✓ ${target}: ${result.path}`);
732
+ }
733
+ else {
734
+ console.log(`- ${target}: skipped`);
735
+ }
736
+ }
737
+ catch (error) {
738
+ console.error(`✗ ${target}: ${error instanceof Error ? error.message : String(error)}`);
739
+ }
740
+ }
741
+ if (updatedCount === 0) {
742
+ console.error('No MCP target was updated.');
743
+ process.exit(1);
744
+ }
745
+ console.log('\nMCP setup complete.');
746
+ }
747
+ parseSyncArgs(args) {
748
+ let prune = false;
749
+ let yes = false;
750
+ let scope;
751
+ const targets = [];
752
+ for (let i = 0; i < args.length; i += 1) {
753
+ const arg = args[i];
754
+ if (arg === '--prune') {
755
+ prune = true;
756
+ continue;
757
+ }
758
+ if (arg === '--yes' || arg === '-y') {
759
+ yes = true;
760
+ continue;
761
+ }
762
+ if (arg === '--scope') {
763
+ const value = args[i + 1];
764
+ if (!value)
765
+ throw new Error('Missing value for --scope');
766
+ const parsedScope = SYNC_SCOPE_ALIASES[value.trim().toLowerCase()];
767
+ if (!parsedScope)
768
+ throw new Error(`Unsupported sync scope: ${value}`);
769
+ scope = parsedScope;
770
+ i += 1;
771
+ continue;
772
+ }
773
+ if (arg === '--target') {
774
+ const value = args[i + 1];
775
+ if (!value)
776
+ throw new Error('Missing value for --target');
777
+ const parsedTarget = SYNC_TARGET_ALIASES[value.trim().toLowerCase()];
778
+ if (!parsedTarget)
779
+ throw new Error(`Unsupported sync target: ${value}`);
780
+ targets.push(parsedTarget);
781
+ i += 1;
782
+ continue;
783
+ }
784
+ throw new Error(`Unknown sync option: ${arg}`);
785
+ }
786
+ return { prune, yes, scope, targets: [...new Set(targets)] };
787
+ }
788
+ parseLocalSkillArgs(args, options = {}) {
789
+ try {
790
+ const parsed = this.parseSyncArgs(args);
791
+ if (parsed.prune && !options.allowPruneFlag) {
792
+ throw new Error('--prune is only supported by invokora sync or invokora skill prune');
793
+ }
794
+ return parsed;
795
+ }
796
+ catch (error) {
797
+ console.error(error instanceof Error ? error.message : String(error));
798
+ process.exit(1);
799
+ }
800
+ }
801
+ async resolveSyncDestinations(parsed) {
802
+ const scope = parsed.scope ?? (parsed.yes ? 'project' : await this.prompts.pickSyncScope());
803
+ const targets = parsed.targets.length > 0
804
+ ? parsed.targets
805
+ : parsed.yes
806
+ ? ['codex']
807
+ : await this.prompts.pickSyncTargets(scope);
808
+ return targets.map((target) => this.shellManager.resolveSyncDestination(target, scope));
809
+ }
810
+ async syncSkills(options, config = this.requireConfig(), destinations = [this.shellManager.resolveSyncDestination('codex', 'project')], selectedSkills) {
811
+ const client = this.apiClient(config);
812
+ const skills = selectedSkills ?? await this.loadActiveSyncableSkills(config, client);
813
+ const syncedContentBySkill = await this.downloadManagedSkillContent(client, skills);
814
+ const aggregate = { written: 0, pruned: 0, activeSlugs: new Set() };
815
+ for (const destination of destinations) {
816
+ const result = this.shellManager.sync(skills, {
817
+ ...options,
818
+ rootDir: destination.rootDir,
819
+ destinationLabel: destination.label,
820
+ }, syncedContentBySkill);
821
+ result.activeSlugs.forEach((slug) => aggregate.activeSlugs.add(slug));
822
+ aggregate.written += result.written;
823
+ aggregate.pruned += result.pruned;
824
+ if (!options.quiet) {
825
+ console.log(`✓ ${destination.label}: synced ${result.written} skill(s) to ${destination.rootDir}`);
826
+ if (options.prune) {
827
+ console.log(` pruned ${result.pruned} stale skill(s)`);
828
+ }
829
+ }
830
+ }
831
+ if (!options.quiet) {
832
+ console.log(`Sync complete for ${destinations.length} destination(s).`);
833
+ }
834
+ return aggregate;
835
+ }
836
+ async loadActiveSyncableSkills(config, client = this.apiClient(config)) {
837
+ return (await client.listSyncableDetails()).filter((skill) => skill.status === 'active');
838
+ }
839
+ async pickSyncableSkills(skills) {
840
+ const selectedIDs = new Set(await this.prompts.pickSyncableSkills(skills.map((skill) => ({
841
+ value: skill.id,
842
+ label: skill.name || skill.id,
843
+ hint: skill.description || skill.access_source,
844
+ }))));
845
+ return skills.filter((skill) => selectedIDs.has(skill.id));
846
+ }
847
+ async pickSkillsForAdd(skills) {
848
+ if (skills.length === 0) {
849
+ return [];
850
+ }
851
+ const mode = await this.prompts.pickSyncableSkillAddMode();
852
+ if (mode === 'all') {
853
+ return skills;
854
+ }
855
+ if (mode === 'none') {
856
+ return [];
857
+ }
858
+ return this.pickSyncableSkills(skills);
859
+ }
860
+ async pickSyncableWorkflows(workflows) {
861
+ const selectedIDs = new Set(await this.prompts.pickSyncableWorkflows(workflows.map((workflow) => ({
862
+ value: workflow.id,
863
+ label: workflow.name || workflow.id,
864
+ hint: `${workflow.source} · ${workflow.skill_ids.length} skill(s)${workflow.description ? ` · ${workflow.description}` : ''}`,
865
+ }))));
866
+ return workflows.filter((workflow) => selectedIDs.has(workflow.id));
867
+ }
868
+ async loadSyncableWorkflows(client) {
869
+ const workflows = [];
870
+ const entitlements = await client.listEntitlements();
871
+ const marketplaceWorkflowIDs = [...new Set(entitlements.map((entitlement) => entitlement.workflow_id).filter((id) => Boolean(id)))];
872
+ for (const workflowID of marketplaceWorkflowIDs) {
873
+ const detail = await client.getWorkflowDetail(workflowID);
874
+ workflows.push({
875
+ id: `marketplace:${workflowID}`,
876
+ name: detail.workflow.name || workflowID,
877
+ description: detail.workflow.description || '',
878
+ source: 'marketplace',
879
+ skill_ids: this.workflowSkillIDs(detail.components),
880
+ });
881
+ }
882
+ const personalWorkflows = await client.listPersonalWorkflows();
883
+ for (const workflow of personalWorkflows.filter((item) => item.status === undefined || item.status === 'active')) {
884
+ const detail = await client.getPersonalWorkflowDetail(workflow.id);
885
+ workflows.push({
886
+ id: `personal:${workflow.id}`,
887
+ name: detail.workflow.name || workflow.name || workflow.id,
888
+ description: detail.workflow.description || workflow.description || '',
889
+ source: 'personal',
890
+ skill_ids: this.workflowSkillIDs(detail.components),
891
+ });
892
+ }
893
+ return workflows;
894
+ }
895
+ filterWorkflowsWithSyncableSkills(workflows, skills) {
896
+ const syncableSkillIDs = new Set(skills.map((skill) => skill.id));
897
+ return workflows
898
+ .map((workflow) => ({
899
+ ...workflow,
900
+ skill_ids: workflow.skill_ids.filter((skillID) => syncableSkillIDs.has(skillID)),
901
+ }))
902
+ .filter((workflow) => workflow.skill_ids.length > 0);
903
+ }
904
+ workflowSkillIDs(components) {
905
+ const ids = components.map((component) => {
906
+ if ('skill' in component)
907
+ return component.skill?.id;
908
+ return component.id;
909
+ });
910
+ return [...new Set(ids.filter((id) => Boolean(id)))];
911
+ }
912
+ async pickLocalManagedSkills(destinations) {
913
+ const choices = destinations.flatMap((destination) => this.shellManager.listManagedSkills(destination.rootDir).map((skill) => ({
914
+ value: `${destination.label}\u0000${skill.slug}`,
915
+ label: skill.slug,
916
+ hint: `${destination.label}${skill.skill_id ? ` · ${skill.skill_id}` : ''}`,
917
+ })));
918
+ const selectedValues = await this.prompts.pickLocalManagedSkills(choices);
919
+ const selectedByDestination = new Map();
920
+ for (const selectedValue of selectedValues) {
921
+ const [label, slug] = selectedValue.split('\u0000');
922
+ if (!label || !slug)
923
+ continue;
924
+ const slugs = selectedByDestination.get(label) ?? [];
925
+ slugs.push(slug);
926
+ selectedByDestination.set(label, slugs);
927
+ }
928
+ return selectedByDestination;
929
+ }
930
+ async downloadManagedSkillContent(client, skills) {
931
+ const syncedContentBySkill = new Map();
932
+ for (const skill of skills) {
933
+ if (!this.shouldWriteManagedContent(skill))
934
+ continue;
935
+ syncedContentBySkill.set(skill.id, await client.getSyncableSkillContent(skill.id));
936
+ }
937
+ return syncedContentBySkill;
938
+ }
939
+ shouldWriteManagedContent(skill) {
940
+ if (skill.access_source === 'personal') {
941
+ return true;
942
+ }
943
+ if (skill.access_source !== 'entitlement') {
944
+ return false;
945
+ }
946
+ return skill.billing_mode === 'public_version_lock' || skill.billing_mode === 'public_buyout_updates';
947
+ }
948
+ apiClient(config = this.requireConfig()) {
949
+ return new InvokoraApiClient(config);
950
+ }
951
+ supportUrl(config, params) {
952
+ const q = new URLSearchParams();
953
+ for (const [key, value] of Object.entries(params)) {
954
+ if (value)
955
+ q.set(key, value);
956
+ }
957
+ const query = q.toString();
958
+ return new URL(query ? `/workspace/support?${query}` : '/workspace/support', config.backend_url).toString();
959
+ }
960
+ requireConfig() {
961
+ try {
962
+ return this.configStore.require();
963
+ }
964
+ catch (error) {
965
+ console.error(error instanceof Error ? error.message : String(error));
966
+ process.exit(1);
967
+ }
968
+ }
969
+ printUsage(args = []) {
970
+ console.log(getCommandHelpText(args.filter((arg) => typeof arg === 'string')));
971
+ }
972
+ printMcpDefaults(config) {
973
+ console.log(`Default model: ${config.default_model || '(backend default)'}`);
974
+ console.log(`Default reason: ${config.default_reason || '(backend default)'}`);
975
+ }
976
+ }
977
+ function parseMcpDefaultsArgs(args) {
978
+ const parsed = {
979
+ changed: false,
980
+ clearModel: false,
981
+ clearReason: false,
982
+ };
983
+ for (let i = 0; i < args.length; i += 1) {
984
+ const arg = args[i];
985
+ if (arg === '--model') {
986
+ const value = args[i + 1]?.trim();
987
+ if (!value)
988
+ throw new Error('Missing value for --model');
989
+ parsed.model = value;
990
+ parsed.changed = true;
991
+ i += 1;
992
+ continue;
993
+ }
994
+ if (arg === '--reason') {
995
+ const value = args[i + 1];
996
+ if (!value)
997
+ throw new Error('Missing value for --reason');
998
+ parsed.reason = parseReasoningEffort(value);
999
+ parsed.changed = true;
1000
+ i += 1;
1001
+ continue;
1002
+ }
1003
+ if (arg === '--clear-model') {
1004
+ parsed.clearModel = true;
1005
+ parsed.changed = true;
1006
+ continue;
1007
+ }
1008
+ if (arg === '--clear-reason') {
1009
+ parsed.clearReason = true;
1010
+ parsed.changed = true;
1011
+ continue;
1012
+ }
1013
+ throw new Error(`Unknown mcp defaults option: ${arg}`);
1014
+ }
1015
+ if (parsed.model !== undefined && parsed.clearModel) {
1016
+ throw new Error('--model and --clear-model cannot be used together');
1017
+ }
1018
+ if (parsed.reason !== undefined && parsed.clearReason) {
1019
+ throw new Error('--reason and --clear-reason cannot be used together');
1020
+ }
1021
+ return parsed;
1022
+ }
1023
+ function parseReasoningEffort(value) {
1024
+ const normalized = value.trim().toLowerCase();
1025
+ if (normalized === 'low' || normalized === 'medium' || normalized === 'high' || normalized === 'xhigh') {
1026
+ return normalized;
1027
+ }
1028
+ throw new Error('--reason must be one of: low, medium, high, xhigh');
1029
+ }
1030
+ function resolveHelpTopic(args) {
1031
+ const tokens = args.filter((arg) => !isHelpFlag(arg));
1032
+ const [cmd, subcommand] = tokens;
1033
+ if (cmd === 'login')
1034
+ return 'login';
1035
+ if (cmd === 'language')
1036
+ return 'language';
1037
+ if (cmd === 'sync')
1038
+ return 'sync';
1039
+ if (cmd === 'skill') {
1040
+ if (subcommand === 'add')
1041
+ return 'skill-add';
1042
+ if (subcommand === 'prune')
1043
+ return 'skill-prune';
1044
+ if (subcommand === 'remove')
1045
+ return 'skill-remove';
1046
+ return 'skill';
1047
+ }
1048
+ if (cmd === 'workflow') {
1049
+ if (subcommand === 'add')
1050
+ return 'workflow-add';
1051
+ return 'workflow';
1052
+ }
1053
+ if (cmd === 'mcp') {
1054
+ if (subcommand === 'serve')
1055
+ return 'mcp-serve';
1056
+ if (subcommand === 'setup')
1057
+ return 'mcp-setup';
1058
+ if (subcommand === 'defaults')
1059
+ return 'mcp-defaults';
1060
+ return 'mcp';
1061
+ }
1062
+ return 'root';
1063
+ }
1064
+ function isHelpRequest(cmd, args) {
1065
+ return isHelpFlag(cmd) || args.some(isHelpFlag);
1066
+ }
1067
+ function isHelpFlag(arg) {
1068
+ return arg === '-h' || arg === '--help';
1069
+ }
1070
+ export async function cmdLogin(rawArgs) {
1071
+ await new InvokoraCliApp().cmdLogin(rawArgs);
1072
+ }
1073
+ export function isMainModule(importMetaUrl) {
1074
+ const entryPoint = process.argv[1];
1075
+ if (typeof entryPoint !== 'string')
1076
+ return false;
1077
+ try {
1078
+ const currentFile = fileURLToPath(importMetaUrl);
1079
+ return realpathSync(entryPoint) === realpathSync(currentFile);
1080
+ }
1081
+ catch {
1082
+ return pathToFileURL(entryPoint).href === importMetaUrl;
1083
+ }
1084
+ }
1085
+ export async function main(argv = process.argv) {
1086
+ await new InvokoraCliApp().run(argv);
1087
+ }