@tablation/crew 0.33.0 → 0.34.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/crew.example.yaml CHANGED
@@ -29,8 +29,7 @@ ship:
29
29
 
30
30
  # Optional (CREW-1511): named toolchains — a harness adapter plus the binary
31
31
  # and model to run it with. The `agent:` block above keeps working and
32
- # defines an implicit toolchain named `claude`. Only the `claude` harness
33
- # exists today. See docs/toolchains.md.
32
+ # defines an implicit toolchain named `claude`. Harnesses: claude, gemini. See docs/toolchains.md.
34
33
  # toolchain: claude # the ship's default; a route may override it
35
34
  # toolchains:
36
35
  # claude-wrapped:
@@ -42,6 +41,7 @@ ship:
42
41
  # deep: { model: claude-opus-5-5 }
43
42
  # args: [] # appended to the harness argv
44
43
  # env: {} # added to the session environment
44
+ # gemini: { harness: gemini, bin: gemini, model: gemini-2.5-pro }
45
45
 
46
46
  # shell: bash # defaults per platform
47
47
  extraPath: "/opt/homebrew/bin" # prepended for hooks
package/dist/cli.js CHANGED
@@ -616,6 +616,28 @@ function geminiMcpState(s, url) {
616
616
  if (e.headers?.Authorization !== `Bearer $${PAIR_KEY_VAR}`) return { state: "different", detail: `it does not read its key from $${PAIR_KEY_VAR}` };
617
617
  return { state: "registered" };
618
618
  }
619
+ function inspectGeminiMcp(o) {
620
+ const p = geminiPaths(o.home ?? homedir2());
621
+ const settings = readGeminiSettings(p.config);
622
+ if (!settings) return { state: "different", detail: `${p.config} is not a JSON object` };
623
+ const st = geminiMcpState(settings, o.url);
624
+ return st.state === "registered" ? { state: "registered", url: o.url } : st;
625
+ }
626
+ function ensureGeminiMcp(o) {
627
+ const p = geminiPaths(o.home ?? homedir2());
628
+ const settings = readGeminiSettings(p.config);
629
+ if (!settings) return `gemini: ${p.config} is not a JSON object \u2014 fix it by hand; nothing was written`;
630
+ const st = geminiMcpState(settings, o.url);
631
+ if (st.state === "registered") return `gemini: ${SERVER} MCP server already registered in ${p.config}`;
632
+ if (st.state === "different" && !o.replace) {
633
+ return `gemini: left ${p.config} alone \u2014 a "${SERVER}" server exists and ${st.detail}. Re-run with --mcp replace to overwrite it.`;
634
+ }
635
+ if (!o.dryRun) {
636
+ writeFileMkdir(p.config, `${JSON.stringify({ ...settings, mcpServers: { ...settings.mcpServers, [SERVER]: geminiEntry(o.url) } }, null, 2)}
637
+ `);
638
+ }
639
+ return `gemini: ${o.dryRun ? "(dry run) would register" : "registered"} ${SERVER} in ${p.config} (key read from $${PAIR_KEY_VAR})`;
640
+ }
619
641
  var geminiPair = {
620
642
  harness: "gemini",
621
643
  displayName: "Gemini CLI",
@@ -828,9 +850,120 @@ function harnessPromptNote(harness) {
828
850
 
829
851
  // src/toolchain.ts
830
852
  import { basename } from "path";
853
+
854
+ // src/adapters/gemini.ts
855
+ var BILLING_VARS_TO_UNSET2 = [
856
+ "GEMINI_API_KEY",
857
+ "GOOGLE_API_KEY",
858
+ "GOOGLE_GENAI_USE_VERTEXAI",
859
+ "GOOGLE_GENAI_USE_GCA",
860
+ "GOOGLE_CLOUD_PROJECT",
861
+ "GOOGLE_CLOUD_PROJECT_ID",
862
+ "GOOGLE_CLOUD_LOCATION",
863
+ "GOOGLE_APPLICATION_CREDENTIALS",
864
+ "GOOGLE_GEMINI_BASE_URL",
865
+ "GEMINI_MODEL"
866
+ ];
867
+ var READ_TOOLS = ["read_file", "read_many_files", "list_directory", "glob", "search_file_content"];
868
+ function geminiGrantFor(p) {
869
+ if (p.level === "read") return { approvalMode: "default", allowed: [...READ_TOOLS, "run_shell_command"] };
870
+ if (p.level === "write") return { approvalMode: "default", allowed: [...READ_TOOLS, "run_shell_command", "write_file"] };
871
+ return { approvalMode: "yolo", allowed: [] };
872
+ }
873
+ function geminiGrantArgs(role) {
874
+ const g = geminiGrantFor(profileFor(role));
875
+ return ["--approval-mode", g.approvalMode, ...g.allowed.length ? ["--allowed-tools", g.allowed.join(",")] : []];
876
+ }
877
+ function mapGeminiLine(line) {
878
+ const obj = line;
879
+ if (!obj || typeof obj !== "object") return [];
880
+ if (obj.type === "message" && obj.role === "assistant" && obj.content) return [{ kind: "text", text: obj.content }];
881
+ if (obj.type === "tool_use") return [{ kind: "tool", tool: obj.tool_name, target: toolTarget(obj.tool_name === "run_shell_command" ? "Bash" : obj.tool_name, obj.parameters) }];
882
+ return [];
883
+ }
884
+ function createGeminiStreamMapper() {
885
+ let pending = "";
886
+ const flush = () => {
887
+ const text = pending;
888
+ pending = "";
889
+ return text ? [{ kind: "text", text }] : [];
890
+ };
891
+ return {
892
+ flush,
893
+ map(line) {
894
+ const obj = line;
895
+ if (obj && typeof obj === "object" && obj.type === "message" && obj.role === "assistant") {
896
+ if (obj.content) pending += obj.content;
897
+ return [];
898
+ }
899
+ return [...flush(), ...mapGeminiLine(line)];
900
+ }
901
+ };
902
+ }
903
+ function extractGeminiResult(line) {
904
+ const obj = line;
905
+ if (obj?.type === "init") return obj.session_id ? { sessionId: obj.session_id } : void 0;
906
+ if (!obj || obj.type !== "result") return void 0;
907
+ const s = obj.stats;
908
+ return {
909
+ inputTokens: s?.input_tokens,
910
+ outputTokens: s?.output_tokens,
911
+ cacheReadTokens: s?.cached
912
+ };
913
+ }
914
+ var geminiAdapter = {
915
+ harness: "gemini",
916
+ displayName: "Gemini CLI",
917
+ vendor: "google",
918
+ ownsModel: (model) => model.startsWith("gemini-"),
919
+ // Gemini CLI has no per-run reasoning knob, so a tier's `reasoning` is ignored.
920
+ defaultBin: "gemini",
921
+ defaultModel: "gemini-2.5-pro",
922
+ defaultTiers: {
923
+ light: { model: "gemini-2.5-flash" },
924
+ standard: { model: "gemini-2.5-pro" },
925
+ deep: { model: "gemini-2.5-pro" }
926
+ },
927
+ limitations: [
928
+ "the tool grant is an allow list of tools that skip confirmation, not a sandbox: run_shell_command is allowed in every profile and sub-commands cannot be restricted",
929
+ "there is no deny list: destructive shell patterns (rm -rf, forced pushes, curl | sh) are not refused at the tool layer, unlike the Claude adapter",
930
+ "the edit profile runs in yolo mode (auto-approves every tool); the write profile (QA) withholds replace, not shell writes",
931
+ "design tools (skills, design canvas) are unavailable on this harness",
932
+ "the tracker MCP server is read from ~/.gemini/settings.json (there is no per-run --mcp-config), and MCP tool calls in the read and write profiles are not on the allow list",
933
+ "the stream carries no thinking blocks and streams assistant text in fragments, so the runner coalesces fragments into one Agent Log cycle per assistant turn; no turn count or cost is reported"
934
+ ],
935
+ invocation(spec, o) {
936
+ const args = [
937
+ // The prompt arrives on stdin; `-p` puts the CLI in non-interactive mode.
938
+ "-p",
939
+ "",
940
+ "--model",
941
+ o.model,
942
+ "--output-format",
943
+ "stream-json",
944
+ ...geminiGrantArgs(o.role),
945
+ ...spec.args
946
+ ];
947
+ return spec.launcher ? { bin: spec.launcherBin ?? spec.launcher, args: [spec.bin, ...args] } : { bin: spec.bin, args };
948
+ },
949
+ runEnv: (spec) => ({ ...spec.env }),
950
+ provider: () => "google",
951
+ unsetEnv: BILLING_VARS_TO_UNSET2,
952
+ mapStreamLine: mapGeminiLine,
953
+ createStreamMapper: createGeminiStreamMapper,
954
+ extractResult: extractGeminiResult,
955
+ extractTurnTokens: () => void 0,
956
+ inspectMcp(o) {
957
+ return inspectGeminiMcp(o);
958
+ },
959
+ ensureMcp: (o) => ensureGeminiMcp(o)
960
+ };
961
+
962
+ // src/toolchain.ts
831
963
  var TIERS = ["light", "standard", "deep"];
832
964
  var ADAPTERS = {
833
- [claudeAdapter.harness]: claudeAdapter
965
+ [claudeAdapter.harness]: claudeAdapter,
966
+ [geminiAdapter.harness]: geminiAdapter
834
967
  };
835
968
  var HARNESS_NAMES = Object.keys(ADAPTERS);
836
969
  function isHarness(name) {
@@ -6847,6 +6980,7 @@ function spawnAgent(plan, emit) {
6847
6980
  let result;
6848
6981
  const cycles = [];
6849
6982
  const attribution = new TicketAttributionTracker();
6983
+ const mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
6850
6984
  const handleLine = (line) => {
6851
6985
  if (!line) return;
6852
6986
  try {
@@ -6861,9 +6995,16 @@ function spawnAgent(plan, emit) {
6861
6995
  return;
6862
6996
  }
6863
6997
  try {
6864
- result = adapter.extractResult(parsed) ?? result;
6998
+ const extracted = adapter.extractResult(parsed);
6999
+ if (extracted) result = { ...result, ...extracted };
7000
+ consume(parsed, mapper.map(parsed));
7001
+ } catch {
7002
+ }
7003
+ };
7004
+ const consume = (parsed, events) => {
7005
+ {
6865
7006
  let turnText = "";
6866
- for (const ev of adapter.mapStreamLine(parsed)) {
7007
+ for (const ev of events) {
6867
7008
  const e = {
6868
7009
  at: (/* @__PURE__ */ new Date()).toISOString(),
6869
7010
  cycle: emit.cycle,
@@ -6901,9 +7042,8 @@ function spawnAgent(plan, emit) {
6901
7042
  `);
6902
7043
  }
6903
7044
  }
6904
- const turnTokens = adapter.extractTurnTokens(parsed);
7045
+ const turnTokens = parsed === void 0 ? void 0 : adapter.extractTurnTokens(parsed);
6905
7046
  if (turnTokens !== void 0) attribution.add(turnText, turnTokens);
6906
- } catch {
6907
7047
  }
6908
7048
  };
6909
7049
  child.stdout.on("data", (chunk) => {
@@ -6919,6 +7059,10 @@ function spawnAgent(plan, emit) {
6919
7059
  });
6920
7060
  child.on("close", async (code) => {
6921
7061
  if (buf) handleLine(buf);
7062
+ try {
7063
+ consume(void 0, mapper.flush());
7064
+ } catch {
7065
+ }
6922
7066
  await Promise.all([endSink(rawSink), endSink(eventsSink)]);
6923
7067
  if (spawnFailed) return;
6924
7068
  const attributedTicket = attribution.winner(plan.ticket);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",