@generativereality/cctabs 0.4.0 → 0.4.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/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +185 -84
- package/package.json +1 -1
- package/skills/cctabs/SKILL.md +10 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cctabs",
|
|
3
3
|
"description": "Claude Code tab manager. Terminal tabs as the UI, no tmux. Claude can orchestrate its own sibling sessions.",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "generativereality",
|
|
7
7
|
"url": "https://cctabs.com"
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { consola } from "consola";
|
|
|
11
11
|
import * as p from "@clack/prompts";
|
|
12
12
|
//#region package.json
|
|
13
13
|
var name = "@generativereality/cctabs";
|
|
14
|
-
var version = "0.4.
|
|
14
|
+
var version = "0.4.2";
|
|
15
15
|
var description = "Claude Code tab manager. Terminal tabs as the UI, no tmux.";
|
|
16
16
|
var package_default = {
|
|
17
17
|
name,
|
|
@@ -651,6 +651,8 @@ var TabbyAdapter = class {
|
|
|
651
651
|
"claude.ai/code",
|
|
652
652
|
"⏵⏵ bypass",
|
|
653
653
|
"⏵⏵ auto",
|
|
654
|
+
"new task?",
|
|
655
|
+
"Checking for updates",
|
|
654
656
|
"Thinking",
|
|
655
657
|
"Hatching",
|
|
656
658
|
"Composing",
|
|
@@ -694,6 +696,27 @@ var TabbyAdapter = class {
|
|
|
694
696
|
async renameTab(tabId, name) {
|
|
695
697
|
await this.http("PUT", `/api/tabs/${tabId}/title`, { title: name });
|
|
696
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* Fast path: the plugin's POST /api/tabs/new accepts {cwd, title, command,
|
|
701
|
+
* args} and returns the new tab's uuid synchronously. This collapses the
|
|
702
|
+
* whole newTab → waitForNewBlock → renameTab → wait-for-shell-prompt →
|
|
703
|
+
* sendInput sequence into a single round-trip, and (because the uuid is
|
|
704
|
+
* returned, not discovered by diffing) lets the caller open many tabs at once.
|
|
705
|
+
*/
|
|
706
|
+
async openTabDirect(opts) {
|
|
707
|
+
this.ensureHealthy();
|
|
708
|
+
const uuid = (await this.http("POST", "/api/tabs/new", {
|
|
709
|
+
cwd: opts.cwd,
|
|
710
|
+
title: opts.title,
|
|
711
|
+
command: opts.command,
|
|
712
|
+
args: opts.args
|
|
713
|
+
}))?.uuid;
|
|
714
|
+
if (!uuid) throw new Error("Tabby plugin did not return a tab uuid");
|
|
715
|
+
return {
|
|
716
|
+
blockId: uuid,
|
|
717
|
+
tabId: uuid
|
|
718
|
+
};
|
|
719
|
+
}
|
|
697
720
|
async sendInput(blockId, text) {
|
|
698
721
|
return this.http("POST", `/api/tabs/${blockId}/send`, { data: text });
|
|
699
722
|
}
|
|
@@ -1259,66 +1282,115 @@ async function waitForScrollbackMatch(adapter, blockId, pattern, label, timeoutM
|
|
|
1259
1282
|
}
|
|
1260
1283
|
throw new Error(`Timed out waiting for ${label}`);
|
|
1261
1284
|
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Wait for Claude's input prompt, then send the initial task and reliably
|
|
1287
|
+
* submit it.
|
|
1288
|
+
*
|
|
1289
|
+
* Reliability matters because a naive "send text, then send \r" loses the
|
|
1290
|
+
* Enter for multi-line prompts: the terminal treats the burst as a bracketed
|
|
1291
|
+
* paste and swallows a \r that arrives inside the paste window, leaving the
|
|
1292
|
+
* text sitting unsent in the input box. We fix that two ways:
|
|
1293
|
+
* 1. Wrap the prompt in explicit bracketed-paste markers so the (possibly
|
|
1294
|
+
* multi-line) text is ingested as one paste and the following Enter lands
|
|
1295
|
+
* *outside* it — an unambiguous submit, not a newline.
|
|
1296
|
+
* 2. Verify the turn actually started (a spinner / "esc to interrupt" hint
|
|
1297
|
+
* appears only while Claude is processing, never at the idle prompt), and
|
|
1298
|
+
* re-send Enter a few times if not, since a large paste can still be
|
|
1299
|
+
* mid-ingest when the first Enter arrives.
|
|
1300
|
+
*/
|
|
1301
|
+
async function sendInitialPrompt(adapter, blockId, initialPromptFile) {
|
|
1302
|
+
try {
|
|
1303
|
+
await waitForScrollbackMatch(adapter, blockId, "❯", "Claude prompt", 3e4);
|
|
1304
|
+
} catch {
|
|
1305
|
+
adapter.closeSocket();
|
|
1306
|
+
throw new Error("Claude prompt (❯) never appeared — not sending initial prompt. Check that claude started successfully.");
|
|
1307
|
+
}
|
|
1308
|
+
const prompt = readFileSync(initialPromptFile, "utf-8").trimEnd();
|
|
1309
|
+
await adapter.sendInput(blockId, `\x1b[200~${prompt}\x1b[201~`);
|
|
1310
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
1311
|
+
await new Promise((r) => setTimeout(r, attempt === 0 ? 300 : 500));
|
|
1312
|
+
await adapter.sendInput(blockId, "\r");
|
|
1313
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1314
|
+
const tail = adapter.scrollback(blockId, 40);
|
|
1315
|
+
const compact = tail.replace(/\s+/g, "");
|
|
1316
|
+
if (/[✻✽✶✳✢]/.test(tail) || /esctointerrupt/i.test(compact)) return;
|
|
1317
|
+
}
|
|
1318
|
+
consola.warn("Could not confirm the initial prompt was submitted — it may be sitting in the input box. Press Enter in the tab to send it.");
|
|
1319
|
+
}
|
|
1262
1320
|
async function openSession(opts) {
|
|
1263
1321
|
const { tabName, claudeCmd, workspaceQuery, initialPromptFile, envVars, modelOverride } = opts;
|
|
1322
|
+
const tailDelayMs = opts.tailDelayMs ?? 2e3;
|
|
1264
1323
|
const dir = resolve(opts.dir.replace(/^~/, homedir()));
|
|
1265
|
-
if (!existsSync(dir)) {
|
|
1266
|
-
consola.error(`Directory does not exist: ${dir}`);
|
|
1267
|
-
process.exit(1);
|
|
1268
|
-
}
|
|
1324
|
+
if (!existsSync(dir)) throw new Error(`Directory does not exist: ${dir}`);
|
|
1269
1325
|
const config = loadConfig();
|
|
1270
1326
|
const adapter = requireAdapter();
|
|
1327
|
+
const timing = !!process.env.CCTABS_TIMING;
|
|
1328
|
+
let tPhase = Date.now();
|
|
1329
|
+
const mark = (label) => {
|
|
1330
|
+
if (!timing) return;
|
|
1331
|
+
const now = Date.now();
|
|
1332
|
+
consola.log(` ⏱ ${tabName} ${label}: ${now - tPhase}ms`);
|
|
1333
|
+
tPhase = now;
|
|
1334
|
+
};
|
|
1335
|
+
if (adapter.openTabDirect) {
|
|
1336
|
+
const extraFlags = config.claude.flags.join(" ");
|
|
1337
|
+
const namePart = claudeCmd.includes("--resume") ? "" : ` --name ${JSON.stringify(tabName)}`;
|
|
1338
|
+
const modelPart = modelOverride ? ` --model ${JSON.stringify(modelOverride)}` : "";
|
|
1339
|
+
const envPrefix = envVars ? shellQuoteEnv$1(envVars) : "";
|
|
1340
|
+
const claudeCore = `claude${extraFlags ? " " + extraFlags : ""} ${claudeCmd.replace(/^claude\s*/, "")}${namePart}${modelPart}`.replace(/\s+/g, " ").trim();
|
|
1341
|
+
const shell = process.env.SHELL ?? "/bin/zsh";
|
|
1342
|
+
const launch = `${envPrefix}exec ${claudeCore}`;
|
|
1343
|
+
const { blockId, tabId } = await adapter.openTabDirect({
|
|
1344
|
+
cwd: dir,
|
|
1345
|
+
title: tabName,
|
|
1346
|
+
command: shell,
|
|
1347
|
+
args: [
|
|
1348
|
+
"-l",
|
|
1349
|
+
"-i",
|
|
1350
|
+
"-c",
|
|
1351
|
+
launch
|
|
1352
|
+
]
|
|
1353
|
+
});
|
|
1354
|
+
mark("openTabDirect");
|
|
1355
|
+
if (initialPromptFile) await sendInitialPrompt(adapter, blockId, initialPromptFile);
|
|
1356
|
+
adapter.closeSocket();
|
|
1357
|
+
return tabId;
|
|
1358
|
+
}
|
|
1271
1359
|
let focusWindowId;
|
|
1272
1360
|
if (workspaceQuery) {
|
|
1273
1361
|
const { workspaces } = await adapter.getAllData();
|
|
1274
1362
|
const matches = adapter.resolveWorkspace(workspaces, workspaceQuery);
|
|
1275
|
-
if (!matches.length) {
|
|
1276
|
-
consola.error(`No workspace matching '${workspaceQuery}'`);
|
|
1277
|
-
process.exit(1);
|
|
1278
|
-
}
|
|
1363
|
+
if (!matches.length) throw new Error(`No workspace matching '${workspaceQuery}'`);
|
|
1279
1364
|
const { data, windowId } = matches[0];
|
|
1280
|
-
if (!windowId) {
|
|
1281
|
-
consola.error(`Workspace '${data.name}' has no open window`);
|
|
1282
|
-
process.exit(1);
|
|
1283
|
-
}
|
|
1365
|
+
if (!windowId) throw new Error(`Workspace '${data.name}' has no open window`);
|
|
1284
1366
|
focusWindowId = windowId;
|
|
1285
1367
|
consola.info(`Workspace: ${data.name}`);
|
|
1286
1368
|
}
|
|
1287
1369
|
const beforeIds = new Set(adapter.blocksList().filter((b) => b.view === "term").map((b) => b.blockid));
|
|
1370
|
+
mark("beforeIds");
|
|
1288
1371
|
await adapter.newTab(focusWindowId);
|
|
1372
|
+
mark("newTab");
|
|
1289
1373
|
const result = await adapter.waitForNewBlock(beforeIds);
|
|
1290
|
-
if (!result)
|
|
1291
|
-
|
|
1292
|
-
process.exit(1);
|
|
1293
|
-
}
|
|
1374
|
+
if (!result) throw new Error("Timed out waiting for new terminal block");
|
|
1375
|
+
mark("waitForNewBlock");
|
|
1294
1376
|
const { blockId, tabId } = result;
|
|
1295
1377
|
await adapter.renameTab(tabId, tabName);
|
|
1378
|
+
mark("renameTab");
|
|
1296
1379
|
try {
|
|
1297
1380
|
await waitForScrollbackMatch(adapter, blockId, /[$%>]\s*$/, "shell prompt", 1e4, 250);
|
|
1298
1381
|
} catch {
|
|
1299
|
-
|
|
1300
|
-
process.exit(1);
|
|
1382
|
+
throw new Error("Shell prompt never appeared in new tab — aborting. Check your shell profile (e.g. nvm default alias).");
|
|
1301
1383
|
}
|
|
1384
|
+
mark("shellPrompt");
|
|
1302
1385
|
const extraFlags = config.claude.flags.join(" ");
|
|
1303
1386
|
const namePart = claudeCmd.includes("--resume") ? "" : ` --name ${JSON.stringify(tabName)}`;
|
|
1304
1387
|
const modelPart = modelOverride ? ` --model ${JSON.stringify(modelOverride)}` : "";
|
|
1305
1388
|
const envPrefix = envVars ? shellQuoteEnv$1(envVars) : "";
|
|
1306
1389
|
const cmd = `cd ${JSON.stringify(dir)} && ${envPrefix}claude${extraFlags ? " " + extraFlags : ""} ${claudeCmd.replace(/^claude\s*/, "")}${namePart}${modelPart}\r`;
|
|
1307
1390
|
await adapter.sendInput(blockId, cmd);
|
|
1308
|
-
if (initialPromptFile)
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
} catch {
|
|
1312
|
-
consola.error("Claude prompt (❯) never appeared — not sending initial prompt. Check that claude started successfully.");
|
|
1313
|
-
adapter.closeSocket();
|
|
1314
|
-
process.exit(1);
|
|
1315
|
-
}
|
|
1316
|
-
const prompt = readFileSync(initialPromptFile, "utf-8").trimEnd();
|
|
1317
|
-
await adapter.sendInput(blockId, prompt);
|
|
1318
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
1319
|
-
await adapter.sendInput(blockId, "\r");
|
|
1320
|
-
}
|
|
1321
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
1391
|
+
if (initialPromptFile) await sendInitialPrompt(adapter, blockId, initialPromptFile);
|
|
1392
|
+
if (tailDelayMs > 0) await new Promise((r) => setTimeout(r, tailDelayMs));
|
|
1393
|
+
mark("tail");
|
|
1322
1394
|
adapter.closeSocket();
|
|
1323
1395
|
return tabId;
|
|
1324
1396
|
}
|
|
@@ -2339,24 +2411,29 @@ async function runManifestMode(manifestPath, createMissing, dryRun) {
|
|
|
2339
2411
|
}
|
|
2340
2412
|
}
|
|
2341
2413
|
adapter.closeSocket();
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2414
|
+
const spawnOne = async (entry) => {
|
|
2415
|
+
try {
|
|
2416
|
+
const claudeCmd = entry.session_id ? `claude --resume ${entry.session_id} --name ${JSON.stringify(entry.name)}` : "claude";
|
|
2417
|
+
const newTabId = await openSession({
|
|
2418
|
+
tabName: entry.name,
|
|
2419
|
+
dir: entry.dir,
|
|
2420
|
+
claudeCmd,
|
|
2421
|
+
tailDelayMs: 500
|
|
2422
|
+
});
|
|
2423
|
+
const sid = entry.session_id ? entry.session_id.slice(0, 8) + "…" : "fresh";
|
|
2424
|
+
results.push({
|
|
2425
|
+
name: entry.name,
|
|
2426
|
+
result: `✔ spawned [${newTabId.slice(0, 8)}] (${sid})`
|
|
2427
|
+
});
|
|
2428
|
+
} catch (err) {
|
|
2429
|
+
results.push({
|
|
2430
|
+
name: entry.name,
|
|
2431
|
+
result: `✘ spawn failed: ${err.message}`
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
if (typeof adapter.openTabDirect === "function") await Promise.all(toSpawn.map(spawnOne));
|
|
2436
|
+
else for (const entry of toSpawn) await spawnOne(entry);
|
|
2360
2437
|
console.log("\nRestore summary:");
|
|
2361
2438
|
for (const r of results) console.log(` ${r.name}: ${r.result}`);
|
|
2362
2439
|
}
|
|
@@ -2391,6 +2468,7 @@ async function runLegacyMode(rawDir, dryRun) {
|
|
|
2391
2468
|
const extraFlags = loadConfig().claude.flags.join(" ");
|
|
2392
2469
|
const results = [];
|
|
2393
2470
|
const toRecreate = [];
|
|
2471
|
+
const resolved = [];
|
|
2394
2472
|
for (const tab of toResume) {
|
|
2395
2473
|
let sessionId = null;
|
|
2396
2474
|
let sessionDir = null;
|
|
@@ -2437,8 +2515,21 @@ async function runLegacyMode(rawDir, dryRun) {
|
|
|
2437
2515
|
});
|
|
2438
2516
|
continue;
|
|
2439
2517
|
}
|
|
2440
|
-
|
|
2441
|
-
|
|
2518
|
+
resolved.push({
|
|
2519
|
+
tab,
|
|
2520
|
+
sessionId,
|
|
2521
|
+
sessionDir
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
if (!dryRun) {
|
|
2525
|
+
const unknownTabs = resolved.filter((r) => r.tab.status === "unknown");
|
|
2526
|
+
const emptyById = /* @__PURE__ */ new Map();
|
|
2527
|
+
await Promise.all(unknownTabs.map(async (r) => {
|
|
2528
|
+
emptyById.set(r.tab.tabId, await adapter.confirmScrollbackEmpty(r.tab.blockId));
|
|
2529
|
+
}));
|
|
2530
|
+
for (const r of resolved) {
|
|
2531
|
+
const { tab, sessionId, sessionDir } = r;
|
|
2532
|
+
if (tab.status === "unknown" && emptyById.get(tab.tabId)) {
|
|
2442
2533
|
const blockIds = (tabsById.get(tab.tabId) ?? []).map((b) => b.blockid);
|
|
2443
2534
|
toRecreate.push({
|
|
2444
2535
|
name: tab.name,
|
|
@@ -2453,15 +2544,15 @@ async function runLegacyMode(rawDir, dryRun) {
|
|
|
2453
2544
|
});
|
|
2454
2545
|
continue;
|
|
2455
2546
|
}
|
|
2547
|
+
consola.log(` ${tab.name} → resuming session ${sessionId.slice(0, 8)}… in ${sessionDir} (send)`);
|
|
2548
|
+
const cmd = `cd ${JSON.stringify(sessionDir)} && claude${extraFlags ? " " + extraFlags : ""} --resume ${sessionId} --name ${JSON.stringify(tab.name)}\r`;
|
|
2549
|
+
await adapter.sendInput(tab.blockId, cmd);
|
|
2550
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2551
|
+
results.push({
|
|
2552
|
+
name: tab.name,
|
|
2553
|
+
result: "sent"
|
|
2554
|
+
});
|
|
2456
2555
|
}
|
|
2457
|
-
consola.log(` ${tab.name} → resuming session ${sessionId.slice(0, 8)}… in ${sessionDir} (send)`);
|
|
2458
|
-
const cmd = `cd ${JSON.stringify(sessionDir)} && claude${extraFlags ? " " + extraFlags : ""} --resume ${sessionId} --name ${JSON.stringify(tab.name)}\r`;
|
|
2459
|
-
await adapter.sendInput(tab.blockId, cmd);
|
|
2460
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
2461
|
-
results.push({
|
|
2462
|
-
name: tab.name,
|
|
2463
|
-
result: "sent"
|
|
2464
|
-
});
|
|
2465
2556
|
}
|
|
2466
2557
|
if (!dryRun) {
|
|
2467
2558
|
const sent = results.filter((r) => r.result === "sent");
|
|
@@ -2481,18 +2572,23 @@ async function runLegacyMode(rawDir, dryRun) {
|
|
|
2481
2572
|
for (const t of toRecreate) for (const bid of t.blockIds) adapter.deleteBlock(bid);
|
|
2482
2573
|
adapter.closeSocket();
|
|
2483
2574
|
consola.info(`Recreating ${toRecreate.length} dead tab(s)…`);
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2575
|
+
const recreateOne = async (t) => {
|
|
2576
|
+
try {
|
|
2577
|
+
const newTabId = await openSession({
|
|
2578
|
+
tabName: t.name,
|
|
2579
|
+
dir: t.sessionDir,
|
|
2580
|
+
claudeCmd: `claude --resume ${t.sessionId} --name ${JSON.stringify(t.name)}`,
|
|
2581
|
+
tailDelayMs: 500
|
|
2582
|
+
});
|
|
2583
|
+
const r = results.find((x) => x.name === t.name);
|
|
2584
|
+
r.result = `✔ recreated [${newTabId.slice(0, 8)}]`;
|
|
2585
|
+
} catch (err) {
|
|
2586
|
+
const r = results.find((x) => x.name === t.name);
|
|
2587
|
+
r.result = `✘ recreate failed: ${err.message}`;
|
|
2588
|
+
}
|
|
2589
|
+
};
|
|
2590
|
+
if (typeof adapter.openTabDirect === "function") await Promise.all(toRecreate.map(recreateOne));
|
|
2591
|
+
else for (const t of toRecreate) await recreateOne(t);
|
|
2496
2592
|
} else adapter.closeSocket();
|
|
2497
2593
|
console.log("\nRestore summary:");
|
|
2498
2594
|
for (const r of results) console.log(` ${r.name}: ${r.result}`);
|
|
@@ -2603,18 +2699,18 @@ function checkTabbyPlugin() {
|
|
|
2603
2699
|
}
|
|
2604
2700
|
/**
|
|
2605
2701
|
* Probe whether `node` is findable in a freshly spawned shell — the canonical
|
|
2606
|
-
* symptom of the macOS
|
|
2607
|
-
* node'` simulates the same login
|
|
2608
|
-
* that cctabs
|
|
2609
|
-
* tabs will also fail to find Node, every plugin MCP
|
|
2610
|
-
* will ENOENT, and the cctabs CLI itself becomes
|
|
2611
|
-
* tabs (chicken-and-egg). The
|
|
2612
|
-
*
|
|
2613
|
-
* workaround for users on older versions or non-Tabby terminals.
|
|
2702
|
+
* symptom of the macOS PATH-sourcing bug. Spawning `zsh -l -i -c 'command -v
|
|
2703
|
+
* node'` simulates the same login + interactive shell init (/etc/zprofile →
|
|
2704
|
+
* path_helper, then ~/.zshrc) that cctabs uses when it opens new Tabby tabs.
|
|
2705
|
+
* If this fails, brand-new tabs will also fail to find Node, every plugin MCP
|
|
2706
|
+
* that shells out to npx will ENOENT, and the cctabs CLI itself becomes
|
|
2707
|
+
* unusable from inside those tabs (chicken-and-egg). The flags must match
|
|
2708
|
+
* open-session.ts to keep the doctor honest.
|
|
2614
2709
|
*/
|
|
2615
2710
|
function checkSpawnedShellPath() {
|
|
2616
2711
|
const r = spawnSync("zsh", [
|
|
2617
2712
|
"-l",
|
|
2713
|
+
"-i",
|
|
2618
2714
|
"-c",
|
|
2619
2715
|
"command -v node"
|
|
2620
2716
|
], {
|
|
@@ -2629,8 +2725,8 @@ function checkSpawnedShellPath() {
|
|
|
2629
2725
|
return {
|
|
2630
2726
|
name: "Spawned shell PATH",
|
|
2631
2727
|
status: "warn",
|
|
2632
|
-
detail: r.error?.message ?? r.stderr?.trim() ?? "node not found in a login zsh",
|
|
2633
|
-
hint: "A login zsh cannot find `node`. Either node is not installed, or PATH is broken.
|
|
2728
|
+
detail: r.error?.message ?? r.stderr?.trim() ?? "node not found in a login+interactive zsh",
|
|
2729
|
+
hint: "A login+interactive zsh cannot find `node`. Either node is not installed, or PATH is broken. cctabs spawns tabs with `zsh -l -i -c` so both ~/.zprofile and ~/.zshrc are sourced — if your PATH-extending logic lives elsewhere (e.g. a sourced file that bails on non-interactive), move the `export PATH=...` lines into ~/.zshenv as a belt-and-braces fix."
|
|
2634
2730
|
};
|
|
2635
2731
|
}
|
|
2636
2732
|
function checkWaveDb() {
|
|
@@ -3301,7 +3397,12 @@ async function run() {
|
|
|
3301
3397
|
}
|
|
3302
3398
|
//#endregion
|
|
3303
3399
|
//#region src/index.ts
|
|
3304
|
-
updateNotifier({ pkg: package_default })
|
|
3400
|
+
const notifier = updateNotifier({ pkg: package_default });
|
|
3401
|
+
notifier.notify();
|
|
3402
|
+
if (notifier.update && notifier.update.latest !== notifier.update.current) {
|
|
3403
|
+
const { current, latest } = notifier.update;
|
|
3404
|
+
process.stdout.write(`[cctabs] OUTDATED ${current} < ${latest} — run: npm install -g ${name}@latest\n`);
|
|
3405
|
+
}
|
|
3305
3406
|
run().catch((err) => {
|
|
3306
3407
|
console.error(err instanceof Error ? err.message : String(err));
|
|
3307
3408
|
process.exit(1);
|
package/package.json
CHANGED
package/skills/cctabs/SKILL.md
CHANGED
|
@@ -44,6 +44,14 @@ npm install -g @generativereality/cctabs
|
|
|
44
44
|
|
|
45
45
|
Do not modify PATH or npm configuration beyond this.
|
|
46
46
|
|
|
47
|
+
### Check the installed version isn't stale
|
|
48
|
+
|
|
49
|
+
On your first cctabs invocation in a session, look at the version banner cctabs prints (`(@generativereality/cctabs vX.Y.Z)`) and at any `[cctabs] OUTDATED ...` warning line in the output. If you see the warning — or if the banner version is older than the version in this skill's `plugin.json` — tell the user:
|
|
50
|
+
|
|
51
|
+
> *"Your installed cctabs is `vX.Y.Z`; the current release is `vA.B.C`. Want me to upgrade with `npm install -g @generativereality/cctabs@latest` before continuing?"*
|
|
52
|
+
|
|
53
|
+
Don't silently work around an outdated CLI: detection heuristics, command flags, and bug fixes diverge between versions, so misbehavior on the user's machine is often "binary on PATH lags behind the plugin docs you're reading." The Claude Code marketplace plugin update path only refreshes this skill — the npm-installed CLI binary is a separate channel and must be upgraded explicitly.
|
|
54
|
+
|
|
47
55
|
### Tabby users: a one-time plugin install is needed
|
|
48
56
|
|
|
49
57
|
Wave Terminal works out of the box. **Tabby additionally needs a small companion plugin** that exposes a localhost HTTP API the cctabs CLI talks to.
|
|
@@ -66,7 +74,7 @@ On approval, run `cctabs install-tabby-plugin --yes`. Tabby quits ~2s after the
|
|
|
66
74
|
|
|
67
75
|
If the user wants to keep their other Tabby tabs intact, run `cctabs install-tabby-plugin --no-restart` instead and tell them to quit + reopen Tabby themselves.
|
|
68
76
|
|
|
69
|
-
`cctabs doctor` is also available for a deliberate environment check
|
|
77
|
+
`cctabs doctor` is also available for a deliberate environment check. It adapts to whichever terminal you're running in — terminal detection runs either way; on Wave it additionally inspects Accessibility permission and scans the Wave DB for orphan tabids; on Tabby it probes the cctabs plugin's localhost health endpoint. Useful if something feels off, but **not required as a preflight** since every command fails loudly on its own.
|
|
70
78
|
|
|
71
79
|
#### Auto-install + auto-restart (recommended)
|
|
72
80
|
|
|
@@ -263,7 +271,7 @@ cctabs resume api ~/Dev/myapp
|
|
|
263
271
|
|
|
264
272
|
## Workflow: Restoring tabs after a reboot
|
|
265
273
|
|
|
266
|
-
After a
|
|
274
|
+
After a terminal restart or computer reboot, every tab loses its Claude session and shows up with `terminal` or `unknown` status (true for both Wave and Tabby). `cctabs restore` walks every such tab, looks up its session by name across **all** Claude project directories, and re-attaches in place.
|
|
267
275
|
|
|
268
276
|
```bash
|
|
269
277
|
cctabs restore # search all projects (default)
|