kritya 0.8.48-beta → 0.8.50-beta
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 +3 -0
- package/dist/agent/compactor.js +25 -2
- package/dist/agent/loop.js +25 -4
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/registry.js +169 -0
- package/dist/commands/templates.js +77 -0
- package/dist/config/models.js +52 -0
- package/dist/engine.js +2 -2
- package/dist/git/git.js +69 -0
- package/dist/git/pr.js +204 -0
- package/dist/headless.js +2 -2
- package/dist/index.js +2 -2
- package/dist/ui/App.js +1 -1
- package/dist/ui/ModelPicker.js +18 -5
- package/dist/ui/useAgent.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -175,6 +175,9 @@ In-session commands (type `/` to see them with autocomplete; letters filter the
|
|
|
175
175
|
| `/diff` | show the cumulative git diff of this session's changes |
|
|
176
176
|
| `/init` | scan the repo and generate a `KRITYA.md` project-memory file |
|
|
177
177
|
| `/commit` | have the agent review, stage, and commit the current git changes |
|
|
178
|
+
| `/pr` | push the branch and open a GitHub PR: `/pr [--draft] [--title T] [--body B] [--base BR]` |
|
|
179
|
+
| `/summarize` | one-shot summary of the session so far (history is left untouched) |
|
|
180
|
+
| `/template` | prompt templates: list, /template <name> [words], /template save\|delete <name> |
|
|
178
181
|
| `/web-search <query>` | search the web via Tavily; results are shown and added to context |
|
|
179
182
|
| `/mcp` | MCP server status; `/mcp add\|remove <name>`, `/mcp login\|logout <name>`, `/mcp trust` ([more](docs/CONFIGURATION.md#mcp-servers)) |
|
|
180
183
|
| `/skills` | list discovered skills (project + user-global) and why any were skipped |
|
package/dist/agent/compactor.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { estimateHistoryTokens } from "./tokens.js";
|
|
1
2
|
/** How many recent messages survive compaction untouched. */
|
|
2
3
|
const KEEP_TAIL = 8;
|
|
4
|
+
/** The briefing prompt shared by compaction and the one-shot /summarize. */
|
|
5
|
+
export const SUMMARY_SYSTEM_PROMPT = "You summarize coding-session transcripts. Produce a dense briefing: the user's goals, " +
|
|
6
|
+
"key decisions, files created/modified (with paths), commands run and their outcomes, " +
|
|
7
|
+
"current state, and open items. Plain text, no preamble.";
|
|
3
8
|
/**
|
|
4
9
|
* Split history into an older part to summarize and a recent tail to keep.
|
|
5
10
|
* The tail never starts on a tool reply: the cut moves back to the assistant
|
|
@@ -61,8 +66,7 @@ export function fallbackSummary(toSummarize) {
|
|
|
61
66
|
}
|
|
62
67
|
return lines.join("\n");
|
|
63
68
|
}
|
|
64
|
-
/** Render messages as a plain-text transcript for the summarization request. */
|
|
65
|
-
export function renderTranscript(messages) {
|
|
69
|
+
/** Render messages as a plain-text transcript for the summarization request. */ export function renderTranscript(messages) {
|
|
66
70
|
return messages
|
|
67
71
|
.map((m) => {
|
|
68
72
|
if (m.role === "tool") {
|
|
@@ -76,3 +80,22 @@ export function renderTranscript(messages) {
|
|
|
76
80
|
})
|
|
77
81
|
.join("\n\n");
|
|
78
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Choose which messages the one-shot summary covers. The whole history when
|
|
85
|
+
* it fits comfortably; otherwise the newest messages that fit within half
|
|
86
|
+
* the context window (the summarization call needs headroom for its reply).
|
|
87
|
+
* Oldest drop first, and the selection never starts on a tool reply, so
|
|
88
|
+
* call/reply pairs stay together. Returns the selection and how many oldest
|
|
89
|
+
* messages were dropped.
|
|
90
|
+
*/
|
|
91
|
+
export function selectMessagesForSummary(history, contextWindow) {
|
|
92
|
+
const budget = Math.floor(contextWindow / 2);
|
|
93
|
+
let messages = history;
|
|
94
|
+
while (messages.length > 1 && estimateHistoryTokens(messages) > budget) {
|
|
95
|
+
messages = messages.slice(1);
|
|
96
|
+
}
|
|
97
|
+
while (messages.length > 1 && messages[0].role === "tool") {
|
|
98
|
+
messages = messages.slice(1);
|
|
99
|
+
}
|
|
100
|
+
return { messages, dropped: history.length - messages.length };
|
|
101
|
+
}
|
package/dist/agent/loop.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isContextOverflowError } from "../provider/client.js";
|
|
2
|
-
import { splitForCompaction, renderTranscript, fallbackSummary } from "./compactor.js";
|
|
2
|
+
import { splitForCompaction, renderTranscript, fallbackSummary, selectMessagesForSummary, SUMMARY_SYSTEM_PROMPT, } from "./compactor.js";
|
|
3
3
|
import { estimateHistoryTokens, estimateTokens } from "./tokens.js";
|
|
4
4
|
import { buildSystemPrompt } from "./systemPrompt.js";
|
|
5
5
|
import { NOOP_TRACER } from "../telemetry/tracer.js";
|
|
@@ -325,6 +325,29 @@ export class Agent {
|
|
|
325
325
|
this.onCompactEnd?.();
|
|
326
326
|
}
|
|
327
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* One-shot briefing of the session so far, for display. Unlike compact(),
|
|
330
|
+
* this never touches the history — it just asks the model to summarize
|
|
331
|
+
* what happened and returns the text. A failure is reported, not degraded:
|
|
332
|
+
* nothing is being destroyed, so there is no need for the mechanical
|
|
333
|
+
* fallback that compaction uses.
|
|
334
|
+
*/
|
|
335
|
+
async summarize(signal) {
|
|
336
|
+
this.kill.assertLive();
|
|
337
|
+
if (this.history.length <= 4) {
|
|
338
|
+
return "Nothing to summarize yet — the session just started.";
|
|
339
|
+
}
|
|
340
|
+
const { messages, dropped } = selectMessagesForSummary(this.history, this.contextWindow);
|
|
341
|
+
const result = await this.client.chat(this.getModel(), [
|
|
342
|
+
{ role: "system", content: SUMMARY_SYSTEM_PROMPT },
|
|
343
|
+
{ role: "user", content: renderTranscript(messages) },
|
|
344
|
+
], [], { onTextDelta: () => { }, onReasoningDelta: () => { } }, signal, { tracer: this.tracer, parent: this.currentTurnSpan ?? undefined });
|
|
345
|
+
const summary = result.text.trim() || "(summary unavailable)";
|
|
346
|
+
return dropped > 0
|
|
347
|
+
? `Session summary (most recent ${messages.length} messages — ` +
|
|
348
|
+
`${dropped} oldest omitted to fit the context window):\n\n${summary}`
|
|
349
|
+
: `Session summary:\n\n${summary}`;
|
|
350
|
+
}
|
|
328
351
|
async doCompact(signal, compactSpan) {
|
|
329
352
|
const { toSummarize, keep } = splitForCompaction(this.history);
|
|
330
353
|
if (!toSummarize.length) {
|
|
@@ -344,9 +367,7 @@ export class Agent {
|
|
|
344
367
|
const result = await this.client.chat(this.getModel(), [
|
|
345
368
|
{
|
|
346
369
|
role: "system",
|
|
347
|
-
content:
|
|
348
|
-
"key decisions, files created/modified (with paths), commands run and their outcomes, " +
|
|
349
|
-
"current state, and open items. Plain text, no preamble.",
|
|
370
|
+
content: SUMMARY_SYSTEM_PROMPT,
|
|
350
371
|
},
|
|
351
372
|
{ role: "user", content: renderTranscript(toSummarize) },
|
|
352
373
|
], [], { onTextDelta: () => { }, onReasoningDelta: () => { } }, signal, { tracer: this.tracer, parent: compactSpan });
|
package/dist/commands/doctor.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { BUILTIN_PROVIDERS, CONFIG_DIR, CONFIG_FILE, legacyGlobalModel, loadConfig, loadDotEnv, privacyModeFor, resolveProvider, } from "../config/config.js";
|
|
6
|
-
import {
|
|
6
|
+
import { defaultModelFor, contextWindowFor } from "../config/models.js";
|
|
7
7
|
import { retentionDaysFor } from "../config/retention.js";
|
|
8
8
|
import { assertJsonWithinLimits } from "../config/jsonSafety.js";
|
|
9
9
|
import { isGitRepo } from "../agent/worktree.js";
|
|
@@ -144,7 +144,7 @@ function providerSection(config, provider) {
|
|
|
144
144
|
else {
|
|
145
145
|
checks.push(fail(`no API key resolved for provider "${provider.name}"`));
|
|
146
146
|
}
|
|
147
|
-
const model = resolveEffectiveModel(provider.name, [config.providers?.[provider.name]?.model, legacyGlobalModel(config, provider.name)], provider.name === "switchyard" ? SWITCHYARD_ROUTE_ID :
|
|
147
|
+
const model = resolveEffectiveModel(provider.name, [config.providers?.[provider.name]?.model, legacyGlobalModel(config, provider.name)], provider.name === "switchyard" ? SWITCHYARD_ROUTE_ID : defaultModelFor(provider.name));
|
|
148
148
|
checks.push(ok(`model: ${model} (context ${contextWindowFor(model, config).toLocaleString("en-US")})`));
|
|
149
149
|
// A pinned provider name with no matching entry is a silent misconfiguration:
|
|
150
150
|
// resolveProvider falls back to NVIDIA's URL, so requests go somewhere the
|
|
@@ -10,6 +10,9 @@ import { scanPluginMcpServers } from "../plugins/mcp.js";
|
|
|
10
10
|
import { listProviders } from "../config/config.js";
|
|
11
11
|
import { expandCommand } from "./custom.js";
|
|
12
12
|
import { recordCommandUse } from "../ui/recentCommands.js";
|
|
13
|
+
import { allTemplates, applyTemplate, isValidName, lastUserText, loadTemplates, saveTemplates, } from "./templates.js";
|
|
14
|
+
import { gitBranch, gitCommitsAhead, gitDefaultBranch, gitHasUpstream, gitPush, gitRefExists, gitRemoteUrl, gitUnpushedCount, } from "../git/git.js";
|
|
15
|
+
import { createPullRequest, findOpenPullRequest, parseGitHubRemote, parsePrFlags, prBodyFor, prTitleFor, resolveGitHubToken, } from "../git/pr.js";
|
|
13
16
|
import { artifactExists, artifactPath, BACK_COMMAND, clearProjectState, DEFAULT_GATES, listProjects, loadProjectState, MAX_REVISITS, nextPhase, parseIdea, parsePhase, phaseBlocker, phaseDoneMessage, phaseIndex, phasePrompt, planRun, prefillAfter, renameProject, resumeProject, revisitsRemaining, saveProjectState, shouldStopAfter, staleArtifacts, takeRevisit, PHASE_COMMAND, PHASE_ORDER, PHASE_SUMMARY, slugify, } from "../agent/workflow.js";
|
|
14
17
|
const WORKFLOW = "Workflow";
|
|
15
18
|
const SESSION = "Session";
|
|
@@ -92,11 +95,26 @@ export const BUILTIN_COMMANDS = [
|
|
|
92
95
|
description: "have the agent stage and commit the current changes",
|
|
93
96
|
category: SESSION,
|
|
94
97
|
},
|
|
98
|
+
{
|
|
99
|
+
name: "/pr",
|
|
100
|
+
description: "push the branch and open a GitHub PR: /pr [--draft] [--title T] [--body B] [--base BR]",
|
|
101
|
+
category: SESSION,
|
|
102
|
+
},
|
|
95
103
|
{
|
|
96
104
|
name: "/compact",
|
|
97
105
|
description: "summarize older conversation to free context space",
|
|
98
106
|
category: SESSION,
|
|
99
107
|
},
|
|
108
|
+
{
|
|
109
|
+
name: "/summarize",
|
|
110
|
+
description: "one-shot summary of the session so far (history is left untouched)",
|
|
111
|
+
category: SESSION,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "/template",
|
|
115
|
+
description: "prompt templates: list, /template <name> [words], /template save|delete <name>",
|
|
116
|
+
category: SESSION,
|
|
117
|
+
},
|
|
100
118
|
{ name: "/clear", description: "start a fresh conversation", category: SESSION },
|
|
101
119
|
{
|
|
102
120
|
name: "/undo",
|
|
@@ -803,6 +821,79 @@ const handlers = {
|
|
|
803
821
|
ctx.setPhase("input");
|
|
804
822
|
});
|
|
805
823
|
},
|
|
824
|
+
"/template": (ctx) => {
|
|
825
|
+
ctx.addItem({ kind: "user", text: ctx.raw.trim() });
|
|
826
|
+
const say = (text) => ctx.addItem({ kind: "info", text });
|
|
827
|
+
const custom = loadTemplates();
|
|
828
|
+
const all = allTemplates(custom);
|
|
829
|
+
const arg = ctx.arg.trim();
|
|
830
|
+
if (!arg) {
|
|
831
|
+
const names = Object.keys(all).sort();
|
|
832
|
+
if (!names.length) {
|
|
833
|
+
say("No prompt templates yet. Save your last message as one: /template save <name>");
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const lines = names.map((n) => custom[n] !== undefined ? ` ${n} (yours)` : ` ${n} (built-in)`);
|
|
837
|
+
say("Prompt templates — `/template <name> [words...]` drops one into the input line:\n" +
|
|
838
|
+
lines.join("\n") +
|
|
839
|
+
"\n\nSave your last message: `/template save <name>` · remove one: `/template delete <name>`");
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const [sub, ...rest] = arg.split(/\s+/);
|
|
843
|
+
if (sub === "save" || sub === "delete") {
|
|
844
|
+
const name = rest[0] ?? "";
|
|
845
|
+
if (!isValidName(name)) {
|
|
846
|
+
say(`Usage: /template ${sub} <name> — letters, numbers, - and _ only, max 32 chars.`);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (sub === "save") {
|
|
850
|
+
const text = lastUserText(ctx.agent.history);
|
|
851
|
+
if (!text) {
|
|
852
|
+
say("Nothing to save — send a message first, then `/template save <name>`.");
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
custom[name] = text;
|
|
856
|
+
saveTemplates(custom);
|
|
857
|
+
say(`Saved template "${name}" (${text.length} chars). Use it: /template ${name}`);
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
if (custom[name] === undefined) {
|
|
861
|
+
say(`No saved template named "${name}". Built-ins can't be deleted.`);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
delete custom[name];
|
|
865
|
+
saveTemplates(custom);
|
|
866
|
+
say(`Deleted template "${name}".`);
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const template = all[sub];
|
|
871
|
+
if (template === undefined) {
|
|
872
|
+
say(`No template named "${sub}". See /template for the list.`);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
// Into the input line, not executed: the user edits and hits Enter.
|
|
876
|
+
ctx.setPrefill(applyTemplate(template, rest.join(" ")));
|
|
877
|
+
say(`Template "${sub}" is in the input line — edit it and hit Enter. Nothing was sent.`);
|
|
878
|
+
},
|
|
879
|
+
"/summarize": (ctx) => {
|
|
880
|
+
ctx.addItem({ kind: "user", text: "/summarize" });
|
|
881
|
+
ctx.setPhase("working");
|
|
882
|
+
ctx.setActivity("Summarizing session…");
|
|
883
|
+
return ctx.agent
|
|
884
|
+
.summarize()
|
|
885
|
+
.then((summary) => {
|
|
886
|
+
ctx.addItem({ kind: "info", text: summary });
|
|
887
|
+
})
|
|
888
|
+
.catch((err) => ctx.addItem({
|
|
889
|
+
kind: "info",
|
|
890
|
+
text: `Summarization failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
891
|
+
}))
|
|
892
|
+
.finally(() => {
|
|
893
|
+
ctx.setActivity(null);
|
|
894
|
+
ctx.setPhase("input");
|
|
895
|
+
});
|
|
896
|
+
},
|
|
806
897
|
"/init": (ctx) => {
|
|
807
898
|
ctx.addItem({ kind: "user", text: "/init" });
|
|
808
899
|
return ctx.runAgent("Explore this repository (README, package/build files, src layout, test setup) and write " +
|
|
@@ -821,6 +912,84 @@ const handlers = {
|
|
|
821
912
|
"and create a commit with a well-written conventional-commit message that describes " +
|
|
822
913
|
`the change.${trailerInstruction} Do not push. Show the final commit hash and message.`);
|
|
823
914
|
},
|
|
915
|
+
"/pr": async (ctx) => {
|
|
916
|
+
ctx.addItem({ kind: "user", text: ctx.raw.trim() });
|
|
917
|
+
const say = (text) => ctx.addItem({ kind: "info", text });
|
|
918
|
+
const opts = parsePrFlags(ctx.arg);
|
|
919
|
+
const ws = ctx.workspace;
|
|
920
|
+
const branch = gitBranch(ws);
|
|
921
|
+
if (!branch) {
|
|
922
|
+
say("Not a git repository — /pr needs one to push from.");
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const remote = gitRemoteUrl(ws);
|
|
926
|
+
const gh = remote ? parseGitHubRemote(remote) : null;
|
|
927
|
+
if (!gh) {
|
|
928
|
+
say("No GitHub origin remote found — /pr opens pull requests on GitHub.\n" +
|
|
929
|
+
"Set it with: git remote add origin <your-github-repo-url>");
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const base = opts.base ?? gitDefaultBranch(ws);
|
|
933
|
+
if (!gitRefExists(ws, base)) {
|
|
934
|
+
say(`Base branch "${base}" does not exist locally. Pass one that does: /pr --base <branch>`);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (branch === base) {
|
|
938
|
+
say(`You are on "${branch}", the default branch — a pull request needs a feature branch.\n` +
|
|
939
|
+
"Create one first: git checkout -b <name>");
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const auth = resolveGitHubToken();
|
|
943
|
+
if (!auth) {
|
|
944
|
+
say("No GitHub token found. Provide one of:\n" +
|
|
945
|
+
" export GITHUB_TOKEN=<token> (a classic or fine-grained PAT from github.com/settings/tokens)\n" +
|
|
946
|
+
" gh auth login (then /pr reads it via `gh auth token`)");
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const commits = gitCommitsAhead(ws, base);
|
|
950
|
+
if (commits.length === 0) {
|
|
951
|
+
say(`"${branch}" has no commits that "${base}" does not — nothing to open a PR for.`);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
// Push first: the PR is opened against the pushed branch. Only pushes
|
|
955
|
+
// when there is something to send — a no-op push that fails on network
|
|
956
|
+
// would otherwise block opening the PR for no reason.
|
|
957
|
+
if (!gitHasUpstream(ws, branch) || gitUnpushedCount(ws) > 0) {
|
|
958
|
+
say(`Pushing ${branch} to origin…`);
|
|
959
|
+
const err = gitPush(ws, branch, !gitHasUpstream(ws, branch));
|
|
960
|
+
if (err) {
|
|
961
|
+
say(err);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const title = prTitleFor(commits, branch, opts.title);
|
|
966
|
+
const body = prBodyFor(commits, opts.body);
|
|
967
|
+
try {
|
|
968
|
+
const pr = await createPullRequest({
|
|
969
|
+
owner: gh.owner,
|
|
970
|
+
repo: gh.repo,
|
|
971
|
+
head: branch,
|
|
972
|
+
base,
|
|
973
|
+
title,
|
|
974
|
+
body,
|
|
975
|
+
draft: opts.draft,
|
|
976
|
+
token: auth.token,
|
|
977
|
+
});
|
|
978
|
+
say(`✓ PR #${pr.number} opened${opts.draft ? " (draft)" : ""}: ${pr.url}`);
|
|
979
|
+
}
|
|
980
|
+
catch (err) {
|
|
981
|
+
// A 422 almost always means the PR already exists — report it instead
|
|
982
|
+
// of failing on a duplicate.
|
|
983
|
+
if (err.alreadyExists) {
|
|
984
|
+
const existing = await findOpenPullRequest(gh.owner, gh.repo, branch, auth.token);
|
|
985
|
+
say(existing
|
|
986
|
+
? `A pull request for "${branch}" already exists: PR #${existing.number}: ${existing.url}`
|
|
987
|
+
: "GitHub says a pull request for this branch already exists, but it could not be found.");
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
say(err.message);
|
|
991
|
+
}
|
|
992
|
+
},
|
|
824
993
|
"/flow-brainstorm": async (ctx) => {
|
|
825
994
|
const req = takeFlowFlags(ctx.arg);
|
|
826
995
|
if (req.error) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { CONFIG_DIR } from "../config/config.js";
|
|
4
|
+
import { writeFileAtomicSync } from "../atomicWrite.js";
|
|
5
|
+
/**
|
|
6
|
+
* Prompt templates: named starter prompts kept in ~/.kritya/templates.json.
|
|
7
|
+
* `/template <name> [words...]` drops the template into the input line with
|
|
8
|
+
* `{{input}}` replaced by the words (nothing runs without the user hitting
|
|
9
|
+
* Enter); `/template save <name>` stores the last sent message as a template.
|
|
10
|
+
*/
|
|
11
|
+
const TEMPLATES_FILE = "templates.json";
|
|
12
|
+
const INPUT_PLACEHOLDER = "{{input}}";
|
|
13
|
+
const MAX_NAME_LENGTH = 32;
|
|
14
|
+
const NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
15
|
+
/** Starter templates; a user template with the same name wins. */
|
|
16
|
+
export const BUILTIN_TEMPLATES = {
|
|
17
|
+
review: "Review this diff for bugs, edge cases, and style issues:\n\n{{input}}",
|
|
18
|
+
explain: "Explain this code like I'm new to the codebase:\n\n{{input}}",
|
|
19
|
+
};
|
|
20
|
+
export function isValidName(name) {
|
|
21
|
+
return name.length > 0 && name.length <= MAX_NAME_LENGTH && NAME_RE.test(name);
|
|
22
|
+
}
|
|
23
|
+
/** The user's own templates from disk. Never throws — missing or broken file means none. */
|
|
24
|
+
export function loadTemplates(dir = CONFIG_DIR) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, TEMPLATES_FILE), "utf8"));
|
|
27
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
28
|
+
return {};
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
31
|
+
if (typeof v === "string" && isValidName(k))
|
|
32
|
+
out[k] = v;
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function saveTemplates(custom, dir = CONFIG_DIR) {
|
|
41
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
42
|
+
writeFileAtomicSync(path.join(dir, TEMPLATES_FILE), JSON.stringify(custom, null, 2) + "\n");
|
|
43
|
+
}
|
|
44
|
+
/** Built-ins under the user's own, so a custom template can override a starter. */
|
|
45
|
+
export function allTemplates(custom) {
|
|
46
|
+
return { ...BUILTIN_TEMPLATES, ...custom };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fill `{{input}}` with the user's words. With no words, the placeholder is
|
|
50
|
+
* dropped rather than left visible; with no placeholder, the words are
|
|
51
|
+
* appended so `/template review src/foo.ts` still does something sensible.
|
|
52
|
+
*/
|
|
53
|
+
export function applyTemplate(template, input) {
|
|
54
|
+
const words = input.trim();
|
|
55
|
+
if (!template.includes(INPUT_PLACEHOLDER)) {
|
|
56
|
+
return words ? `${template.trimEnd()} ${words}` : template;
|
|
57
|
+
}
|
|
58
|
+
const filled = template.split(INPUT_PLACEHOLDER).join(words);
|
|
59
|
+
return words ? filled : filled.replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The last prompt the user actually sent. Slash commands never reach the
|
|
63
|
+
* agent history, but anything starting with "/" is skipped anyway, along
|
|
64
|
+
* with non-text (multimodal) messages.
|
|
65
|
+
*/
|
|
66
|
+
export function lastUserText(history) {
|
|
67
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
68
|
+
const m = history[i];
|
|
69
|
+
if (m.role !== "user" || typeof m.content !== "string")
|
|
70
|
+
continue;
|
|
71
|
+
const text = m.content.trim();
|
|
72
|
+
if (!text || text.startsWith("/"))
|
|
73
|
+
continue;
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
package/dist/config/models.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** The provider a `ModelInfo` belongs to, defaulting the historical NVIDIA catalog. */
|
|
2
|
+
export function providerOfModel(m) {
|
|
3
|
+
return m.provider ?? "nvidia";
|
|
4
|
+
}
|
|
1
5
|
/** Fallback context window when neither config nor the registry knows the model. */
|
|
2
6
|
export const DEFAULT_CONTEXT_WINDOW = 120_000;
|
|
3
7
|
/**
|
|
@@ -63,8 +67,56 @@ export const CURATED_MODELS = [
|
|
|
63
67
|
note: "fast + cheap",
|
|
64
68
|
contextWindow: 1_048_576,
|
|
65
69
|
},
|
|
70
|
+
// Groq (https://console.groq.com/docs/models). Context windows verified
|
|
71
|
+
// against the docs table; both are 131,072. llama-3.3-70b-versatile is
|
|
72
|
+
// enterprise-only on Groq ("Contact Sales", no self-serve), so it is
|
|
73
|
+
// deliberately not listed — it 404s on a normal API key.
|
|
74
|
+
{
|
|
75
|
+
id: "openai/gpt-oss-120b",
|
|
76
|
+
label: "GPT-OSS 120B",
|
|
77
|
+
provider: "groq",
|
|
78
|
+
note: "default",
|
|
79
|
+
contextWindow: 131_072,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "openai/gpt-oss-20b",
|
|
83
|
+
label: "GPT-OSS 20B",
|
|
84
|
+
provider: "groq",
|
|
85
|
+
note: "fast + cheap",
|
|
86
|
+
contextWindow: 131_072,
|
|
87
|
+
},
|
|
66
88
|
];
|
|
89
|
+
/** Models registered for one provider, in registry order. */
|
|
90
|
+
export function modelsForProvider(provider) {
|
|
91
|
+
return CURATED_MODELS.filter((m) => providerOfModel(m) === provider);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The provider a curated model belongs to, or undefined for a model this
|
|
95
|
+
* registry doesn't know (custom models, arbitrary `-m` ids).
|
|
96
|
+
*/
|
|
97
|
+
export function curatedProviderFor(modelId) {
|
|
98
|
+
const m = CURATED_MODELS.find((x) => x.id === modelId);
|
|
99
|
+
return m ? providerOfModel(m) : undefined;
|
|
100
|
+
}
|
|
101
|
+
/** The curated default model for a provider, if it has one. */
|
|
102
|
+
export function defaultModelForProvider(provider) {
|
|
103
|
+
const models = modelsForProvider(provider);
|
|
104
|
+
if (models.length === 0)
|
|
105
|
+
return undefined;
|
|
106
|
+
// Prefer an entry flagged `note: "default"`, else the first registered.
|
|
107
|
+
return (models.find((m) => m.note === "default") ?? models[0]).id;
|
|
108
|
+
}
|
|
67
109
|
export const DEFAULT_MODEL = CURATED_MODELS[0].id;
|
|
110
|
+
/**
|
|
111
|
+
* The default model id for a given provider. Providers with a curated list use
|
|
112
|
+
* their own default, so `-p groq` with no `-m` sends a Groq model id rather
|
|
113
|
+
* than the NVIDIA default (which the Groq API would reject). Providers with no
|
|
114
|
+
* curated models keep the historical behaviour of falling back to the NVIDIA
|
|
115
|
+
* default, since those are callers passing an arbitrary provider/model pair.
|
|
116
|
+
*/
|
|
117
|
+
export function defaultModelFor(provider) {
|
|
118
|
+
return defaultModelForProvider(provider) ?? DEFAULT_MODEL;
|
|
119
|
+
}
|
|
68
120
|
/**
|
|
69
121
|
* Context window for a model. Explicit config.contextWindow always wins; then
|
|
70
122
|
* the curated registry; then the default. Custom models can carry a window via
|
package/dist/engine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { Agent } from "./agent/loop.js";
|
|
3
3
|
import { CONFIG_DIR, legacyGlobalModel, loadConfig, loadDotEnv, privacyModeFor, resolveProvider, } from "./config/config.js";
|
|
4
|
-
import {
|
|
4
|
+
import { defaultModelFor, contextWindowFor } from "./config/models.js";
|
|
5
5
|
import { PermissionManager } from "./permissions/permissions.js";
|
|
6
6
|
import { loadRules } from "./permissions/rules.js";
|
|
7
7
|
import { ProviderClient } from "./provider/client.js";
|
|
@@ -41,7 +41,7 @@ export async function createEngineSession(dir, opts = {}) {
|
|
|
41
41
|
throw new Error(`No API key found for provider "${provider.name}". Set it via env var, a .env file, or ~/.kritya/config.json.`);
|
|
42
42
|
}
|
|
43
43
|
const providerDefaultModel = config.providers?.[provider.name]?.model;
|
|
44
|
-
let currentModel = resolveEffectiveModel(provider.name, [opts.model, providerDefaultModel, legacyGlobalModel(config, provider.name)], provider.name === "switchyard" ? SWITCHYARD_ROUTE_ID :
|
|
44
|
+
let currentModel = resolveEffectiveModel(provider.name, [opts.model, providerDefaultModel, legacyGlobalModel(config, provider.name)], provider.name === "switchyard" ? SWITCHYARD_ROUTE_ID : defaultModelFor(provider.name));
|
|
45
45
|
const sampling = {
|
|
46
46
|
temperature: provider.temperature,
|
|
47
47
|
topP: provider.topP,
|
package/dist/git/git.js
CHANGED
|
@@ -17,6 +17,75 @@ export function gitBranch(cwd) {
|
|
|
17
17
|
const out = git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
18
18
|
return out || null;
|
|
19
19
|
}
|
|
20
|
+
/** Remote URL of `origin`, or null when there is none. */
|
|
21
|
+
export function gitRemoteUrl(cwd) {
|
|
22
|
+
const out = git(cwd, ["remote", "get-url", "origin"]);
|
|
23
|
+
return out || null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The repo's default branch, from the origin/HEAD symref. Falls back to
|
|
27
|
+
* "main" when the symref is missing (a repo cloned without --no-remote-head
|
|
28
|
+
* oddities, or a remote the local clone has never fetched).
|
|
29
|
+
*/
|
|
30
|
+
export function gitDefaultBranch(cwd) {
|
|
31
|
+
const out = git(cwd, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
|
|
32
|
+
const m = out && /^refs\/remotes\/origin\/(.+)$/.exec(out);
|
|
33
|
+
return m ? m[1] : "main";
|
|
34
|
+
}
|
|
35
|
+
/** Whether `branch` has an upstream configured. */
|
|
36
|
+
export function gitHasUpstream(cwd, branch) {
|
|
37
|
+
return git(cwd, ["rev-parse", "--verify", `refs/remotes/${branch}@{upstream}`]) !== null;
|
|
38
|
+
}
|
|
39
|
+
/** Whether `ref` (branch, tag, …) exists locally. */
|
|
40
|
+
export function gitRefExists(cwd, ref) {
|
|
41
|
+
return git(cwd, ["rev-parse", "--verify", ref]) !== null;
|
|
42
|
+
}
|
|
43
|
+
/** Commits on the current branch not yet on its upstream. 0 when there is no upstream. */
|
|
44
|
+
export function gitUnpushedCount(cwd) {
|
|
45
|
+
const out = git(cwd, ["rev-list", "--count", "@{u}..HEAD"]);
|
|
46
|
+
const n = out === null ? NaN : parseInt(out, 10);
|
|
47
|
+
return Number.isFinite(n) ? n : 0;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Commits on HEAD that `base` does not have, oldest first. Empty when the
|
|
51
|
+
* branch is up to date with base (or on any git failure).
|
|
52
|
+
*/
|
|
53
|
+
export function gitCommitsAhead(cwd, base) {
|
|
54
|
+
// \x1e separates records, \x1f separates subject from body — both are
|
|
55
|
+
// control characters that cannot appear in a commit message as written by
|
|
56
|
+
// any sane tooling, and far less likely than newlines.
|
|
57
|
+
const out = git(cwd, ["log", "--reverse", "--format=%s%x1f%b%x1e", `${base}..HEAD`]);
|
|
58
|
+
if (!out)
|
|
59
|
+
return [];
|
|
60
|
+
return out
|
|
61
|
+
.split("\x1e")
|
|
62
|
+
.filter((rec) => rec.trim().length > 0)
|
|
63
|
+
.map((rec) => {
|
|
64
|
+
const [subject = "", body = ""] = rec.split("\x1f");
|
|
65
|
+
return { subject: subject.trim(), body: body.trim() };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Push the branch, setting the upstream when it has none. Returns an error
|
|
70
|
+
* message rather than throwing — the caller is a slash command reporting
|
|
71
|
+
* back to the user. The push runs under the user's own git credentials,
|
|
72
|
+
* exactly as if they had typed it themselves.
|
|
73
|
+
*/
|
|
74
|
+
export function gitPush(cwd, branch, setUpstream) {
|
|
75
|
+
const args = setUpstream ? ["push", "-u", "origin", branch] : ["push", "origin", branch];
|
|
76
|
+
try {
|
|
77
|
+
execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 60000 });
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
const stderr = err?.stderr?.toString().trim();
|
|
82
|
+
// Git's own error already says what went wrong (auth, rejected, no
|
|
83
|
+
// network); keep the first few lines so the user sees that, not a
|
|
84
|
+
// stack trace.
|
|
85
|
+
const detail = stderr ? stderr.split("\n").slice(0, 4).join("\n") : err.message;
|
|
86
|
+
return `git push failed:\n${detail}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
20
89
|
/** Branch + working-tree status, capped at 30 lines; null outside a repo. */
|
|
21
90
|
export function gitStatusShort(cwd) {
|
|
22
91
|
const out = git(cwd, ["status", "--porcelain", "-b"]);
|
package/dist/git/pr.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Parse `owner/repo` out of the usual origin URL shapes:
|
|
4
|
+
* `https://github.com/owner/repo(.git)`, `git@github.com:owner/repo(.git)`,
|
|
5
|
+
* and `ssh://git@github.com/owner/repo(.git)`. Returns null for anything
|
|
6
|
+
* else — a non-GitHub remote, or a shape we do not recognise, is not
|
|
7
|
+
* something `/pr` can open a pull request against.
|
|
8
|
+
*/
|
|
9
|
+
export function parseGitHubRemote(url) {
|
|
10
|
+
const t = url.trim();
|
|
11
|
+
const m = /^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(t) ??
|
|
12
|
+
/^(?:ssh:\/\/)?git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(t) ??
|
|
13
|
+
/^ssh:\/\/git@github\.com\/([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(t);
|
|
14
|
+
if (!m)
|
|
15
|
+
return null;
|
|
16
|
+
return { owner: m[1], repo: m[2] };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Find a token for the GitHub API without asking the user to paste one into
|
|
20
|
+
* chat. `GITHUB_TOKEN` wins when set; otherwise `gh auth token` is tried
|
|
21
|
+
* when the gh CLI is installed and logged in. Returns null when neither
|
|
22
|
+
* works, and the caller tells the user exactly how to provide one.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveGitHubToken(env = process.env) {
|
|
25
|
+
const fromEnv = env.GITHUB_TOKEN?.trim();
|
|
26
|
+
if (fromEnv)
|
|
27
|
+
return { token: fromEnv, source: "GITHUB_TOKEN" };
|
|
28
|
+
try {
|
|
29
|
+
const fromGh = execFileSync("gh", ["auth", "token"], {
|
|
30
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
31
|
+
timeout: 5000,
|
|
32
|
+
})
|
|
33
|
+
.toString()
|
|
34
|
+
.trim();
|
|
35
|
+
if (fromGh)
|
|
36
|
+
return { token: fromGh, source: "gh CLI" };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// gh missing or not logged in — fall through to null.
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Pull the `/pr` flags out of the command argument. `--title` and `--body`
|
|
45
|
+
* take every word up to the next flag, so titles can contain spaces without
|
|
46
|
+
* quoting and flags can come in any order; `--base` takes one word (or
|
|
47
|
+
* `--base=<branch>`). A bare `/pr` takes no positional argument, so anything
|
|
48
|
+
* else is ignored.
|
|
49
|
+
*/
|
|
50
|
+
export function parsePrFlags(arg) {
|
|
51
|
+
const opts = { draft: false };
|
|
52
|
+
const tokens = arg.trim().split(/\s+/).filter(Boolean);
|
|
53
|
+
const isFlag = (t) => t === "--draft" ||
|
|
54
|
+
t === "--title" ||
|
|
55
|
+
t === "--body" ||
|
|
56
|
+
t === "--base" ||
|
|
57
|
+
t.startsWith("--base=");
|
|
58
|
+
let i = 0;
|
|
59
|
+
while (i < tokens.length) {
|
|
60
|
+
const t = tokens[i];
|
|
61
|
+
if (t === "--draft") {
|
|
62
|
+
opts.draft = true;
|
|
63
|
+
i += 1;
|
|
64
|
+
}
|
|
65
|
+
else if (t === "--title" || t === "--body") {
|
|
66
|
+
const words = [];
|
|
67
|
+
i += 1;
|
|
68
|
+
while (i < tokens.length && !isFlag(tokens[i])) {
|
|
69
|
+
words.push(tokens[i]);
|
|
70
|
+
i += 1;
|
|
71
|
+
}
|
|
72
|
+
const value = words.join(" ").trim() || undefined;
|
|
73
|
+
if (t === "--title")
|
|
74
|
+
opts.title = value;
|
|
75
|
+
else
|
|
76
|
+
opts.body = value;
|
|
77
|
+
}
|
|
78
|
+
else if (t === "--base" || t.startsWith("--base=")) {
|
|
79
|
+
const inline = t.startsWith("--base=") ? t.slice("--base=".length).trim() : "";
|
|
80
|
+
const next = tokens[i + 1];
|
|
81
|
+
if (inline) {
|
|
82
|
+
opts.base = inline;
|
|
83
|
+
i += 1;
|
|
84
|
+
}
|
|
85
|
+
else if (next && !isFlag(next)) {
|
|
86
|
+
opts.base = next;
|
|
87
|
+
i += 2;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
i += 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
i += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return opts;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The PR title when the user did not pass `--title`. One commit ahead names
|
|
101
|
+
* itself; several get the branch name, since no single commit speaks for the
|
|
102
|
+
* change. An explicit `--title` always wins.
|
|
103
|
+
*/
|
|
104
|
+
export function prTitleFor(commits, branch, override) {
|
|
105
|
+
if (override?.trim())
|
|
106
|
+
return override.trim();
|
|
107
|
+
if (commits.length === 1)
|
|
108
|
+
return commits[0].subject || branch;
|
|
109
|
+
return branch;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The PR body when the user did not pass `--body`. One commit contributes
|
|
113
|
+
* its own body (often empty — fine); several become a list of subjects so
|
|
114
|
+
* the PR still says what it contains. An explicit `--body` always wins.
|
|
115
|
+
*/
|
|
116
|
+
export function prBodyFor(commits, override) {
|
|
117
|
+
if (override !== undefined)
|
|
118
|
+
return override;
|
|
119
|
+
if (commits.length <= 1)
|
|
120
|
+
return commits[0]?.body || undefined;
|
|
121
|
+
return commits.map((c) => `- ${c.subject}`).join("\n");
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Create the pull request via the GitHub REST API. `fetchFn` is injectable
|
|
125
|
+
* so tests can fake the API without network access.
|
|
126
|
+
*
|
|
127
|
+
* A 422 from GitHub usually means a PR for the branch already exists — the
|
|
128
|
+
* caller then looks it up and reports its URL rather than failing.
|
|
129
|
+
*/
|
|
130
|
+
export async function createPullRequest(req, fetchFn = fetch) {
|
|
131
|
+
let res;
|
|
132
|
+
try {
|
|
133
|
+
res = await fetchFn(`https://api.github.com/repos/${req.owner}/${req.repo}/pulls`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
Authorization: `Bearer ${req.token}`,
|
|
137
|
+
Accept: "application/vnd.github+json",
|
|
138
|
+
"Content-Type": "application/json",
|
|
139
|
+
"User-Agent": "kritya",
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
title: req.title,
|
|
143
|
+
head: req.head,
|
|
144
|
+
base: req.base,
|
|
145
|
+
body: req.body ?? "",
|
|
146
|
+
draft: req.draft,
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
throw new Error(`Could not reach api.github.com: ${err.message}`, {
|
|
152
|
+
cause: err,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (res.status === 401) {
|
|
156
|
+
throw new Error("GitHub rejected the token (401). Check that GITHUB_TOKEN is valid, or re-run `gh auth login`.");
|
|
157
|
+
}
|
|
158
|
+
if (res.status === 403) {
|
|
159
|
+
throw new Error("GitHub refused the request (403) — the token may lack repo scope, or the API rate limit was hit.");
|
|
160
|
+
}
|
|
161
|
+
if (res.status === 404) {
|
|
162
|
+
throw new Error(`Repository ${req.owner}/${req.repo} not found (404). The token may not have access to it.`);
|
|
163
|
+
}
|
|
164
|
+
if (res.status === 422) {
|
|
165
|
+
const err = new Error("ALREADY_EXISTS");
|
|
166
|
+
err.alreadyExists = true;
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
if (!res.ok) {
|
|
170
|
+
throw new Error(`GitHub API returned ${res.status}.`);
|
|
171
|
+
}
|
|
172
|
+
const data = (await res.json());
|
|
173
|
+
if (typeof data.number !== "number" || typeof data.html_url !== "string") {
|
|
174
|
+
throw new Error("GitHub's response did not include the pull request URL.");
|
|
175
|
+
}
|
|
176
|
+
return { number: data.number, url: data.html_url };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Find an already-open PR from `head` into any base, so `/pr` reports the
|
|
180
|
+
* existing one instead of failing on a duplicate. Returns null when there
|
|
181
|
+
* is none (or the lookup itself fails — absence of evidence either way).
|
|
182
|
+
*/
|
|
183
|
+
export async function findOpenPullRequest(owner, repo, head, token, fetchFn = fetch) {
|
|
184
|
+
try {
|
|
185
|
+
const res = await fetchFn(`https://api.github.com/repos/${owner}/${repo}/pulls?head=${owner}:${head}&state=open`, {
|
|
186
|
+
headers: {
|
|
187
|
+
Authorization: `Bearer ${token}`,
|
|
188
|
+
Accept: "application/vnd.github+json",
|
|
189
|
+
"User-Agent": "kritya",
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
if (!res.ok)
|
|
193
|
+
return null;
|
|
194
|
+
const data = (await res.json());
|
|
195
|
+
const first = Array.isArray(data) ? data[0] : undefined;
|
|
196
|
+
if (first && typeof first.number === "number" && typeof first.html_url === "string") {
|
|
197
|
+
return { number: first.number, url: first.html_url };
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
package/dist/headless.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { Agent } from "./agent/loop.js";
|
|
3
3
|
import { KillSwitchError } from "./agent/killSwitch.js";
|
|
4
4
|
import { CONFIG_DIR, legacyGlobalModel, listProviders, loadConfig, loadDotEnv, privacyModeFor, resolveProvider, } from "./config/config.js";
|
|
5
|
-
import {
|
|
5
|
+
import { defaultModelFor, contextWindowFor } from "./config/models.js";
|
|
6
6
|
import { PermissionManager } from "./permissions/permissions.js";
|
|
7
7
|
import { loadRules } from "./permissions/rules.js";
|
|
8
8
|
import { ProviderClient, RetryExhaustedError, friendlyProviderErrorHint, } from "./provider/client.js";
|
|
@@ -125,7 +125,7 @@ export async function runHeadless(args) {
|
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
const providerDefaultModel = config.providers?.[provider.name]?.model;
|
|
128
|
-
const model = resolveEffectiveModel(provider.name, [args.model, providerDefaultModel, legacyGlobalModel(config, provider.name)], provider.name === "switchyard" ? SWITCHYARD_ROUTE_ID :
|
|
128
|
+
const model = resolveEffectiveModel(provider.name, [args.model, providerDefaultModel, legacyGlobalModel(config, provider.name)], provider.name === "switchyard" ? SWITCHYARD_ROUTE_ID : defaultModelFor(provider.name));
|
|
129
129
|
const staleModelWarning = staleSwitchyardModelWarning(provider.name, model, args.model);
|
|
130
130
|
if (staleModelWarning)
|
|
131
131
|
process.stderr.write(`${staleModelWarning}\n`);
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
import { Agent } from "./agent/loop.js";
|
|
7
7
|
import { CONFIG_DIR, appendGlobalEnvVar, legacyGlobalModel, loadConfig, loadDotEnv, privacyModeFor, resolveProvider, } from "./config/config.js";
|
|
8
|
-
import {
|
|
8
|
+
import { defaultModelFor, contextWindowFor } from "./config/models.js";
|
|
9
9
|
import { PermissionManager } from "./permissions/permissions.js";
|
|
10
10
|
import { loadRules } from "./permissions/rules.js";
|
|
11
11
|
import { ProviderClient } from "./provider/client.js";
|
|
@@ -404,7 +404,7 @@ async function main() {
|
|
|
404
404
|
}
|
|
405
405
|
const providerDefaultModel = config.providers?.[provider.name]?.model;
|
|
406
406
|
const modelRef = {
|
|
407
|
-
current: resolveEffectiveModel(provider.name, [args.model, providerDefaultModel, legacyGlobalModel(config, provider.name)], provider.name === "switchyard" ? SWITCHYARD_ROUTE_ID :
|
|
407
|
+
current: resolveEffectiveModel(provider.name, [args.model, providerDefaultModel, legacyGlobalModel(config, provider.name)], provider.name === "switchyard" ? SWITCHYARD_ROUTE_ID : defaultModelFor(provider.name)),
|
|
408
408
|
};
|
|
409
409
|
const staleModelWarning = staleSwitchyardModelWarning(provider.name, modelRef.current, args.model);
|
|
410
410
|
if (staleModelWarning)
|
package/dist/ui/App.js
CHANGED
|
@@ -721,7 +721,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
721
721
|
}
|
|
722
722
|
addItem({ kind: "user", text: `${text} (queued)` });
|
|
723
723
|
void expandMentions(text).then((t) => agent.queueSteer(t));
|
|
724
|
-
}, placeholder: "steer the agent\u2026 (Enter to queue)" })] })] })), phase === "permission" && permission && (_jsx(PermissionPrompt, { toolName: permission.toolName, summary: permission.summary, diff: permission.diff, warning: permission.warning, onDecision: onPermissionDecision })), phase === "elicitation" && elicitation && (_jsx(ElicitationPrompt, { message: elicitation.message, fields: elicitation.fields, onDecision: onElicitationDecision })), phase === "confirmMode" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "Switch to accept-edits mode?" }), _jsx(Text, { children: "File writes and edits will auto-approve without asking." }), _jsx(Text, { dimColor: true, children: "Destructive shell commands (rm -rf, force-push, etc.) always still ask, in every mode." }), _jsx(Text, { dimColor: true, children: "Shift+Tab again moves to dry-run mode; once more back to normal." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "Yes (y)" }), " \u00B7 No (n/Esc)"] }) })] })), phase === "confirmBypassMode" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Switch to auto mode? (sandbox-gated)" }), _jsx(Text, { children: "EVERY tool call will auto-approve, including destructive shell commands (rm -rf, force-push, etc.) \u2014 nothing will ask first." }), _jsx(Text, { dimColor: true, children: "The sandbox \u2014 not a human \u2014 is what contains any damage this mode causes." }), _jsx(Text, { dimColor: true, children: "Shift+Tab again goes back to normal." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "red", children: "Yes (y)" }), " \u00B7 No (n/Esc)"] }) })] })), phase === "model" && (_jsx(ModelPicker, { current: model, customModels: config.customModels ?? [], onSelect: (id) => {
|
|
724
|
+
}, placeholder: "steer the agent\u2026 (Enter to queue)" })] })] })), phase === "permission" && permission && (_jsx(PermissionPrompt, { toolName: permission.toolName, summary: permission.summary, diff: permission.diff, warning: permission.warning, onDecision: onPermissionDecision })), phase === "elicitation" && elicitation && (_jsx(ElicitationPrompt, { message: elicitation.message, fields: elicitation.fields, onDecision: onElicitationDecision })), phase === "confirmMode" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "Switch to accept-edits mode?" }), _jsx(Text, { children: "File writes and edits will auto-approve without asking." }), _jsx(Text, { dimColor: true, children: "Destructive shell commands (rm -rf, force-push, etc.) always still ask, in every mode." }), _jsx(Text, { dimColor: true, children: "Shift+Tab again moves to dry-run mode; once more back to normal." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "Yes (y)" }), " \u00B7 No (n/Esc)"] }) })] })), phase === "confirmBypassMode" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Switch to auto mode? (sandbox-gated)" }), _jsx(Text, { children: "EVERY tool call will auto-approve, including destructive shell commands (rm -rf, force-push, etc.) \u2014 nothing will ask first." }), _jsx(Text, { dimColor: true, children: "The sandbox \u2014 not a human \u2014 is what contains any damage this mode causes." }), _jsx(Text, { dimColor: true, children: "Shift+Tab again goes back to normal." }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "red", children: "Yes (y)" }), " \u00B7 No (n/Esc)"] }) })] })), phase === "model" && (_jsx(ModelPicker, { current: model, provider: provider, customModels: config.customModels ?? [], onSelect: (id) => {
|
|
725
725
|
setPhase("input");
|
|
726
726
|
setModelEverywhere(id);
|
|
727
727
|
}, onCancel: () => setPhase("input") })), phase === "resume" && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "magenta", children: ["Resume a session", " ", _jsx(Text, { dimColor: true, children: "(type to search \u00B7 Esc for a fresh one \u00B7 -r <name> to skip this)" })] }), resumeFilter ? (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "filter: " }), resumeFilter] })) : null, _jsx(SelectList, { items: (resumeSessions ?? [])
|
package/dist/ui/ModelPicker.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import { CURATED_MODELS } from "../config/models.js";
|
|
3
|
+
import { CURATED_MODELS, providerOfModel } from "../config/models.js";
|
|
4
4
|
import { SelectList } from "./SelectList.js";
|
|
5
|
-
|
|
5
|
+
/** Provider label for a hint, e.g. `groq` — what the user would pass to `-p`. */
|
|
6
|
+
function providerHint(m) {
|
|
7
|
+
return providerOfModel(m);
|
|
8
|
+
}
|
|
9
|
+
export function ModelPicker({ current, provider, customModels, onSelect, onCancel, }) {
|
|
10
|
+
// Show the active provider's models first and unlabelled, then any other
|
|
11
|
+
// providers' under a `provider/` prefix — so a Groq user sees Groq models
|
|
12
|
+
// at the top, and an NVIDIA user still sees the rest without the picker
|
|
13
|
+
// silently hiding them or lying about which provider they belong to.
|
|
14
|
+
const own = CURATED_MODELS.filter((m) => providerOfModel(m) === provider);
|
|
15
|
+
const others = CURATED_MODELS.filter((m) => providerOfModel(m) !== provider);
|
|
16
|
+
// Fall back to the whole list when the active provider has no curated
|
|
17
|
+
// models, so the picker is never empty.
|
|
18
|
+
const listed = own.length > 0 ? [...own, ...others] : CURATED_MODELS;
|
|
6
19
|
const items = [
|
|
7
|
-
...
|
|
20
|
+
...listed.map((m) => ({
|
|
8
21
|
label: m.label + (m.id === current ? " (current)" : ""),
|
|
9
22
|
value: m.id,
|
|
10
|
-
hint: m.
|
|
23
|
+
hint: `${m.id} · ${providerHint(m)}${m.note ? ` · ${m.note}` : ""}`,
|
|
11
24
|
})),
|
|
12
25
|
...customModels.map((m) => ({
|
|
13
26
|
label: (m.label ?? m.id) + (m.id === current ? " (current)" : ""),
|
|
@@ -15,5 +28,5 @@ export function ModelPicker({ current, customModels, onSelect, onCancel, }) {
|
|
|
15
28
|
hint: `${m.id} · custom`,
|
|
16
29
|
})),
|
|
17
30
|
];
|
|
18
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "magenta", children: ["Select model", " ",
|
|
31
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "magenta", children: ["Select model", " ", _jsxs(Text, { dimColor: true, children: ["(Esc to cancel, or /model <id> for any ", provider, " model)"] })] }), _jsx(SelectList, { items: items, onSelect: onSelect, onCancel: onCancel })] }));
|
|
19
32
|
}
|
package/dist/ui/useAgent.js
CHANGED
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { gitBranch } from "../git/git.js";
|
|
4
4
|
import { listProviders, resolveProvider, saveConfig, saveProviderModel, } from "../config/config.js";
|
|
5
|
-
import {
|
|
5
|
+
import { defaultModelFor, contextWindowFor, displayModelId } from "../config/models.js";
|
|
6
6
|
import { ProviderClient, RetryExhaustedError, friendlyProviderErrorHint, } from "../provider/client.js";
|
|
7
7
|
import { createSwitchyardClient } from "../provider/switchyardClient.js";
|
|
8
8
|
import { SWITCHYARD_ROUTE_ID, resolveEffectiveModel } from "../provider/switchyardSidecar.js";
|
|
@@ -382,7 +382,7 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
382
382
|
providerDefaultModel,
|
|
383
383
|
name === "switchyard" ? SWITCHYARD_ROUTE_ID : undefined,
|
|
384
384
|
modelRef.current,
|
|
385
|
-
], name === "switchyard" ? SWITCHYARD_ROUTE_ID :
|
|
385
|
+
], name === "switchyard" ? SWITCHYARD_ROUTE_ID : defaultModelFor(name));
|
|
386
386
|
let note = `Switched provider to ${name} — conversation history kept.`;
|
|
387
387
|
if (targetModel !== modelRef.current) {
|
|
388
388
|
setModelEverywhere(targetModel);
|