runwork 0.9.0 → 0.9.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.
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
4
  import { parse, stringify } from 'smol-toml';
@@ -11,6 +11,8 @@ function isRunworkManagedCodexKey(key) {
11
11
  import { writeHintToFile, writeTeamInstructionsToFile, removeHintFromFile, removeTeamInstructionsFromFile } from './utils/instruction-hint.js';
12
12
  import { querySqlite } from '../utils/sqlite.js';
13
13
  import { whichBinary, isAppRunning } from '../utils/which.js';
14
+ import { runAgentDetection } from './detection.js';
15
+ import { getRegistryAgent } from './registry.js';
14
16
  export class CodexAdapter {
15
17
  name = 'Codex';
16
18
  slug = 'codex';
@@ -237,6 +239,123 @@ export class CodexAdapter {
237
239
  catch { /* best-effort */ }
238
240
  return null;
239
241
  }
242
+ async readSkillUsage(lastSyncAt) {
243
+ // Codex rollout JSONL files contain a session_meta entry whose
244
+ // `instructions` field lists all skills available in the session.
245
+ // Skills appear as "- name: description (file: path/SKILL.md)".
246
+ // This gives us reach data (which skills were loaded), not invocation.
247
+ //
248
+ // Rollout files live at ~/.codex/sessions/<year>/<month>/<day>/rollout-*.jsonl
249
+ // and are linked from the threads table via rollout_path.
250
+ try {
251
+ const sessionsDir = join(homedir(), '.codex', 'sessions');
252
+ if (!existsSync(sessionsDir))
253
+ return null;
254
+ const sinceMs = lastSyncAt ? new Date(lastSyncAt).getTime() : 0;
255
+ const skillCounts = new Map();
256
+ // Walk session directories by year/month/day
257
+ const walkDir = (dir) => {
258
+ let entries;
259
+ try {
260
+ entries = readdirSync(dir);
261
+ }
262
+ catch {
263
+ return;
264
+ }
265
+ for (const entry of entries) {
266
+ const fullPath = join(dir, entry);
267
+ if (entry.endsWith('.jsonl')) {
268
+ let fileStat;
269
+ try {
270
+ fileStat = statSync(fullPath);
271
+ }
272
+ catch {
273
+ continue;
274
+ }
275
+ if (fileStat.mtimeMs <= sinceMs)
276
+ continue;
277
+ this.parseRolloutForSkills(fullPath, sinceMs, skillCounts);
278
+ }
279
+ else {
280
+ // Recurse into year/month/day subdirectories
281
+ try {
282
+ if (statSync(fullPath).isDirectory())
283
+ walkDir(fullPath);
284
+ }
285
+ catch {
286
+ continue;
287
+ }
288
+ }
289
+ }
290
+ };
291
+ walkDir(sessionsDir);
292
+ if (skillCounts.size === 0)
293
+ return null;
294
+ const results = [];
295
+ for (const [skillName, { count, lastTs }] of skillCounts) {
296
+ results.push({
297
+ skillName,
298
+ count,
299
+ lastUsedAt: new Date(lastTs).toISOString(),
300
+ });
301
+ }
302
+ return results;
303
+ }
304
+ catch {
305
+ return null;
306
+ }
307
+ }
308
+ /** Parse a rollout JSONL file for skill data */
309
+ parseRolloutForSkills(filePath, sinceMs, skillCounts) {
310
+ let content;
311
+ try {
312
+ content = readFileSync(filePath, 'utf-8');
313
+ }
314
+ catch {
315
+ return;
316
+ }
317
+ for (const line of content.split('\n')) {
318
+ if (!line)
319
+ continue;
320
+ let entry;
321
+ try {
322
+ entry = JSON.parse(line);
323
+ }
324
+ catch {
325
+ continue;
326
+ }
327
+ if (entry.type !== 'session_meta')
328
+ continue;
329
+ const payload = entry.payload;
330
+ if (!payload)
331
+ continue;
332
+ const tsRaw = entry.timestamp;
333
+ const ts = typeof tsRaw === 'string' ? Date.parse(tsRaw) : 0;
334
+ if (ts <= sinceMs)
335
+ continue;
336
+ const instructions = payload.instructions;
337
+ if (typeof instructions !== 'string')
338
+ continue;
339
+ // Parse "- name: description (file: path/SKILL.md)" entries
340
+ // from the "### Available skills" section
341
+ const skillRegex = /^- ([^:]+):\s+.+\(file:\s+.+\/SKILL\.md\)/gm;
342
+ let match;
343
+ while ((match = skillRegex.exec(instructions)) !== null) {
344
+ const skillName = match[1].trim();
345
+ const existing = skillCounts.get(skillName);
346
+ if (existing) {
347
+ existing.count++;
348
+ if (ts > existing.lastTs)
349
+ existing.lastTs = ts;
350
+ }
351
+ else {
352
+ skillCounts.set(skillName, { count: 1, lastTs: ts });
353
+ }
354
+ }
355
+ // Only process the first session_meta per file
356
+ break;
357
+ }
358
+ }
240
359
  // ── Codex Desktop app workspace registration ──────────────────────
241
360
  /**
242
361
  * Register a workspace directory in the Codex desktop app's project list.
@@ -294,3 +413,27 @@ export class CodexAdapter {
294
413
  return 'written';
295
414
  }
296
415
  }
416
+ /**
417
+ * Codex Desktop (OpenAI's GUI app) reads its config from the same `~/.codex`
418
+ * directory as the Codex CLI, so every sync/cleanup behavior inherits from
419
+ * CodexAdapter unchanged. Only the identity (slug/name) and the installation
420
+ * detection differ -- the desktop app is detected by application bundle
421
+ * presence, not by a `codex` binary on PATH.
422
+ */
423
+ export class CodexDesktopAdapter extends CodexAdapter {
424
+ name = 'Codex';
425
+ slug = 'codex-app';
426
+ async detect() {
427
+ const def = getRegistryAgent('codex-app');
428
+ if (!def)
429
+ return false;
430
+ return runAgentDetection(def.detection);
431
+ }
432
+ // Usage stats come from ~/.codex/state_5.sqlite, which is shared between
433
+ // Codex CLI and Codex Desktop. Leaving the read here would double-count
434
+ // sessions/tokens whenever both adapters are detected on the same machine.
435
+ // Defer to CodexAdapter (slug 'codex') as the sole source of Codex stats.
436
+ async readUsageStats() {
437
+ return null;
438
+ }
439
+ }
@@ -2,7 +2,7 @@ import { ClaudeCodeAdapter } from './claude-code.js';
2
2
  import { ClaudeDesktopAdapter } from './claude-desktop.js';
3
3
  import { CursorAdapter } from './cursor.js';
4
4
  import { WindsurfAdapter } from './windsurf.js';
5
- import { CodexAdapter } from './codex.js';
5
+ import { CodexAdapter, CodexDesktopAdapter } from './codex.js';
6
6
  import { ClineAdapter } from './cline.js';
7
7
  import { GeminiAdapter } from './gemini.js';
8
8
  import { GenericAgentAdapter } from './generic-adapter.js';
@@ -17,6 +17,7 @@ const CUSTOM_ADAPTERS = [
17
17
  new CursorAdapter(),
18
18
  new WindsurfAdapter(),
19
19
  new CodexAdapter(),
20
+ new CodexDesktopAdapter(),
20
21
  new ClineAdapter(),
21
22
  new GeminiAdapter(),
22
23
  ];
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Shared agent-installation detection used by the generic adapter and by
3
+ * custom adapters that opt in to the registry's declared detection rules
4
+ * (e.g. Codex Desktop, which detects via `/Applications/Codex.app` on macOS
5
+ * and the AppX package on Windows rather than a `codex` binary on PATH).
6
+ *
7
+ * Mirrors the desktop's agent-detection.ts logic but uses Node APIs instead
8
+ * of Tauri plugins.
9
+ */
10
+ import type { AgentDetection } from './registry-data.js';
11
+ /**
12
+ * Synchronous detection — returns true when the agent is installed according
13
+ * to its registry `detection` rules. Supports every method the desktop app
14
+ * understands: `binary`, `path`, `windows-appx`, `windows-start-app`, and
15
+ * nested `any` combinators. Windows-only methods return false off Windows.
16
+ */
17
+ export declare function runAgentDetection(detection: AgentDetection): boolean;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Shared agent-installation detection used by the generic adapter and by
3
+ * custom adapters that opt in to the registry's declared detection rules
4
+ * (e.g. Codex Desktop, which detects via `/Applications/Codex.app` on macOS
5
+ * and the AppX package on Windows rather than a `codex` binary on PATH).
6
+ *
7
+ * Mirrors the desktop's agent-detection.ts logic but uses Node APIs instead
8
+ * of Tauri plugins.
9
+ */
10
+ import { execFileSync } from 'child_process';
11
+ import { existsSync } from 'fs';
12
+ import { homedir, platform } from 'os';
13
+ import { isAbsolute, join } from 'path';
14
+ import { whichBinary } from '../utils/which.js';
15
+ import { resolvePlatformString } from './registry.js';
16
+ function isWindows() {
17
+ return platform() === 'win32';
18
+ }
19
+ function powershellQuote(value) {
20
+ return `'${value.replace(/'/g, "''")}'`;
21
+ }
22
+ function toList(value) {
23
+ return Array.isArray(value) ? value : [value];
24
+ }
25
+ function runPowerShell(script) {
26
+ try {
27
+ execFileSync('powershell', ['-NoProfile', '-Command', script], { stdio: 'pipe' });
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ function checkPath(target) {
35
+ const resolved = resolvePlatformString(target);
36
+ if (!resolved)
37
+ return false;
38
+ if (isAbsolute(resolved))
39
+ return existsSync(resolved);
40
+ return existsSync(join(homedir(), resolved));
41
+ }
42
+ function checkWindowsAppxPackage(target) {
43
+ if (!isWindows())
44
+ return false;
45
+ for (const pkg of toList(target)) {
46
+ const script = `$p = Get-AppxPackage -Name ${powershellQuote(pkg)} -ErrorAction SilentlyContinue; if ($null -ne $p) { exit 0 } exit 1`;
47
+ if (runPowerShell(script))
48
+ return true;
49
+ }
50
+ return false;
51
+ }
52
+ function checkWindowsStartApp(target) {
53
+ if (!isWindows())
54
+ return false;
55
+ for (const pattern of toList(target)) {
56
+ const script = `$a = Get-StartApps -Name ${powershellQuote(pattern)} -ErrorAction SilentlyContinue | Select-Object -First 1; if ($null -ne $a) { exit 0 } exit 1`;
57
+ if (runPowerShell(script))
58
+ return true;
59
+ }
60
+ return false;
61
+ }
62
+ /**
63
+ * Synchronous detection — returns true when the agent is installed according
64
+ * to its registry `detection` rules. Supports every method the desktop app
65
+ * understands: `binary`, `path`, `windows-appx`, `windows-start-app`, and
66
+ * nested `any` combinators. Windows-only methods return false off Windows.
67
+ */
68
+ export function runAgentDetection(detection) {
69
+ switch (detection.method) {
70
+ case 'binary': {
71
+ const target = resolvePlatformString(detection.target);
72
+ return !!target && !!whichBinary(target);
73
+ }
74
+ case 'path':
75
+ return checkPath(detection.target);
76
+ case 'windows-appx':
77
+ return checkWindowsAppxPackage(detection.target);
78
+ case 'windows-start-app':
79
+ return checkWindowsStartApp(detection.target);
80
+ case 'any':
81
+ for (const probe of detection.target) {
82
+ if (runAgentDetection(probe))
83
+ return true;
84
+ }
85
+ return false;
86
+ default:
87
+ return false;
88
+ }
89
+ }
@@ -1,11 +1,11 @@
1
1
  import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'fs';
2
- import { isAbsolute, join } from 'path';
2
+ import { join } from 'path';
3
3
  import { homedir } from 'os';
4
- import { whichBinary } from '../utils/which.js';
5
4
  import { buildSkillMd } from './types.js';
6
5
  import { mergeJsonMcpServers, removeRunworkMcpServers } from './utils/json-config.js';
7
6
  import { writeHintToFile, writeTeamInstructionsToFile, removeHintFromFile, removeTeamInstructionsFromFile } from './utils/instruction-hint.js';
8
7
  import { resolvePlatformString, resolveToAbsolute } from './registry.js';
8
+ import { runAgentDetection } from './detection.js';
9
9
  /**
10
10
  * Generic adapter that handles any agent from the registry using its
11
11
  * skillsPaths, mcpConfigPath, and instructionFile metadata.
@@ -25,17 +25,7 @@ export class GenericAgentAdapter {
25
25
  this.slug = def.slug;
26
26
  }
27
27
  async detect() {
28
- const target = resolvePlatformString(this.def.detection.target);
29
- if (!target)
30
- return false;
31
- if (this.def.detection.method === 'binary') {
32
- return !!whichBinary(target);
33
- }
34
- // path detection
35
- if (isAbsolute(target)) {
36
- return existsSync(target);
37
- }
38
- return existsSync(join(homedir(), target));
28
+ return runAgentDetection(this.def.detection);
39
29
  }
40
30
  supportsMcpScope(scope) {
41
31
  if (!this.def.mcpConfigPath)
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Unified agent registry (types + data + browser-safe lookups).
3
+ *
4
+ * This file is the SINGLE SOURCE OF TRUTH for every agent Runwork knows about:
5
+ * CLI adapters read from here (via `./registry.js`), and the desktop app
6
+ * re-exports from here (via the `@cli-agent-registry` path alias).
7
+ *
8
+ * Keep this file browser-safe: no `os`, `path`, `fs`, or other Node-only
9
+ * imports. Node-only path resolvers live next to this file in `./registry.ts`.
10
+ */
11
+ export type AgentCategory = 'cli' | 'ide' | 'desktop' | 'extension';
12
+ /** Platform-specific string. Plain string = same on all platforms. */
13
+ export type PlatformString = string | {
14
+ default?: string;
15
+ macos?: string;
16
+ windows?: string;
17
+ linux?: string;
18
+ };
19
+ /**
20
+ * Detection strategy for an agent. Supports simple binary/path checks plus
21
+ * richer Windows-specific methods and a nested `any` combinator used by the
22
+ * desktop app to detect agents that ship as both GUI app and CLI binary.
23
+ */
24
+ export type AgentDetection = {
25
+ method: 'binary' | 'path';
26
+ target: PlatformString;
27
+ } | {
28
+ method: 'windows-appx' | 'windows-start-app';
29
+ target: string | string[];
30
+ } | {
31
+ method: 'any';
32
+ target: AgentDetection[];
33
+ };
34
+ export interface AgentLaunch {
35
+ app?: {
36
+ macos?: string;
37
+ windows?: string;
38
+ linux?: string;
39
+ };
40
+ cli?: string;
41
+ cliAcceptsPrompt?: boolean;
42
+ }
43
+ export interface AgentQuickStart {
44
+ launchHint?: string;
45
+ panelHint?: string;
46
+ skillCommand?: string;
47
+ examplePrompts?: string[];
48
+ }
49
+ export interface ManualSetupStep {
50
+ id: string;
51
+ label: string;
52
+ }
53
+ /**
54
+ * Optional downloadable artifact generated inline during a manual setup step
55
+ * (e.g. a plugin zip the user uploads into the agent).
56
+ */
57
+ export interface AgentManualSetupArtifact {
58
+ label: string;
59
+ filename: string;
60
+ /** CLI subcommand that generates the artifact, e.g. "build-plugin" */
61
+ command: string;
62
+ }
63
+ /** Where a manual-setup step is inserted in the onboarding journey. */
64
+ export type ManualSetupSlot = 'installation' | 'connecting' | 'try-it';
65
+ export interface AgentManualSetup {
66
+ title: string;
67
+ steps: ManualSetupStep[];
68
+ opensAgent?: boolean;
69
+ /** Where in the journey to show this step. Defaults to 'try-it'. */
70
+ showAfter?: ManualSetupSlot;
71
+ downloadArtifact?: AgentManualSetupArtifact;
72
+ }
73
+ export interface AgentDefinition {
74
+ slug: string;
75
+ name: string;
76
+ aliases?: string[];
77
+ description: string;
78
+ category: AgentCategory;
79
+ detection: AgentDetection;
80
+ /** Desktop launch hints (GUI app name, CLI command) */
81
+ launch?: AgentLaunch;
82
+ /** Logo identifier used by the desktop UI */
83
+ logo?: string;
84
+ /** Public download/install URL shown in the desktop onboarding */
85
+ downloadUrl?: string;
86
+ /** Whether the CLI knows how to auto-install this agent */
87
+ autoInstallable?: boolean;
88
+ /** Skill file directories relative to $HOME (global) or project root (project) */
89
+ skillsPaths?: {
90
+ global?: PlatformString;
91
+ project?: PlatformString;
92
+ };
93
+ /** MCP config file path relative to $HOME */
94
+ mcpConfigPath?: PlatformString;
95
+ /** MCP config JSON key where servers are stored (default: 'mcpServers') */
96
+ mcpConfigKey?: string;
97
+ /** First-class agent = full Runwork support (custom adapter or rich desktop onboarding) */
98
+ firstClass?: boolean;
99
+ /** Instruction file path relative to $HOME (global) or project root (project) */
100
+ instructionFile?: {
101
+ global?: PlatformString;
102
+ project?: PlatformString;
103
+ };
104
+ /** Skill file format */
105
+ skillFormat?: 'skill-md' | 'mdc' | 'windsurf-md' | 'gemini-flat';
106
+ /** Desktop onboarding hint copy */
107
+ quickStart?: AgentQuickStart;
108
+ /** Manual setup steps required after installation (desktop onboarding) */
109
+ manualSetup?: AgentManualSetup;
110
+ }
111
+ export declare function getAgent(slug: string): AgentDefinition | undefined;
112
+ export declare function getAgentByName(name: string): AgentDefinition | undefined;
113
+ export declare function getAgents(): AgentDefinition[];
114
+ export declare function getDetectableAgents(): AgentDefinition[];
115
+ export declare function getLaunchableAgents(): AgentDefinition[];
116
+ export declare function getFirstClassAgents(): AgentDefinition[];
117
+ export declare function getAgentsByCategory(category: AgentCategory): AgentDefinition[];
118
+ export declare function isCLIAgent(slug: string): boolean;
119
+ export declare function isIDEAgent(slug: string): boolean;
120
+ export declare function isDesktopAgent(slug: string): boolean;
121
+ /** Alias of {@link getAgent} kept for backward compatibility with CLI callers. */
122
+ export declare const getRegistryAgent: typeof getAgent;
123
+ /** Alias of {@link getAgents} kept for backward compatibility with CLI callers. */
124
+ export declare const getRegistryAgents: typeof getAgents;
125
+ export declare function getFirstClassSlugs(): string[];
126
+ export declare function getCustomAdapterSlugs(): Set<string>;