agent-sh 0.12.9 → 0.12.11

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.
@@ -0,0 +1,143 @@
1
+ /**
2
+ * rtk-proxy — transparently rewrites bash commands to `rtk <command>`
3
+ * so the LLM sees rtk's compressed output (60-90% token reduction on
4
+ * common dev commands: git, cargo, npm, jest, pytest, ls, grep, …).
5
+ *
6
+ * Demonstrates: `ctx.advise("tool:execute", …)` wrapping + line-buffered
7
+ * stream scrub.
8
+ *
9
+ * Compound commands like `cd X && pytest` rewrite the last segment only.
10
+ * Pipes, subshells, and redirects are skipped (unsafe to wrap).
11
+ *
12
+ * Requires the `rtk` binary on PATH (https://github.com/rtk-ai/rtk).
13
+ *
14
+ * Settings (~/.agent-sh/settings.json):
15
+ * { "rtk-proxy": { "enabled": true, "ultraCompact": false,
16
+ * "extraPrefixes": [], "excludePrefixes": [] } }
17
+ *
18
+ * Usage:
19
+ * ash -e ./examples/extensions/rtk-proxy.ts
20
+ * cp examples/extensions/rtk-proxy.ts ~/.agent-sh/extensions/
21
+ */
22
+ import { execSync } from "node:child_process";
23
+ import type { ExtensionContext } from "agent-sh/types";
24
+
25
+ const DEFAULT_PREFIXES = new Set([
26
+ "git", "gh",
27
+ "ls", "tree", "find", "grep", "rg", "cat",
28
+ "cargo", "npm", "pnpm", "yarn",
29
+ "jest", "vitest", "pytest", "playwright",
30
+ "go", "ruff", "tsc", "eslint", "prettier", "biome",
31
+ "docker", "kubectl",
32
+ "aws",
33
+ "pip", "bundle", "rake", "rspec", "rubocop",
34
+ "golangci-lint", "next",
35
+ "prisma",
36
+ ]);
37
+
38
+ // Pipes, subshells, redirections — unsafe to wrap. Compound operators
39
+ // (&&, ||, ;) are handled by splitting and rewriting only the last segment.
40
+ const UNSAFE_SEGMENT_RE = /[|`()$><]/;
41
+
42
+ function firstToken(cmd: string): string {
43
+ const m = cmd.trimStart().match(/^(\S+)/);
44
+ return m ? m[1] : "";
45
+ }
46
+
47
+ // Caveat: textual split, no quoting awareness. A literal `&&` inside a
48
+ // quoted argument will split there. Acceptable today because no current
49
+ // prefix-token command takes args containing `&&`/`||`/`;`. If that
50
+ // changes, switch to a proper shell tokenizer.
51
+ function splitLastSegment(cmd: string): [string, string, string] | null {
52
+ const match = cmd.match(/^(.*)(&&|\|\||;)\s*(\S.*)$/s);
53
+ if (!match) return null;
54
+ return [match[1].trimEnd(), match[2], match[3]];
55
+ }
56
+
57
+ function rewriteForRtk(cmd: string, prefixes: Set<string>, flag: string): string | null {
58
+ const tok = firstToken(cmd);
59
+ if (!tok || tok === "rtk") return null;
60
+ // Escape hatch: `command foo` forces raw passthrough.
61
+ if (tok === "command") return null;
62
+
63
+ const parts = splitLastSegment(cmd);
64
+ if (parts) {
65
+ const [prefix, sep, lastSeg] = parts;
66
+ if (UNSAFE_SEGMENT_RE.test(lastSeg)) return null;
67
+ if (!prefixes.has(firstToken(lastSeg))) return null;
68
+ return `${prefix} ${sep} RTK_TELEMETRY_DISABLED=1 rtk ${flag}${lastSeg}`;
69
+ }
70
+
71
+ if (UNSAFE_SEGMENT_RE.test(cmd)) return null;
72
+ if (!prefixes.has(tok)) return null;
73
+ return `RTK_TELEMETRY_DISABLED=1 rtk ${flag}${cmd}`;
74
+ }
75
+
76
+ export default function activate(ctx: ExtensionContext) {
77
+ const config = ctx.getExtensionSettings("rtk-proxy", {
78
+ enabled: true,
79
+ ultraCompact: false,
80
+ extraPrefixes: [] as string[],
81
+ excludePrefixes: [] as string[],
82
+ });
83
+ if (!config.enabled) return;
84
+
85
+ try {
86
+ execSync("command -v rtk", { stdio: "ignore" });
87
+ } catch {
88
+ ctx.bus.emit("ui:info", {
89
+ message: "rtk-proxy: `rtk` binary not on PATH — extension inactive.",
90
+ });
91
+ return;
92
+ }
93
+
94
+ const prefixes = new Set([...DEFAULT_PREFIXES, ...config.extraPrefixes]);
95
+ for (const p of config.excludePrefixes) prefixes.delete(p);
96
+ const flag = config.ultraCompact ? "--ultra-compact " : "";
97
+
98
+ ctx.registerInstruction("rtk-proxy",
99
+ "The rtk-proxy extension transparently rewrites bash commands like " +
100
+ "`git status`, `cargo test`, `pytest` to their rtk-compressed equivalents " +
101
+ "before execution. Output will be condensed (errors/failures first, " +
102
+ "boilerplate stripped). For raw unfiltered output, prefix with `command ` " +
103
+ "(e.g. `command git log`) or pipe (`git log | cat`) — both skip the rewrite.",
104
+ );
105
+
106
+ // rtk prints a nag line when it sees ~/.claude/ but no hook. We're doing
107
+ // the rewrite ourselves, so strip the advisory from streamed + final output.
108
+ const NAG_RE = /^\[(?:rtk|warn)\][^\n]*No hook installed[^\n]*\n?/gm;
109
+ const scrub = (s: string) => s.replace(NAG_RE, "");
110
+
111
+ ctx.advise("tool:execute", async (next, toolCtx) => {
112
+ if (toolCtx.name !== "bash") return next(toolCtx);
113
+ const command = toolCtx.args?.command;
114
+ if (typeof command !== "string") return next(toolCtx);
115
+
116
+ const rewritten = rewriteForRtk(command, prefixes, flag);
117
+ if (rewritten === null) return next(toolCtx);
118
+
119
+ toolCtx.args = { ...toolCtx.args, command: rewritten };
120
+
121
+ // Line-buffer the stream so the nag-line scrub works across chunks.
122
+ const origOnChunk = toolCtx.onChunk;
123
+ if (origOnChunk) {
124
+ let buf = "";
125
+ toolCtx.onChunk = (chunk: string) => {
126
+ buf += chunk;
127
+ const lastNl = buf.lastIndexOf("\n");
128
+ if (lastNl !== -1) {
129
+ origOnChunk(scrub(buf.slice(0, lastNl + 1)));
130
+ buf = buf.slice(lastNl + 1);
131
+ }
132
+ };
133
+ const result = await next(toolCtx);
134
+ if (buf) origOnChunk(scrub(buf));
135
+ return { ...result, content: scrub(result.content) };
136
+ }
137
+ return next(toolCtx);
138
+ });
139
+
140
+ ctx.bus.emit("ui:info", {
141
+ message: `rtk-proxy active (${prefixes.size} command prefixes).`,
142
+ });
143
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sh",
3
- "version": "0.12.9",
3
+ "version": "0.12.11",
4
4
  "description": "A shell-first terminal where AI is one keystroke away",
5
5
  "type": "module",
6
6
  "main": "dist/core.js",
@@ -1,87 +0,0 @@
1
- /**
2
- * OpenRouter provider extension.
3
- *
4
- * Registers OpenRouter as a provider and fetches its full model catalog
5
- * at startup. Models appear in /model autocomplete as "model [openrouter]"
6
- * and are available for cycling with Shift+Tab.
7
- *
8
- * Model capabilities (reasoning, context window) are read from the
9
- * OpenRouter API response — no hardcoded model lists.
10
- *
11
- * Setup:
12
- * export OPENROUTER_API_KEY="your-key"
13
- *
14
- * Usage:
15
- * agent-sh -e ./examples/extensions/openrouter.ts
16
- *
17
- * # Or add to settings.json:
18
- * { "extensions": ["./examples/extensions/openrouter.ts"] }
19
- */
20
- import type { ExtensionContext } from "agent-sh/types";
21
-
22
- const BASE_URL = "https://openrouter.ai/api/v1";
23
- const API_KEY = process.env.OPENROUTER_API_KEY ?? "";
24
-
25
- /** Curated default models — used immediately while the full catalog loads. */
26
- const DEFAULT_MODELS = [
27
- "anthropic/claude-sonnet-4",
28
- "google/gemini-2.5-pro-preview",
29
- "openai/gpt-4.1",
30
- "deepseek/deepseek-r1",
31
- "meta-llama/llama-4-maverick",
32
- ];
33
-
34
- interface OpenRouterModel {
35
- id: string;
36
- name: string;
37
- context_length?: number;
38
- supported_parameters?: string[];
39
- pricing?: { prompt: string; completion: string };
40
- }
41
-
42
- export default function activate({ bus }: ExtensionContext): void {
43
- if (!API_KEY) {
44
- bus.emit("ui:error", {
45
- message: "OpenRouter extension: OPENROUTER_API_KEY not set. Skipping.",
46
- });
47
- return;
48
- }
49
-
50
- // Register provider immediately with curated defaults
51
- bus.emit("provider:register", {
52
- id: "openrouter",
53
- apiKey: API_KEY,
54
- baseURL: BASE_URL,
55
- defaultModel: DEFAULT_MODELS[0],
56
- models: DEFAULT_MODELS,
57
- });
58
-
59
- // Fetch full model catalog in background, re-register with capabilities
60
- fetchModels().then((models) => {
61
- if (models.length > 0) {
62
- bus.emit("provider:register", {
63
- id: "openrouter",
64
- apiKey: API_KEY,
65
- baseURL: BASE_URL,
66
- defaultModel: DEFAULT_MODELS[0],
67
- supportsReasoningEffort: true,
68
- models: models.map((m) => ({
69
- id: m.id,
70
- reasoning: m.supported_parameters?.includes("reasoning") ?? false,
71
- contextWindow: m.context_length,
72
- })),
73
- });
74
- }
75
- }).catch(() => {
76
- // Silently fall back to curated defaults
77
- });
78
- }
79
-
80
- async function fetchModels(): Promise<OpenRouterModel[]> {
81
- const res = await fetch(`${BASE_URL}/models`, {
82
- headers: { Authorization: `Bearer ${API_KEY}` },
83
- });
84
- if (!res.ok) return [];
85
- const data = await res.json();
86
- return (data.data ?? []) as OpenRouterModel[];
87
- }