runwork 0.6.2 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/install-scripts.test.d.ts +1 -0
- package/dist/__tests__/install-scripts.test.js +247 -0
- package/dist/agents/__tests__/claude-code-plugin.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-code-plugin.test.js +47 -0
- package/dist/agents/__tests__/claude-code-stats.test.js +143 -78
- package/dist/agents/__tests__/claude-desktop-plugin-tree.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-desktop-plugin-tree.test.js +161 -0
- package/dist/agents/__tests__/claude-desktop-plugin.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-desktop-plugin.test.js +117 -0
- package/dist/agents/__tests__/claude-desktop-rpm.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-desktop-rpm.test.js +126 -0
- package/dist/agents/__tests__/codex-instructions.test.d.ts +1 -0
- package/dist/agents/__tests__/codex-instructions.test.js +57 -0
- package/dist/agents/__tests__/codex-stats.test.js +9 -0
- package/dist/agents/__tests__/graceful-degradation.test.js +3 -3
- package/dist/agents/__tests__/intro-skill.test.js +11 -2
- package/dist/agents/__tests__/skill-slug.test.d.ts +1 -0
- package/dist/agents/__tests__/skill-slug.test.js +77 -0
- package/dist/agents/claude-code.d.ts +3 -1
- package/dist/agents/claude-code.js +233 -58
- package/dist/agents/claude-desktop-plugin-tree.d.ts +44 -0
- package/dist/agents/claude-desktop-plugin-tree.js +92 -0
- package/dist/agents/claude-desktop.d.ts +20 -1
- package/dist/agents/claude-desktop.js +238 -61
- package/dist/agents/cline.d.ts +3 -1
- package/dist/agents/cline.js +44 -2
- package/dist/agents/codex.d.ts +3 -1
- package/dist/agents/codex.js +85 -14
- package/dist/agents/cursor.d.ts +12 -1
- package/dist/agents/cursor.js +98 -25
- package/dist/agents/gemini.d.ts +3 -1
- package/dist/agents/gemini.js +42 -4
- package/dist/agents/generic-adapter.d.ts +3 -1
- package/dist/agents/generic-adapter.js +59 -4
- package/dist/agents/intro-skill.js +77 -31
- package/dist/agents/types.d.ts +42 -2
- package/dist/agents/types.js +61 -6
- package/dist/agents/utils/instruction-hint.d.ts +10 -0
- package/dist/agents/utils/instruction-hint.js +46 -0
- package/dist/agents/utils/json-config.d.ts +6 -1
- package/dist/agents/utils/json-config.js +31 -4
- package/dist/agents/windsurf.d.ts +3 -1
- package/dist/agents/windsurf.js +37 -2
- package/dist/commands/__tests__/sync-telemetry.test.d.ts +1 -0
- package/dist/commands/__tests__/sync-telemetry.test.js +281 -0
- package/dist/commands/__tests__/upgrade.test.d.ts +1 -0
- package/dist/commands/__tests__/upgrade.test.js +174 -0
- package/dist/commands/build-plugin.d.ts +2 -0
- package/dist/commands/build-plugin.js +135 -0
- package/dist/commands/init.d.ts +8 -2
- package/dist/commands/init.js +19 -7
- package/dist/commands/sync-telemetry.d.ts +33 -0
- package/dist/commands/sync-telemetry.js +186 -0
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +84 -118
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.js +145 -0
- package/dist/commands/upgrade.d.ts +12 -0
- package/dist/commands/upgrade.js +67 -16
- package/dist/generated/bundled-types.js +36 -36
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/health/__tests__/cli-distribution-checks.test.d.ts +1 -0
- package/dist/health/__tests__/cli-distribution-checks.test.js +199 -0
- package/dist/health/checks.d.ts +2 -0
- package/dist/health/checks.js +131 -18
- package/dist/health/runner.js +6 -2
- package/dist/index.js +4 -0
- package/dist/sync/executor.d.ts +2 -0
- package/dist/sync/executor.js +8 -4
- package/dist/types.d.ts +2 -0
- package/dist/ui/banner.js +15 -6
- package/dist/utils/__tests__/sqlite.test.js +84 -72
- package/dist/utils/sqlite-adapter.d.ts +48 -0
- package/dist/utils/sqlite-adapter.js +147 -0
- package/dist/utils/sqlite.d.ts +12 -5
- package/dist/utils/sqlite.js +12 -22
- package/dist/utils/zip.d.ts +6 -0
- package/dist/utils/zip.js +28 -3
- package/package.json +2 -4
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `runwork upgrade`.
|
|
3
|
+
*
|
|
4
|
+
* The module under test has top-level side effects (it builds a Commander
|
|
5
|
+
* command and captures fetch/process references), so we exercise the exported
|
|
6
|
+
* __internal helpers against a local HTTP server that serves a fake
|
|
7
|
+
* /cli/latest.json.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import { createServer } from 'node:http';
|
|
11
|
+
import { __internal } from '../upgrade.js';
|
|
12
|
+
function startJsonServer(body, status = 200) {
|
|
13
|
+
return new Promise((resolvePromise) => {
|
|
14
|
+
const server = createServer((req, res) => {
|
|
15
|
+
if (req.url === '/cli/latest.json') {
|
|
16
|
+
if (body === null) {
|
|
17
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
18
|
+
res.end('{"error": "not found"}');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
res.writeHead(status, {
|
|
22
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
23
|
+
});
|
|
24
|
+
res.end(JSON.stringify(body));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
res.writeHead(404);
|
|
28
|
+
res.end('not found');
|
|
29
|
+
});
|
|
30
|
+
server.listen(0, '127.0.0.1', () => {
|
|
31
|
+
const addr = server.address();
|
|
32
|
+
if (addr && typeof addr === 'object') {
|
|
33
|
+
const url = `http://127.0.0.1:${addr.port}`;
|
|
34
|
+
const stop = () => new Promise((resolveStop) => {
|
|
35
|
+
server.close(() => resolveStop());
|
|
36
|
+
});
|
|
37
|
+
resolvePromise({ url, stop });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
describe('__internal.fetchLatestVersion', () => {
|
|
43
|
+
let originalBase;
|
|
44
|
+
let originalFetch;
|
|
45
|
+
let serverStop = null;
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
originalBase = process.env.RUNWORK_DOWNLOAD_BASE_URL;
|
|
48
|
+
originalFetch = globalThis.fetch;
|
|
49
|
+
});
|
|
50
|
+
afterEach(async () => {
|
|
51
|
+
if (serverStop)
|
|
52
|
+
await serverStop();
|
|
53
|
+
serverStop = null;
|
|
54
|
+
globalThis.fetch = originalFetch;
|
|
55
|
+
if (originalBase === undefined)
|
|
56
|
+
delete process.env.RUNWORK_DOWNLOAD_BASE_URL;
|
|
57
|
+
else
|
|
58
|
+
process.env.RUNWORK_DOWNLOAD_BASE_URL = originalBase;
|
|
59
|
+
});
|
|
60
|
+
it('parses the version field from a valid manifest', async () => {
|
|
61
|
+
const server = await startJsonServer({
|
|
62
|
+
name: 'runwork',
|
|
63
|
+
version: '1.2.3',
|
|
64
|
+
publishedAt: '2026-04-14T00:00:00Z',
|
|
65
|
+
artifacts: {},
|
|
66
|
+
});
|
|
67
|
+
serverStop = server.stop;
|
|
68
|
+
// Override fetch URL by intercepting via globalThis.fetch
|
|
69
|
+
const realFetch = globalThis.fetch;
|
|
70
|
+
globalThis.fetch = ((input, init) => {
|
|
71
|
+
const url = typeof input === 'string' ? input : input.toString();
|
|
72
|
+
if (url === __internal.LATEST_JSON_URL) {
|
|
73
|
+
return realFetch(`${server.url}/cli/latest.json`, init);
|
|
74
|
+
}
|
|
75
|
+
return realFetch(input, init);
|
|
76
|
+
});
|
|
77
|
+
const version = await __internal.fetchLatestVersion();
|
|
78
|
+
expect(version).toBe('1.2.3');
|
|
79
|
+
});
|
|
80
|
+
it('strips a leading v from the version', async () => {
|
|
81
|
+
const server = await startJsonServer({ version: 'v9.9.9' });
|
|
82
|
+
serverStop = server.stop;
|
|
83
|
+
const realFetch = globalThis.fetch;
|
|
84
|
+
globalThis.fetch = ((input, init) => {
|
|
85
|
+
const url = typeof input === 'string' ? input : input.toString();
|
|
86
|
+
if (url === __internal.LATEST_JSON_URL) {
|
|
87
|
+
return realFetch(`${server.url}/cli/latest.json`, init);
|
|
88
|
+
}
|
|
89
|
+
return realFetch(input, init);
|
|
90
|
+
});
|
|
91
|
+
const version = await __internal.fetchLatestVersion();
|
|
92
|
+
expect(version).toBe('9.9.9');
|
|
93
|
+
});
|
|
94
|
+
it('returns null on HTTP error', async () => {
|
|
95
|
+
const server = await startJsonServer(null, 500);
|
|
96
|
+
serverStop = server.stop;
|
|
97
|
+
const realFetch = globalThis.fetch;
|
|
98
|
+
globalThis.fetch = ((input, init) => {
|
|
99
|
+
const url = typeof input === 'string' ? input : input.toString();
|
|
100
|
+
if (url === __internal.LATEST_JSON_URL) {
|
|
101
|
+
return realFetch(`${server.url}/cli/latest.json`, init);
|
|
102
|
+
}
|
|
103
|
+
return realFetch(input, init);
|
|
104
|
+
});
|
|
105
|
+
const version = await __internal.fetchLatestVersion();
|
|
106
|
+
expect(version).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
it('returns null when fetch throws', async () => {
|
|
109
|
+
globalThis.fetch = (() => {
|
|
110
|
+
throw new Error('network down');
|
|
111
|
+
});
|
|
112
|
+
const version = await __internal.fetchLatestVersion();
|
|
113
|
+
expect(version).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
it('returns null when the manifest has no version field', async () => {
|
|
116
|
+
const server = await startJsonServer({ name: 'runwork' });
|
|
117
|
+
serverStop = server.stop;
|
|
118
|
+
const realFetch = globalThis.fetch;
|
|
119
|
+
globalThis.fetch = ((input, init) => {
|
|
120
|
+
const url = typeof input === 'string' ? input : input.toString();
|
|
121
|
+
if (url === __internal.LATEST_JSON_URL) {
|
|
122
|
+
return realFetch(`${server.url}/cli/latest.json`, init);
|
|
123
|
+
}
|
|
124
|
+
return realFetch(input, init);
|
|
125
|
+
});
|
|
126
|
+
const version = await __internal.fetchLatestVersion();
|
|
127
|
+
expect(version).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('__internal endpoint constants', () => {
|
|
131
|
+
it('all endpoints live on runwork.ai', () => {
|
|
132
|
+
expect(__internal.BASE_URL).toBe('https://runwork.ai');
|
|
133
|
+
expect(__internal.LATEST_JSON_URL).toBe('https://runwork.ai/cli/latest.json');
|
|
134
|
+
expect(__internal.INSTALL_SH_URL).toBe('https://runwork.ai/install.sh');
|
|
135
|
+
expect(__internal.INSTALL_PS1_URL).toBe('https://runwork.ai/install.ps1');
|
|
136
|
+
});
|
|
137
|
+
it('does not reference api.github.com or github.com/.../releases', () => {
|
|
138
|
+
const values = Object.values(__internal).filter((v) => typeof v === 'string');
|
|
139
|
+
for (const v of values) {
|
|
140
|
+
expect(v).not.toContain('api.github.com');
|
|
141
|
+
expect(v).not.toContain('/releases');
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe('__internal.detectInstallMethod', () => {
|
|
146
|
+
const originalArgv1 = process.argv[1];
|
|
147
|
+
afterEach(() => {
|
|
148
|
+
process.argv[1] = originalArgv1;
|
|
149
|
+
});
|
|
150
|
+
it('detects homebrew installs', () => {
|
|
151
|
+
process.argv[1] = '/opt/homebrew/Cellar/runwork/0.6.2/bin/runwork';
|
|
152
|
+
expect(__internal.detectInstallMethod()).toBe('brew');
|
|
153
|
+
});
|
|
154
|
+
it('detects bun global installs', () => {
|
|
155
|
+
process.argv[1] = '/Users/user/.bun/install/global/node_modules/runwork/bin/runwork.js';
|
|
156
|
+
expect(__internal.detectInstallMethod()).toBe('bun');
|
|
157
|
+
});
|
|
158
|
+
it('detects pnpm global installs', () => {
|
|
159
|
+
process.argv[1] = '/Users/user/.local/share/pnpm/global/5/node_modules/runwork/bin/runwork.js';
|
|
160
|
+
expect(__internal.detectInstallMethod()).toBe('pnpm');
|
|
161
|
+
});
|
|
162
|
+
it('detects yarn global installs', () => {
|
|
163
|
+
process.argv[1] = '/Users/user/.yarn/bin/runwork';
|
|
164
|
+
expect(__internal.detectInstallMethod()).toBe('yarn');
|
|
165
|
+
});
|
|
166
|
+
it('detects volta installs', () => {
|
|
167
|
+
process.argv[1] = '/Users/user/.volta/tools/image/packages/runwork/bin/runwork';
|
|
168
|
+
expect(__internal.detectInstallMethod()).toBe('volta');
|
|
169
|
+
});
|
|
170
|
+
it('falls back to binary for standalone paths', () => {
|
|
171
|
+
process.argv[1] = '/Users/user/.runwork/bin/runwork';
|
|
172
|
+
expect(__internal.detectInstallMethod()).toBe('binary');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { resolve, join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { requireAuth } from '../auth/store.js';
|
|
6
|
+
import { ApiClient } from '../api/client.js';
|
|
7
|
+
import { getAdapterBySlug } from '../agents/detect.js';
|
|
8
|
+
import { RUNWORK_MCP_PREFIX, RUNWORK_WORKSPACE_MCP_NAME } from '../agents/types.js';
|
|
9
|
+
import { generateIntroSkill, buildAppSkillDescription } from '../agents/intro-skill.js';
|
|
10
|
+
function loadSetupState(filePath) {
|
|
11
|
+
if (!existsSync(filePath))
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export const buildPluginCommand = new Command('build-plugin')
|
|
21
|
+
.description('Build an installable plugin archive for an agent (e.g. Claude Desktop)')
|
|
22
|
+
.requiredOption('--agent <slug>', 'Target agent slug (e.g. claude-desktop)')
|
|
23
|
+
.option('--output <path>', 'Output zip path', 'runwork-plugin.zip')
|
|
24
|
+
.action(async (opts) => {
|
|
25
|
+
const adapter = getAdapterBySlug(opts.agent);
|
|
26
|
+
if (!adapter) {
|
|
27
|
+
console.error(`Unknown agent: ${opts.agent}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
if (!adapter.buildPluginZip) {
|
|
31
|
+
console.error(`Agent "${adapter.name}" does not support plugin zip build.`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const credentials = requireAuth();
|
|
35
|
+
// Find the first available setup state so we know which workspace to
|
|
36
|
+
// pull content from. We don't care about project vs user scope here —
|
|
37
|
+
// plugin archives are always user-scoped for the agent they're uploaded to.
|
|
38
|
+
const projectStatePath = join(process.cwd(), '.runwork', 'setup.json');
|
|
39
|
+
const userStatePath = join(homedir(), '.runwork', 'setup.json');
|
|
40
|
+
const state = loadSetupState(projectStatePath) ?? loadSetupState(userStatePath);
|
|
41
|
+
if (!state) {
|
|
42
|
+
console.error('No setup state found. Run `runwork setup` first.');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
const client = new ApiClient(credentials);
|
|
46
|
+
// Refresh workspace name/slug if missing or stale (matches sync behavior).
|
|
47
|
+
if (!state.workspaceName || state.workspaceName === state.workspaceId || !state.workspaceSlug) {
|
|
48
|
+
try {
|
|
49
|
+
const workspaces = await client.listWorkspaces();
|
|
50
|
+
const ws = workspaces.find((w) => w.id === state.workspaceId);
|
|
51
|
+
if (ws?.name)
|
|
52
|
+
state.workspaceName = ws.name;
|
|
53
|
+
if (ws?.slug)
|
|
54
|
+
state.workspaceSlug = ws.slug;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* best-effort */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
console.log(`Building plugin for ${adapter.name} from workspace ${state.workspaceName || state.workspaceId}...`);
|
|
61
|
+
// Fetch the same inputs sync uses. The plugin archive must contain
|
|
62
|
+
// identical content to what sync would write into the plugin directory,
|
|
63
|
+
// including workspace team instructions + per-agent extra instructions.
|
|
64
|
+
const [allSkills, mcpServers, externalSkills, registries, connectedIntegrations, onboardingConfig,] = await Promise.all([
|
|
65
|
+
client.listWorkspaceSkills(state.workspaceId, true),
|
|
66
|
+
client.listMcpServers(state.workspaceId),
|
|
67
|
+
client.listExternalSkills(state.workspaceId),
|
|
68
|
+
client.getWorkspaceAll(state.workspaceId).catch(() => null),
|
|
69
|
+
client
|
|
70
|
+
.listConnectedIntegrations(state.workspaceId)
|
|
71
|
+
.then((list) => list.map((i) => i.canonicalId ?? i.integrationId))
|
|
72
|
+
.catch(() => []),
|
|
73
|
+
client.getOnboardingConfig(state.workspaceId).catch(() => null),
|
|
74
|
+
]);
|
|
75
|
+
const baseUrl = credentials.baseUrl || 'https://runwork.ai';
|
|
76
|
+
const mcpEntries = mcpServers
|
|
77
|
+
.filter((s) => s.enabled)
|
|
78
|
+
.map((s) => ({
|
|
79
|
+
name: `${RUNWORK_MCP_PREFIX}${s.name}`,
|
|
80
|
+
url: s.url,
|
|
81
|
+
transport: s.transport,
|
|
82
|
+
headers: { Authorization: `Bearer ${credentials.apiKey}` },
|
|
83
|
+
description: `Runwork MCP server: ${s.name}. Provides tools for interacting with this workspace resource.`,
|
|
84
|
+
}));
|
|
85
|
+
mcpEntries.push({
|
|
86
|
+
name: RUNWORK_WORKSPACE_MCP_NAME,
|
|
87
|
+
url: `${baseUrl}/api/workspaces/${state.workspaceId}/mcp`,
|
|
88
|
+
transport: 'streamable-http',
|
|
89
|
+
headers: { Authorization: `Bearer ${credentials.apiKey}` },
|
|
90
|
+
description: "Runwork workspace tools: query entities, trigger workflows, manage integrations, run agents, and access shared data across your team's apps.",
|
|
91
|
+
});
|
|
92
|
+
const remoteSkills = [];
|
|
93
|
+
for (const s of externalSkills) {
|
|
94
|
+
remoteSkills.push({ name: s.name, content: s.content, source: 'external' });
|
|
95
|
+
}
|
|
96
|
+
for (const s of allSkills) {
|
|
97
|
+
if (s.type === 'app' && s.appId && s.content?.trim()) {
|
|
98
|
+
remoteSkills.push({ name: s.name, content: s.content, source: 'app', appId: s.appId });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const introSkill = generateIntroSkill({
|
|
102
|
+
workspaceName: state.workspaceName || state.workspaceId,
|
|
103
|
+
workspaceId: state.workspaceId,
|
|
104
|
+
workspaceSlug: state.workspaceSlug,
|
|
105
|
+
registries,
|
|
106
|
+
connectedIntegrations,
|
|
107
|
+
mcpServerCount: mcpEntries.length,
|
|
108
|
+
skillCount: remoteSkills.length,
|
|
109
|
+
mcpServerNames: mcpEntries.map((e) => e.name.replace(RUNWORK_MCP_PREFIX, '')),
|
|
110
|
+
skillNames: remoteSkills.map((s) => s.name),
|
|
111
|
+
});
|
|
112
|
+
// When the adapter exposes skills through MCP already, skip duplicating
|
|
113
|
+
// app skills into the plugin — same rule sync applies per-adapter.
|
|
114
|
+
const skipAppSkills = adapter.mcpProvidesSkills && mcpEntries.length > 0;
|
|
115
|
+
const skillFiles = [
|
|
116
|
+
introSkill,
|
|
117
|
+
...remoteSkills
|
|
118
|
+
.filter((s) => !(skipAppSkills && s.source === 'app'))
|
|
119
|
+
.map((s) => ({
|
|
120
|
+
name: s.name,
|
|
121
|
+
filename: s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
|
122
|
+
content: s.content,
|
|
123
|
+
description: s.source === 'app'
|
|
124
|
+
? buildAppSkillDescription(s.name, registries) || `${s.name} - Runwork workspace application`
|
|
125
|
+
: `${s.name} - Runwork workspace skill`,
|
|
126
|
+
})),
|
|
127
|
+
];
|
|
128
|
+
// Merge team instructions + per-agent extra instructions (same rule as sync).
|
|
129
|
+
const teamInstructions = onboardingConfig?.config?.teamInstructions;
|
|
130
|
+
const agentExtra = onboardingConfig?.config?.agentConfigs?.[adapter.slug]?.extraInstructions;
|
|
131
|
+
const mergedInstructions = [teamInstructions, agentExtra].filter(Boolean).join('\n\n') || null;
|
|
132
|
+
const outputPath = resolve(opts.output);
|
|
133
|
+
await adapter.buildPluginZip(skillFiles, mcpEntries, mergedInstructions, outputPath);
|
|
134
|
+
console.log(`Wrote ${outputPath}`);
|
|
135
|
+
});
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -9,7 +9,13 @@ export interface InitResult {
|
|
|
9
9
|
workspaceId: string;
|
|
10
10
|
workspaceName: string;
|
|
11
11
|
}
|
|
12
|
-
export
|
|
12
|
+
export interface ExecInitOptions {
|
|
13
|
+
/** When true, create the app in cwd/<slug> instead of ~/.runwork/<slug>. */
|
|
14
|
+
here?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Default parent directory for new apps when --here is not passed. */
|
|
17
|
+
export declare const DEFAULT_APPS_DIR: string;
|
|
18
|
+
export declare function execInit(client: ApiClient, appName: string, workspace: WorkspaceInfo, options?: ExecInitOptions): Promise<InitResult>;
|
|
13
19
|
/** Full create flow: prompt for name/workspace, init, and run agent wizard */
|
|
14
|
-
export declare function runCreateFlow(name?: string, workspaceFlag?: string): Promise<void>;
|
|
20
|
+
export declare function runCreateFlow(name?: string, workspaceFlag?: string, options?: ExecInitOptions): Promise<void>;
|
|
15
21
|
export declare const initCommand: Command;
|
package/dist/commands/init.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { execFileSync } from 'child_process';
|
|
3
3
|
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
4
4
|
import { join, resolve } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
5
6
|
import { requireAuth } from '../auth/store.js';
|
|
6
7
|
import { ApiClient } from '../api/client.js';
|
|
7
8
|
import { promptSelect, promptInput } from '../utils/prompt.js';
|
|
@@ -12,15 +13,25 @@ import { removeNestedGitDirs } from '../utils/fs.js';
|
|
|
12
13
|
import { runAgentWizard } from '../ui/banner.js';
|
|
13
14
|
import { shouldOutputJson, jsonOut } from '../utils/output.js';
|
|
14
15
|
import { buildInitGuide, buildErrorResponse } from '../utils/agent-guidance.js';
|
|
15
|
-
|
|
16
|
+
/** Default parent directory for new apps when --here is not passed. */
|
|
17
|
+
export const DEFAULT_APPS_DIR = join(homedir(), '.runwork', 'apps');
|
|
18
|
+
export async function execInit(client, appName, workspace, options = {}) {
|
|
16
19
|
const app = await client.initApp(workspace.id, appName);
|
|
17
20
|
const slug = app.slug || appName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
18
|
-
const
|
|
21
|
+
const parentDir = options.here ? process.cwd() : DEFAULT_APPS_DIR;
|
|
22
|
+
if (!options.here && !existsSync(parentDir)) {
|
|
23
|
+
mkdirSync(parentDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
const dir = join(parentDir, slug);
|
|
19
26
|
if (existsSync(dir)) {
|
|
20
27
|
console.error(`Directory "${dir}" already exists.`);
|
|
21
28
|
process.exit(1);
|
|
22
29
|
}
|
|
23
30
|
mkdirSync(dir, { recursive: true });
|
|
31
|
+
if (!options.here) {
|
|
32
|
+
console.log(`Creating app in ${dir}`);
|
|
33
|
+
console.log('(Pass --here to scaffold in the current directory instead.)');
|
|
34
|
+
}
|
|
24
35
|
// Download and extract skeleton template first
|
|
25
36
|
console.log('Downloading project template...');
|
|
26
37
|
try {
|
|
@@ -68,7 +79,7 @@ export async function execInit(client, appName, workspace) {
|
|
|
68
79
|
// Nothing to commit is fine
|
|
69
80
|
}
|
|
70
81
|
execFileSync('git', ['push', '-u', 'runwork', 'main'], { cwd: dir, stdio: 'pipe' });
|
|
71
|
-
console.log(`\nApp "${app.name}" initialized in ${dir}
|
|
82
|
+
console.log(`\nApp "${app.name}" initialized in ${dir}`);
|
|
72
83
|
return {
|
|
73
84
|
appId: app.id,
|
|
74
85
|
appName: app.name,
|
|
@@ -79,7 +90,7 @@ export async function execInit(client, appName, workspace) {
|
|
|
79
90
|
};
|
|
80
91
|
}
|
|
81
92
|
/** Full create flow: prompt for name/workspace, init, and run agent wizard */
|
|
82
|
-
export async function runCreateFlow(name, workspaceFlag) {
|
|
93
|
+
export async function runCreateFlow(name, workspaceFlag, options = {}) {
|
|
83
94
|
const useJson = shouldOutputJson(undefined);
|
|
84
95
|
const creds = requireAuth();
|
|
85
96
|
const client = new ApiClient(creds);
|
|
@@ -117,13 +128,13 @@ export async function runCreateFlow(name, workspaceFlag) {
|
|
|
117
128
|
if (!useJson) {
|
|
118
129
|
console.log(`Creating "${appName}" in workspace "${workspace.name}"...`);
|
|
119
130
|
}
|
|
120
|
-
const initResult = await execInit(client, appName, workspace);
|
|
131
|
+
const initResult = await execInit(client, appName, workspace, options);
|
|
121
132
|
if (useJson) {
|
|
122
133
|
const response = {
|
|
123
134
|
success: true,
|
|
124
135
|
command: 'init',
|
|
125
136
|
result: initResult,
|
|
126
|
-
guide: buildInitGuide(initResult.appName, initResult.
|
|
137
|
+
guide: buildInitGuide(initResult.appName, initResult.directory),
|
|
127
138
|
};
|
|
128
139
|
jsonOut(response);
|
|
129
140
|
return;
|
|
@@ -135,6 +146,7 @@ export const initCommand = new Command('init')
|
|
|
135
146
|
.description('Create a new Runwork app')
|
|
136
147
|
.argument('[name]', 'App name')
|
|
137
148
|
.option('--workspace <name-or-id>', 'Workspace name or ID (skips interactive selection)')
|
|
149
|
+
.option('--here', `Scaffold in the current directory instead of ${DEFAULT_APPS_DIR}/<slug>`)
|
|
138
150
|
.action(async (name, options) => {
|
|
139
|
-
await runCreateFlow(name, options?.workspace);
|
|
151
|
+
await runCreateFlow(name, options?.workspace, { here: options?.here });
|
|
140
152
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AgentAdapter, AgentUsageStats, SkillUsageEntry } from '../agents/types.js';
|
|
2
|
+
import type { SetupState } from '../types.js';
|
|
3
|
+
export interface TelemetryEvent {
|
|
4
|
+
eventType: string;
|
|
5
|
+
agentSlug?: string;
|
|
6
|
+
metadata?: Record<string, unknown>;
|
|
7
|
+
timestamp: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TelemetryAdapterResult {
|
|
10
|
+
slug: string;
|
|
11
|
+
name: string;
|
|
12
|
+
stats: AgentUsageStats | null | 'unsupported';
|
|
13
|
+
skills: SkillUsageEntry[] | null | 'unsupported';
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface TelemetryCollection {
|
|
17
|
+
events: TelemetryEvent[];
|
|
18
|
+
adapterResults: TelemetryAdapterResult[];
|
|
19
|
+
newlyReportedAgents: string[];
|
|
20
|
+
healthReported: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface CollectTelemetryParams {
|
|
23
|
+
adapters: AgentAdapter[];
|
|
24
|
+
state: SetupState;
|
|
25
|
+
syncedSkillsCount: number;
|
|
26
|
+
syncedMcpServersCount: number;
|
|
27
|
+
teamInstructionsApplied: boolean;
|
|
28
|
+
agentConfigsApplied: number;
|
|
29
|
+
now?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function collectTelemetryEvents(params: CollectTelemetryParams): Promise<TelemetryCollection>;
|
|
32
|
+
export declare function printTelemetryVerbose(result: TelemetryCollection): void;
|
|
33
|
+
export declare function summarizeTelemetryForDryRun(result: TelemetryCollection): string;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
export async function collectTelemetryEvents(params) {
|
|
2
|
+
const now = params.now ?? new Date().toISOString();
|
|
3
|
+
const events = [];
|
|
4
|
+
const adapterResults = [];
|
|
5
|
+
const previouslyReported = new Set(params.state.reportedAgentSlugs ?? []);
|
|
6
|
+
const newlyReportedAgents = [];
|
|
7
|
+
for (const slug of params.state.configuredAgents) {
|
|
8
|
+
if (!previouslyReported.has(slug)) {
|
|
9
|
+
events.push({
|
|
10
|
+
eventType: 'local_agent.detected',
|
|
11
|
+
agentSlug: slug,
|
|
12
|
+
metadata: { installed: true },
|
|
13
|
+
timestamp: now,
|
|
14
|
+
});
|
|
15
|
+
newlyReportedAgents.push(slug);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (const adapter of params.adapters) {
|
|
19
|
+
let stats = 'unsupported';
|
|
20
|
+
let skills = 'unsupported';
|
|
21
|
+
let error;
|
|
22
|
+
if (adapter.readUsageStats) {
|
|
23
|
+
try {
|
|
24
|
+
stats = await adapter.readUsageStats(params.state.lastSyncAt ?? null);
|
|
25
|
+
if (stats && stats.hasNewActivity) {
|
|
26
|
+
events.push({
|
|
27
|
+
eventType: 'local_agent.usage',
|
|
28
|
+
agentSlug: adapter.slug,
|
|
29
|
+
metadata: {
|
|
30
|
+
sessionCount: stats.sessionCount,
|
|
31
|
+
messageCount: stats.messageCount,
|
|
32
|
+
toolCallCount: stats.toolCallCount,
|
|
33
|
+
tokensUsed: stats.tokensUsed,
|
|
34
|
+
aiLinesAdded: stats.aiLinesAdded,
|
|
35
|
+
aiLinesRemoved: stats.aiLinesRemoved,
|
|
36
|
+
lastActiveAt: stats.lastActiveAt,
|
|
37
|
+
modelsUsed: stats.modelsUsed,
|
|
38
|
+
mcpToolCount: stats.mcpToolCount,
|
|
39
|
+
...stats.extra,
|
|
40
|
+
},
|
|
41
|
+
timestamp: now,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
error = err instanceof Error ? err.message : String(err);
|
|
47
|
+
stats = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (adapter.readSkillUsage) {
|
|
51
|
+
try {
|
|
52
|
+
skills = await adapter.readSkillUsage(params.state.lastSyncAt ?? null);
|
|
53
|
+
if (skills && skills.length > 0) {
|
|
54
|
+
for (const entry of skills) {
|
|
55
|
+
events.push({
|
|
56
|
+
eventType: 'local_agent.skill_used',
|
|
57
|
+
agentSlug: adapter.slug,
|
|
58
|
+
metadata: {
|
|
59
|
+
skillName: entry.skillName,
|
|
60
|
+
count: entry.count,
|
|
61
|
+
lastUsedAt: entry.lastUsedAt,
|
|
62
|
+
},
|
|
63
|
+
timestamp: now,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
70
|
+
error = error ? `${error}; skills: ${msg}` : `skills: ${msg}`;
|
|
71
|
+
skills = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
adapterResults.push({
|
|
75
|
+
slug: adapter.slug,
|
|
76
|
+
name: adapter.name,
|
|
77
|
+
stats,
|
|
78
|
+
skills,
|
|
79
|
+
error,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const lastHealthReport = params.state.lastHealthReportAt
|
|
83
|
+
? new Date(params.state.lastHealthReportAt)
|
|
84
|
+
: null;
|
|
85
|
+
const shouldReportHealth = !lastHealthReport ||
|
|
86
|
+
Date.now() - lastHealthReport.getTime() > 24 * 60 * 60 * 1000;
|
|
87
|
+
let healthReported = false;
|
|
88
|
+
if (shouldReportHealth) {
|
|
89
|
+
for (const adapter of params.adapters) {
|
|
90
|
+
try {
|
|
91
|
+
const version = await adapter.readVersion?.();
|
|
92
|
+
events.push({
|
|
93
|
+
eventType: 'local_agent.health',
|
|
94
|
+
agentSlug: adapter.slug,
|
|
95
|
+
metadata: {
|
|
96
|
+
version,
|
|
97
|
+
installed: true,
|
|
98
|
+
platform: process.platform,
|
|
99
|
+
arch: process.arch,
|
|
100
|
+
},
|
|
101
|
+
timestamp: now,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Health reporting is best-effort
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
healthReported = true;
|
|
109
|
+
}
|
|
110
|
+
events.push({
|
|
111
|
+
eventType: 'local_agent.sync_completed',
|
|
112
|
+
metadata: {
|
|
113
|
+
syncedSkills: params.syncedSkillsCount,
|
|
114
|
+
syncedMcpServers: params.syncedMcpServersCount,
|
|
115
|
+
teamInstructionsApplied: params.teamInstructionsApplied,
|
|
116
|
+
agentConfigsApplied: params.agentConfigsApplied,
|
|
117
|
+
},
|
|
118
|
+
timestamp: now,
|
|
119
|
+
});
|
|
120
|
+
return { events, adapterResults, newlyReportedAgents, healthReported };
|
|
121
|
+
}
|
|
122
|
+
function formatStatsLine(stats) {
|
|
123
|
+
const parts = [];
|
|
124
|
+
parts.push(`${stats.sessionCount} session${stats.sessionCount === 1 ? '' : 's'}`);
|
|
125
|
+
if (stats.messageCount)
|
|
126
|
+
parts.push(`${stats.messageCount} msgs`);
|
|
127
|
+
if (stats.toolCallCount)
|
|
128
|
+
parts.push(`${stats.toolCallCount} tool calls`);
|
|
129
|
+
if (stats.tokensUsed)
|
|
130
|
+
parts.push(`${stats.tokensUsed} tokens`);
|
|
131
|
+
if (stats.aiLinesAdded || stats.aiLinesRemoved) {
|
|
132
|
+
parts.push(`+${stats.aiLinesAdded}/-${stats.aiLinesRemoved} lines`);
|
|
133
|
+
}
|
|
134
|
+
if (stats.mcpToolCount)
|
|
135
|
+
parts.push(`${stats.mcpToolCount} mcp tools`);
|
|
136
|
+
if (stats.modelsUsed && stats.modelsUsed.length > 0) {
|
|
137
|
+
parts.push(`models: ${stats.modelsUsed.join(',')}`);
|
|
138
|
+
}
|
|
139
|
+
if (stats.lastActiveAt)
|
|
140
|
+
parts.push(`last ${stats.lastActiveAt}`);
|
|
141
|
+
return parts.join(', ');
|
|
142
|
+
}
|
|
143
|
+
export function printTelemetryVerbose(result) {
|
|
144
|
+
console.log('\n Telemetry collection:');
|
|
145
|
+
for (const r of result.adapterResults) {
|
|
146
|
+
let statsLabel;
|
|
147
|
+
if (r.stats === 'unsupported') {
|
|
148
|
+
statsLabel = 'no stats reader';
|
|
149
|
+
}
|
|
150
|
+
else if (r.stats === null || !r.stats.hasNewActivity) {
|
|
151
|
+
statsLabel = 'no new activity';
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
statsLabel = formatStatsLine(r.stats);
|
|
155
|
+
}
|
|
156
|
+
let skillsLabel;
|
|
157
|
+
if (r.skills === 'unsupported') {
|
|
158
|
+
skillsLabel = 'no skill reader';
|
|
159
|
+
}
|
|
160
|
+
else if (r.skills === null || r.skills.length === 0) {
|
|
161
|
+
skillsLabel = 'none';
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
skillsLabel = `${r.skills.length} skill${r.skills.length === 1 ? '' : 's'}`;
|
|
165
|
+
}
|
|
166
|
+
const errSuffix = r.error ? ` (error: ${r.error})` : '';
|
|
167
|
+
console.log(` [${r.name}] stats: ${statsLabel}; skills: ${skillsLabel}${errSuffix}`);
|
|
168
|
+
}
|
|
169
|
+
if (result.newlyReportedAgents.length > 0) {
|
|
170
|
+
console.log(`\n First-time detections: ${result.newlyReportedAgents.join(', ')}`);
|
|
171
|
+
}
|
|
172
|
+
if (result.healthReported) {
|
|
173
|
+
console.log(' Health report: due (daily)');
|
|
174
|
+
}
|
|
175
|
+
console.log(`\n Events to send (${result.events.length}):`);
|
|
176
|
+
const json = JSON.stringify(result.events, null, 2);
|
|
177
|
+
console.log(json
|
|
178
|
+
.split('\n')
|
|
179
|
+
.map(l => ' ' + l)
|
|
180
|
+
.join('\n'));
|
|
181
|
+
}
|
|
182
|
+
export function summarizeTelemetryForDryRun(result) {
|
|
183
|
+
const activeCount = result.adapterResults.filter(r => r.stats && r.stats !== 'unsupported' && r.stats.hasNewActivity).length;
|
|
184
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
185
|
+
return `Telemetry preview: ${plural(result.events.length, 'event')} would be sent (${plural(activeCount, 'adapter')} with activity). Use --verbose for details.`;
|
|
186
|
+
}
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface SyncOptions {
|
|
|
5
5
|
pullOnly: boolean;
|
|
6
6
|
prefer?: 'local' | 'remote';
|
|
7
7
|
yes: boolean;
|
|
8
|
+
verbose?: boolean;
|
|
8
9
|
}
|
|
9
10
|
export declare function syncFromState(state: SetupState, statePath: string, credentials: Credentials, opts: SyncOptions): Promise<void>;
|
|
10
11
|
export declare const syncCommand: Command;
|