runwork 0.13.4 → 0.15.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 (95) hide show
  1. package/bundled-types/core-endpoints.d.ts +16 -4
  2. package/dist/__tests__/install-scripts.test.js +33 -0
  3. package/dist/agents/__tests__/chatgpt-registry.test.d.ts +1 -0
  4. package/dist/agents/__tests__/chatgpt-registry.test.js +61 -0
  5. package/dist/agents/__tests__/detection.test.js +3 -0
  6. package/dist/agents/claude-desktop-plugin-tree.js +23 -8
  7. package/dist/agents/claude-desktop.js +6 -3
  8. package/dist/agents/codex.js +15 -7
  9. package/dist/agents/detection.js +3 -0
  10. package/dist/agents/intro-skill.js +5 -3
  11. package/dist/agents/registry-data.d.ts +59 -2
  12. package/dist/agents/registry-data.js +176 -0
  13. package/dist/api/client.d.ts +5 -0
  14. package/dist/api/client.js +8 -0
  15. package/dist/commands/__tests__/clone-args.test.js +8 -1
  16. package/dist/commands/__tests__/clone-source-result.test.d.ts +1 -0
  17. package/dist/commands/__tests__/clone-source-result.test.js +45 -0
  18. package/dist/commands/__tests__/mcp-entries.test.d.ts +1 -0
  19. package/dist/commands/__tests__/mcp-entries.test.js +48 -0
  20. package/dist/commands/clone.d.ts +33 -0
  21. package/dist/commands/clone.js +105 -10
  22. package/dist/commands/deploy.js +128 -11
  23. package/dist/commands/dev.js +72 -32
  24. package/dist/commands/doctor.js +64 -3
  25. package/dist/commands/info.d.ts +3 -0
  26. package/dist/commands/info.js +11 -0
  27. package/dist/commands/mcp-entries.d.ts +13 -0
  28. package/dist/commands/mcp-entries.js +26 -0
  29. package/dist/commands/sync.js +20 -11
  30. package/dist/commands/validate.d.ts +2 -0
  31. package/dist/commands/validate.js +89 -0
  32. package/dist/deploy/__tests__/deploy-state.test.d.ts +1 -0
  33. package/dist/deploy/__tests__/deploy-state.test.js +80 -0
  34. package/dist/deploy/__tests__/deploy-status.test.d.ts +1 -0
  35. package/dist/deploy/__tests__/deploy-status.test.js +41 -0
  36. package/dist/deploy/__tests__/detach-args.test.d.ts +1 -0
  37. package/dist/deploy/__tests__/detach-args.test.js +30 -0
  38. package/dist/deploy/deploy-state.d.ts +36 -0
  39. package/dist/deploy/deploy-state.js +63 -0
  40. package/dist/deploy/deploy-status.d.ts +21 -0
  41. package/dist/deploy/deploy-status.js +36 -0
  42. package/dist/deploy/detach.d.ts +32 -0
  43. package/dist/deploy/detach.js +71 -0
  44. package/dist/dev/__tests__/session.test.js +39 -0
  45. package/dist/dev/__tests__/startup-sync.test.d.ts +1 -0
  46. package/dist/dev/__tests__/startup-sync.test.js +32 -0
  47. package/dist/dev/session.d.ts +9 -0
  48. package/dist/dev/session.js +4 -1
  49. package/dist/dev/startup-sync.d.ts +23 -0
  50. package/dist/dev/startup-sync.js +14 -0
  51. package/dist/generated/bundled-types.js +1 -1
  52. package/dist/generated/version.d.ts +1 -1
  53. package/dist/generated/version.js +1 -1
  54. package/dist/git/__tests__/classify-sync-error.test.d.ts +1 -0
  55. package/dist/git/__tests__/classify-sync-error.test.js +57 -0
  56. package/dist/git/__tests__/ensure-git-credential-helper.test.d.ts +1 -0
  57. package/dist/git/__tests__/ensure-git-credential-helper.test.js +133 -0
  58. package/dist/git/__tests__/ensure-runwork-remote.test.d.ts +1 -0
  59. package/dist/git/__tests__/ensure-runwork-remote.test.js +109 -0
  60. package/dist/git/__tests__/repo-config.test.d.ts +1 -0
  61. package/dist/git/__tests__/repo-config.test.js +26 -0
  62. package/dist/git/classify-sync-error.d.ts +21 -0
  63. package/dist/git/classify-sync-error.js +111 -0
  64. package/dist/git/credentials.d.ts +43 -0
  65. package/dist/git/credentials.js +67 -0
  66. package/dist/git/remote.d.ts +37 -0
  67. package/dist/git/remote.js +75 -0
  68. package/dist/git/repo-config.d.ts +8 -0
  69. package/dist/git/repo-config.js +29 -0
  70. package/dist/health/__tests__/checks.test.js +2 -2
  71. package/dist/health/__tests__/fix.test.d.ts +1 -0
  72. package/dist/health/__tests__/fix.test.js +72 -0
  73. package/dist/health/__tests__/runner-filter.test.d.ts +1 -0
  74. package/dist/health/__tests__/runner-filter.test.js +61 -0
  75. package/dist/health/checks.d.ts +7 -0
  76. package/dist/health/checks.js +50 -36
  77. package/dist/health/fix.d.ts +22 -0
  78. package/dist/health/fix.js +62 -0
  79. package/dist/health/runner.d.ts +7 -1
  80. package/dist/health/runner.js +40 -27
  81. package/dist/index.js +2 -0
  82. package/dist/tools/types.d.ts +5 -0
  83. package/dist/types.d.ts +3 -0
  84. package/dist/utils/__tests__/help-json.test.d.ts +1 -0
  85. package/dist/utils/__tests__/help-json.test.js +39 -0
  86. package/dist/utils/__tests__/ignore-matcher.test.js +18 -1
  87. package/dist/utils/help-json.d.ts +14 -0
  88. package/dist/utils/help-json.js +4 -1
  89. package/dist/validate/__tests__/validate.test.d.ts +1 -0
  90. package/dist/validate/__tests__/validate.test.js +42 -0
  91. package/dist/validate/freshness.d.ts +28 -0
  92. package/dist/validate/freshness.js +48 -0
  93. package/dist/validate/preview.d.ts +21 -0
  94. package/dist/validate/preview.js +39 -0
  95. package/package.json +1 -1
@@ -10,9 +10,16 @@ import { z } from 'zod';
10
10
  import type { Hono } from 'hono';
11
11
  type ZodSchema = z.ZodType<any, any, any>;
12
12
  /**
13
- * Authentication type for public endpoints
13
+ * Authentication requirement declared by an endpoint definition.
14
14
  */
15
15
  export type EndpointAuthType = 'apiKey' | 'public';
16
+ /**
17
+ * Caller principal type resolved by the platform and passed via the
18
+ * `X-Public-Endpoint-Auth-Type` header. `user` and `workspace_key` are
19
+ * authenticated principals the platform has already authorized; `api_key`
20
+ * additionally carries a validated key id; `none` is unauthenticated.
21
+ */
22
+ export type EndpointPrincipalType = 'none' | 'api_key' | 'workspace_key' | 'user';
16
23
  /**
17
24
  * HTTP methods supported by endpoints
18
25
  */
@@ -53,10 +60,14 @@ export interface EndpointContext<TQuery = Record<string, unknown>, TBody = unkno
53
60
  * Authentication information passed to handlers
54
61
  */
55
62
  export interface EndpointAuthInfo {
56
- /** Authentication type used */
63
+ /** Authentication requirement declared by the endpoint */
57
64
  type: EndpointAuthType;
58
- /** API key ID (if authenticated with API key) */
65
+ /** Resolved caller principal type (api_key, workspace_key, user, none) */
66
+ principalType?: EndpointPrincipalType;
67
+ /** API key ID (if authenticated with an API key) */
59
68
  apiKeyId?: string;
69
+ /** User ID (if authenticated as a workspace member) */
70
+ userId?: string;
60
71
  /** Scopes granted to the API key */
61
72
  scopes?: string[];
62
73
  /** Whether request is authenticated */
@@ -112,8 +123,9 @@ export declare function isEndpointDefinition(entry: unknown): entry is EndpointD
112
123
  export interface ParsedEndpointRequest {
113
124
  endpointId: string;
114
125
  appId: string;
115
- authType: EndpointAuthType;
126
+ authType: EndpointPrincipalType;
116
127
  apiKeyId?: string;
128
+ userId?: string;
117
129
  scopes?: string[];
118
130
  }
119
131
  /**
@@ -165,6 +165,39 @@ describe('install.sh (end-to-end)', () => {
165
165
  const defaultBinary = join(workDir, '.runwork', 'bin', 'runwork');
166
166
  expect(existsSync(defaultBinary)).toBe(true);
167
167
  });
168
+ it('persists ~/.runwork/bin to a shell profile, idempotently, on a default install', async () => {
169
+ const linkDir = join(workDir, 'link');
170
+ const exportLine = 'export PATH="$HOME/.runwork/bin:$PATH"';
171
+ const marker = '# Added by Runwork CLI installer';
172
+ const profiles = ['.zshrc', '.bashrc', '.bash_profile', '.profile'].map((p) => join(workDir, p));
173
+ const read = (p) => (existsSync(p) ? readFileSync(p, 'utf-8') : '');
174
+ // First install with the default install dir ($HOME/.runwork/bin) triggers
175
+ // PATH persistence.
176
+ const r1 = await runInstallSh(envFor({ RUNWORK_BIN_LINK_DIR: linkDir }));
177
+ expect(r1.status, `installer failed:\n${r1.stdout}\n${r1.stderr}`).toBe(0);
178
+ const written = profiles.filter((p) => read(p).includes(exportLine));
179
+ expect(written.length, 'expected the export line in at least one profile').toBeGreaterThan(0);
180
+ expect(profiles.some((p) => read(p).includes(marker))).toBe(true);
181
+ // Re-running must NOT append a second time (guarded on .runwork/bin).
182
+ const r2 = await runInstallSh(envFor({ RUNWORK_BIN_LINK_DIR: linkDir }));
183
+ expect(r2.status).toBe(0);
184
+ for (const p of written) {
185
+ const occurrences = read(p).split('.runwork/bin').length - 1;
186
+ expect(occurrences, `profile ${p} should reference .runwork/bin exactly once`).toBe(1);
187
+ }
188
+ });
189
+ it('does NOT touch shell profiles when a custom RUNWORK_INSTALL_DIR is used', async () => {
190
+ const result = await runInstallSh(envFor({
191
+ RUNWORK_INSTALL_DIR: join(workDir, 'install'),
192
+ RUNWORK_BIN_LINK_DIR: join(workDir, 'link'),
193
+ }));
194
+ expect(result.status).toBe(0);
195
+ const profiles = ['.zshrc', '.bashrc', '.bash_profile', '.profile'].map((p) => join(workDir, p));
196
+ for (const p of profiles) {
197
+ if (existsSync(p))
198
+ expect(readFileSync(p, 'utf-8')).not.toContain('.runwork/bin');
199
+ }
200
+ });
168
201
  it('aborts on sha256 mismatch and leaves the install directory untouched', async () => {
169
202
  // Tamper with the archive the server is about to serve.
170
203
  writeFileSync(archivePath, Buffer.from('tampered payload'));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getAgent, getAgentFamily, getFamilyAgents, isConnectOnlyAgent, isWebAgent, } from '../registry-data.js';
3
+ describe('ChatGPT registry entries', () => {
4
+ it('registers both Web and Desktop entries', () => {
5
+ const web = getAgent('chatgpt');
6
+ const app = getAgent('chatgpt-app');
7
+ expect(web).toBeDefined();
8
+ expect(app).toBeDefined();
9
+ expect(web?.category).toBe('web');
10
+ expect(app?.category).toBe('desktop');
11
+ expect(web?.firstClass).toBe(true);
12
+ expect(app?.firstClass).toBe(true);
13
+ });
14
+ it('treats Web entry as always-available with a URL launch', () => {
15
+ const web = getAgent('chatgpt');
16
+ expect(web?.detection).toEqual({ method: 'always' });
17
+ expect(web?.launch?.url).toContain('chatgpt.com');
18
+ expect(isWebAgent('chatgpt')).toBe(true);
19
+ expect(isWebAgent('chatgpt-app')).toBe(false);
20
+ });
21
+ it('marks both entries connect-only (no local config paths)', () => {
22
+ for (const slug of ['chatgpt', 'chatgpt-app']) {
23
+ const agent = getAgent(slug);
24
+ expect(isConnectOnlyAgent(agent)).toBe(true);
25
+ expect(agent?.skillsPaths).toBeUndefined();
26
+ expect(agent?.mcpConfigPath).toBeUndefined();
27
+ expect(agent?.instructionFile).toBeUndefined();
28
+ }
29
+ });
30
+ it('does not mark file-backed agents as connect-only', () => {
31
+ expect(isConnectOnlyAgent(getAgent('claude-code'))).toBe(false);
32
+ expect(isConnectOnlyAgent(getAgent('codex'))).toBe(false);
33
+ });
34
+ it('groups both entries under one family', () => {
35
+ expect(getAgentFamily('chatgpt')).toBe('chatgpt');
36
+ expect(getAgentFamily('chatgpt-app')).toBe('chatgpt');
37
+ const family = getFamilyAgents('chatgpt').map((a) => a.slug).sort();
38
+ expect(family).toEqual(['chatgpt', 'chatgpt-app']);
39
+ });
40
+ it('falls back to the slug as family for standalone agents', () => {
41
+ expect(getAgentFamily('claude-code')).toBe('claude-code');
42
+ expect(getFamilyAgents('claude-code').map((a) => a.slug)).toEqual(['claude-code']);
43
+ });
44
+ it('provides team/personal manual-setup variants and a copyable MCP URL', () => {
45
+ for (const slug of ['chatgpt', 'chatgpt-app']) {
46
+ const setup = getAgent(slug)?.manualSetup;
47
+ expect(setup?.showAfter).toBe('connecting');
48
+ expect(setup?.copyValue?.token).toBe('{mcpUrl}');
49
+ const keys = setup?.variants?.map((v) => v.key).sort();
50
+ expect(keys).toEqual(['personal', 'team']);
51
+ // Every variant carries at least one step.
52
+ for (const variant of setup?.variants ?? []) {
53
+ expect(variant.steps.length).toBeGreaterThan(0);
54
+ }
55
+ }
56
+ });
57
+ it('uses url-prompt resume for connect-only ChatGPT entries', () => {
58
+ expect(getAgent('chatgpt')?.resumeCapability?.mode).toBe('url-prompt');
59
+ expect(getAgent('chatgpt-app')?.resumeCapability?.mode).toBe('url-prompt');
60
+ });
61
+ });
@@ -83,4 +83,7 @@ describe('runAgentDetection', () => {
83
83
  ],
84
84
  })).toBe(false);
85
85
  });
86
+ it('always method reports true without any local probe (web-only agents)', async () => {
87
+ expect(await runAgentDetection({ method: 'always' })).toBe(true);
88
+ });
86
89
  });
@@ -1,6 +1,6 @@
1
1
  import { chmodSync, mkdirSync, rmSync, existsSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
- import { appendTokenToUrl, buildSkillMd } from './types.js';
3
+ import { appendTokenToUrl, buildSkillMd, RUNWORK_WORKSPACE_MCP_NAME } from './types.js';
4
4
  import { SESSION_START_HOOK_SCRIPT } from './session-start-hook.js';
5
5
  /**
6
6
  * Write <destDir>/.claude-plugin/plugin.json.
@@ -24,13 +24,28 @@ export function writePluginMcpConfig(destDir, mcpServers) {
24
24
  mkdirSync(destDir, { recursive: true });
25
25
  const mcpEntries = {};
26
26
  for (const server of mcpServers) {
27
- const token = server.headers?.Authorization?.replace(/^Bearer\s+/i, '');
28
- const connectorUrl = token ? appendTokenToUrl(server.url, token) : server.url;
29
- mcpEntries[server.name] = {
30
- type: 'http',
31
- url: connectorUrl,
32
- ...(server.description ? { description: server.description } : {}),
33
- };
27
+ if (server.name === RUNWORK_WORKSPACE_MCP_NAME) {
28
+ // Preserve the exact, known-working behavior for the Runwork connector: embed
29
+ // the workspace key in the URL, no headers field. Claude Desktop's native
30
+ // connector did not reliably honor request headers here, which is why the token
31
+ // lives in the URL — do NOT change this.
32
+ const token = server.headers?.Authorization?.replace(/^Bearer\s+/i, '');
33
+ mcpEntries[server.name] = {
34
+ type: 'http',
35
+ url: token ? appendTokenToUrl(server.url, token) : server.url,
36
+ ...(server.description ? { description: server.description } : {}),
37
+ };
38
+ }
39
+ else {
40
+ // Third-party servers: forward the auth header map (same .mcp.json schema
41
+ // claude-code uses). Their secrets must NEVER be placed in the URL.
42
+ mcpEntries[server.name] = {
43
+ type: 'http',
44
+ url: server.url,
45
+ ...(server.headers ? { headers: server.headers } : {}),
46
+ ...(server.description ? { description: server.description } : {}),
47
+ };
48
+ }
34
49
  }
35
50
  writeFileSync(join(destDir, '.mcp.json'), JSON.stringify({ mcpServers: mcpEntries }, null, 2));
36
51
  }
@@ -189,12 +189,15 @@ export class ClaudeDesktopAdapter {
189
189
  // For cowork connectors, embed the auth token in the URL query string.
190
190
  // This lets Claude Desktop connect without requiring the user to go through
191
191
  // an OAuth flow or manually click "Connect" in the Connectors UI.
192
- // Write to claude_desktop_config.json via mcp-remote (chat mode fallback)
192
+ // Write to claude_desktop_config.json via mcp-remote (chat mode fallback).
193
+ // Forward every auth header (Authorization, X-Api-Key, etc.) as a --header arg.
193
194
  const entries = {};
194
195
  for (const s of servers) {
195
196
  const args = ['mcp-remote', s.url];
196
- if (s.headers?.Authorization) {
197
- args.push('--header', `Authorization:${s.headers.Authorization}`);
197
+ if (s.headers) {
198
+ for (const [name, value] of Object.entries(s.headers)) {
199
+ args.push('--header', `${name}:${value}`);
200
+ }
198
201
  }
199
202
  entries[s.name] = {
200
203
  command: 'npx',
@@ -45,15 +45,23 @@ export class CodexAdapter {
45
45
  delete mcpServers[key];
46
46
  }
47
47
  }
48
- // Add new entries (sanitize names to match Codex's ^[a-zA-Z0-9_-]+$ requirement)
48
+ // Add new entries (sanitize names to match Codex's ^[a-zA-Z0-9_-]+$ requirement).
49
49
  for (const s of servers) {
50
50
  const safeName = s.name.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
51
- // Embed auth token in URL so Codex doesn't need a separate env var
52
- const token = s.headers?.Authorization?.replace(/^Bearer\s+/i, '');
53
- const serverUrl = token ? appendTokenToUrl(s.url, token) : s.url;
54
- mcpServers[safeName] = {
55
- url: serverUrl,
56
- };
51
+ const entry = { url: s.url };
52
+ if (s.name === RUNWORK_WORKSPACE_MCP_NAME) {
53
+ // Preserve the known-working behavior: embed the workspace key in the URL.
54
+ // Codex's header-based auth was historically unreliable here, so do NOT change it.
55
+ const token = s.headers?.Authorization?.replace(/^Bearer\s+/i, '');
56
+ if (token)
57
+ entry.url = appendTokenToUrl(s.url, token);
58
+ }
59
+ else if (s.headers && Object.keys(s.headers).length > 0) {
60
+ // External servers: forward headers via Codex's documented http_headers field
61
+ // (static literal values). These had no prior working path, so this is additive.
62
+ entry.http_headers = s.headers;
63
+ }
64
+ mcpServers[safeName] = entry;
57
65
  }
58
66
  mkdirSync(join(configPath, '..'), { recursive: true });
59
67
  writeFileSync(configPath, stringify(parsed));
@@ -85,6 +85,9 @@ export async function runAgentDetection(detection) {
85
85
  const probes = await Promise.all(detection.target.map((p) => runAgentDetection(p)));
86
86
  return probes.some(Boolean);
87
87
  }
88
+ case 'always':
89
+ // Web-only agents have no local footprint; always treated as available.
90
+ return true;
88
91
  default:
89
92
  return false;
90
93
  }
@@ -300,14 +300,14 @@ export function generateIntroSkill(ctx) {
300
300
  lines.push('| `runwork info --json` | Full workspace and app context |');
301
301
  lines.push('| `runwork init my-app` | Create a new app |');
302
302
  lines.push('| `runwork dev` | Start local development (watches files, syncs automatically) |');
303
- lines.push('| `runwork deploy` | Deploy to production |');
303
+ lines.push('| `runwork deploy` | Deploy to production (`--detach` runs in the background to survive shell timeouts; `runwork deploy --status` polls the result) |');
304
304
  lines.push('| `runwork entities list` | Browse data across apps |');
305
305
  lines.push('| `runwork entities records <name>` | Query entity records |');
306
306
  lines.push('| `runwork workflows trigger <name>` | Trigger a workflow |');
307
307
  lines.push('| `runwork integrations call <id> <method> <path>` | Call an integration API |');
308
308
  lines.push('| `runwork integrations search <query>` | Search 3,200+ available integrations |');
309
309
  lines.push('| `runwork logs` | View app logs |');
310
- lines.push('| `runwork doctor` | Check system health |');
310
+ lines.push('| `runwork doctor` | Check system health (`--fix` auto-remediates git auth/remote; `--check <names>` scopes output) |');
311
311
  lines.push('| `runwork setup` | Set up workspace connection and sync to agents |');
312
312
  lines.push('| `runwork sync` | Sync skills, MCP servers, and settings to local agents |');
313
313
  lines.push('');
@@ -323,6 +323,8 @@ export function generateIntroSkill(ctx) {
323
323
  lines.push('');
324
324
  lines.push('> By default, `runwork init` scaffolds into `~/.runwork/apps/<slug>` so every app has a predictable home. Pass `--here` to scaffold in the current directory instead.');
325
325
  lines.push('');
326
+ lines.push('> Clone/init into a normal working directory (e.g. under your home folder). Some sandboxed "outputs" mounts disallow the unlink operations git uses for pack files, which can corrupt the repo ("unable to unlink \'.git/objects/pack/...\': Operation not permitted"). If you hit that, switch to a standard path.');
327
+ lines.push('');
326
328
  lines.push('**CLI development workflow:**');
327
329
  lines.push('1. `runwork init my-app` -- scaffold the app (created at `~/.runwork/apps/my-app`)');
328
330
  lines.push('2. `cd ~/.runwork/apps/my-app` -- enter the app directory');
@@ -385,7 +387,7 @@ export function generateIntroSkill(ctx) {
385
387
  lines.push('- **Connect workspace:** `runwork login` then `runwork setup`');
386
388
  lines.push('- **Connect an integration:** Visit the workspace settings in the web dashboard at https://runwork.ai, or use `runwork integrations search <query>` to find services');
387
389
  lines.push('- **Web dashboard:** https://runwork.ai for visual management of everything');
388
- lines.push('- **Troubleshoot:** `runwork doctor` checks system health; `runwork logs` shows app output');
390
+ lines.push('- **Troubleshoot:** `runwork doctor` checks system health (`runwork doctor --fix` auto-repairs git auth and the runwork remote); `runwork logs` shows app output');
389
391
  lines.push('');
390
392
  // References
391
393
  lines.push('### References');
@@ -8,7 +8,7 @@
8
8
  * Keep this file browser-safe: no `os`, `path`, `fs`, or other Node-only
9
9
  * imports. Node-only path resolvers live next to this file in `./registry.ts`.
10
10
  */
11
- export type AgentCategory = 'cli' | 'ide' | 'desktop' | 'extension';
11
+ export type AgentCategory = 'cli' | 'ide' | 'desktop' | 'extension' | 'web';
12
12
  export type { PlatformString, Detection, InstallableTool } from '../tools/types.js';
13
13
  import type { InstallableTool, Detection, PlatformString } from '../tools/types.js';
14
14
  /** @deprecated use `Detection` from ../tools/types — kept as alias for back-compat. */
@@ -21,6 +21,13 @@ export interface AgentLaunch {
21
21
  };
22
22
  cli?: string;
23
23
  cliAcceptsPrompt?: boolean;
24
+ /**
25
+ * Web/URL launch target opened in the default browser. `{prompt}` is
26
+ * substituted with a URL-encoded prompt (e.g. ChatGPT's `?prompt=`), or
27
+ * stripped when no prompt is supplied. Used by web-only agents and as the
28
+ * fallback for desktop agents whose app is not installed.
29
+ */
30
+ url?: string;
24
31
  }
25
32
  export interface AgentQuickStart {
26
33
  launchHint?: string;
@@ -44,6 +51,28 @@ export interface AgentManualSetupArtifact {
44
51
  }
45
52
  /** Where a manual-setup step is inserted in the onboarding journey. */
46
53
  export type ManualSetupSlot = 'installation' | 'connecting' | 'try-it';
54
+ /**
55
+ * A plan-conditional variant of a manual-setup checklist. When an agent's
56
+ * connector setup differs by account plan (e.g. ChatGPT Team vs Personal),
57
+ * each variant carries its own steps; the onboarding UI selects the variant
58
+ * matching the workspace's stored plan. Agents without plan branching omit
59
+ * `variants` and use the flat `steps`.
60
+ */
61
+ export interface AgentManualSetupVariant {
62
+ key: 'team' | 'personal';
63
+ label: string;
64
+ title?: string;
65
+ steps: ManualSetupStep[];
66
+ }
67
+ /**
68
+ * A copy-to-clipboard value surfaced alongside the manual-setup steps. The
69
+ * onboarding UI substitutes `token` with a real value (e.g. `{mcpUrl}` →
70
+ * the workspace MCP URL) before rendering the copy control.
71
+ */
72
+ export interface AgentManualSetupCopyValue {
73
+ label: string;
74
+ token: '{mcpUrl}';
75
+ }
47
76
  export interface AgentManualSetup {
48
77
  title: string;
49
78
  steps: ManualSetupStep[];
@@ -51,6 +80,10 @@ export interface AgentManualSetup {
51
80
  /** Where in the journey to show this step. Defaults to 'try-it'. */
52
81
  showAfter?: ManualSetupSlot;
53
82
  downloadArtifact?: AgentManualSetupArtifact;
83
+ /** Plan-conditional step lists. When present, the UI renders the variant matching the workspace plan instead of `steps`. */
84
+ variants?: AgentManualSetupVariant[];
85
+ /** A value the user copies into the agent during setup (e.g. the workspace MCP URL). */
86
+ copyValue?: AgentManualSetupCopyValue;
54
87
  }
55
88
  /**
56
89
  * How a recipient can resume a shared conversation locally in this agent.
@@ -72,12 +105,18 @@ export interface AgentManualSetup {
72
105
  * paste-prompt path. Used for walled-garden agents like
73
106
  * Microsoft Copilot or Claude Desktop Chat mode.
74
107
  *
108
+ * - 'url-prompt' Open the agent's `launch.url` (or app) with a prefilled
109
+ * prompt that instructs the agent to fetch and continue
110
+ * the shared conversation via Runwork's MCP tools. Used by
111
+ * connect-only agents (ChatGPT) that have MCP access but no
112
+ * local session files to drop into.
113
+ *
75
114
  * Bundle placement logic (which directory, which filename) is per-agent and
76
115
  * lives in the resume command implementation, not here. This data field stays
77
116
  * declarative.
78
117
  */
79
118
  export interface AgentResumeCapability {
80
- mode: 'cli-resume' | 'file-drop-only' | 'unsupported';
119
+ mode: 'cli-resume' | 'file-drop-only' | 'unsupported' | 'url-prompt';
81
120
  /** Bundle format this agent reads natively (must match a bundle's `format`). */
82
121
  nativeBundleFormat?: 'claude-jsonl' | 'codex-rollout' | string;
83
122
  /** Template for the resume command (cli-resume only). `{uuid}` is substituted with the share's session id. */
@@ -88,6 +127,13 @@ export interface AgentResumeCapability {
88
127
  export interface AgentDefinition extends InstallableTool {
89
128
  aliases?: string[];
90
129
  category: AgentCategory;
130
+ /**
131
+ * Family identifier grouping multiple entries that share one account/
132
+ * connector/OAuth identity (e.g. `chatgpt` + `chatgpt-app` → 'chatgpt').
133
+ * Drives the shared plan toggle, connect-step dedup, and family-level
134
+ * adoption. Undefined for standalone agents.
135
+ */
136
+ family?: string;
91
137
  /** Desktop launch hints (GUI app name, CLI command) */
92
138
  launch?: AgentLaunch;
93
139
  /** Whether the CLI knows how to auto-install this agent */
@@ -127,6 +173,17 @@ export declare function getAgentsByCategory(category: AgentCategory): AgentDefin
127
173
  export declare function isCLIAgent(slug: string): boolean;
128
174
  export declare function isIDEAgent(slug: string): boolean;
129
175
  export declare function isDesktopAgent(slug: string): boolean;
176
+ export declare function isWebAgent(slug: string): boolean;
177
+ /**
178
+ * Connect-only agents have no local footprint to write: no skills directory,
179
+ * no MCP config file, no instruction file. The connector lives in the user's
180
+ * account (e.g. ChatGPT). `runwork sync` skips these for all local writes.
181
+ */
182
+ export declare function isConnectOnlyAgent(agent: AgentDefinition | undefined): boolean;
183
+ /** Family identifier for an agent (e.g. 'chatgpt' for both ChatGPT entries), or its slug when standalone. */
184
+ export declare function getAgentFamily(slug: string): string | undefined;
185
+ /** All agents sharing the given family identifier. */
186
+ export declare function getFamilyAgents(family: string): AgentDefinition[];
130
187
  /** Alias of {@link getAgent} kept for backward compatibility with CLI callers. */
131
188
  export declare const getRegistryAgent: typeof getAgent;
132
189
  /** Alias of {@link getAgents} kept for backward compatibility with CLI callers. */
@@ -246,6 +246,158 @@ const AGENT_REGISTRY = [
246
246
  ],
247
247
  },
248
248
  },
249
+ {
250
+ // ChatGPT Web. Connect-only: no local files. Shares one account/connector/
251
+ // OAuth identity with `chatgpt-app` (family: 'chatgpt'). Launches in the
252
+ // browser; chatgpt.com supports `?prompt=` for open + resume.
253
+ slug: 'chatgpt',
254
+ name: 'ChatGPT (Web)',
255
+ aliases: ['ChatGPT'],
256
+ description: "OpenAI's ChatGPT in the browser, connected to your workspace via MCP",
257
+ category: 'web',
258
+ family: 'chatgpt',
259
+ detection: { method: 'always' },
260
+ launch: { url: 'https://chatgpt.com/?prompt={prompt}' },
261
+ logo: 'openai',
262
+ downloadUrl: 'https://chatgpt.com',
263
+ firstClass: true,
264
+ resumeCapability: {
265
+ mode: 'url-prompt',
266
+ manualOpenHint: 'Opens ChatGPT with a prompt that fetches and continues the shared conversation via your Runwork workspace tools.',
267
+ },
268
+ quickStart: {
269
+ launchHint: 'Open chatgpt.com',
270
+ examplePrompts: [
271
+ "What can my team's Runwork workspace do?",
272
+ "Check my team's recent activity",
273
+ ],
274
+ },
275
+ manualSetup: {
276
+ title: 'Connect Runwork in ChatGPT',
277
+ showAfter: 'connecting',
278
+ copyValue: { label: 'Workspace MCP URL', token: '{mcpUrl}' },
279
+ // Flat steps mirror the 'personal' variant as a safe fallback when no
280
+ // plan is resolved. The onboarding UI prefers `variants` by plan.
281
+ steps: [
282
+ { id: 'open', label: 'Open chatgpt.com and sign in' },
283
+ { id: 'settings-apps', label: 'Open the profile menu (bottom-left), then Settings, then Apps' },
284
+ { id: 'dev-mode', label: 'Click "Advanced settings" and turn on "Developer mode" (skip if it is already on)' },
285
+ { id: 'create-app', label: 'Go back to Apps and click "Create app"' },
286
+ { id: 'fill', label: 'In the New App dialog, set Name to "Runwork", keep Connection on "Server URL", and paste the Workspace MCP URL above into the Server URL field' },
287
+ { id: 'oauth', label: 'Leave Authentication set to "OAuth", check "I understand and want to continue", then click Create' },
288
+ { id: 'authorize', label: 'Complete the OAuth sign-in to authorize Runwork for your account' },
289
+ { id: 'confirm', label: 'In a new chat, ask "Do you have access to Runwork tools?" to confirm (or check Settings, then Apps, for Runwork)' },
290
+ ],
291
+ variants: [
292
+ {
293
+ key: 'team',
294
+ label: 'Team / Enterprise',
295
+ title: 'Connect the Runwork app in ChatGPT',
296
+ steps: [
297
+ { id: 'open', label: 'Open chatgpt.com and sign in' },
298
+ { id: 'settings-apps', label: 'Open the profile menu (bottom-left), then Settings, then Apps' },
299
+ { id: 'find', label: 'Find the Runwork app published by your workspace admin and open it' },
300
+ { id: 'connect', label: 'Click Connect and complete the OAuth sign-in to authorize Runwork' },
301
+ { id: 'confirm', label: 'In a new chat, ask "Do you have access to Runwork tools?" to confirm (or check Settings, then Apps, for Runwork)' },
302
+ ],
303
+ },
304
+ {
305
+ key: 'personal',
306
+ label: 'Personal',
307
+ title: 'Create the Runwork connector in ChatGPT',
308
+ steps: [
309
+ { id: 'open', label: 'Open chatgpt.com and sign in' },
310
+ { id: 'settings-apps', label: 'Open the profile menu (bottom-left), then Settings, then Apps' },
311
+ { id: 'dev-mode', label: 'Click "Advanced settings" and turn on "Developer mode" (skip if it is already on)' },
312
+ { id: 'create-app', label: 'Go back to Apps and click "Create app"' },
313
+ { id: 'fill', label: 'In the New App dialog, set Name to "Runwork", keep Connection on "Server URL", and paste the Workspace MCP URL above into the Server URL field' },
314
+ { id: 'oauth', label: 'Leave Authentication set to "OAuth", check "I understand and want to continue", then click Create' },
315
+ { id: 'authorize', label: 'Complete the OAuth sign-in to authorize Runwork for your account' },
316
+ { id: 'confirm', label: 'In a new chat, ask "Do you have access to Runwork tools?" to confirm (or check Settings, then Apps, for Runwork)' },
317
+ ],
318
+ },
319
+ ],
320
+ },
321
+ },
322
+ {
323
+ // ChatGPT Desktop app. Connect-only (no local files); shares the same
324
+ // account/connector/OAuth identity as `chatgpt` Web. Launches the app,
325
+ // falling back to the web URL when the app is not installed.
326
+ slug: 'chatgpt-app',
327
+ name: 'ChatGPT Desktop',
328
+ description: "OpenAI's ChatGPT desktop app, connected to your workspace via MCP",
329
+ category: 'desktop',
330
+ family: 'chatgpt',
331
+ detection: {
332
+ method: 'any',
333
+ target: [
334
+ { method: 'path', target: { macos: '/Applications/ChatGPT.app' } },
335
+ { method: 'windows-appx', target: 'OpenAI.ChatGPT' },
336
+ ],
337
+ },
338
+ launch: { app: { macos: 'ChatGPT', windows: 'ChatGPT' }, url: 'https://chatgpt.com/?prompt={prompt}' },
339
+ logo: 'openai',
340
+ downloadUrl: 'https://chatgpt.com/download',
341
+ firstClass: true,
342
+ resumeCapability: {
343
+ mode: 'url-prompt',
344
+ manualOpenHint: 'Opens ChatGPT with a prompt that fetches and continues the shared conversation via your Runwork workspace tools.',
345
+ },
346
+ quickStart: {
347
+ launchHint: 'Open the ChatGPT desktop app',
348
+ examplePrompts: [
349
+ "What can my team's Runwork workspace do?",
350
+ "Check my team's recent activity",
351
+ ],
352
+ },
353
+ manualSetup: {
354
+ title: 'Connect Runwork in ChatGPT',
355
+ showAfter: 'connecting',
356
+ copyValue: { label: 'Workspace MCP URL', token: '{mcpUrl}' },
357
+ steps: [
358
+ { id: 'install', label: 'Install the ChatGPT desktop app from chatgpt.com/download if you have not already' },
359
+ { id: 'open-web', label: 'Connectors are created on chatgpt.com: open it in your browser and sign in' },
360
+ { id: 'settings-apps', label: 'Open the profile menu (bottom-left), then Settings, then Apps' },
361
+ { id: 'dev-mode', label: 'Click "Advanced settings" and turn on "Developer mode" (skip if it is already on)' },
362
+ { id: 'create-app', label: 'Go back to Apps and click "Create app"' },
363
+ { id: 'fill', label: 'In the New App dialog, set Name to "Runwork", keep Connection on "Server URL", and paste the Workspace MCP URL above into the Server URL field' },
364
+ { id: 'oauth', label: 'Leave Authentication set to "OAuth", check "I understand and want to continue", then click Create' },
365
+ { id: 'authorize', label: 'Complete the OAuth sign-in to authorize Runwork for your account' },
366
+ { id: 'confirm', label: 'Open the ChatGPT desktop app and, in a new chat, ask "Do you have access to Runwork tools?" to confirm' },
367
+ ],
368
+ variants: [
369
+ {
370
+ key: 'team',
371
+ label: 'Team / Enterprise',
372
+ title: 'Connect the Runwork app in ChatGPT',
373
+ steps: [
374
+ { id: 'install', label: 'Install the ChatGPT desktop app from chatgpt.com/download if you have not already' },
375
+ { id: 'open', label: 'Open the ChatGPT desktop app and sign in' },
376
+ { id: 'settings-apps', label: 'Open the profile menu (bottom-left), then Settings, then Apps' },
377
+ { id: 'find', label: 'Find the Runwork app published by your workspace admin and open it' },
378
+ { id: 'connect', label: 'Click Connect and complete the OAuth sign-in to authorize Runwork' },
379
+ { id: 'confirm', label: 'In a new chat, ask "Do you have access to Runwork tools?" to confirm (or check Settings, then Apps, for Runwork)' },
380
+ ],
381
+ },
382
+ {
383
+ key: 'personal',
384
+ label: 'Personal',
385
+ title: 'Create the Runwork connector in ChatGPT',
386
+ steps: [
387
+ { id: 'install', label: 'Install the ChatGPT desktop app from chatgpt.com/download if you have not already' },
388
+ { id: 'open-web', label: 'Connectors are created on chatgpt.com: open it in your browser and sign in' },
389
+ { id: 'settings-apps', label: 'Open the profile menu (bottom-left), then Settings, then Apps' },
390
+ { id: 'dev-mode', label: 'Click "Advanced settings" and turn on "Developer mode" (skip if it is already on)' },
391
+ { id: 'create-app', label: 'Go back to Apps and click "Create app"' },
392
+ { id: 'fill', label: 'In the New App dialog, set Name to "Runwork", keep Connection on "Server URL", and paste the Workspace MCP URL above into the Server URL field' },
393
+ { id: 'oauth', label: 'Leave Authentication set to "OAuth", check "I understand and want to continue", then click Create' },
394
+ { id: 'authorize', label: 'Complete the OAuth sign-in to authorize Runwork for your account' },
395
+ { id: 'confirm', label: 'Open the ChatGPT desktop app and, in a new chat, ask "Do you have access to Runwork tools?" to confirm' },
396
+ ],
397
+ },
398
+ ],
399
+ },
400
+ },
249
401
  {
250
402
  slug: 'windsurf',
251
403
  name: 'Windsurf',
@@ -481,6 +633,30 @@ export function isIDEAgent(slug) {
481
633
  export function isDesktopAgent(slug) {
482
634
  return getAgent(slug)?.category === 'desktop';
483
635
  }
636
+ export function isWebAgent(slug) {
637
+ return getAgent(slug)?.category === 'web';
638
+ }
639
+ /**
640
+ * Connect-only agents have no local footprint to write: no skills directory,
641
+ * no MCP config file, no instruction file. The connector lives in the user's
642
+ * account (e.g. ChatGPT). `runwork sync` skips these for all local writes.
643
+ */
644
+ export function isConnectOnlyAgent(agent) {
645
+ if (!agent)
646
+ return false;
647
+ return !agent.skillsPaths && !agent.mcpConfigPath && !agent.instructionFile;
648
+ }
649
+ /** Family identifier for an agent (e.g. 'chatgpt' for both ChatGPT entries), or its slug when standalone. */
650
+ export function getAgentFamily(slug) {
651
+ const agent = getAgent(slug);
652
+ if (!agent)
653
+ return undefined;
654
+ return agent.family ?? agent.slug;
655
+ }
656
+ /** All agents sharing the given family identifier. */
657
+ export function getFamilyAgents(family) {
658
+ return AGENT_REGISTRY.filter((a) => (a.family ?? a.slug) === family);
659
+ }
484
660
  /** Alias of {@link getAgent} kept for backward compatibility with CLI callers. */
485
661
  export const getRegistryAgent = getAgent;
486
662
  /** Alias of {@link getAgents} kept for backward compatibility with CLI callers. */
@@ -141,6 +141,11 @@ export declare class ApiClient {
141
141
  transport: 'sse' | 'streamable-http';
142
142
  }): Promise<McpServerConfig>;
143
143
  removeMcpServer(workspaceId: string, serverId: string): Promise<void>;
144
+ /**
145
+ * Resolve decrypted auth headers for header-auth MCP servers, scoped to the
146
+ * authenticated user (their own + workspace-wide credentials). Keyed by server id.
147
+ */
148
+ resolveMcpCredentials(workspaceId: string): Promise<Record<string, Record<string, string>>>;
144
149
  searchCommunitySkills(query: string, limit?: number, includeContent?: boolean): Promise<{
145
150
  query: string;
146
151
  count: number;
@@ -194,6 +194,14 @@ export class ApiClient {
194
194
  async removeMcpServer(workspaceId, serverId) {
195
195
  await this.request(`/api/workspaces/${workspaceId}/mcp-servers/${serverId}`, { method: 'DELETE' });
196
196
  }
197
+ /**
198
+ * Resolve decrypted auth headers for header-auth MCP servers, scoped to the
199
+ * authenticated user (their own + workspace-wide credentials). Keyed by server id.
200
+ */
201
+ async resolveMcpCredentials(workspaceId) {
202
+ const res = await this.request(`/api/workspaces/${workspaceId}/mcp-credentials/resolved`);
203
+ return res.data.servers;
204
+ }
197
205
  // --- Community Marketplace ---
198
206
  async searchCommunitySkills(query, limit = 10, includeContent = false) {
199
207
  const res = await this.request(`/api/community/skills/search?q=${encodeURIComponent(query)}&limit=${limit}&include_content=${includeContent}`);