runwork 0.5.0 → 0.6.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 (116) hide show
  1. package/dist/agents/__tests__/claude-code-stats.test.d.ts +1 -0
  2. package/dist/agents/__tests__/claude-code-stats.test.js +153 -0
  3. package/dist/agents/__tests__/claude-desktop-stats.test.d.ts +1 -0
  4. package/dist/agents/__tests__/claude-desktop-stats.test.js +280 -0
  5. package/dist/agents/__tests__/codex-stats.test.d.ts +1 -0
  6. package/dist/agents/__tests__/codex-stats.test.js +262 -0
  7. package/dist/agents/__tests__/cursor-stats.test.d.ts +1 -0
  8. package/dist/agents/__tests__/cursor-stats.test.js +183 -0
  9. package/dist/agents/__tests__/graceful-degradation.test.d.ts +1 -0
  10. package/dist/agents/__tests__/graceful-degradation.test.js +123 -0
  11. package/dist/agents/__tests__/instruction-hint.test.d.ts +1 -0
  12. package/dist/agents/__tests__/instruction-hint.test.js +69 -0
  13. package/dist/agents/__tests__/intro-skill.test.d.ts +1 -0
  14. package/dist/agents/__tests__/intro-skill.test.js +135 -0
  15. package/dist/agents/__tests__/schema-validation.test.d.ts +1 -0
  16. package/dist/agents/__tests__/schema-validation.test.js +212 -0
  17. package/dist/agents/claude-code.d.ts +16 -1
  18. package/dist/agents/claude-code.js +344 -21
  19. package/dist/agents/claude-desktop.d.ts +10 -1
  20. package/dist/agents/claude-desktop.js +250 -8
  21. package/dist/agents/cline.d.ts +13 -0
  22. package/dist/agents/cline.js +88 -0
  23. package/dist/agents/codex.d.ts +7 -2
  24. package/dist/agents/codex.js +130 -18
  25. package/dist/agents/cursor.d.ts +6 -1
  26. package/dist/agents/cursor.js +164 -7
  27. package/dist/agents/detect.js +30 -7
  28. package/dist/agents/gemini.d.ts +6 -3
  29. package/dist/agents/gemini.js +57 -20
  30. package/dist/agents/generic-adapter.d.ts +23 -0
  31. package/dist/agents/generic-adapter.js +106 -0
  32. package/dist/agents/intro-skill.d.ts +36 -0
  33. package/dist/agents/intro-skill.js +358 -0
  34. package/dist/agents/registry.d.ts +53 -0
  35. package/dist/agents/registry.js +219 -0
  36. package/dist/agents/types.d.ts +44 -1
  37. package/dist/agents/types.js +11 -4
  38. package/dist/agents/utils/instruction-hint.d.ts +11 -0
  39. package/dist/agents/utils/instruction-hint.js +62 -0
  40. package/dist/agents/utils/json-config.js +3 -3
  41. package/dist/agents/windsurf.d.ts +2 -0
  42. package/dist/agents/windsurf.js +26 -9
  43. package/dist/api/client.d.ts +85 -2
  44. package/dist/api/client.js +40 -10
  45. package/dist/auth/__tests__/login-flow.test.d.ts +1 -0
  46. package/dist/auth/__tests__/login-flow.test.js +125 -0
  47. package/dist/auth/login-flow.d.ts +16 -0
  48. package/dist/auth/login-flow.js +101 -1
  49. package/dist/commands/clone.d.ts +9 -1
  50. package/dist/commands/clone.js +40 -12
  51. package/dist/commands/deploy.js +27 -4
  52. package/dist/commands/dev.js +35 -1
  53. package/dist/commands/doctor.d.ts +2 -0
  54. package/dist/commands/doctor.js +82 -0
  55. package/dist/commands/info.js +6 -2
  56. package/dist/commands/init.d.ts +10 -2
  57. package/dist/commands/init.js +58 -15
  58. package/dist/commands/integrations.js +47 -10
  59. package/dist/commands/login.js +31 -4
  60. package/dist/commands/mcp.js +143 -15
  61. package/dist/commands/setup.js +47 -122
  62. package/dist/commands/skills.js +230 -12
  63. package/dist/commands/sync.d.ts +8 -0
  64. package/dist/commands/sync.js +398 -65
  65. package/dist/commands/welcome.js +6 -3
  66. package/dist/generated/version.d.ts +1 -1
  67. package/dist/generated/version.js +1 -1
  68. package/dist/health/__tests__/checks.test.d.ts +1 -0
  69. package/dist/health/__tests__/checks.test.js +223 -0
  70. package/dist/health/checks.d.ts +20 -0
  71. package/dist/health/checks.js +392 -0
  72. package/dist/health/runner.d.ts +2 -0
  73. package/dist/health/runner.js +26 -0
  74. package/dist/health/types.d.ts +12 -0
  75. package/dist/health/types.js +1 -0
  76. package/dist/index.js +32 -0
  77. package/dist/sync/__tests__/change-detect.test.d.ts +1 -0
  78. package/dist/sync/__tests__/change-detect.test.js +123 -0
  79. package/dist/sync/__tests__/hash.test.d.ts +1 -0
  80. package/dist/sync/__tests__/hash.test.js +18 -0
  81. package/dist/sync/change-detect.d.ts +19 -0
  82. package/dist/sync/change-detect.js +136 -0
  83. package/dist/sync/conflict-ui.d.ts +5 -0
  84. package/dist/sync/conflict-ui.js +78 -0
  85. package/dist/sync/executor.d.ts +21 -0
  86. package/dist/sync/executor.js +98 -0
  87. package/dist/sync/hash.d.ts +1 -0
  88. package/dist/sync/hash.js +4 -0
  89. package/dist/sync/types.d.ts +24 -0
  90. package/dist/sync/types.js +1 -0
  91. package/dist/types.d.ts +12 -0
  92. package/dist/ui/__tests__/banner.test.js +17 -1
  93. package/dist/ui/banner.d.ts +1 -1
  94. package/dist/ui/banner.js +27 -2
  95. package/dist/utils/__tests__/resolve.test.d.ts +1 -0
  96. package/dist/utils/__tests__/resolve.test.js +95 -0
  97. package/dist/utils/__tests__/sqlite.test.d.ts +1 -0
  98. package/dist/utils/__tests__/sqlite.test.js +77 -0
  99. package/dist/utils/agent-guidance.d.ts +32 -0
  100. package/dist/utils/agent-guidance.js +106 -0
  101. package/dist/utils/fs.js +2 -2
  102. package/dist/utils/help-json.d.ts +56 -0
  103. package/dist/utils/help-json.js +85 -0
  104. package/dist/utils/resolve.d.ts +12 -0
  105. package/dist/utils/resolve.js +52 -0
  106. package/dist/utils/setup-state.d.ts +3 -0
  107. package/dist/utils/setup-state.js +19 -0
  108. package/dist/utils/sqlite.d.ts +7 -0
  109. package/dist/utils/sqlite.js +24 -0
  110. package/dist/utils/which.d.ts +6 -0
  111. package/dist/utils/which.js +18 -0
  112. package/package.json +7 -3
  113. package/dist/agents/copilot-cli.d.ts +0 -10
  114. package/dist/agents/copilot-cli.js +0 -46
  115. package/dist/agents/copilot-vscode.d.ts +0 -10
  116. package/dist/agents/copilot-vscode.js +0 -37
@@ -3,12 +3,14 @@ export interface McpServerEntry {
3
3
  url: string;
4
4
  transport: 'sse' | 'streamable-http';
5
5
  headers?: Record<string, string>;
6
+ description?: string;
6
7
  }
7
8
  export interface SkillFile {
8
9
  name: string;
9
10
  filename: string;
10
11
  content: string;
11
12
  description: string;
13
+ appId?: string;
12
14
  }
13
15
  export interface AgentAdapter {
14
16
  /** Display name, e.g. "Claude Code" */
@@ -21,13 +23,54 @@ export interface AgentAdapter {
21
23
  writeMcpServers(servers: McpServerEntry[], scope: 'project' | 'user'): Promise<void>;
22
24
  /** Write skill files to agent's skills directory */
23
25
  writeSkills(skills: SkillFile[], scope: 'project' | 'user'): Promise<void>;
26
+ /** Write a minimal Runwork hint to the agent's instruction file (CLAUDE.md, AGENTS.md, etc.) */
27
+ writeInstructionHint(hint: string, scope: 'project' | 'user'): Promise<void>;
24
28
  /** Whether this agent supports skills/commands */
25
29
  supportsSkills(): boolean;
26
30
  /** Whether this agent supports MCP config at the given scope */
27
31
  supportsMcpScope(scope: 'project' | 'user'): boolean;
32
+ /** Write team-managed instructions (separate marker block from auto-generated hint) */
33
+ writeTeamInstructions?(instructions: string, scope: 'project' | 'user'): Promise<void>;
34
+ /** Write agent-specific config overrides from workspace admin (model, permissions) */
35
+ writeAgentConfig?(config: AgentConfigOverride, scope: 'project' | 'user'): Promise<void>;
36
+ /** Read passive usage stats from local agent files (best-effort, no hooks) */
37
+ readUsageStats?(lastSyncAt: string | null): Promise<AgentUsageStats | null>;
38
+ /** Read which skills were used locally since last sync (best-effort) */
39
+ readSkillUsage?(lastSyncAt: string | null): Promise<SkillUsageEntry[] | null>;
40
+ /** Get the agent's version string (best-effort) */
41
+ readVersion?(): Promise<string | null>;
42
+ }
43
+ export interface AgentConfigOverride {
44
+ extraInstructions?: string;
45
+ modelPreference?: string;
46
+ permissionRules?: {
47
+ allow?: string[];
48
+ deny?: string[];
49
+ defaultMode?: string;
50
+ };
51
+ }
52
+ export interface AgentUsageStats {
53
+ hasNewActivity: boolean;
54
+ sessionCount: number;
55
+ messageCount: number;
56
+ toolCallCount: number;
57
+ tokensUsed: number;
58
+ aiLinesAdded: number;
59
+ aiLinesRemoved: number;
60
+ lastActiveAt: string | null;
61
+ modelsUsed?: string[];
62
+ mcpToolCount?: number;
63
+ extra?: Record<string, unknown>;
64
+ }
65
+ export interface SkillUsageEntry {
66
+ skillName: string;
67
+ count: number;
68
+ lastUsedAt: string;
28
69
  }
29
70
  /** Prefix for MCP server entries managed by runwork */
30
- export declare const RUNWORK_MCP_PREFIX = "runwork-";
71
+ export declare const RUNWORK_MCP_PREFIX = "Runwork: ";
72
+ /** Legacy prefix used before human-readable names were introduced */
73
+ export declare const RUNWORK_MCP_PREFIX_LEGACY = "runwork-";
31
74
  /**
32
75
  * Build a SKILL.md file with YAML frontmatter.
33
76
  * Ensures the content always has proper frontmatter for agent discovery.
@@ -1,13 +1,20 @@
1
1
  /** Prefix for MCP server entries managed by runwork */
2
- export const RUNWORK_MCP_PREFIX = 'runwork-';
2
+ export const RUNWORK_MCP_PREFIX = 'Runwork: ';
3
+ /** Legacy prefix used before human-readable names were introduced */
4
+ export const RUNWORK_MCP_PREFIX_LEGACY = 'runwork-';
3
5
  /**
4
6
  * Build a SKILL.md file with YAML frontmatter.
5
7
  * Ensures the content always has proper frontmatter for agent discovery.
6
8
  */
7
9
  export function buildSkillMd(skill) {
8
- // If content already has frontmatter, return as-is
10
+ const description = skill.description || `${skill.name} skill`;
11
+ // If content already has frontmatter, ensure description is present
9
12
  if (skill.content.trimStart().startsWith('---\n')) {
10
- return skill.content;
13
+ const hasDescription = /^description:\s*.+$/m.test(skill.content);
14
+ if (hasDescription)
15
+ return skill.content;
16
+ // Inject description into existing frontmatter
17
+ return skill.content.replace(/^---\n/, `---\nname: ${skill.name}\ndescription: ${description}\n`);
11
18
  }
12
- return `---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\n${skill.content}`;
19
+ return `---\nname: ${skill.name}\ndescription: ${description}\n---\n\n${skill.content}`;
13
20
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Append or replace a Runwork instruction hint in an agent's instruction file.
3
+ * If the file contains existing Runwork markers, replaces that section.
4
+ * Otherwise appends the hint at the end.
5
+ */
6
+ export declare function writeHintToFile(filePath: string, hint: string): void;
7
+ /**
8
+ * Append or replace team instructions in an agent's instruction file.
9
+ * Uses separate markers from the auto-generated hint so both coexist.
10
+ */
11
+ export declare function writeTeamInstructionsToFile(filePath: string, instructions: string): void;
@@ -0,0 +1,62 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { dirname } from 'path';
3
+ const START_MARKER = '<!-- runwork:start -->';
4
+ const END_MARKER = '<!-- runwork:end -->';
5
+ const TEAM_START_MARKER = '<!-- runwork-team:start -->';
6
+ const TEAM_END_MARKER = '<!-- runwork-team:end -->';
7
+ /**
8
+ * Append or replace a Runwork instruction hint in an agent's instruction file.
9
+ * If the file contains existing Runwork markers, replaces that section.
10
+ * Otherwise appends the hint at the end.
11
+ */
12
+ export function writeHintToFile(filePath, hint) {
13
+ mkdirSync(dirname(filePath), { recursive: true });
14
+ let content = '';
15
+ if (existsSync(filePath)) {
16
+ content = readFileSync(filePath, 'utf-8');
17
+ }
18
+ const startIdx = content.indexOf(START_MARKER);
19
+ const endIdx = content.indexOf(END_MARKER);
20
+ if (startIdx >= 0 && endIdx >= 0) {
21
+ // Replace existing section
22
+ content = content.substring(0, startIdx) + hint + content.substring(endIdx + END_MARKER.length);
23
+ }
24
+ else {
25
+ // Append with spacing
26
+ if (content.length > 0 && !content.endsWith('\n')) {
27
+ content += '\n';
28
+ }
29
+ if (content.length > 0) {
30
+ content += '\n';
31
+ }
32
+ content += hint + '\n';
33
+ }
34
+ writeFileSync(filePath, content);
35
+ }
36
+ /**
37
+ * Append or replace team instructions in an agent's instruction file.
38
+ * Uses separate markers from the auto-generated hint so both coexist.
39
+ */
40
+ export function writeTeamInstructionsToFile(filePath, instructions) {
41
+ mkdirSync(dirname(filePath), { recursive: true });
42
+ let content = '';
43
+ if (existsSync(filePath)) {
44
+ content = readFileSync(filePath, 'utf-8');
45
+ }
46
+ const block = `${TEAM_START_MARKER}\n${instructions}\n${TEAM_END_MARKER}`;
47
+ const startIdx = content.indexOf(TEAM_START_MARKER);
48
+ const endIdx = content.indexOf(TEAM_END_MARKER);
49
+ if (startIdx >= 0 && endIdx >= 0) {
50
+ content = content.substring(0, startIdx) + block + content.substring(endIdx + TEAM_END_MARKER.length);
51
+ }
52
+ else {
53
+ if (content.length > 0 && !content.endsWith('\n')) {
54
+ content += '\n';
55
+ }
56
+ if (content.length > 0) {
57
+ content += '\n';
58
+ }
59
+ content += block + '\n';
60
+ }
61
+ writeFileSync(filePath, content);
62
+ }
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
2
  import { dirname } from 'path';
3
- import { RUNWORK_MCP_PREFIX } from '../types.js';
3
+ import { RUNWORK_MCP_PREFIX, RUNWORK_MCP_PREFIX_LEGACY } from '../types.js';
4
4
  /**
5
5
  * Read a JSON config file. Returns empty object if file doesn't exist.
6
6
  */
@@ -29,9 +29,9 @@ export function writeJsonConfig(filePath, config) {
29
29
  export function mergeJsonMcpServers(filePath, servers, topKey) {
30
30
  const config = readJsonConfig(filePath);
31
31
  const existing = (config[topKey] || {});
32
- // Remove all runwork-* entries
32
+ // Remove all Runwork-managed entries (current and legacy prefixes)
33
33
  for (const key of Object.keys(existing)) {
34
- if (key.startsWith(RUNWORK_MCP_PREFIX)) {
34
+ if (key.startsWith(RUNWORK_MCP_PREFIX) || key.startsWith(RUNWORK_MCP_PREFIX_LEGACY)) {
35
35
  delete existing[key];
36
36
  }
37
37
  }
@@ -7,4 +7,6 @@ export declare class WindsurfAdapter implements AgentAdapter {
7
7
  supportsSkills(): boolean;
8
8
  writeMcpServers(servers: McpServerEntry[], _scope: 'project' | 'user'): Promise<void>;
9
9
  writeSkills(skills: SkillFile[], scope: 'project' | 'user'): Promise<void>;
10
+ writeTeamInstructions(instructions: string, scope: 'project' | 'user'): Promise<void>;
11
+ writeInstructionHint(hint: string, scope: 'project' | 'user'): Promise<void>;
10
12
  }
@@ -1,10 +1,16 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir, platform } from 'os';
4
- import { execFileSync } from 'child_process';
4
+ import { whichBinary } from '../utils/which.js';
5
5
  import { mergeJsonMcpServers } from './utils/json-config.js';
6
+ function getWindsurfDataDir() {
7
+ if (platform() === 'win32') {
8
+ return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'Codeium', 'windsurf');
9
+ }
10
+ return join(homedir(), '.codeium', 'windsurf');
11
+ }
6
12
  function getConfigPath() {
7
- return join(homedir(), '.codeium', 'windsurf', 'mcp_config.json');
13
+ return join(getWindsurfDataDir(), 'mcp_config.json');
8
14
  }
9
15
  export class WindsurfAdapter {
10
16
  name = 'Windsurf';
@@ -13,13 +19,7 @@ export class WindsurfAdapter {
13
19
  const os = platform();
14
20
  if (os === 'darwin' && existsSync('/Applications/Windsurf.app'))
15
21
  return true;
16
- try {
17
- execFileSync('which', ['windsurf'], { stdio: 'pipe' });
18
- return true;
19
- }
20
- catch {
21
- return false;
22
- }
22
+ return !!whichBinary('windsurf');
23
23
  }
24
24
  supportsMcpScope(scope) {
25
25
  return scope === 'user';
@@ -47,4 +47,21 @@ export class WindsurfAdapter {
47
47
  writeFileSync(join(rulesDir, `${skill.filename}.md`), content);
48
48
  }
49
49
  }
50
+ async writeTeamInstructions(instructions, scope) {
51
+ const filePath = scope === 'project'
52
+ ? join(process.cwd(), '.windsurf', 'rules', 'runwork-team.md')
53
+ : join(getWindsurfDataDir(), 'rules', 'runwork-team.md');
54
+ const content = `---\ntrigger: always_on\ndescription: "Team instructions from Runwork workspace"\n---\n\n${instructions}`;
55
+ mkdirSync(join(filePath, '..'), { recursive: true });
56
+ writeFileSync(filePath, content);
57
+ }
58
+ async writeInstructionHint(hint, scope) {
59
+ // Windsurf uses rules files
60
+ const filePath = scope === 'project'
61
+ ? join(process.cwd(), '.windsurf', 'rules', 'runwork.md')
62
+ : join(getWindsurfDataDir(), 'rules', 'runwork.md');
63
+ const content = `---\ntrigger: always\n---\n\n${hint}`;
64
+ mkdirSync(join(filePath, '..'), { recursive: true });
65
+ writeFileSync(filePath, content);
66
+ }
50
67
  }
@@ -1,4 +1,19 @@
1
1
  import type { Credentials, AppInfo, WorkspaceInfo, DevSession, WorkspaceAllData, WorkspaceSkill, ExternalSkillRegistration, McpServerConfig, RegistryEntity, WorkflowInfo, TriggerWorkflowResponse, WorkflowInstancesData, WorkflowInstanceDetailData, ScheduleInfo, TriggerScheduleResponse, ScheduleHistoryData, WorkspaceEndpoint, FileStorageInfo, ListBucketFilesData, ComponentInfo, ApiKeyInfo, ApiKeyCreateResponse, AgentInfo, AgentStatusResponse, AgentExecutionRecord } from '../types.js';
2
+ export interface AgentConfigOverride {
3
+ extraInstructions?: string;
4
+ modelPreference?: string;
5
+ permissionRules?: {
6
+ allow?: string[];
7
+ deny?: string[];
8
+ defaultMode?: string;
9
+ };
10
+ }
11
+ export interface OnboardingConfigResponse {
12
+ config: {
13
+ teamInstructions: string | null;
14
+ agentConfigs: Record<string, AgentConfigOverride> | null;
15
+ } | null;
16
+ }
2
17
  export declare class ApiClient {
3
18
  private baseUrl;
4
19
  private apiKey;
@@ -63,11 +78,13 @@ export declare class ApiClient {
63
78
  filePath: string;
64
79
  fileContents: string;
65
80
  }>, deletedFiles?: string[]): Promise<void>;
66
- searchIntegrations(query: string, limit?: number): Promise<{
81
+ searchIntegrations(query: string, limit?: number, workspaceId?: string): Promise<{
67
82
  results: Array<{
68
83
  id: string;
69
84
  name: string;
70
85
  provider: string;
86
+ connectedByUser?: boolean;
87
+ usedByTeammates?: boolean;
71
88
  docs?: {
72
89
  name: string;
73
90
  baseUrl: string;
@@ -85,6 +102,17 @@ export declare class ApiClient {
85
102
  status?: string;
86
103
  createdAt?: string;
87
104
  }>>;
105
+ getTeamIntegrationUsage(workspaceId: string): Promise<Array<{
106
+ canonicalId: string;
107
+ integrationId: string;
108
+ userCount: number;
109
+ users: Array<{
110
+ id: string;
111
+ name: string;
112
+ avatarUrl?: string | null;
113
+ }>;
114
+ connectedByMe: boolean;
115
+ }>>;
88
116
  /**
89
117
  * Get git remote URL for an app.
90
118
  * Both canonical ({baseUrl}/api/git/{workspaceId}/{appId}) and
@@ -92,7 +120,7 @@ export declare class ApiClient {
92
120
  */
93
121
  getWorkspaceAll(workspaceId: string): Promise<WorkspaceAllData>;
94
122
  getGitRemoteUrl(workspaceId: string, appId: string): string;
95
- listWorkspaceSkills(workspaceId: string): Promise<WorkspaceSkill[]>;
123
+ listWorkspaceSkills(workspaceId: string, includeContent?: boolean): Promise<WorkspaceSkill[]>;
96
124
  listExternalSkills(workspaceId: string): Promise<ExternalSkillRegistration[]>;
97
125
  importExternalSkill(workspaceId: string, skill: {
98
126
  name: string;
@@ -113,6 +141,54 @@ export declare class ApiClient {
113
141
  transport: 'sse' | 'streamable-http';
114
142
  }): Promise<McpServerConfig>;
115
143
  removeMcpServer(workspaceId: string, serverId: string): Promise<void>;
144
+ searchCommunitySkills(query: string, limit?: number, includeContent?: boolean): Promise<{
145
+ query: string;
146
+ count: number;
147
+ skills: Array<{
148
+ id: string;
149
+ title: string;
150
+ description: string | null;
151
+ source: string;
152
+ refs: {
153
+ skills_sh: string;
154
+ github: string;
155
+ raw: string | null;
156
+ };
157
+ install_count: number;
158
+ relevance_score: number;
159
+ content: string | null;
160
+ }>;
161
+ }>;
162
+ getCommunitySkillContent(source: string, skillId: string): Promise<{
163
+ id: string;
164
+ title: string;
165
+ description: string | null;
166
+ source: string;
167
+ content: string | null;
168
+ refs: {
169
+ skills_sh: string;
170
+ github: string;
171
+ raw: string | null;
172
+ };
173
+ install_count: number;
174
+ }>;
175
+ searchCommunityMcpServers(query: string, limit?: number): Promise<{
176
+ servers: Array<{
177
+ name: string;
178
+ description: string;
179
+ title: string | null;
180
+ version: string;
181
+ websiteUrl: string | null;
182
+ remotes: Array<{
183
+ type: 'sse' | 'streamable-http';
184
+ url: string;
185
+ }>;
186
+ keywords: string[];
187
+ publisher: string | null;
188
+ }>;
189
+ nextCursor: string | null;
190
+ count: number;
191
+ }>;
116
192
  listEntities(workspaceId: string): Promise<RegistryEntity[]>;
117
193
  listEntityRecords(workspaceId: string, entityName: string, opts?: {
118
194
  limit?: number;
@@ -183,6 +259,13 @@ export declare class ApiClient {
183
259
  getAgentStatus(workspaceId: string, agentName: string, appId: string): Promise<AgentStatusResponse>;
184
260
  listAgentExecutions(workspaceId: string): Promise<AgentExecutionRecord[]>;
185
261
  getAgentExecutionStream(workspaceId: string, sessionId: string): Promise<string>;
262
+ getOnboardingConfig(workspaceId: string): Promise<OnboardingConfigResponse>;
263
+ reportTelemetry(workspaceId: string, events: Array<{
264
+ eventType: string;
265
+ agentSlug?: string;
266
+ metadata?: Record<string, unknown>;
267
+ timestamp: string;
268
+ }>): Promise<void>;
186
269
  callIntegrationProxy(integrationDbId: string, method: string, path: string, opts?: {
187
270
  body?: unknown;
188
271
  headers?: Record<string, string>;
@@ -133,8 +133,10 @@ export class ApiClient {
133
133
  body: JSON.stringify({ files, deletedFiles }),
134
134
  });
135
135
  }
136
- async searchIntegrations(query, limit = 20) {
136
+ async searchIntegrations(query, limit = 20, workspaceId) {
137
137
  const params = new URLSearchParams({ q: query, limit: String(limit) });
138
+ if (workspaceId)
139
+ params.set('workspaceId', workspaceId);
138
140
  const res = await this.request(`/api/integrations/search?${params}`);
139
141
  return res.data;
140
142
  }
@@ -142,6 +144,10 @@ export class ApiClient {
142
144
  const res = await this.request(`/api/workspaces/${workspaceId}/integrations`);
143
145
  return res.data.integrations;
144
146
  }
147
+ async getTeamIntegrationUsage(workspaceId) {
148
+ const res = await this.request(`/api/workspaces/${workspaceId}/integrations/team-usage`);
149
+ return res.data.usage;
150
+ }
145
151
  /**
146
152
  * Get git remote URL for an app.
147
153
  * Both canonical ({baseUrl}/api/git/{workspaceId}/{appId}) and
@@ -155,36 +161,52 @@ export class ApiClient {
155
161
  return `${this.baseUrl}/api/git/${workspaceId}/${appId}`;
156
162
  }
157
163
  // --- Workspace Chat API (skills & MCP) ---
158
- async listWorkspaceSkills(workspaceId) {
159
- const res = await this.request(`/api/workspace-chat/${workspaceId}/skills`);
164
+ async listWorkspaceSkills(workspaceId, includeContent = false) {
165
+ const query = includeContent ? '?includeContent=true' : '';
166
+ const res = await this.request(`/api/workspaces/${workspaceId}/skills${query}`);
160
167
  return res.data.skills;
161
168
  }
162
169
  async listExternalSkills(workspaceId) {
163
- const res = await this.request(`/api/workspace-chat/${workspaceId}/external-skills`);
170
+ const res = await this.request(`/api/workspaces/${workspaceId}/external-skills`);
164
171
  return res.data.skills;
165
172
  }
166
173
  async importExternalSkill(workspaceId, skill) {
167
- const res = await this.request(`/api/workspace-chat/${workspaceId}/external-skills`, { method: 'POST', body: JSON.stringify(skill) });
174
+ const res = await this.request(`/api/workspaces/${workspaceId}/external-skills`, { method: 'POST', body: JSON.stringify(skill) });
168
175
  return res.data.skill;
169
176
  }
170
177
  async updateExternalSkill(workspaceId, skillId, skill) {
171
- const res = await this.request(`/api/workspace-chat/${workspaceId}/external-skills/${skillId}`, { method: 'PUT', body: JSON.stringify(skill) });
178
+ const res = await this.request(`/api/workspaces/${workspaceId}/external-skills/${skillId}`, { method: 'PUT', body: JSON.stringify(skill) });
172
179
  return res.data.skill;
173
180
  }
174
181
  async getAppSkillContent(workspaceId, appId) {
175
- const res = await this.request(`/api/workspace-chat/${workspaceId}/apps/${appId}/skill`);
182
+ const res = await this.request(`/api/workspaces/${workspaceId}/apps/${appId}/skill`);
176
183
  return res.data.skill;
177
184
  }
178
185
  async listMcpServers(workspaceId) {
179
- const res = await this.request(`/api/workspace-chat/${workspaceId}/mcp-servers`);
186
+ const res = await this.request(`/api/workspaces/${workspaceId}/mcp-servers`);
180
187
  return res.data.servers;
181
188
  }
182
189
  async addMcpServer(workspaceId, server) {
183
- const res = await this.request(`/api/workspace-chat/${workspaceId}/mcp-servers`, { method: 'POST', body: JSON.stringify(server) });
190
+ const res = await this.request(`/api/workspaces/${workspaceId}/mcp-servers`, { method: 'POST', body: JSON.stringify(server) });
184
191
  return res.data.server;
185
192
  }
186
193
  async removeMcpServer(workspaceId, serverId) {
187
- await this.request(`/api/workspace-chat/${workspaceId}/mcp-servers/${serverId}`, { method: 'DELETE' });
194
+ await this.request(`/api/workspaces/${workspaceId}/mcp-servers/${serverId}`, { method: 'DELETE' });
195
+ }
196
+ // --- Community Marketplace ---
197
+ async searchCommunitySkills(query, limit = 10, includeContent = false) {
198
+ const res = await this.request(`/api/community/skills/search?q=${encodeURIComponent(query)}&limit=${limit}&include_content=${includeContent}`);
199
+ return res.data;
200
+ }
201
+ async getCommunitySkillContent(source, skillId) {
202
+ // source is "owner/repo", keep the slash as a path separator (route is /skills/:owner/:repo/:skillId)
203
+ const encodedSource = source.split('/').map(encodeURIComponent).join('/');
204
+ const res = await this.request(`/api/community/skills/${encodedSource}/${encodeURIComponent(skillId)}`);
205
+ return res.data;
206
+ }
207
+ async searchCommunityMcpServers(query, limit = 20) {
208
+ const res = await this.request(`/api/community/mcp-servers/search?q=${encodeURIComponent(query)}&limit=${limit}`);
209
+ return res.data;
188
210
  }
189
211
  // --- Workspace Registry: Entities ---
190
212
  async listEntities(workspaceId) {
@@ -324,6 +346,14 @@ export class ApiClient {
324
346
  const res = await this.request(`/api/workspaces/${workspaceId}/agent-executions/${encodeURIComponent(sessionId)}/stream`);
325
347
  return res.data.stream;
326
348
  }
349
+ // --- Team Telemetry ---
350
+ async getOnboardingConfig(workspaceId) {
351
+ const res = await this.request(`/api/workspaces/${workspaceId}/onboarding-config`);
352
+ return res.data;
353
+ }
354
+ async reportTelemetry(workspaceId, events) {
355
+ await this.request(`/api/workspaces/${workspaceId}/team/telemetry`, { method: 'POST', body: JSON.stringify({ events }) });
356
+ }
327
357
  // --- Integrations: Proxy Call ---
328
358
  async callIntegrationProxy(integrationDbId, method, path, opts) {
329
359
  const targetPath = opts?.query ? `${path}?${opts.query}` : path;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ // Mock only the side-effect dependencies, not ApiClient
3
+ vi.mock('../store.js', () => ({
4
+ saveCredentials: vi.fn(),
5
+ }));
6
+ vi.mock('../../git/credentials.js', () => ({
7
+ configureGitCredentials: vi.fn().mockResolvedValue(undefined),
8
+ }));
9
+ import { saveCredentials } from '../store.js';
10
+ beforeEach(() => {
11
+ vi.clearAllMocks();
12
+ vi.spyOn(console, 'log').mockImplementation(() => { });
13
+ vi.spyOn(console, 'error').mockImplementation(() => { });
14
+ });
15
+ describe('performLoginNoOpen', () => {
16
+ it('with printOnly prints URL and returns null without polling', async () => {
17
+ // Mock ApiClient at the instance level via fetch
18
+ const originalFetch = globalThis.fetch;
19
+ const mockFetch = vi.fn().mockResolvedValue({
20
+ ok: true,
21
+ status: 200,
22
+ json: () => Promise.resolve({
23
+ data: { sessionId: 'sess-123', loginUrl: 'https://runwork.ai/cli-auth?session=sess-123' },
24
+ }),
25
+ });
26
+ globalThis.fetch = mockFetch;
27
+ try {
28
+ const { performLoginNoOpen } = await import('../login-flow.js');
29
+ const result = await performLoginNoOpen('https://runwork.ai', true);
30
+ expect(console.log).toHaveBeenCalledWith('https://runwork.ai/cli-auth?session=sess-123');
31
+ expect(result).toBeNull();
32
+ expect(saveCredentials).not.toHaveBeenCalled();
33
+ }
34
+ finally {
35
+ globalThis.fetch = originalFetch;
36
+ }
37
+ });
38
+ });
39
+ describe('performLoginWithApiKey', () => {
40
+ it('validates key by listing workspaces and saves credentials', async () => {
41
+ const originalFetch = globalThis.fetch;
42
+ const mockFetch = vi.fn().mockResolvedValue({
43
+ ok: true,
44
+ status: 200,
45
+ json: () => Promise.resolve({
46
+ data: [{ id: 'ws-1', name: 'My Workspace' }],
47
+ }),
48
+ });
49
+ globalThis.fetch = mockFetch;
50
+ try {
51
+ const { performLoginWithApiKey } = await import('../login-flow.js');
52
+ const result = await performLoginWithApiKey('rw_test123');
53
+ expect(saveCredentials).toHaveBeenCalledWith(expect.objectContaining({
54
+ apiKey: 'rw_test123',
55
+ defaultWorkspaceId: 'ws-1',
56
+ defaultWorkspaceName: 'My Workspace',
57
+ }));
58
+ expect(result.apiKey).toBe('rw_test123');
59
+ }
60
+ finally {
61
+ globalThis.fetch = originalFetch;
62
+ }
63
+ });
64
+ it('saves credentials with custom base URL', async () => {
65
+ const originalFetch = globalThis.fetch;
66
+ const mockFetch = vi.fn().mockResolvedValue({
67
+ ok: true,
68
+ status: 200,
69
+ json: () => Promise.resolve({ data: [{ id: 'ws-1', name: 'WS' }] }),
70
+ });
71
+ globalThis.fetch = mockFetch;
72
+ try {
73
+ const { performLoginWithApiKey } = await import('../login-flow.js');
74
+ const result = await performLoginWithApiKey('rw_key', 'https://custom.runwork.dev');
75
+ expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('https://custom.runwork.dev'), expect.anything());
76
+ expect(result.baseUrl).toBe('https://custom.runwork.dev');
77
+ }
78
+ finally {
79
+ globalThis.fetch = originalFetch;
80
+ }
81
+ });
82
+ it('handles no workspaces (sets no default)', async () => {
83
+ const originalFetch = globalThis.fetch;
84
+ const mockFetch = vi.fn().mockResolvedValue({
85
+ ok: true,
86
+ status: 200,
87
+ json: () => Promise.resolve({ data: [] }),
88
+ });
89
+ globalThis.fetch = mockFetch;
90
+ try {
91
+ const { performLoginWithApiKey } = await import('../login-flow.js');
92
+ const result = await performLoginWithApiKey('rw_key');
93
+ expect(saveCredentials).toHaveBeenCalledWith(expect.objectContaining({
94
+ apiKey: 'rw_key',
95
+ defaultWorkspaceId: undefined,
96
+ defaultWorkspaceName: undefined,
97
+ }));
98
+ expect(result.apiKey).toBe('rw_key');
99
+ }
100
+ finally {
101
+ globalThis.fetch = originalFetch;
102
+ }
103
+ });
104
+ it('exits on invalid API key', async () => {
105
+ const originalFetch = globalThis.fetch;
106
+ globalThis.fetch = vi.fn().mockResolvedValue({
107
+ ok: false,
108
+ status: 401,
109
+ json: () => Promise.resolve({ error: 'Unauthorized' }),
110
+ text: () => Promise.resolve('Unauthorized'),
111
+ });
112
+ const mockExit = vi.spyOn(process, 'exit').mockImplementation(((code) => {
113
+ throw new Error(`process.exit(${code})`);
114
+ }));
115
+ try {
116
+ const { performLoginWithApiKey } = await import('../login-flow.js');
117
+ await expect(performLoginWithApiKey('bad-key')).rejects.toThrow('process.exit(1)');
118
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Invalid API key'));
119
+ }
120
+ finally {
121
+ globalThis.fetch = originalFetch;
122
+ mockExit.mockRestore();
123
+ }
124
+ });
125
+ });
@@ -8,3 +8,19 @@ import type { Credentials } from '../types.js';
8
8
  * Returns the credentials on success, exits process on timeout.
9
9
  */
10
10
  export declare function performLogin(baseUrl?: string): Promise<Credentials>;
11
+ /**
12
+ * Initiate a login session without opening the browser.
13
+ * Prints the login URL to stdout and polls for completion.
14
+ * If printOnly is true, prints the URL and returns null (exits without polling).
15
+ */
16
+ export declare function performLoginNoOpen(baseUrl?: string, printOnly?: boolean): Promise<Credentials | null>;
17
+ /**
18
+ * Resume a previously initiated login session by providing the login URL.
19
+ * Extracts the session ID from the URL and polls until authenticated.
20
+ */
21
+ export declare function performLoginFromUrl(loginUrl: string): Promise<Credentials>;
22
+ /**
23
+ * Authenticate directly with an API key (no browser flow).
24
+ * Validates the key by calling the API, then saves credentials.
25
+ */
26
+ export declare function performLoginWithApiKey(apiKey: string, baseUrl?: string): Promise<Credentials>;