@elyracode/coding-agent 0.9.2 → 0.9.4
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/CHANGELOG.md +15 -3
- package/dist/core/agent-session.d.ts +19 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +42 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +8 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +8 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +3 -0
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/extension-write.d.ts +46 -0
- package/dist/core/tools/extension-write.d.ts.map +1 -0
- package/dist/core/tools/extension-write.js +227 -0
- package/dist/core/tools/extension-write.js.map +1 -0
- package/dist/modes/interactive/components/earendil-announcement.js +1 -1
- package/dist/modes/interactive/components/earendil-announcement.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +16 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +273 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/clipboard-native.js +1 -1
- package/dist/utils/clipboard-native.js.map +1 -1
- package/docs/skills.md +1 -1
- package/docs/usage.md +0 -2
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +5 -5
|
@@ -6,7 +6,7 @@ import * as crypto from "node:crypto";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { getProviders, } from "@elyracode/ai";
|
|
9
|
+
import { completeSimple, getProviders, } from "@elyracode/ai";
|
|
10
10
|
import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@elyracode/tui";
|
|
11
11
|
import { spawn, spawnSync } from "child_process";
|
|
12
12
|
import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getBlueprintsDir, getDebugLogPath, getDocsPath, getProjectBlueprintsDir, getProjectSnippetsDir, getSelfUpdateCommand, getShareViewerUrl, getSnippetsDir, PACKAGE_NAME, VERSION, } from "../../config.js";
|
|
@@ -85,6 +85,20 @@ function isDeadTerminalError(error) {
|
|
|
85
85
|
return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
|
|
86
86
|
}
|
|
87
87
|
const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING = "Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage.";
|
|
88
|
+
const REVIEW_SYSTEM_PROMPT = `You are a senior software engineer doing a focused code review of a diff. You did not write this code and have no prior context, so judge it on its own merits.
|
|
89
|
+
|
|
90
|
+
Review for, in priority order:
|
|
91
|
+
1. Correctness and bugs (logic errors, off-by-one, null/undefined, race conditions, incorrect edge-case handling)
|
|
92
|
+
2. Security (injection, unsafe input handling, leaked secrets, missing authz/validation)
|
|
93
|
+
3. Error handling and resource cleanup
|
|
94
|
+
4. Clarity and maintainability
|
|
95
|
+
|
|
96
|
+
Rules:
|
|
97
|
+
- Be specific: reference the file and the exact change. Quote the relevant line when useful.
|
|
98
|
+
- Be concise. Prioritize the few things that matter; skip nitpicks unless nothing else is wrong.
|
|
99
|
+
- If the diff looks correct and well done, say so plainly rather than inventing problems.
|
|
100
|
+
- Do not restate the whole diff. Only report findings.
|
|
101
|
+
- Format as a short Markdown list grouped by severity (Critical / Important / Minor), and end with a one-line overall assessment.`;
|
|
88
102
|
function isAnthropicSubscriptionAuthKey(apiKey) {
|
|
89
103
|
return typeof apiKey === "string" && apiKey.startsWith("sk-ant-oat");
|
|
90
104
|
}
|
|
@@ -2033,6 +2047,16 @@ export class InteractiveMode {
|
|
|
2033
2047
|
await this.handleDiffCommand(text.slice("/diff".length).trim());
|
|
2034
2048
|
return;
|
|
2035
2049
|
}
|
|
2050
|
+
if (text === "/review" || text.startsWith("/review ")) {
|
|
2051
|
+
this.editor.setText("");
|
|
2052
|
+
await this.handleReviewCommand(text.slice("/review".length).trim());
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
if (text === "/bisect" || text.startsWith("/bisect ")) {
|
|
2056
|
+
this.editor.setText("");
|
|
2057
|
+
await this.handleBisectCommand(text.slice("/bisect".length).trim());
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2036
2060
|
if (text === "/pin" || text.startsWith("/pin ")) {
|
|
2037
2061
|
this.editor.setText("");
|
|
2038
2062
|
this.handlePinCommand(text.startsWith("/pin ") ? text.slice(5).trim() : undefined);
|
|
@@ -3260,6 +3284,7 @@ export class InteractiveMode {
|
|
|
3260
3284
|
smartRouting: this.settingsManager.getSmartRouting(),
|
|
3261
3285
|
codebaseMemory: this.settingsManager.getCodebaseMemory(),
|
|
3262
3286
|
autoSkills: this.settingsManager.getAutoSkills(),
|
|
3287
|
+
autoExtensions: this.settingsManager.getAutoExtensions(),
|
|
3263
3288
|
diffInEditor: this.settingsManager.getDiffInEditor(),
|
|
3264
3289
|
doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
|
|
3265
3290
|
treeFilterMode: this.settingsManager.getTreeFilterMode(),
|
|
@@ -3354,6 +3379,9 @@ export class InteractiveMode {
|
|
|
3354
3379
|
onAutoSkillsChange: (enabled) => {
|
|
3355
3380
|
this.settingsManager.setAutoSkills(enabled);
|
|
3356
3381
|
},
|
|
3382
|
+
onAutoExtensionsChange: (enabled) => {
|
|
3383
|
+
this.settingsManager.setAutoExtensions(enabled);
|
|
3384
|
+
},
|
|
3357
3385
|
onDiffInEditorChange: (enabled) => {
|
|
3358
3386
|
this.settingsManager.setDiffInEditor(enabled);
|
|
3359
3387
|
},
|
|
@@ -3814,6 +3842,8 @@ export class InteractiveMode {
|
|
|
3814
3842
|
` ${theme.fg("muted", "/theme")} Switch theme`,
|
|
3815
3843
|
` ${theme.fg("muted", "/cost")} Token usage and cost`,
|
|
3816
3844
|
` ${theme.fg("muted", "/diff")} Show changes (--session, --cached, or <path>)`,
|
|
3845
|
+
` ${theme.fg("muted", "/review")} Get a second-opinion code review of your changes`,
|
|
3846
|
+
` ${theme.fg("muted", "/bisect <cmd>")} Find which turn broke things (git bisect over checkpoints)`,
|
|
3817
3847
|
` ${theme.fg("muted", "/pin <path>")} Pin file to context`,
|
|
3818
3848
|
` ${theme.fg("muted", "/memory")} View project memory`,
|
|
3819
3849
|
` ${theme.fg("muted", "/blueprint")} Apply session template`,
|
|
@@ -3947,6 +3977,247 @@ export class InteractiveMode {
|
|
|
3947
3977
|
this.showError("Failed to run git diff.");
|
|
3948
3978
|
}
|
|
3949
3979
|
}
|
|
3980
|
+
/**
|
|
3981
|
+
* /review — ask a model for a fresh, independent critique of the current
|
|
3982
|
+
* changes. By default reviews uncommitted changes; --session scopes to what
|
|
3983
|
+
* the agent changed this session. An optional path filters the diff.
|
|
3984
|
+
*/
|
|
3985
|
+
async handleReviewCommand(args) {
|
|
3986
|
+
const cwd = this.sessionManager.getCwd();
|
|
3987
|
+
const tokens = args.split(/\s+/).filter(Boolean);
|
|
3988
|
+
let sessionMode = false;
|
|
3989
|
+
const pathArgs = [];
|
|
3990
|
+
for (const token of tokens) {
|
|
3991
|
+
if (token === "--session" || token === "-s")
|
|
3992
|
+
sessionMode = true;
|
|
3993
|
+
else
|
|
3994
|
+
pathArgs.push(token);
|
|
3995
|
+
}
|
|
3996
|
+
const runGit = (gitArgs) => {
|
|
3997
|
+
const result = spawnSync("git", gitArgs, {
|
|
3998
|
+
cwd,
|
|
3999
|
+
encoding: "utf-8",
|
|
4000
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
4001
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4002
|
+
});
|
|
4003
|
+
return result.stdout ?? "";
|
|
4004
|
+
};
|
|
4005
|
+
const inside = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
4006
|
+
cwd,
|
|
4007
|
+
encoding: "utf-8",
|
|
4008
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4009
|
+
});
|
|
4010
|
+
if (inside.status !== 0) {
|
|
4011
|
+
this.showError("Not a git repository.");
|
|
4012
|
+
return;
|
|
4013
|
+
}
|
|
4014
|
+
const pathSpec = pathArgs.length > 0 ? ["--", ...pathArgs] : [];
|
|
4015
|
+
let diff;
|
|
4016
|
+
if (sessionMode) {
|
|
4017
|
+
const baseline = this.session.getSessionBaselineRef();
|
|
4018
|
+
if (!baseline) {
|
|
4019
|
+
this.showStatus("No session changes to review yet (the agent hasn't modified files this session).");
|
|
4020
|
+
return;
|
|
4021
|
+
}
|
|
4022
|
+
diff = runGit(["diff", baseline, ...pathSpec]);
|
|
4023
|
+
}
|
|
4024
|
+
else {
|
|
4025
|
+
diff = runGit(["diff", "HEAD", ...pathSpec]);
|
|
4026
|
+
if (!diff.trim())
|
|
4027
|
+
diff = runGit(["diff", ...pathSpec]);
|
|
4028
|
+
}
|
|
4029
|
+
if (!diff.trim()) {
|
|
4030
|
+
this.showStatus("No changes to review.");
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
4033
|
+
const model = this.session.model;
|
|
4034
|
+
if (!model) {
|
|
4035
|
+
this.showError("No model selected. Use /model to choose one first.");
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4038
|
+
// Cap very large diffs so the review request stays within limits.
|
|
4039
|
+
const MAX_DIFF_CHARS = 60_000;
|
|
4040
|
+
const truncated = diff.length > MAX_DIFF_CHARS;
|
|
4041
|
+
const diffForReview = truncated ? `${diff.slice(0, MAX_DIFF_CHARS)}\n... (diff truncated)` : diff;
|
|
4042
|
+
const scopeLabel = sessionMode ? "this session's changes" : "uncommitted changes";
|
|
4043
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
4044
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
4045
|
+
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", `Code Review \u2014 ${scopeLabel}`)), 1, 0));
|
|
4046
|
+
const loader = new BorderedLoader(this.ui, theme, `Reviewing with ${model.name}...`, { cancellable: false });
|
|
4047
|
+
this.chatContainer.addChild(loader);
|
|
4048
|
+
this.ui.requestRender();
|
|
4049
|
+
const finishLoader = () => {
|
|
4050
|
+
loader.dispose();
|
|
4051
|
+
this.chatContainer.removeChild(loader);
|
|
4052
|
+
};
|
|
4053
|
+
try {
|
|
4054
|
+
const apiKey = await this.session.modelRegistry.getApiKeyForProvider(model.provider);
|
|
4055
|
+
const context = {
|
|
4056
|
+
systemPrompt: REVIEW_SYSTEM_PROMPT,
|
|
4057
|
+
messages: [
|
|
4058
|
+
{
|
|
4059
|
+
role: "user",
|
|
4060
|
+
content: `Review the following diff and report your findings.\n\n\`\`\`diff\n${diffForReview}\n\`\`\``,
|
|
4061
|
+
timestamp: Date.now(),
|
|
4062
|
+
},
|
|
4063
|
+
],
|
|
4064
|
+
};
|
|
4065
|
+
const result = await completeSimple(model, context, apiKey ? { apiKey } : undefined);
|
|
4066
|
+
const reviewText = result.content
|
|
4067
|
+
.filter((c) => c.type === "text")
|
|
4068
|
+
.map((c) => c.text)
|
|
4069
|
+
.join("\n")
|
|
4070
|
+
.trim();
|
|
4071
|
+
finishLoader();
|
|
4072
|
+
if (!reviewText) {
|
|
4073
|
+
this.chatContainer.addChild(new Text(theme.fg("muted", "The reviewer returned no findings."), 1, 0));
|
|
4074
|
+
}
|
|
4075
|
+
else {
|
|
4076
|
+
this.chatContainer.addChild(new Markdown(reviewText, 1, 1, this.getMarkdownThemeWithSettings()));
|
|
4077
|
+
if (truncated) {
|
|
4078
|
+
this.chatContainer.addChild(new Text(theme.fg("muted", "(Diff was large and truncated for review.)"), 1, 0));
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
4082
|
+
this.ui.requestRender();
|
|
4083
|
+
}
|
|
4084
|
+
catch (error) {
|
|
4085
|
+
finishLoader();
|
|
4086
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
4087
|
+
this.chatContainer.addChild(new Text(theme.fg("error", `Review failed: ${msg}`), 1, 0));
|
|
4088
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
4089
|
+
this.ui.requestRender();
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
/**
|
|
4093
|
+
* /bisect — binary-search the session's checkpoints to find which agent turn
|
|
4094
|
+
* introduced a problem, like `git bisect` but over the agent's own steps.
|
|
4095
|
+
*
|
|
4096
|
+
* With a command argument it runs automatically (exit 0 = good, non-zero =
|
|
4097
|
+
* bad), like `git bisect run`. Without one, it asks you to mark each step
|
|
4098
|
+
* good or bad after testing manually. The working tree is always restored at
|
|
4099
|
+
* the end.
|
|
4100
|
+
*/
|
|
4101
|
+
async handleBisectCommand(command) {
|
|
4102
|
+
const points = this.session.getRewindPoints();
|
|
4103
|
+
if (points.length < 2) {
|
|
4104
|
+
this.showStatus("Not enough checkpoints to bisect. Checkpoints are created as the agent runs.");
|
|
4105
|
+
return;
|
|
4106
|
+
}
|
|
4107
|
+
const snapshot = this.session.createWorkingTreeSnapshot();
|
|
4108
|
+
if (!snapshot) {
|
|
4109
|
+
this.showError("Not a git repository — /bisect needs git checkpoints.");
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
4112
|
+
const cwd = this.sessionManager.getCwd();
|
|
4113
|
+
const describe = (i) => `${i + 1}/${points.length}: ${points[i].text.replace(/\n/g, " ").slice(0, 60)}`;
|
|
4114
|
+
const runCommand = () => new Promise((resolve) => {
|
|
4115
|
+
const child = spawn("sh", ["-c", command], {
|
|
4116
|
+
cwd,
|
|
4117
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4118
|
+
timeout: 120_000,
|
|
4119
|
+
});
|
|
4120
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
4121
|
+
child.on("error", () => resolve(1));
|
|
4122
|
+
});
|
|
4123
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
4124
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
4125
|
+
const title = command ? `Bisecting with: ${command}` : "Bisecting (test each checkpoint manually)";
|
|
4126
|
+
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", title)), 1, 0));
|
|
4127
|
+
const status = new Text(theme.fg("muted", "Preparing..."), 1, 0);
|
|
4128
|
+
this.chatContainer.addChild(status);
|
|
4129
|
+
this.ui.requestRender();
|
|
4130
|
+
// Ask whether a step is good. Returns true=good, false=bad, undefined=abort.
|
|
4131
|
+
const askGoodBad = async (index) => {
|
|
4132
|
+
if (command) {
|
|
4133
|
+
status.setText(theme.fg("muted", `Testing checkpoint ${describe(index)}...`));
|
|
4134
|
+
this.ui.requestRender();
|
|
4135
|
+
await new Promise((r) => setTimeout(r, 16));
|
|
4136
|
+
const code = await runCommand();
|
|
4137
|
+
return code === 0;
|
|
4138
|
+
}
|
|
4139
|
+
const choice = await this.showExtensionSelector(`Restored files to checkpoint ${describe(index)}.\nTest now (in another terminal), then choose:`, ["Good (works here)", "Bad (broken here)", "Abort"]);
|
|
4140
|
+
if (!choice || choice === "Abort")
|
|
4141
|
+
return undefined;
|
|
4142
|
+
return choice.startsWith("Good");
|
|
4143
|
+
};
|
|
4144
|
+
let aborted = false;
|
|
4145
|
+
let culpritIndex;
|
|
4146
|
+
try {
|
|
4147
|
+
// In automated mode, verify the current state is actually bad first.
|
|
4148
|
+
if (command) {
|
|
4149
|
+
status.setText(theme.fg("muted", "Checking current state..."));
|
|
4150
|
+
this.ui.requestRender();
|
|
4151
|
+
await new Promise((r) => setTimeout(r, 16));
|
|
4152
|
+
if ((await runCommand()) === 0) {
|
|
4153
|
+
status.setText(theme.fg("muted", "Current state passes the command — nothing to bisect."));
|
|
4154
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
4155
|
+
this.ui.requestRender();
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
// lo = known good, hi = known bad. Find the first bad checkpoint in (lo, hi].
|
|
4160
|
+
let lo = 0;
|
|
4161
|
+
let hi = points.length - 1;
|
|
4162
|
+
// Establish that the earliest checkpoint is good.
|
|
4163
|
+
const earliestRestore = this.session.restoreFilesToCheckpoint(points[0].entryId);
|
|
4164
|
+
if (!earliestRestore.ok) {
|
|
4165
|
+
this.showError(`Bisect failed to restore checkpoint: ${earliestRestore.error}`);
|
|
4166
|
+
aborted = true;
|
|
4167
|
+
}
|
|
4168
|
+
else {
|
|
4169
|
+
const earliestGood = await askGoodBad(0);
|
|
4170
|
+
if (earliestGood === undefined) {
|
|
4171
|
+
aborted = true;
|
|
4172
|
+
}
|
|
4173
|
+
else if (!earliestGood) {
|
|
4174
|
+
status.setText(theme.fg("muted", "The earliest checkpoint is already bad — the cause predates this session."));
|
|
4175
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
4176
|
+
this.ui.requestRender();
|
|
4177
|
+
aborted = true;
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
while (!aborted && hi - lo > 1) {
|
|
4181
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
4182
|
+
const restore = this.session.restoreFilesToCheckpoint(points[mid].entryId);
|
|
4183
|
+
if (!restore.ok) {
|
|
4184
|
+
this.showError(`Bisect failed to restore checkpoint: ${restore.error}`);
|
|
4185
|
+
aborted = true;
|
|
4186
|
+
break;
|
|
4187
|
+
}
|
|
4188
|
+
const good = await askGoodBad(mid);
|
|
4189
|
+
if (good === undefined) {
|
|
4190
|
+
aborted = true;
|
|
4191
|
+
break;
|
|
4192
|
+
}
|
|
4193
|
+
if (good)
|
|
4194
|
+
lo = mid;
|
|
4195
|
+
else
|
|
4196
|
+
hi = mid;
|
|
4197
|
+
}
|
|
4198
|
+
if (!aborted)
|
|
4199
|
+
culpritIndex = hi;
|
|
4200
|
+
}
|
|
4201
|
+
finally {
|
|
4202
|
+
const restored = this.session.restoreFilesToSnapshot(snapshot);
|
|
4203
|
+
if (!restored.ok) {
|
|
4204
|
+
this.showError(`Bisect could not restore your working tree: ${restored.error}`);
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
if (aborted) {
|
|
4208
|
+
status.setText(theme.fg("muted", "Bisect stopped. Your working tree was restored."));
|
|
4209
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
4210
|
+
this.ui.requestRender();
|
|
4211
|
+
return;
|
|
4212
|
+
}
|
|
4213
|
+
if (culpritIndex !== undefined) {
|
|
4214
|
+
const culprit = points[culpritIndex];
|
|
4215
|
+
status.setText(`${theme.fg("success", "First bad checkpoint:")} ${theme.fg("accent", culprit.text.replace(/\n/g, " ").slice(0, 80))}`);
|
|
4216
|
+
this.chatContainer.addChild(new Text(theme.fg("muted", "The change introduced at this turn is the likely culprit. Use /rewind to return here."), 1, 0));
|
|
4217
|
+
this.chatContainer.addChild(new DynamicBorder());
|
|
4218
|
+
this.ui.requestRender();
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
3950
4221
|
/**
|
|
3951
4222
|
* Open read-only text content in the user's external editor ($VISUAL/$EDITOR).
|
|
3952
4223
|
* Suspends the TUI while the editor runs, then restores it. The `suffix`
|
|
@@ -5016,6 +5287,7 @@ export class InteractiveMode {
|
|
|
5016
5287
|
"@elyracode/laravel -- Deep Laravel understanding: models, routes, architecture analysis",
|
|
5017
5288
|
"@elyracode/btw -- Side conversations: ask questions in parallel without affecting the main session",
|
|
5018
5289
|
"@elyracode/youtrack -- YouTrack: issues, comments, tags, links, projects, time tracking",
|
|
5290
|
+
"@elyracode/semantic-index -- Semantic code search: find code by meaning via local embeddings",
|
|
5019
5291
|
];
|
|
5020
5292
|
this.showSelector((done) => {
|
|
5021
5293
|
const selector = new ExtensionSelectorComponent("Install Extension", packages, (selected) => {
|