@bramblex/codex-workbench 0.1.16 → 0.1.18

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/README.md CHANGED
@@ -140,6 +140,8 @@ codex-workbench auto-detects installed backends by checking their session direct
140
140
 
141
141
  Session metadata such as custom names, notes, and archive state is stored in workbench's own metadata file, not inside backend session files.
142
142
 
143
+ Every provider owns the full workbench command surface it advertises: new, resume, fork, archive, unarchive, and delete. A provider can implement an operation through its native CLI, workbench metadata, or file operations, but callers should not need provider-specific fallback logic.
144
+
143
145
  ---
144
146
 
145
147
  ## CLI commands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bramblex/codex-workbench",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Terminal workbench for browsing and managing local and SSH Codex sessions.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  "LICENSE"
34
34
  ],
35
35
  "scripts": {
36
- "test": "node -e \"const fs=require('fs'),{spawnSync}=require('child_process');function files(d){return fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>e.isDirectory()?files(d+'/'+e.name):e.name.endsWith('.js')?[d+'/'+e.name]:[])}for(const f of files('src')){const r=spawnSync(process.execPath,['--check',f],{stdio:'inherit'});if(r.status)process.exit(r.status)}\" && node --check bin/codex-workbench && node --check scripts/pty-codex.js && node --check scripts/tui-pty-codex.js && node --check scripts/blessed-xterm-codex.js && node test/codex-bin.test.js && node test/blessed-compat.test.js && node test/config-paths.test.js && node test/session-sources.test.js && node test/workbench-config.test.js && node test/smoke.js",
36
+ "test": "node -e \"const fs=require('fs'),{spawnSync}=require('child_process');function files(d){return fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>e.isDirectory()?files(d+'/'+e.name):e.name.endsWith('.js')?[d+'/'+e.name]:[])}for(const f of files('src')){const r=spawnSync(process.execPath,['--check',f],{stdio:'inherit'});if(r.status)process.exit(r.status)}\" && node --check bin/codex-workbench && node --check scripts/pty-codex.js && node --check scripts/tui-pty-codex.js && node --check scripts/blessed-xterm-codex.js && node test/codex-bin.test.js && node test/blessed-compat.test.js && node test/config-paths.test.js && node test/session-sources.test.js && node test/update-checker.test.js && node test/workbench-config.test.js && node test/smoke.js",
37
37
  "pty:codex": "node scripts/pty-codex.js",
38
38
  "tui:codex": "node scripts/tui-pty-codex.js",
39
39
  "xterm:codex": "node scripts/blessed-xterm-codex.js"
@@ -249,6 +249,14 @@ function runSessionCommand(command, session, args, inherit) {
249
249
  module.exports = {
250
250
  id: 'codex',
251
251
  label: 'Codex',
252
+ capabilities: {
253
+ new: true,
254
+ resume: true,
255
+ fork: true,
256
+ archive: true,
257
+ unarchive: true,
258
+ delete: true,
259
+ },
252
260
  isAvailable,
253
261
  getSessionFiles,
254
262
  parseSession,
@@ -8,7 +8,7 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { spawn, spawnSync } = require('child_process');
10
10
  const { HOME, PI_CODING_AGENT_DIR } = require('../config');
11
- const { updateMetadata } = require('../model/metadata');
11
+ const { removeMetadata, updateMetadata } = require('../model/metadata');
12
12
 
13
13
  // ---------------------------------------------------------------------------
14
14
  // Paths
@@ -272,9 +272,9 @@ function runSessionCommand(command, session, args, inherit) {
272
272
  return runArgv(argv, cwd, inherit);
273
273
  }
274
274
  case 'delete': {
275
- // pi has no delete CLI – just remove the file
276
- // A force flag is handled by session-sources calling deleteSessionFile
277
- return -1; // signal that file-based deletion should be used
275
+ fs.unlinkSync(session.file);
276
+ removeMetadata(session);
277
+ return 0;
278
278
  }
279
279
  case 'archive':
280
280
  case 'unarchive': {
@@ -312,6 +312,14 @@ function resolveBin() {
312
312
  module.exports = {
313
313
  id: 'pi',
314
314
  label: 'pi',
315
+ capabilities: {
316
+ new: true,
317
+ resume: true,
318
+ fork: true,
319
+ archive: true,
320
+ unarchive: true,
321
+ delete: true,
322
+ },
315
323
  isAvailable,
316
324
  getSessionFiles,
317
325
  parseSession,
@@ -105,6 +105,7 @@ function providerSummary(provider) {
105
105
  return {
106
106
  id: provider.id,
107
107
  label: provider.label || provider.id,
108
+ capabilities: provider.capabilities || {},
108
109
  };
109
110
  }
110
111
 
@@ -121,19 +122,15 @@ function listSourceBackends(source) {
121
122
  .map((backend) => ({
122
123
  id: String(backend.id),
123
124
  label: backend.label ? String(backend.label) : String(backend.id),
125
+ capabilities: backend.capabilities && typeof backend.capabilities === 'object'
126
+ ? backend.capabilities
127
+ : {},
124
128
  }));
125
129
  }
126
130
 
127
131
  function runSourceSessionCommand(session, command, args) {
128
132
  if (!session.sourceRemote) {
129
- const status = runCodexCommand(command, session, args);
130
- // If the provider returns -1 (e.g. pi delete = file-based), fall through to file deletion
131
- if (status === -1) {
132
- const { deleteSessionFile } = require('../model/session-store');
133
- deleteSessionFile(session);
134
- return 0;
135
- }
136
- return status;
133
+ return runCodexCommand(command, session, args);
137
134
  }
138
135
  const source = configuredSourceOrThrow(session.sourceId);
139
136
  const tty = command === 'resume' || command === 'fork';
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+ const pkg = require('../../package.json');
5
+
6
+ const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(pkg.name).replace(/^%40/, '@')}/latest`;
7
+
8
+ function parseVersion(version) {
9
+ return String(version || '')
10
+ .replace(/^v/, '')
11
+ .split('.')
12
+ .map((part) => Number.parseInt(part, 10))
13
+ .map((part) => (Number.isFinite(part) ? part : 0));
14
+ }
15
+
16
+ function compareVersions(a, b) {
17
+ const left = parseVersion(a);
18
+ const right = parseVersion(b);
19
+ const length = Math.max(left.length, right.length);
20
+ for (let i = 0; i < length; i += 1) {
21
+ const delta = (left[i] || 0) - (right[i] || 0);
22
+ if (delta !== 0) return delta;
23
+ }
24
+ return 0;
25
+ }
26
+
27
+ function fetchLatestVersion(timeoutMs = 1500) {
28
+ return new Promise((resolve) => {
29
+ const req = https.get(REGISTRY_URL, {
30
+ headers: { accept: 'application/json', 'user-agent': `${pkg.name}/${pkg.version}` },
31
+ timeout: timeoutMs,
32
+ }, (res) => {
33
+ if (res.statusCode !== 200) {
34
+ res.resume();
35
+ resolve(null);
36
+ return;
37
+ }
38
+ let body = '';
39
+ res.setEncoding('utf8');
40
+ res.on('data', (chunk) => { body += chunk; });
41
+ res.on('end', () => {
42
+ try {
43
+ const payload = JSON.parse(body);
44
+ resolve(payload && payload.version ? String(payload.version) : null);
45
+ } catch {
46
+ resolve(null);
47
+ }
48
+ });
49
+ });
50
+
51
+ req.on('timeout', () => {
52
+ req.destroy();
53
+ resolve(null);
54
+ });
55
+ req.on('error', () => resolve(null));
56
+ });
57
+ }
58
+
59
+ async function checkForUpdate(currentVersion = pkg.version) {
60
+ const latestVersion = await fetchLatestVersion();
61
+ if (!latestVersion || compareVersions(latestVersion, currentVersion) <= 0) return null;
62
+ return {
63
+ currentVersion,
64
+ latestVersion,
65
+ };
66
+ }
67
+
68
+ module.exports = {
69
+ checkForUpdate,
70
+ compareVersions,
71
+ fetchLatestVersion,
72
+ };
@@ -21,6 +21,7 @@ const {
21
21
  updateSourceMetadata,
22
22
  } = require('../services/session-sources');
23
23
  const { usableCwd } = require('../services/codex-runner');
24
+ const { checkForUpdate } = require('../services/update-checker');
24
25
  const { createDirectoryPicker } = require('./directory-picker');
25
26
 
26
27
  async function runWorkbench() {
@@ -42,6 +43,7 @@ async function runWorkbench() {
42
43
  let activePanel = 'projects';
43
44
  let remoteLoadId = 0;
44
45
  let remoteLoading = false;
46
+ let updateInfo = null;
45
47
  let closed = false;
46
48
 
47
49
  const screen = blessed.screen({
@@ -466,7 +468,8 @@ async function runWorkbench() {
466
468
  const render = () => {
467
469
  applyLayout();
468
470
  const visible = currentSessions();
469
- header.setContent(` ${appTitle}\n ${visible.length}/${sessions.length} visible ${groupDisplayName(currentGroup())}`);
471
+ const updateText = updateInfo ? ` Update available: v${updateInfo.latestVersion}` : '';
472
+ header.setContent(` ${appTitle}${updateText}\n ${visible.length}/${sessions.length} visible ${groupDisplayName(currentGroup())}`);
470
473
  detailBox.setContent(detailContent(selectedSession()));
471
474
  updateFocusStyles();
472
475
  screen.render();
@@ -891,6 +894,11 @@ async function runWorkbench() {
891
894
  projectsList.focus();
892
895
  render();
893
896
  startRemoteReload(true);
897
+ checkForUpdate(pkg.version).then((nextUpdateInfo) => {
898
+ if (closed || !nextUpdateInfo) return;
899
+ updateInfo = nextUpdateInfo;
900
+ render();
901
+ });
894
902
 
895
903
  return new Promise(() => {});
896
904
  }