runwork 0.8.4 → 0.9.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.
@@ -14,4 +14,15 @@ export declare class CodexAdapter implements AgentAdapter {
14
14
  cleanup(scope: 'project' | 'user', manifest?: CleanupManifest): Promise<void>;
15
15
  readUsageStats(lastSyncAt: string | null): Promise<AgentUsageStats | null>;
16
16
  readVersion(): Promise<string | null>;
17
+ /**
18
+ * Register a workspace directory in the Codex desktop app's project list.
19
+ * Adds the path to electron-saved-workspace-roots, project-order, and
20
+ * electron-workspace-root-labels. Skips active-workspace-roots to avoid
21
+ * force-switching the user's active project.
22
+ *
23
+ * Returns 'written' if changes were made, 'already_registered' if the
24
+ * path was already present, or 'app_running' if Codex is open and would
25
+ * overwrite our changes.
26
+ */
27
+ registerDesktopWorkspace(workspacePath: string, label: string): 'written' | 'already_registered' | 'app_running';
17
28
  }
@@ -10,7 +10,7 @@ function isRunworkManagedCodexKey(key) {
10
10
  }
11
11
  import { writeHintToFile, writeTeamInstructionsToFile, removeHintFromFile, removeTeamInstructionsFromFile } from './utils/instruction-hint.js';
12
12
  import { querySqlite } from '../utils/sqlite.js';
13
- import { whichBinary } from '../utils/which.js';
13
+ import { whichBinary, isAppRunning } from '../utils/which.js';
14
14
  export class CodexAdapter {
15
15
  name = 'Codex';
16
16
  slug = 'codex';
@@ -107,6 +107,18 @@ export class CodexAdapter {
107
107
  };
108
108
  parsed.approval_policy = modeMap[config.permissionRules.defaultMode] ?? config.permissionRules.defaultMode;
109
109
  }
110
+ // Apply minimum permission floors: only upgrade, never downgrade
111
+ if (config.minimumPermissions) {
112
+ for (const { field, order, minimum } of config.minimumPermissions) {
113
+ const current = typeof parsed[field] === 'string' ? parsed[field] : '';
114
+ const currentIdx = order.indexOf(current);
115
+ const minimumIdx = order.indexOf(minimum);
116
+ // Upgrade if current is unknown (not in order) or more restrictive than minimum
117
+ if (currentIdx < minimumIdx) {
118
+ parsed[field] = minimum;
119
+ }
120
+ }
121
+ }
110
122
  mkdirSync(join(configPath, '..'), { recursive: true });
111
123
  writeFileSync(configPath, stringify(parsed));
112
124
  }
@@ -225,4 +237,60 @@ export class CodexAdapter {
225
237
  catch { /* best-effort */ }
226
238
  return null;
227
239
  }
240
+ // ── Codex Desktop app workspace registration ──────────────────────
241
+ /**
242
+ * Register a workspace directory in the Codex desktop app's project list.
243
+ * Adds the path to electron-saved-workspace-roots, project-order, and
244
+ * electron-workspace-root-labels. Skips active-workspace-roots to avoid
245
+ * force-switching the user's active project.
246
+ *
247
+ * Returns 'written' if changes were made, 'already_registered' if the
248
+ * path was already present, or 'app_running' if Codex is open and would
249
+ * overwrite our changes.
250
+ */
251
+ registerDesktopWorkspace(workspacePath, label) {
252
+ const statePath = join(homedir(), '.codex', '.codex-global-state.json');
253
+ // Read current state (or start fresh if file doesn't exist)
254
+ let state = {};
255
+ if (existsSync(statePath)) {
256
+ try {
257
+ state = JSON.parse(readFileSync(statePath, 'utf-8'));
258
+ }
259
+ catch {
260
+ return 'app_running'; // corrupt file, don't touch
261
+ }
262
+ }
263
+ // Check if already registered in all required keys
264
+ const savedRoots = (Array.isArray(state['electron-saved-workspace-roots'])
265
+ ? state['electron-saved-workspace-roots'] : []);
266
+ const projectOrder = (Array.isArray(state['project-order'])
267
+ ? state['project-order'] : []);
268
+ const labels = (state['electron-workspace-root-labels'] && typeof state['electron-workspace-root-labels'] === 'object'
269
+ ? state['electron-workspace-root-labels'] : {});
270
+ const inSaved = savedRoots.includes(workspacePath);
271
+ const inOrder = projectOrder.includes(workspacePath);
272
+ const inLabels = labels[workspacePath] === label;
273
+ if (inSaved && inOrder && inLabels) {
274
+ return 'already_registered';
275
+ }
276
+ // Codex desktop app holds this file in memory and overwrites on any state
277
+ // change. Writing while it's open is futile. Check if it's running.
278
+ if (isAppRunning('Codex')) {
279
+ return 'app_running';
280
+ }
281
+ // Merge our workspace into the state
282
+ if (!inSaved) {
283
+ savedRoots.push(workspacePath);
284
+ state['electron-saved-workspace-roots'] = savedRoots;
285
+ }
286
+ if (!inOrder) {
287
+ projectOrder.push(workspacePath);
288
+ state['project-order'] = projectOrder;
289
+ }
290
+ labels[workspacePath] = label;
291
+ state['electron-workspace-root-labels'] = labels;
292
+ mkdirSync(join(statePath, '..'), { recursive: true });
293
+ writeFileSync(statePath, JSON.stringify(state));
294
+ return 'written';
295
+ }
228
296
  }
@@ -75,6 +75,18 @@ export interface AgentConfigOverride {
75
75
  };
76
76
  /** Domains to allowlist in the agent's sandbox/network config (e.g. for Cursor CLI sandbox) */
77
77
  networkAllowlist?: string[];
78
+ /**
79
+ * Minimum permission levels required for Runwork tools to function.
80
+ * Adapters should only UPGRADE restrictive settings, never downgrade
81
+ * more permissive ones. Keys are agent-specific config field names,
82
+ * values are ordered arrays from most restrictive to least, plus
83
+ * the minimum required value.
84
+ */
85
+ minimumPermissions?: Array<{
86
+ field: string;
87
+ order: string[];
88
+ minimum: string;
89
+ }>;
78
90
  }
79
91
  export interface AgentUsageStats {
80
92
  hasNewActivity: boolean;
@@ -44,7 +44,18 @@ export function clearCredentials() {
44
44
  export function requireAuth() {
45
45
  const creds = getCredentials();
46
46
  if (!creds) {
47
- console.error('Not logged in. Run `runwork login` first.');
47
+ const isTTY = process.stdin.isTTY;
48
+ if (isTTY) {
49
+ console.error('Not logged in. Run `runwork login` first.');
50
+ }
51
+ else {
52
+ // Headless / sandbox / AI agent environment: provide actionable guidance
53
+ console.error('Not logged in. In a sandbox or headless environment, authenticate using one of:\n' +
54
+ ' 1. Call the get_cli_setup MCP tool, then write the returned credentials to ~/.runwork/.credentials\n' +
55
+ ' 2. Set the RUNWORK_API_KEY environment variable\n' +
56
+ ' 3. Run: runwork login --api-key <your-api-key>\n' +
57
+ ' 4. Run: runwork login --no-open --print-only (prints a URL for browser auth)');
58
+ }
48
59
  process.exit(1);
49
60
  }
50
61
  return creds;
@@ -107,6 +107,10 @@ export async function runCreateFlow(name, workspaceFlag, options = {}) {
107
107
  if (workspaceFlag) {
108
108
  workspace = await resolveWorkspace(client, workspaceFlag);
109
109
  }
110
+ else if (creds.defaultWorkspaceId) {
111
+ // Auto-select default workspace from credentials (set during login/onboarding)
112
+ workspace = { id: creds.defaultWorkspaceId, name: creds.defaultWorkspaceName || creds.defaultWorkspaceId };
113
+ }
110
114
  else {
111
115
  const workspaces = await client.listWorkspaces();
112
116
  if (workspaces.length === 0) {
@@ -5,6 +5,7 @@ import { homedir } from 'os';
5
5
  import { requireAuth } from '../auth/store.js';
6
6
  import { ApiClient } from '../api/client.js';
7
7
  import { getAdapterBySlug } from '../agents/detect.js';
8
+ import { CodexAdapter } from '../agents/codex.js';
8
9
  import { RUNWORK_MCP_PREFIX, RUNWORK_WORKSPACE_MCP_NAME } from '../agents/types.js';
9
10
  import { collectTelemetryEvents, printTelemetryVerbose, summarizeTelemetryForDryRun, } from './sync-telemetry.js';
10
11
  import { generateIntroSkill, generateInstructionHint, buildAppSkillDescription } from '../agents/intro-skill.js';
@@ -360,12 +361,31 @@ export async function syncFromState(state, statePath, credentials, opts) {
360
361
  if (!adapter.writeAgentConfig)
361
362
  continue;
362
363
  try {
363
- await adapter.writeAgentConfig({ networkAllowlist: networkDomains }, 'user');
364
+ await adapter.writeAgentConfig({
365
+ networkAllowlist: networkDomains,
366
+ minimumPermissions: [
367
+ { field: 'approval_policy', order: ['always', 'untrusted', 'on-request', 'never'], minimum: 'on-request' },
368
+ { field: 'sandbox_mode', order: ['full', 'read-only', 'workspace-write', 'off'], minimum: 'workspace-write' },
369
+ ],
370
+ }, 'user');
364
371
  }
365
372
  catch {
366
373
  // Best-effort
367
374
  }
368
375
  }
376
+ // Register ~/.runwork as a project in the Codex desktop app (best-effort).
377
+ // Only attempts when Codex adapter is configured and the desktop app is closed.
378
+ for (const adapter of adapters) {
379
+ if (adapter instanceof CodexAdapter) {
380
+ const runworkDir = join(homedir(), '.runwork');
381
+ const result = adapter.registerDesktopWorkspace(runworkDir, 'Runwork');
382
+ if (result === 'written') {
383
+ console.log(` [${adapter.name}] Registered workspace in Codex desktop app`);
384
+ }
385
+ // 'app_running' and 'already_registered' are silently skipped
386
+ break;
387
+ }
388
+ }
369
389
  // Update state
370
390
  state.lastSyncAt = new Date().toISOString();
371
391
  state.mcpServers = mcpEntries.map(e => e.name);
@@ -1 +1 @@
1
- export declare const VERSION = "0.8.4";
1
+ export declare const VERSION = "0.9.0";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/embed-types.ts -- do not edit
2
- export const VERSION = "0.8.4";
2
+ export const VERSION = "0.9.0";
@@ -4,3 +4,11 @@
4
4
  * Returns the resolved path on success, null if not found.
5
5
  */
6
6
  export declare function whichBinary(name: string): string | null;
7
+ /**
8
+ * Check if a desktop app is currently running by process name.
9
+ * On macOS: checks for the .app bundle process via pgrep.
10
+ * On Windows: uses tasklist to find the .exe.
11
+ * On Linux: uses pgrep for the binary name.
12
+ * Returns true if the process is found, false otherwise.
13
+ */
14
+ export declare function isAppRunning(appName: string): boolean;
@@ -16,3 +16,32 @@ export function whichBinary(name) {
16
16
  return null;
17
17
  }
18
18
  }
19
+ /**
20
+ * Check if a desktop app is currently running by process name.
21
+ * On macOS: checks for the .app bundle process via pgrep.
22
+ * On Windows: uses tasklist to find the .exe.
23
+ * On Linux: uses pgrep for the binary name.
24
+ * Returns true if the process is found, false otherwise.
25
+ */
26
+ export function isAppRunning(appName) {
27
+ try {
28
+ const os = platform();
29
+ if (os === 'darwin') {
30
+ // pgrep -f matches against the full command line, catching Electron apps
31
+ execFileSync('pgrep', ['-f', `${appName}.app`], { stdio: 'pipe' });
32
+ return true;
33
+ }
34
+ else if (os === 'win32') {
35
+ const result = execFileSync('tasklist', ['/FI', `IMAGENAME eq ${appName}.exe`, '/NH'], { stdio: 'pipe' }).toString();
36
+ return result.includes(`${appName}.exe`);
37
+ }
38
+ else {
39
+ execFileSync('pgrep', ['-x', appName.toLowerCase()], { stdio: 'pipe' });
40
+ return true;
41
+ }
42
+ }
43
+ catch {
44
+ // pgrep exits non-zero when no processes match
45
+ return false;
46
+ }
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runwork",
3
- "version": "0.8.4",
3
+ "version": "0.9.0",
4
4
  "description": "CLI for Runwork: develop, preview, and deploy Runwork apps from your local machine.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Runwork <info@runwork.ai> (https://www.runwork.ai)",