@generativereality/cctabs 0.1.4 → 0.2.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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +269 -11
- package/package.json +1 -1
- package/skills/cctabs/SKILL.md +66 -0
|
@@ -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
|
+
"version": "0.2.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "generativereality",
|
|
7
7
|
"url": "https://cctabs.com"
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { consola } from "consola";
|
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
11
11
|
//#region package.json
|
|
12
12
|
var name = "@generativereality/cctabs";
|
|
13
|
-
var version = "0.
|
|
13
|
+
var version = "0.2.0";
|
|
14
14
|
var description = "Claude Code tab manager. Terminal tabs as the UI, no tmux.";
|
|
15
15
|
var package_default = {
|
|
16
16
|
name,
|
|
@@ -540,6 +540,11 @@ function ensureConfigExists() {
|
|
|
540
540
|
}
|
|
541
541
|
//#endregion
|
|
542
542
|
//#region src/core/open-session.ts
|
|
543
|
+
function shellQuoteEnv$1(env) {
|
|
544
|
+
const entries = Object.entries(env);
|
|
545
|
+
if (!entries.length) return "";
|
|
546
|
+
return entries.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ") + " ";
|
|
547
|
+
}
|
|
543
548
|
/** Poll scrollback until a pattern is visible, then return. Rejects on timeout. */
|
|
544
549
|
async function waitForScrollbackMatch(adapter, blockId, pattern, label, timeoutMs, pollInterval = 1e3) {
|
|
545
550
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -554,7 +559,7 @@ async function waitForScrollbackMatch(adapter, blockId, pattern, label, timeoutM
|
|
|
554
559
|
throw new Error(`Timed out waiting for ${label}`);
|
|
555
560
|
}
|
|
556
561
|
async function openSession(opts) {
|
|
557
|
-
const { tabName, claudeCmd, workspaceQuery, initialPromptFile } = opts;
|
|
562
|
+
const { tabName, claudeCmd, workspaceQuery, initialPromptFile, envVars, modelOverride } = opts;
|
|
558
563
|
const dir = resolve(opts.dir.replace(/^~/, homedir()));
|
|
559
564
|
if (!existsSync(dir)) {
|
|
560
565
|
consola.error(`Directory does not exist: ${dir}`);
|
|
@@ -595,7 +600,9 @@ async function openSession(opts) {
|
|
|
595
600
|
}
|
|
596
601
|
const extraFlags = config.claude.flags.join(" ");
|
|
597
602
|
const namePart = claudeCmd.includes("--resume") ? "" : ` --name ${JSON.stringify(tabName)}`;
|
|
598
|
-
const
|
|
603
|
+
const modelPart = modelOverride ? ` --model ${JSON.stringify(modelOverride)}` : "";
|
|
604
|
+
const envPrefix = envVars ? shellQuoteEnv$1(envVars) : "";
|
|
605
|
+
const cmd = `cd ${JSON.stringify(dir)} && ${envPrefix}claude${extraFlags ? " " + extraFlags : ""} ${claudeCmd.replace(/^claude\s*/, "")}${namePart}${modelPart}\r`;
|
|
599
606
|
await adapter.sendInput(blockId, cmd);
|
|
600
607
|
if (initialPromptFile) {
|
|
601
608
|
try {
|
|
@@ -615,6 +622,179 @@ async function openSession(opts) {
|
|
|
615
622
|
return tabId;
|
|
616
623
|
}
|
|
617
624
|
//#endregion
|
|
625
|
+
//#region src/core/backends.ts
|
|
626
|
+
/**
|
|
627
|
+
* Backend presets. Each preset resolves to a set of env vars (prepended to the
|
|
628
|
+
* shell command in the new tab) plus a Claude --model name.
|
|
629
|
+
*
|
|
630
|
+
* The default `anthropic` preset is a no-op: no env vars, no --model override —
|
|
631
|
+
* Claude Code uses its built-in API connection.
|
|
632
|
+
*
|
|
633
|
+
* Ollama-backed presets point ANTHROPIC_BASE_URL at Ollama's
|
|
634
|
+
* Anthropic-compatible /v1/messages endpoint (Ollama ≥ 0.14):
|
|
635
|
+
* https://docs.ollama.com/openai
|
|
636
|
+
*
|
|
637
|
+
* The `*-tee` variants route through the local logging proxy on :11500
|
|
638
|
+
* (`npm run ollama-tee` in the motin-scripts repo) for wire-level inspection.
|
|
639
|
+
*/
|
|
640
|
+
const OLLAMA_LOCAL = "http://localhost:11434";
|
|
641
|
+
const OLLAMA_TEE = "http://localhost:11500";
|
|
642
|
+
/**
|
|
643
|
+
* For Ollama-backed Claude Code sessions we pin the small/fast/haiku model to
|
|
644
|
+
* the same model. Otherwise Claude Code's background "haiku" calls 404 against
|
|
645
|
+
* Ollama because the haiku tag doesn't exist there.
|
|
646
|
+
*/
|
|
647
|
+
function ollamaEnv(baseUrl, model) {
|
|
648
|
+
return {
|
|
649
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
650
|
+
ANTHROPIC_AUTH_TOKEN: "ollama",
|
|
651
|
+
ANTHROPIC_API_KEY: "",
|
|
652
|
+
ANTHROPIC_SMALL_FAST_MODEL: model,
|
|
653
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: model
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const BUILTIN_BACKENDS = {
|
|
657
|
+
anthropic: {
|
|
658
|
+
env: {},
|
|
659
|
+
model: "",
|
|
660
|
+
description: "Default Anthropic API (no override)"
|
|
661
|
+
},
|
|
662
|
+
kimi: {
|
|
663
|
+
env: ollamaEnv(OLLAMA_LOCAL, "kimi-k2.6:cloud"),
|
|
664
|
+
model: "kimi-k2.6:cloud",
|
|
665
|
+
description: "Kimi K2.6 via Ollama Cloud (Pro)"
|
|
666
|
+
},
|
|
667
|
+
"qwen-cloud": {
|
|
668
|
+
env: ollamaEnv(OLLAMA_LOCAL, "qwen3-coder-next:cloud"),
|
|
669
|
+
model: "qwen3-coder-next:cloud",
|
|
670
|
+
description: "Qwen3 Coder Next via Ollama Cloud"
|
|
671
|
+
},
|
|
672
|
+
"gemma-cloud": {
|
|
673
|
+
env: ollamaEnv(OLLAMA_LOCAL, "gemma4:31b-cloud"),
|
|
674
|
+
model: "gemma4:31b-cloud",
|
|
675
|
+
description: "Gemma4 31B via Ollama Cloud"
|
|
676
|
+
},
|
|
677
|
+
"qwen-local": {
|
|
678
|
+
env: ollamaEnv(OLLAMA_LOCAL, "qwen3-coder:30b"),
|
|
679
|
+
model: "qwen3-coder:30b",
|
|
680
|
+
description: "Qwen3 Coder 30B local (18GB)"
|
|
681
|
+
},
|
|
682
|
+
"qwen-next-local": {
|
|
683
|
+
env: ollamaEnv(OLLAMA_LOCAL, "qwen3-coder-next:q3_K_M"),
|
|
684
|
+
model: "qwen3-coder-next:q3_K_M",
|
|
685
|
+
description: "Qwen3 Coder Next Q3_K_M local (38GB) — needs `ollama create` import"
|
|
686
|
+
},
|
|
687
|
+
"gpt-oss": {
|
|
688
|
+
env: ollamaEnv(OLLAMA_LOCAL, "gpt-oss:20b"),
|
|
689
|
+
model: "gpt-oss:20b",
|
|
690
|
+
description: "gpt-oss 20B local (13GB)"
|
|
691
|
+
},
|
|
692
|
+
llama: {
|
|
693
|
+
env: ollamaEnv(OLLAMA_LOCAL, "llama3.1:8b"),
|
|
694
|
+
model: "llama3.1:8b",
|
|
695
|
+
description: "Llama 3.1 8B local (5GB) — note: garbles on Claude Code's 50k system prompt"
|
|
696
|
+
},
|
|
697
|
+
"gemma-local": {
|
|
698
|
+
env: ollamaEnv(OLLAMA_LOCAL, "gemma4:26b"),
|
|
699
|
+
model: "gemma4:26b",
|
|
700
|
+
description: "Gemma4 26B local (17GB)"
|
|
701
|
+
},
|
|
702
|
+
"kimi-tee": {
|
|
703
|
+
env: ollamaEnv(OLLAMA_TEE, "kimi-k2.6:cloud"),
|
|
704
|
+
model: "kimi-k2.6:cloud",
|
|
705
|
+
description: "Kimi via tee proxy (logs to /tmp/ollama-tee.log)"
|
|
706
|
+
},
|
|
707
|
+
"qwen-cloud-tee": {
|
|
708
|
+
env: ollamaEnv(OLLAMA_TEE, "qwen3-coder-next:cloud"),
|
|
709
|
+
model: "qwen3-coder-next:cloud",
|
|
710
|
+
description: "Qwen Cloud via tee proxy"
|
|
711
|
+
},
|
|
712
|
+
"qwen-next-local-tee": {
|
|
713
|
+
env: ollamaEnv(OLLAMA_TEE, "qwen3-coder-next:q3_K_M"),
|
|
714
|
+
model: "qwen3-coder-next:q3_K_M",
|
|
715
|
+
description: "Qwen Next local Q3 via tee proxy"
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
/**
|
|
719
|
+
* Parse a `[backends.<name>]` section from the config TOML. Each section can
|
|
720
|
+
* override env vars and/or model. Format:
|
|
721
|
+
*
|
|
722
|
+
* [backends.my-preset]
|
|
723
|
+
* model = "qwen3-coder-next:cloud"
|
|
724
|
+
* base_url = "http://localhost:11434"
|
|
725
|
+
* auth_token = "ollama" # optional, defaults to "ollama" if base_url is set
|
|
726
|
+
*
|
|
727
|
+
* Or for full control:
|
|
728
|
+
*
|
|
729
|
+
* [backends.my-preset]
|
|
730
|
+
* model = "..."
|
|
731
|
+
* env_ANTHROPIC_BASE_URL = "..."
|
|
732
|
+
* env_ANTHROPIC_AUTH_TOKEN = "..."
|
|
733
|
+
*/
|
|
734
|
+
function loadCustomBackends() {
|
|
735
|
+
if (!existsSync(CONFIG_PATH)) return {};
|
|
736
|
+
const text = readFileSync(CONFIG_PATH, "utf-8");
|
|
737
|
+
const sections = {};
|
|
738
|
+
let section = null;
|
|
739
|
+
for (const raw of text.split("\n")) {
|
|
740
|
+
const line = raw.trim();
|
|
741
|
+
if (!line || line.startsWith("#")) continue;
|
|
742
|
+
if (line.startsWith("[") && line.endsWith("]")) {
|
|
743
|
+
section = line.slice(1, -1).trim();
|
|
744
|
+
sections[section] ??= {};
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (section?.startsWith("backends.") && line.includes("=")) {
|
|
748
|
+
const [rawKey, ...rest] = line.split("=");
|
|
749
|
+
const key = rawKey.trim();
|
|
750
|
+
const val = rest.join("=").trim();
|
|
751
|
+
if (val.startsWith("\"") && val.endsWith("\"")) sections[section][key] = val.slice(1, -1);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
const result = {};
|
|
755
|
+
for (const [section, kv] of Object.entries(sections)) {
|
|
756
|
+
if (!section.startsWith("backends.")) continue;
|
|
757
|
+
const name = section.slice(9);
|
|
758
|
+
const model = kv.model ?? "";
|
|
759
|
+
const env = {};
|
|
760
|
+
if (kv.base_url) {
|
|
761
|
+
const baseUrl = kv.base_url;
|
|
762
|
+
const token = kv.auth_token ?? "ollama";
|
|
763
|
+
Object.assign(env, {
|
|
764
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
765
|
+
ANTHROPIC_AUTH_TOKEN: token,
|
|
766
|
+
ANTHROPIC_API_KEY: ""
|
|
767
|
+
});
|
|
768
|
+
if (model) {
|
|
769
|
+
env.ANTHROPIC_SMALL_FAST_MODEL = model;
|
|
770
|
+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = model;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
for (const [k, v] of Object.entries(kv)) if (k.startsWith("env_")) env[k.slice(4)] = v;
|
|
774
|
+
result[name] = {
|
|
775
|
+
env,
|
|
776
|
+
model,
|
|
777
|
+
description: kv.description ?? `User-defined preset (${CONFIG_PATH})`
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
return result;
|
|
781
|
+
}
|
|
782
|
+
function resolveBackend(name) {
|
|
783
|
+
if (!name) return null;
|
|
784
|
+
return loadCustomBackends()[name] ?? BUILTIN_BACKENDS[name] ?? null;
|
|
785
|
+
}
|
|
786
|
+
function listBackends() {
|
|
787
|
+
const custom = loadCustomBackends();
|
|
788
|
+
const merged = {
|
|
789
|
+
...BUILTIN_BACKENDS,
|
|
790
|
+
...custom
|
|
791
|
+
};
|
|
792
|
+
return Object.entries(merged).map(([name, spec]) => ({
|
|
793
|
+
name,
|
|
794
|
+
description: spec.description ?? ""
|
|
795
|
+
}));
|
|
796
|
+
}
|
|
797
|
+
//#endregion
|
|
618
798
|
//#region src/commands/new.ts
|
|
619
799
|
const newCommand = define({
|
|
620
800
|
name: "new",
|
|
@@ -647,6 +827,16 @@ const newCommand = define({
|
|
|
647
827
|
type: "string",
|
|
648
828
|
short: "p",
|
|
649
829
|
description: "Send initial prompt text once Claude is ready"
|
|
830
|
+
},
|
|
831
|
+
backend: {
|
|
832
|
+
type: "string",
|
|
833
|
+
short: "b",
|
|
834
|
+
description: "Backend preset (e.g. kimi, qwen-cloud, qwen-next-local, gpt-oss). Run `cctabs backends` to list."
|
|
835
|
+
},
|
|
836
|
+
model: {
|
|
837
|
+
type: "string",
|
|
838
|
+
short: "m",
|
|
839
|
+
description: "Override the model name (passed as --model to claude). Useful with --backend ollama-local."
|
|
650
840
|
}
|
|
651
841
|
},
|
|
652
842
|
async run(ctx) {
|
|
@@ -656,10 +846,24 @@ const newCommand = define({
|
|
|
656
846
|
const useWorktree = ctx.values.worktree ?? false;
|
|
657
847
|
const promptFile = ctx.values.file;
|
|
658
848
|
const promptText = ctx.values.prompt;
|
|
849
|
+
const backendName = ctx.values.backend;
|
|
850
|
+
const modelOverride = ctx.values.model;
|
|
659
851
|
if (!name) {
|
|
660
852
|
consola.error("Tab name is required");
|
|
661
853
|
process.exit(1);
|
|
662
854
|
}
|
|
855
|
+
let envVars;
|
|
856
|
+
let resolvedModel = modelOverride;
|
|
857
|
+
if (backendName) {
|
|
858
|
+
const backend = resolveBackend(backendName);
|
|
859
|
+
if (!backend) {
|
|
860
|
+
consola.error(`Unknown backend "${backendName}". Available:`);
|
|
861
|
+
for (const b of listBackends()) consola.log(` ${b.name.padEnd(22)} ${b.description}`);
|
|
862
|
+
process.exit(1);
|
|
863
|
+
}
|
|
864
|
+
envVars = backend.env;
|
|
865
|
+
resolvedModel ??= backend.model || void 0;
|
|
866
|
+
}
|
|
663
867
|
let initialPromptFile;
|
|
664
868
|
if (promptText) {
|
|
665
869
|
initialPromptFile = join(tmpdir(), `cctabs-prompt-${Date.now()}.txt`);
|
|
@@ -670,10 +874,13 @@ const newCommand = define({
|
|
|
670
874
|
dir,
|
|
671
875
|
claudeCmd: useWorktree ? `claude --worktree ${JSON.stringify(name)}` : "claude",
|
|
672
876
|
workspaceQuery: workspace,
|
|
673
|
-
initialPromptFile
|
|
877
|
+
initialPromptFile,
|
|
878
|
+
envVars,
|
|
879
|
+
modelOverride: resolvedModel
|
|
674
880
|
});
|
|
675
|
-
const
|
|
676
|
-
|
|
881
|
+
const wt = useWorktree ? ` (worktree: .claude/worktrees/${name})` : "";
|
|
882
|
+
const be = backendName ? ` [backend: ${backendName}${resolvedModel ? ` → ${resolvedModel}` : ""}]` : "";
|
|
883
|
+
consola.success(`Tab "${name}" [${tabId.slice(0, 8)}] → claude at ${dir}${wt}${be}`);
|
|
677
884
|
}
|
|
678
885
|
});
|
|
679
886
|
//#endregion
|
|
@@ -904,6 +1111,11 @@ function expandSessionId(input, dir) {
|
|
|
904
1111
|
}
|
|
905
1112
|
//#endregion
|
|
906
1113
|
//#region src/commands/resume.ts
|
|
1114
|
+
function shellQuoteEnv(env) {
|
|
1115
|
+
const entries = Object.entries(env);
|
|
1116
|
+
if (!entries.length) return "";
|
|
1117
|
+
return entries.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ") + " ";
|
|
1118
|
+
}
|
|
907
1119
|
function formatAge(mtimeMs) {
|
|
908
1120
|
const mins = Math.round((Date.now() - mtimeMs) / 6e4);
|
|
909
1121
|
if (mins < 60) return `${mins}m ago`;
|
|
@@ -932,6 +1144,16 @@ const resumeCommand = define({
|
|
|
932
1144
|
type: "string",
|
|
933
1145
|
short: "s",
|
|
934
1146
|
description: "Session ID to resume (use when multiple sessions share the same name)"
|
|
1147
|
+
},
|
|
1148
|
+
backend: {
|
|
1149
|
+
type: "string",
|
|
1150
|
+
short: "b",
|
|
1151
|
+
description: "Backend preset (e.g. kimi, qwen-cloud, qwen-next-local). Run `cctabs backends` to list."
|
|
1152
|
+
},
|
|
1153
|
+
model: {
|
|
1154
|
+
type: "string",
|
|
1155
|
+
short: "m",
|
|
1156
|
+
description: "Override the model name (passed as --model to claude)."
|
|
935
1157
|
}
|
|
936
1158
|
},
|
|
937
1159
|
async run(ctx) {
|
|
@@ -942,6 +1164,20 @@ const resumeCommand = define({
|
|
|
942
1164
|
process.exit(1);
|
|
943
1165
|
}
|
|
944
1166
|
const explicitSession = ctx.values.session;
|
|
1167
|
+
const backendName = ctx.values.backend;
|
|
1168
|
+
const modelOverride = ctx.values.model;
|
|
1169
|
+
let envVars;
|
|
1170
|
+
let resolvedModel = modelOverride;
|
|
1171
|
+
if (backendName) {
|
|
1172
|
+
const backend = resolveBackend(backendName);
|
|
1173
|
+
if (!backend) {
|
|
1174
|
+
consola.error(`Unknown backend "${backendName}". Available:`);
|
|
1175
|
+
for (const b of listBackends()) consola.log(` ${b.name.padEnd(22)} ${b.description}`);
|
|
1176
|
+
process.exit(1);
|
|
1177
|
+
}
|
|
1178
|
+
envVars = backend.env;
|
|
1179
|
+
resolvedModel ??= backend.model || void 0;
|
|
1180
|
+
}
|
|
945
1181
|
let sessionId;
|
|
946
1182
|
if (explicitSession) {
|
|
947
1183
|
const expanded = expandSessionId(explicitSession, dir) ?? expandSessionId(explicitSession);
|
|
@@ -1005,14 +1241,18 @@ const resumeCommand = define({
|
|
|
1005
1241
|
const newTabId = await openSession({
|
|
1006
1242
|
tabName: name,
|
|
1007
1243
|
dir,
|
|
1008
|
-
claudeCmd: `claude --resume ${sessionId} --name ${JSON.stringify(name)}
|
|
1244
|
+
claudeCmd: `claude --resume ${sessionId} --name ${JSON.stringify(name)}`,
|
|
1245
|
+
envVars,
|
|
1246
|
+
modelOverride: resolvedModel
|
|
1009
1247
|
});
|
|
1010
1248
|
consola.success(`Tab "${name}" [${newTabId.slice(0, 8)}] → claude --resume ${sessionId.slice(0, 8)}… at ${dir} (recreated)`);
|
|
1011
1249
|
return;
|
|
1012
1250
|
}
|
|
1013
1251
|
}
|
|
1014
1252
|
const extraFlags = loadConfig().claude.flags.join(" ");
|
|
1015
|
-
const
|
|
1253
|
+
const envPrefix = envVars ? shellQuoteEnv(envVars) : "";
|
|
1254
|
+
const modelPart = resolvedModel ? ` --model ${JSON.stringify(resolvedModel)}` : "";
|
|
1255
|
+
const cmd = `cd ${JSON.stringify(dir)} && ${envPrefix}claude${extraFlags ? " " + extraFlags : ""} --resume ${sessionId} --name ${JSON.stringify(name)}${modelPart}\r`;
|
|
1016
1256
|
await adapter.sendInput(termBlock.blockid, cmd);
|
|
1017
1257
|
let verified = false;
|
|
1018
1258
|
const deadline = Date.now() + 15e3;
|
|
@@ -1032,7 +1272,9 @@ const resumeCommand = define({
|
|
|
1032
1272
|
const tabId = await openSession({
|
|
1033
1273
|
tabName: name,
|
|
1034
1274
|
dir,
|
|
1035
|
-
claudeCmd: `claude --resume ${sessionId} --name ${JSON.stringify(name)}
|
|
1275
|
+
claudeCmd: `claude --resume ${sessionId} --name ${JSON.stringify(name)}`,
|
|
1276
|
+
envVars,
|
|
1277
|
+
modelOverride: resolvedModel
|
|
1036
1278
|
});
|
|
1037
1279
|
consola.success(`Tab "${name}" [${tabId.slice(0, 8)}] → claude --resume ${sessionId.slice(0, 8)}… at ${dir} (new tab)`);
|
|
1038
1280
|
} else {
|
|
@@ -1040,7 +1282,9 @@ const resumeCommand = define({
|
|
|
1040
1282
|
const tabId = await openSession({
|
|
1041
1283
|
tabName: name,
|
|
1042
1284
|
dir,
|
|
1043
|
-
claudeCmd: "claude"
|
|
1285
|
+
claudeCmd: "claude",
|
|
1286
|
+
envVars,
|
|
1287
|
+
modelOverride: resolvedModel
|
|
1044
1288
|
});
|
|
1045
1289
|
consola.success(`Tab "${name}" [${tabId.slice(0, 8)}] → claude at ${dir} (new tab, no prior session found)`);
|
|
1046
1290
|
}
|
|
@@ -1497,6 +1741,19 @@ const restoreCommand = define({
|
|
|
1497
1741
|
}
|
|
1498
1742
|
});
|
|
1499
1743
|
//#endregion
|
|
1744
|
+
//#region src/commands/backends.ts
|
|
1745
|
+
const backendsCommand = define({
|
|
1746
|
+
name: "backends",
|
|
1747
|
+
description: "List available Claude Code backend presets (Anthropic, Ollama Cloud, local Ollama)",
|
|
1748
|
+
args: {},
|
|
1749
|
+
async run() {
|
|
1750
|
+
consola.log("Available backends:\n");
|
|
1751
|
+
for (const b of listBackends()) consola.log(` ${b.name.padEnd(22)} ${b.description}`);
|
|
1752
|
+
consola.log("\nUsage: cctabs new <tab> <dir> --backend <name>");
|
|
1753
|
+
consola.log("Add custom presets in ~/.config/cctabs/config.toml under [backends.<name>].");
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
//#endregion
|
|
1500
1757
|
//#region src/commands/index.ts
|
|
1501
1758
|
const defaultCommand = define({
|
|
1502
1759
|
name: "cctabs",
|
|
@@ -1518,7 +1775,8 @@ const subCommands = new Map([
|
|
|
1518
1775
|
["scrollback", scrollbackCommand],
|
|
1519
1776
|
["send", sendCommand],
|
|
1520
1777
|
["config", configCommand],
|
|
1521
|
-
["restore", restoreCommand]
|
|
1778
|
+
["restore", restoreCommand],
|
|
1779
|
+
["backends", backendsCommand]
|
|
1522
1780
|
]);
|
|
1523
1781
|
async function run() {
|
|
1524
1782
|
await cli(process.argv.slice(2), defaultCommand, {
|
package/package.json
CHANGED
package/skills/cctabs/SKILL.md
CHANGED
|
@@ -70,6 +70,7 @@ cctabs new fix-api ~/Dev/myapp --worktree --prompt "checkout PR #102 and fix tes
|
|
|
70
70
|
cctabs sessions # list all tabs with session status
|
|
71
71
|
cctabs list # list all workspaces, tabs, and blocks
|
|
72
72
|
cctabs new <name> [dir] [-w workspace] [-p "prompt"] [-f file] # new tab + claude
|
|
73
|
+
cctabs new <name> [dir] -b <preset> # new tab on a non-Anthropic backend (Ollama)
|
|
73
74
|
cctabs resume <name> [dir] # resume last session (reuses tab or creates one)
|
|
74
75
|
cctabs restore [dir] [--dry] # resume every dead tab (e.g. after a reboot)
|
|
75
76
|
cctabs fork <tab-name> [-n new-name] # fork session into new tab (--resume <id> --fork-session)
|
|
@@ -77,9 +78,74 @@ cctabs close <name-or-id> # close a tab
|
|
|
77
78
|
cctabs rename <name-or-id> <new-name> # rename a tab
|
|
78
79
|
cctabs scrollback <tab-or-block> [n] # read terminal output (default: 50 lines)
|
|
79
80
|
cctabs send <tab-or-block> [text] # send input — arg, --file, or stdin pipe
|
|
81
|
+
cctabs backends # list available backend presets
|
|
80
82
|
cctabs config # show config and path
|
|
81
83
|
```
|
|
82
84
|
|
|
85
|
+
## Backends: running Claude Code on Ollama / Kimi / Qwen / local models
|
|
86
|
+
|
|
87
|
+
By default, `cctabs new` runs `claude` against the Anthropic API. Pass `--backend <preset>` (or `-b`) to launch the tab against a different model provider — useful for cheap/free scratch sessions, privacy-sensitive work, or experimenting with frontier open-weight models.
|
|
88
|
+
|
|
89
|
+
`cctabs` does this by prepending the right env vars (`ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_DEFAULT_HAIKU_MODEL`, etc.) and `--model <name>` to the `claude` command in the new tab.
|
|
90
|
+
|
|
91
|
+
### Built-in presets
|
|
92
|
+
|
|
93
|
+
Run `cctabs backends` for the live list. Common ones:
|
|
94
|
+
|
|
95
|
+
| Preset | What it is | When to use |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `anthropic` (default) | Anthropic API | Production / coding work where capability matters |
|
|
98
|
+
| `kimi` | Kimi K2.6 via Ollama Cloud (Pro tier) | Cheap frontier alternative; ~5s/turn |
|
|
99
|
+
| `qwen-cloud` | Qwen3 Coder Next via Ollama Cloud | Fastest Pro option (~3.8s/turn) |
|
|
100
|
+
| `gemma-cloud` | Gemma4 31B via Ollama Cloud | Cheap general-purpose |
|
|
101
|
+
| `qwen-local` | Qwen3 Coder 30B local (18GB) | Offline / private; slow on M1 |
|
|
102
|
+
| `qwen-next-local` | Qwen3 Coder Next Q3_K_M local (38GB) | Private + most capable local; needs `ollama create` import |
|
|
103
|
+
| `gpt-oss` | gpt-oss 20B local (13GB) | Private; slow; ~100s/turn for 50k system prompt |
|
|
104
|
+
| `llama` | Llama 3.1 8B local | Fast but garbles inside Claude Code's 50k system prompt — capability gate |
|
|
105
|
+
| `*-tee` | Same as above but routed through `:11500` proxy | Wire-level inspection (`ollama-tee` proxy must be running) |
|
|
106
|
+
|
|
107
|
+
### Cost × privacy framing
|
|
108
|
+
|
|
109
|
+
Two axes matter:
|
|
110
|
+
|
|
111
|
+
1. **Cost** — Anthropic Pro $20/mo or Max ($100/$200/mo); Ollama Cloud Pro $20/mo (3 concurrent, includes Kimi/Qwen Cloud); local = free but hardware-bound
|
|
112
|
+
2. **Privacy** — Anthropic API: Anthropic sees prompts. Ollama Cloud: Ollama sees prompts. Local: nothing leaves the laptop
|
|
113
|
+
|
|
114
|
+
Match the tier to the task:
|
|
115
|
+
- Sensitive prompts (client code, customer data) → `qwen-next-local` or `gpt-oss`
|
|
116
|
+
- Routine exploration / orchestration → `anthropic` (default)
|
|
117
|
+
- Cost-sensitive bulk work → `kimi` or `qwen-cloud`
|
|
118
|
+
|
|
119
|
+
### Examples
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Spin up a tab on Kimi for a side experiment
|
|
123
|
+
cctabs new explore-kimi ~/Dev/myapp -b kimi -p "explore alternative API designs"
|
|
124
|
+
|
|
125
|
+
# Local privacy session, slower but no data leaves the laptop
|
|
126
|
+
cctabs new private-refactor ~/Dev/clientwork -b qwen-next-local -W
|
|
127
|
+
|
|
128
|
+
# Compare two models on the same task in parallel
|
|
129
|
+
cctabs new task-anthropic ~/Dev/myapp -p "implement spec X"
|
|
130
|
+
cctabs new task-kimi ~/Dev/myapp -b kimi -p "implement spec X"
|
|
131
|
+
|
|
132
|
+
# Custom local Ollama tag not in built-in presets:
|
|
133
|
+
cctabs new x ~/Dev/myapp -b qwen-local -m my-custom-tag:latest
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Caveats
|
|
137
|
+
|
|
138
|
+
- **Local backends are slow on M1.** A Claude Code turn against the local 50k-token system prompt takes ~100s prefill + generation on M1 Max. Only worth it for non-time-sensitive private work.
|
|
139
|
+
- **Llama 3.1 8B garbles tool calls** under Claude Code's system prompt. Capability gate, not a bug.
|
|
140
|
+
- **Ollama Cloud Pro requires `ollama signin`** (one-time). Free tier denies cloud-tagged models.
|
|
141
|
+
- **Custom presets** can be added in `~/.config/cctabs/config.toml`:
|
|
142
|
+
```toml
|
|
143
|
+
[backends.my-preset]
|
|
144
|
+
model = "qwen3-coder-next:cloud"
|
|
145
|
+
base_url = "http://localhost:11434"
|
|
146
|
+
description = "My custom preset"
|
|
147
|
+
```
|
|
148
|
+
|
|
83
149
|
## Workflow: Checking What's Running
|
|
84
150
|
|
|
85
151
|
Before starting new sessions, always check what's already active:
|