@teammates/cli 0.5.3 → 0.6.1
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/dist/adapter.d.ts +7 -1
- package/dist/adapter.js +29 -4
- package/dist/adapter.test.js +10 -13
- package/dist/adapters/cli-proxy.d.ts +3 -0
- package/dist/adapters/cli-proxy.js +133 -108
- package/dist/cli-utils.d.ts +6 -0
- package/dist/cli-utils.js +17 -0
- package/dist/cli-utils.test.js +50 -1
- package/dist/cli.js +626 -20
- package/dist/compact.d.ts +29 -0
- package/dist/compact.js +131 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/log-parser.d.ts +53 -0
- package/dist/log-parser.js +228 -0
- package/dist/log-parser.test.d.ts +1 -0
- package/dist/log-parser.test.js +113 -0
- package/dist/orchestrator.d.ts +2 -0
- package/dist/orchestrator.js +4 -0
- package/dist/registry.js +4 -4
- package/dist/types.d.ts +30 -0
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* teammates --dir <path> Override .teammates/ location
|
|
9
9
|
*/
|
|
10
10
|
import { exec as execCb, execSync, spawnSync } from "node:child_process";
|
|
11
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
12
12
|
import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
|
|
13
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
13
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
14
14
|
import { createInterface } from "node:readline";
|
|
15
15
|
import { App, ChatView, concat, esc, pen, renderMarkdown, stripAnsi, } from "@teammates/consolonia";
|
|
16
16
|
import chalk from "chalk";
|
|
@@ -18,10 +18,11 @@ import ora from "ora";
|
|
|
18
18
|
import { DAILY_LOG_BUDGET_TOKENS, syncRecallIndex } from "./adapter.js";
|
|
19
19
|
import { AnimatedBanner, } from "./banner.js";
|
|
20
20
|
import { findTeammatesDir, PKG_VERSION, parseCliArgs, printUsage, resolveAdapter, } from "./cli-args.js";
|
|
21
|
-
import { buildConversationContext as buildConvCtx, buildSummarizationPrompt, cleanResponseBody, findAtMention, findSummarizationSplit, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
|
|
22
|
-
import { autoCompactForBudget, buildWisdomPrompt, compactEpisodic, purgeStaleDailies, } from "./compact.js";
|
|
21
|
+
import { buildConversationContext as buildConvCtx, buildSummarizationPrompt, cleanResponseBody, compressConversationEntries, findAtMention, findSummarizationSplit, formatConversationEntry, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
|
|
22
|
+
import { autoCompactForBudget, buildDailyCompressionPrompt, buildMigrationCompressionPrompt, buildWisdomPrompt, compactEpisodic, findUncompressedDailies, purgeStaleDailies, } from "./compact.js";
|
|
23
23
|
import { PromptInput } from "./console/prompt-input.js";
|
|
24
24
|
import { buildTitle } from "./console/startup.js";
|
|
25
|
+
import { buildConversationLog } from "./log-parser.js";
|
|
25
26
|
import { buildImportAdaptationPrompt, copyTemplateFiles, getOnboardingPrompt, importTeammates, } from "./onboard.js";
|
|
26
27
|
import { Orchestrator } from "./orchestrator.js";
|
|
27
28
|
import { loadPersonas, scaffoldFromPersona } from "./personas.js";
|
|
@@ -120,8 +121,11 @@ class TeammatesREPL {
|
|
|
120
121
|
if (this.chatView && cleaned) {
|
|
121
122
|
const t = theme();
|
|
122
123
|
const teammate = result.teammate;
|
|
123
|
-
const
|
|
124
|
+
const ts = Date.now();
|
|
125
|
+
const replyId = `reply-${teammate}-${ts}`;
|
|
126
|
+
const copyId = `copy-${teammate}-${ts}`;
|
|
124
127
|
this._replyContexts.set(replyId, { teammate, message: cleaned });
|
|
128
|
+
this._copyContexts.set(copyId, cleaned);
|
|
125
129
|
this.chatView.appendActionList([
|
|
126
130
|
{
|
|
127
131
|
id: replyId,
|
|
@@ -135,7 +139,7 @@ class TeammatesREPL {
|
|
|
135
139
|
}),
|
|
136
140
|
},
|
|
137
141
|
{
|
|
138
|
-
id:
|
|
142
|
+
id: copyId,
|
|
139
143
|
normalStyle: this.makeSpan({
|
|
140
144
|
text: " [copy]",
|
|
141
145
|
style: { fg: t.textDim },
|
|
@@ -152,15 +156,25 @@ class TeammatesREPL {
|
|
|
152
156
|
this.refreshTeammates();
|
|
153
157
|
this.showPrompt();
|
|
154
158
|
}
|
|
155
|
-
/**
|
|
156
|
-
static
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
/** Target context window in tokens. Conversation history budget is derived from this. */
|
|
160
|
+
static TARGET_CONTEXT_TOKENS = 128_000;
|
|
161
|
+
/** Estimated tokens used by non-conversation prompt sections (identity, wisdom, logs, recall, instructions, task). */
|
|
162
|
+
static PROMPT_OVERHEAD_TOKENS = 32_000;
|
|
163
|
+
/** Chars-per-token approximation (matches adapter.ts). */
|
|
164
|
+
static CHARS_PER_TOKEN = 4;
|
|
165
|
+
/** Character budget for conversation history = (target − overhead) × chars/token. */
|
|
166
|
+
static CONV_HISTORY_CHARS = (TeammatesREPL.TARGET_CONTEXT_TOKENS -
|
|
167
|
+
TeammatesREPL.PROMPT_OVERHEAD_TOKENS) *
|
|
168
|
+
TeammatesREPL.CHARS_PER_TOKEN;
|
|
169
|
+
buildConversationContext(_teammate, snapshot) {
|
|
170
|
+
const history = snapshot ? snapshot.history : this.conversationHistory;
|
|
171
|
+
const summary = snapshot ? snapshot.summary : this.conversationSummary;
|
|
172
|
+
return buildConvCtx(history, summary, TeammatesREPL.CONV_HISTORY_CHARS);
|
|
159
173
|
}
|
|
160
174
|
/**
|
|
161
|
-
* Check if conversation history exceeds the
|
|
175
|
+
* Check if conversation history exceeds the token budget.
|
|
162
176
|
* If so, take the older entries that won't fit, combine with existing summary,
|
|
163
|
-
* and queue a summarization task to the coding agent.
|
|
177
|
+
* and queue a summarization task to the coding agent for high-quality compression.
|
|
164
178
|
*/
|
|
165
179
|
maybeQueueSummarization() {
|
|
166
180
|
const splitIdx = findSummarizationSplit(this.conversationHistory, TeammatesREPL.CONV_HISTORY_CHARS);
|
|
@@ -178,6 +192,23 @@ class TeammatesREPL {
|
|
|
178
192
|
});
|
|
179
193
|
this.kickDrain();
|
|
180
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Pre-dispatch compression: if conversation history exceeds the token budget,
|
|
197
|
+
* mechanically compress older entries into bullet summaries BEFORE building the
|
|
198
|
+
* prompt. This ensures the prompt always fits within the target context window,
|
|
199
|
+
* even if the async agent-quality summarization hasn't completed yet.
|
|
200
|
+
*/
|
|
201
|
+
preDispatchCompress() {
|
|
202
|
+
const totalChars = this.conversationHistory.reduce((sum, e) => sum + formatConversationEntry(e.role, e.text).length, 0);
|
|
203
|
+
if (totalChars <= TeammatesREPL.CONV_HISTORY_CHARS)
|
|
204
|
+
return;
|
|
205
|
+
const splitIdx = findSummarizationSplit(this.conversationHistory, TeammatesREPL.CONV_HISTORY_CHARS);
|
|
206
|
+
if (splitIdx === 0)
|
|
207
|
+
return;
|
|
208
|
+
const toCompress = this.conversationHistory.slice(0, splitIdx);
|
|
209
|
+
this.conversationSummary = compressConversationEntries(toCompress, this.conversationSummary);
|
|
210
|
+
this.conversationHistory.splice(0, splitIdx);
|
|
211
|
+
}
|
|
181
212
|
adapterName;
|
|
182
213
|
teammatesDir;
|
|
183
214
|
taskQueue = [];
|
|
@@ -187,6 +218,8 @@ class TeammatesREPL {
|
|
|
187
218
|
systemActive = new Map();
|
|
188
219
|
/** Agents currently in a silent retry — suppress all events. */
|
|
189
220
|
silentAgents = new Set();
|
|
221
|
+
/** Counter for pending migration compression tasks — triggers re-index when it hits 0. */
|
|
222
|
+
pendingMigrationSyncs = 0;
|
|
190
223
|
/** Per-agent drain locks — prevents double-draining a single agent. */
|
|
191
224
|
agentDrainLocks = new Map();
|
|
192
225
|
/** Stored pasted text keyed by paste number, expanded on Enter. */
|
|
@@ -199,6 +232,8 @@ class TeammatesREPL {
|
|
|
199
232
|
ctrlcPending = false; // true after first Ctrl+C, waiting for second
|
|
200
233
|
ctrlcTimer = null;
|
|
201
234
|
lastCleanedOutput = ""; // last teammate output for clipboard copy
|
|
235
|
+
/** Maps copy action IDs to the cleaned output text for that response. */
|
|
236
|
+
_copyContexts = new Map();
|
|
202
237
|
autoApproveHandoffs = false;
|
|
203
238
|
/** Last debug log file path per teammate — for /debug analysis. */
|
|
204
239
|
lastDebugFiles = new Map();
|
|
@@ -208,6 +243,8 @@ class TeammatesREPL {
|
|
|
208
243
|
pendingHandoffs = [];
|
|
209
244
|
/** Pending retro proposals awaiting user approval. */
|
|
210
245
|
pendingRetroProposals = [];
|
|
246
|
+
/** Pending cross-folder violations awaiting user decision. */
|
|
247
|
+
pendingViolations = [];
|
|
211
248
|
/** Maps reply action IDs to their context (teammate + message). */
|
|
212
249
|
_replyContexts = new Map();
|
|
213
250
|
/** Quoted reply text to expand on next submit. */
|
|
@@ -727,6 +764,135 @@ class TeammatesREPL {
|
|
|
727
764
|
return;
|
|
728
765
|
}
|
|
729
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Audit a task result for cross-folder writes.
|
|
769
|
+
* AI teammates must not write to another teammate's folder.
|
|
770
|
+
* Returns violating file paths (relative), or empty array if clean.
|
|
771
|
+
*/
|
|
772
|
+
auditCrossFolderWrites(teammate, changedFiles) {
|
|
773
|
+
// Normalize .teammates/ prefix for comparison
|
|
774
|
+
const tmPrefix = ".teammates/";
|
|
775
|
+
const ownPrefix = `${tmPrefix}${teammate}/`;
|
|
776
|
+
return changedFiles.filter((f) => {
|
|
777
|
+
const normalized = f.replace(/\\/g, "/");
|
|
778
|
+
// Only care about files inside .teammates/
|
|
779
|
+
if (!normalized.startsWith(tmPrefix))
|
|
780
|
+
return false;
|
|
781
|
+
// Own folder is fine
|
|
782
|
+
if (normalized.startsWith(ownPrefix))
|
|
783
|
+
return false;
|
|
784
|
+
// Shared folders (_prefix) are fine
|
|
785
|
+
const subPath = normalized.slice(tmPrefix.length);
|
|
786
|
+
if (subPath.startsWith("_"))
|
|
787
|
+
return false;
|
|
788
|
+
// Ephemeral folders (.prefix) are fine
|
|
789
|
+
if (subPath.startsWith("."))
|
|
790
|
+
return false;
|
|
791
|
+
// Root-level shared files (USER.md, settings.json, CROSS-TEAM.md, etc.)
|
|
792
|
+
if (!subPath.includes("/"))
|
|
793
|
+
return false;
|
|
794
|
+
// Everything else is a violation
|
|
795
|
+
return true;
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Show cross-folder violation warning with [revert] / [allow] actions.
|
|
800
|
+
*/
|
|
801
|
+
showViolationWarning(teammate, violations) {
|
|
802
|
+
const t = theme();
|
|
803
|
+
this.feedLine(tp.warning(` ⚠ @${teammate} wrote to another teammate's folder:`));
|
|
804
|
+
for (const f of violations) {
|
|
805
|
+
this.feedLine(tp.muted(` ${f}`));
|
|
806
|
+
}
|
|
807
|
+
if (this.chatView) {
|
|
808
|
+
const violationId = `violation-${Date.now()}`;
|
|
809
|
+
const actionIdx = this.chatView.feedLineCount;
|
|
810
|
+
this.chatView.appendActionList([
|
|
811
|
+
{
|
|
812
|
+
id: `revert-${violationId}`,
|
|
813
|
+
normalStyle: this.makeSpan({
|
|
814
|
+
text: " [revert]",
|
|
815
|
+
style: { fg: t.error },
|
|
816
|
+
}),
|
|
817
|
+
hoverStyle: this.makeSpan({
|
|
818
|
+
text: " [revert]",
|
|
819
|
+
style: { fg: t.accent },
|
|
820
|
+
}),
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
id: `allow-${violationId}`,
|
|
824
|
+
normalStyle: this.makeSpan({
|
|
825
|
+
text: " [allow]",
|
|
826
|
+
style: { fg: t.textDim },
|
|
827
|
+
}),
|
|
828
|
+
hoverStyle: this.makeSpan({
|
|
829
|
+
text: " [allow]",
|
|
830
|
+
style: { fg: t.accent },
|
|
831
|
+
}),
|
|
832
|
+
},
|
|
833
|
+
]);
|
|
834
|
+
this.pendingViolations.push({
|
|
835
|
+
id: violationId,
|
|
836
|
+
teammate,
|
|
837
|
+
files: violations,
|
|
838
|
+
actionIdx,
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Handle revert/allow actions for cross-folder violations.
|
|
844
|
+
*/
|
|
845
|
+
handleViolationAction(actionId) {
|
|
846
|
+
const revertMatch = actionId.match(/^revert-(violation-.+)$/);
|
|
847
|
+
if (revertMatch) {
|
|
848
|
+
const vId = revertMatch[1];
|
|
849
|
+
const idx = this.pendingViolations.findIndex((v) => v.id === vId);
|
|
850
|
+
if (idx >= 0 && this.chatView) {
|
|
851
|
+
const v = this.pendingViolations.splice(idx, 1)[0];
|
|
852
|
+
// Revert violating files via git checkout
|
|
853
|
+
for (const f of v.files) {
|
|
854
|
+
try {
|
|
855
|
+
execSync(`git checkout -- "${f}"`, {
|
|
856
|
+
cwd: resolve(this.teammatesDir, ".."),
|
|
857
|
+
stdio: "pipe",
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
catch {
|
|
861
|
+
// File might be untracked — try git rm
|
|
862
|
+
try {
|
|
863
|
+
execSync(`git rm -f "${f}"`, {
|
|
864
|
+
cwd: resolve(this.teammatesDir, ".."),
|
|
865
|
+
stdio: "pipe",
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
catch {
|
|
869
|
+
// Best effort — file may already be clean
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
this.chatView.updateFeedLine(v.actionIdx, this.makeSpan({
|
|
874
|
+
text: ` reverted ${v.files.length} file(s)`,
|
|
875
|
+
style: { fg: theme().success },
|
|
876
|
+
}));
|
|
877
|
+
this.refreshView();
|
|
878
|
+
}
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const allowMatch = actionId.match(/^allow-(violation-.+)$/);
|
|
882
|
+
if (allowMatch) {
|
|
883
|
+
const vId = allowMatch[1];
|
|
884
|
+
const idx = this.pendingViolations.findIndex((v) => v.id === vId);
|
|
885
|
+
if (idx >= 0 && this.chatView) {
|
|
886
|
+
const v = this.pendingViolations.splice(idx, 1)[0];
|
|
887
|
+
this.chatView.updateFeedLine(v.actionIdx, this.makeSpan({
|
|
888
|
+
text: " allowed",
|
|
889
|
+
style: { fg: theme().textDim },
|
|
890
|
+
}));
|
|
891
|
+
this.refreshView();
|
|
892
|
+
}
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
730
896
|
/** Handle bulk handoff actions. */
|
|
731
897
|
handleBulkHandoff(action) {
|
|
732
898
|
if (!this.chatView)
|
|
@@ -976,8 +1142,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
976
1142
|
if (everyoneMatch) {
|
|
977
1143
|
const task = everyoneMatch[1];
|
|
978
1144
|
const names = allNames.filter((n) => n !== this.selfName && n !== this.adapterName);
|
|
1145
|
+
// Atomic snapshot: freeze conversation state ONCE so all agents see
|
|
1146
|
+
// the same context regardless of concurrent preDispatchCompress mutations.
|
|
1147
|
+
const contextSnapshot = {
|
|
1148
|
+
history: this.conversationHistory.map((e) => ({ ...e })),
|
|
1149
|
+
summary: this.conversationSummary,
|
|
1150
|
+
};
|
|
979
1151
|
for (const teammate of names) {
|
|
980
|
-
this.taskQueue.push({ type: "agent", teammate, task });
|
|
1152
|
+
this.taskQueue.push({ type: "agent", teammate, task, contextSnapshot });
|
|
981
1153
|
}
|
|
982
1154
|
const bg = this._userBg;
|
|
983
1155
|
const t = theme();
|
|
@@ -1113,6 +1285,22 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
1113
1285
|
}
|
|
1114
1286
|
finally {
|
|
1115
1287
|
this.systemActive.delete(taskId);
|
|
1288
|
+
// Migration tasks: decrement counter and re-index when all are done
|
|
1289
|
+
if (entry.type === "agent" && entry.migration) {
|
|
1290
|
+
this.pendingMigrationSyncs--;
|
|
1291
|
+
if (this.pendingMigrationSyncs <= 0) {
|
|
1292
|
+
try {
|
|
1293
|
+
await syncRecallIndex(this.teammatesDir);
|
|
1294
|
+
this.feedLine(tp.success(" ✔ v0.6.0 migration complete — indexes rebuilt"));
|
|
1295
|
+
this.refreshView();
|
|
1296
|
+
}
|
|
1297
|
+
catch {
|
|
1298
|
+
/* re-index failed — non-fatal, next startup will retry */
|
|
1299
|
+
}
|
|
1300
|
+
// Persist version LAST — only after all migration tasks finish
|
|
1301
|
+
this.commitVersionUpdate();
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1116
1304
|
}
|
|
1117
1305
|
}
|
|
1118
1306
|
// ─── Onboarding ───────────────────────────────────────────────────
|
|
@@ -1952,9 +2140,51 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
1952
2140
|
compact: new Set([0]),
|
|
1953
2141
|
debug: new Set([0]),
|
|
1954
2142
|
retro: new Set([0]),
|
|
2143
|
+
interrupt: new Set([0]),
|
|
2144
|
+
int: new Set([0]),
|
|
1955
2145
|
};
|
|
1956
2146
|
/** Build param-completion items for the current line, if any. */
|
|
1957
2147
|
getParamItems(cmdName, argsBefore, partial) {
|
|
2148
|
+
// Script subcommand + name completion for /script
|
|
2149
|
+
if (cmdName === "script") {
|
|
2150
|
+
const completedArgs = argsBefore.trim()
|
|
2151
|
+
? argsBefore.trim().split(/\s+/).length
|
|
2152
|
+
: 0;
|
|
2153
|
+
const lower = partial.toLowerCase();
|
|
2154
|
+
if (completedArgs === 0) {
|
|
2155
|
+
// First arg — suggest subcommands
|
|
2156
|
+
const subs = [
|
|
2157
|
+
{ name: "list", desc: "List saved scripts" },
|
|
2158
|
+
{ name: "run", desc: "Run an existing script" },
|
|
2159
|
+
];
|
|
2160
|
+
return subs
|
|
2161
|
+
.filter((s) => s.name.startsWith(lower))
|
|
2162
|
+
.map((s) => ({
|
|
2163
|
+
label: s.name,
|
|
2164
|
+
description: s.desc,
|
|
2165
|
+
completion: `/script ${s.name} `,
|
|
2166
|
+
}));
|
|
2167
|
+
}
|
|
2168
|
+
if (completedArgs === 1 && argsBefore.trim() === "run") {
|
|
2169
|
+
// Second arg after "run" — suggest script filenames
|
|
2170
|
+
const scriptsDir = join(this.teammatesDir, this.selfName, "scripts");
|
|
2171
|
+
let files = [];
|
|
2172
|
+
try {
|
|
2173
|
+
files = readdirSync(scriptsDir).filter((f) => !f.startsWith("."));
|
|
2174
|
+
}
|
|
2175
|
+
catch {
|
|
2176
|
+
// directory doesn't exist yet
|
|
2177
|
+
}
|
|
2178
|
+
return files
|
|
2179
|
+
.filter((f) => f.toLowerCase().startsWith(lower))
|
|
2180
|
+
.map((f) => ({
|
|
2181
|
+
label: f,
|
|
2182
|
+
description: "saved script",
|
|
2183
|
+
completion: `/script run ${f}`,
|
|
2184
|
+
}));
|
|
2185
|
+
}
|
|
2186
|
+
return [];
|
|
2187
|
+
}
|
|
1958
2188
|
// Service name completion for /configure
|
|
1959
2189
|
if (cmdName === "configure" || cmdName === "config") {
|
|
1960
2190
|
const completedArgs = argsBefore.trim()
|
|
@@ -2543,13 +2773,17 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
2543
2773
|
if (id.startsWith("copy-cmd:")) {
|
|
2544
2774
|
this.doCopy(id.slice("copy-cmd:".length));
|
|
2545
2775
|
}
|
|
2546
|
-
else if (id
|
|
2547
|
-
this.
|
|
2776
|
+
else if (id.startsWith("copy-")) {
|
|
2777
|
+
const text = this._copyContexts.get(id);
|
|
2778
|
+
this.doCopy(text || this.lastCleanedOutput || undefined);
|
|
2548
2779
|
}
|
|
2549
2780
|
else if (id.startsWith("retro-approve-") ||
|
|
2550
2781
|
id.startsWith("retro-reject-")) {
|
|
2551
2782
|
this.handleRetroAction(id);
|
|
2552
2783
|
}
|
|
2784
|
+
else if (id.startsWith("revert-") || id.startsWith("allow-")) {
|
|
2785
|
+
this.handleViolationAction(id);
|
|
2786
|
+
}
|
|
2553
2787
|
else if (id.startsWith("approve-") || id.startsWith("reject-")) {
|
|
2554
2788
|
this.handleHandoffAction(id);
|
|
2555
2789
|
}
|
|
@@ -3046,6 +3280,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3046
3280
|
description: "Cancel a queued task by number",
|
|
3047
3281
|
run: (args) => this.cmdCancel(args),
|
|
3048
3282
|
},
|
|
3283
|
+
{
|
|
3284
|
+
name: "interrupt",
|
|
3285
|
+
aliases: ["int"],
|
|
3286
|
+
usage: "/interrupt [teammate] [message]",
|
|
3287
|
+
description: "Interrupt a running agent and resume with a steering message",
|
|
3288
|
+
run: (args) => this.cmdInterrupt(args),
|
|
3289
|
+
},
|
|
3049
3290
|
{
|
|
3050
3291
|
name: "init",
|
|
3051
3292
|
aliases: ["onboard", "setup"],
|
|
@@ -3095,6 +3336,13 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3095
3336
|
description: "Ask a quick side question without interrupting the main conversation",
|
|
3096
3337
|
run: (args) => this.cmdBtw(args),
|
|
3097
3338
|
},
|
|
3339
|
+
{
|
|
3340
|
+
name: "script",
|
|
3341
|
+
aliases: [],
|
|
3342
|
+
usage: "/script [list | run <name> | what should the script do?]",
|
|
3343
|
+
description: "Write and run reusable scripts via the coding agent",
|
|
3344
|
+
run: (args) => this.cmdScript(args),
|
|
3345
|
+
},
|
|
3098
3346
|
{
|
|
3099
3347
|
name: "theme",
|
|
3100
3348
|
aliases: [],
|
|
@@ -3365,6 +3613,114 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3365
3613
|
this.feedLine(concat(tp.muted(" Cancelled: "), tp.accent(`@${cancelDisplay}`), tp.muted(" — "), tp.text(removed.task.slice(0, 60))));
|
|
3366
3614
|
this.refreshView();
|
|
3367
3615
|
}
|
|
3616
|
+
/**
|
|
3617
|
+
* /interrupt [teammate] [message] — Kill a running agent and resume with context.
|
|
3618
|
+
*/
|
|
3619
|
+
async cmdInterrupt(argsStr) {
|
|
3620
|
+
const parts = argsStr.trim().split(/\s+/);
|
|
3621
|
+
const teammateName = parts[0]?.replace(/^@/, "").toLowerCase();
|
|
3622
|
+
const steeringMessage = parts.slice(1).join(" ").trim() ||
|
|
3623
|
+
"Wrap up your current work and report what you've done so far.";
|
|
3624
|
+
if (!teammateName) {
|
|
3625
|
+
this.feedLine(tp.warning(" Usage: /interrupt [teammate] [message]"));
|
|
3626
|
+
this.refreshView();
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
// Resolve display name → internal name
|
|
3630
|
+
const resolvedName = teammateName === this.adapterName ? this.selfName : teammateName;
|
|
3631
|
+
// Check if the teammate has an active task
|
|
3632
|
+
const activeEntry = this.agentActive.get(resolvedName);
|
|
3633
|
+
if (!activeEntry) {
|
|
3634
|
+
this.feedLine(tp.warning(` @${teammateName} has no active task to interrupt.`));
|
|
3635
|
+
this.refreshView();
|
|
3636
|
+
return;
|
|
3637
|
+
}
|
|
3638
|
+
// Check if the adapter supports killing
|
|
3639
|
+
const adapter = this.orchestrator.getAdapter();
|
|
3640
|
+
if (!adapter?.killAgent) {
|
|
3641
|
+
this.feedLine(tp.warning(" This adapter does not support interruption."));
|
|
3642
|
+
this.refreshView();
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
// Show interruption status
|
|
3646
|
+
const displayName = resolvedName === this.selfName ? this.adapterName : resolvedName;
|
|
3647
|
+
this.feedLine(concat(tp.warning(" ⚡ Interrupting "), tp.accent(`@${displayName}`), tp.warning("...")));
|
|
3648
|
+
this.refreshView();
|
|
3649
|
+
try {
|
|
3650
|
+
// Kill the agent process and capture its output
|
|
3651
|
+
const spawnResult = await adapter.killAgent(resolvedName);
|
|
3652
|
+
if (!spawnResult) {
|
|
3653
|
+
this.feedLine(tp.warning(` @${displayName} process already exited.`));
|
|
3654
|
+
this.refreshView();
|
|
3655
|
+
return;
|
|
3656
|
+
}
|
|
3657
|
+
// Get the original full prompt for this agent
|
|
3658
|
+
const _originalFullPrompt = this.lastTaskPrompts.get(resolvedName) ?? "";
|
|
3659
|
+
const originalTask = activeEntry.task;
|
|
3660
|
+
// Parse the conversation log from available sources
|
|
3661
|
+
const presetName = adapter.name ?? "unknown";
|
|
3662
|
+
const { log, toolCallCount, filesChanged } = buildConversationLog(spawnResult.debugFile, spawnResult.stdout, presetName);
|
|
3663
|
+
// Build the resume prompt
|
|
3664
|
+
const resumePrompt = this.buildResumePrompt(originalTask, log, steeringMessage, toolCallCount, filesChanged);
|
|
3665
|
+
// Report what happened
|
|
3666
|
+
const elapsed = this.activeTasks.get(resolvedName)?.startTime
|
|
3667
|
+
? `${((Date.now() - this.activeTasks.get(resolvedName).startTime) / 1000).toFixed(0)}s`
|
|
3668
|
+
: "unknown";
|
|
3669
|
+
this.feedLine(concat(tp.success(" ⚡ Interrupted "), tp.accent(`@${displayName}`), tp.muted(` (${elapsed}, ${toolCallCount} tool calls, ${filesChanged.length} files changed)`)));
|
|
3670
|
+
this.feedLine(concat(tp.muted(" Resuming with: "), tp.text(steeringMessage.slice(0, 70))));
|
|
3671
|
+
this.refreshView();
|
|
3672
|
+
// Clean up the active task state — the drainAgentQueue loop will see
|
|
3673
|
+
// the agent as inactive and the queue entry was already removed
|
|
3674
|
+
this.activeTasks.delete(resolvedName);
|
|
3675
|
+
this.agentActive.delete(resolvedName);
|
|
3676
|
+
if (this.activeTasks.size === 0)
|
|
3677
|
+
this.stopStatusAnimation();
|
|
3678
|
+
// Queue the resumed task
|
|
3679
|
+
this.taskQueue.push({
|
|
3680
|
+
type: "agent",
|
|
3681
|
+
teammate: resolvedName,
|
|
3682
|
+
task: resumePrompt,
|
|
3683
|
+
});
|
|
3684
|
+
this.kickDrain();
|
|
3685
|
+
}
|
|
3686
|
+
catch (err) {
|
|
3687
|
+
this.feedLine(tp.error(` ✖ Failed to interrupt @${displayName}: ${err?.message ?? String(err)}`));
|
|
3688
|
+
this.refreshView();
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
/**
|
|
3692
|
+
* Build a resume prompt from the original task, conversation log, and steering message.
|
|
3693
|
+
*/
|
|
3694
|
+
buildResumePrompt(originalTask, conversationLog, steeringMessage, toolCallCount, filesChanged) {
|
|
3695
|
+
const parts = [];
|
|
3696
|
+
parts.push("<RESUME_CONTEXT>");
|
|
3697
|
+
parts.push("This is a resumed task. You were previously working on this task but were interrupted.");
|
|
3698
|
+
parts.push("Below is the log of what you accomplished before the interruption.");
|
|
3699
|
+
parts.push("");
|
|
3700
|
+
parts.push("DO NOT repeat work that is already done. Check the filesystem for files you already wrote.");
|
|
3701
|
+
parts.push("Continue from where you left off.");
|
|
3702
|
+
parts.push("");
|
|
3703
|
+
parts.push("## What You Did Before Interruption");
|
|
3704
|
+
parts.push("");
|
|
3705
|
+
parts.push(`Tool calls: ${toolCallCount}`);
|
|
3706
|
+
if (filesChanged.length > 0) {
|
|
3707
|
+
parts.push(`Files changed: ${filesChanged.slice(0, 20).join(", ")}${filesChanged.length > 20 ? ` (+${filesChanged.length - 20} more)` : ""}`);
|
|
3708
|
+
}
|
|
3709
|
+
parts.push("");
|
|
3710
|
+
parts.push(conversationLog);
|
|
3711
|
+
parts.push("");
|
|
3712
|
+
parts.push("## Interruption");
|
|
3713
|
+
parts.push("");
|
|
3714
|
+
parts.push(steeringMessage);
|
|
3715
|
+
parts.push("");
|
|
3716
|
+
parts.push("## Your Task Now");
|
|
3717
|
+
parts.push("");
|
|
3718
|
+
parts.push("Continue the original task from where you left off. The original task was:");
|
|
3719
|
+
parts.push("");
|
|
3720
|
+
parts.push(originalTask);
|
|
3721
|
+
parts.push("</RESUME_CONTEXT>");
|
|
3722
|
+
return parts.join("\n");
|
|
3723
|
+
}
|
|
3368
3724
|
/** Drain user tasks for a single agent — runs in parallel with other agents.
|
|
3369
3725
|
* System tasks are handled separately by runSystemTask(). */
|
|
3370
3726
|
async drainAgentQueue(agent) {
|
|
@@ -3378,9 +3734,16 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3378
3734
|
try {
|
|
3379
3735
|
{
|
|
3380
3736
|
// btw and debug tasks skip conversation context (not part of main thread)
|
|
3381
|
-
const
|
|
3382
|
-
|
|
3383
|
-
|
|
3737
|
+
const isMainThread = entry.type !== "btw" && entry.type !== "debug";
|
|
3738
|
+
// Snapshot-aware context building: if the entry has a frozen snapshot
|
|
3739
|
+
// (@everyone), use it directly — no mutation of shared state.
|
|
3740
|
+
// Otherwise, compress live state as before.
|
|
3741
|
+
const snapshot = entry.type === "agent" ? entry.contextSnapshot : undefined;
|
|
3742
|
+
if (isMainThread && !snapshot)
|
|
3743
|
+
this.preDispatchCompress();
|
|
3744
|
+
const extraContext = isMainThread
|
|
3745
|
+
? this.buildConversationContext(entry.teammate, snapshot)
|
|
3746
|
+
: "";
|
|
3384
3747
|
let result = await this.orchestrator.assign({
|
|
3385
3748
|
teammate: entry.teammate,
|
|
3386
3749
|
task: entry.task,
|
|
@@ -3429,6 +3792,14 @@ Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily l
|
|
|
3429
3792
|
}
|
|
3430
3793
|
// Display the (possibly retried) result to the user
|
|
3431
3794
|
this.displayTaskResult(result, entry.type);
|
|
3795
|
+
// Audit cross-folder writes for AI teammates
|
|
3796
|
+
const tmConfig = this.orchestrator.getRegistry().get(entry.teammate);
|
|
3797
|
+
if (tmConfig?.type === "ai" && result.changedFiles.length > 0) {
|
|
3798
|
+
const violations = this.auditCrossFolderWrites(entry.teammate, result.changedFiles);
|
|
3799
|
+
if (violations.length > 0) {
|
|
3800
|
+
this.showViolationWarning(entry.teammate, violations);
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3432
3803
|
// Write debug entry — skip for debug analysis tasks (avoid recursion)
|
|
3433
3804
|
if (entry.type !== "debug") {
|
|
3434
3805
|
this.writeDebugEntry(entry.teammate, entry.task, result, startTime);
|
|
@@ -3933,7 +4304,21 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
3933
4304
|
}
|
|
3934
4305
|
}
|
|
3935
4306
|
}
|
|
4307
|
+
/** Compare two semver strings. Returns true if `a` is less than `b`. */
|
|
4308
|
+
static semverLessThan(a, b) {
|
|
4309
|
+
const pa = a.split(".").map(Number);
|
|
4310
|
+
const pb = b.split(".").map(Number);
|
|
4311
|
+
for (let i = 0; i < 3; i++) {
|
|
4312
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0))
|
|
4313
|
+
return true;
|
|
4314
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0))
|
|
4315
|
+
return false;
|
|
4316
|
+
}
|
|
4317
|
+
return false;
|
|
4318
|
+
}
|
|
3936
4319
|
async startupMaintenance() {
|
|
4320
|
+
// Check and update installed CLI version
|
|
4321
|
+
const versionUpdate = this.checkVersionUpdate();
|
|
3937
4322
|
const tmpDir = join(this.teammatesDir, ".tmp");
|
|
3938
4323
|
// Clean up debug log files older than 1 day
|
|
3939
4324
|
const debugDir = join(tmpDir, "debug");
|
|
@@ -3960,7 +4345,64 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
3960
4345
|
for (const name of teammates) {
|
|
3961
4346
|
await this.runCompact(name, true);
|
|
3962
4347
|
}
|
|
3963
|
-
// 2.
|
|
4348
|
+
// 2. Compress previous day's log for each teammate (queued as system tasks)
|
|
4349
|
+
for (const name of teammates) {
|
|
4350
|
+
try {
|
|
4351
|
+
const compression = await buildDailyCompressionPrompt(join(this.teammatesDir, name));
|
|
4352
|
+
if (compression) {
|
|
4353
|
+
this.taskQueue.push({
|
|
4354
|
+
type: "agent",
|
|
4355
|
+
teammate: name,
|
|
4356
|
+
task: compression.prompt,
|
|
4357
|
+
system: true,
|
|
4358
|
+
});
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
catch {
|
|
4362
|
+
/* compression check failed — non-fatal */
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
// 2b. v0.6.0 migration — compress ALL uncompressed daily logs + re-index
|
|
4366
|
+
const needsMigration = versionUpdate &&
|
|
4367
|
+
(versionUpdate.previous === "" ||
|
|
4368
|
+
TeammatesREPL.semverLessThan(versionUpdate.previous, "0.6.0"));
|
|
4369
|
+
if (needsMigration) {
|
|
4370
|
+
this.feedLine(tp.accent(" ℹ Migrating to v0.6.0 — compressing daily logs..."));
|
|
4371
|
+
this.refreshView();
|
|
4372
|
+
let migrationCount = 0;
|
|
4373
|
+
for (const name of teammates) {
|
|
4374
|
+
try {
|
|
4375
|
+
const uncompressed = await findUncompressedDailies(join(this.teammatesDir, name));
|
|
4376
|
+
if (uncompressed.length === 0)
|
|
4377
|
+
continue;
|
|
4378
|
+
const prompt = await buildMigrationCompressionPrompt(join(this.teammatesDir, name), name, uncompressed);
|
|
4379
|
+
if (prompt) {
|
|
4380
|
+
migrationCount++;
|
|
4381
|
+
this.taskQueue.push({
|
|
4382
|
+
type: "agent",
|
|
4383
|
+
teammate: name,
|
|
4384
|
+
task: prompt,
|
|
4385
|
+
system: true,
|
|
4386
|
+
migration: true,
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
catch {
|
|
4391
|
+
/* migration compression failed — non-fatal */
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
this.pendingMigrationSyncs = migrationCount;
|
|
4395
|
+
// If no migration tasks were actually queued, commit version now
|
|
4396
|
+
if (migrationCount === 0) {
|
|
4397
|
+
this.commitVersionUpdate();
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
else if (versionUpdate) {
|
|
4401
|
+
// No migration needed — commit the version update immediately
|
|
4402
|
+
this.commitVersionUpdate();
|
|
4403
|
+
}
|
|
4404
|
+
this.kickDrain();
|
|
4405
|
+
// 3. Purge daily logs older than 30 days (disk + Vectra)
|
|
3964
4406
|
const { Indexer } = await import("@teammates/recall");
|
|
3965
4407
|
const indexer = new Indexer({ teammatesDir: this.teammatesDir });
|
|
3966
4408
|
for (const name of teammates) {
|
|
@@ -3975,7 +4417,7 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
3975
4417
|
/* purge failed — non-fatal */
|
|
3976
4418
|
}
|
|
3977
4419
|
}
|
|
3978
|
-
//
|
|
4420
|
+
// 4. Sync recall indexes (bundled library call)
|
|
3979
4421
|
try {
|
|
3980
4422
|
await syncRecallIndex(this.teammatesDir);
|
|
3981
4423
|
}
|
|
@@ -3983,6 +4425,60 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
3983
4425
|
/* sync failed — non-fatal */
|
|
3984
4426
|
}
|
|
3985
4427
|
}
|
|
4428
|
+
/**
|
|
4429
|
+
* Check if the CLI version has changed since last run.
|
|
4430
|
+
* Does NOT update settings.json — call `commitVersionUpdate()` after
|
|
4431
|
+
* migration tasks are complete to persist the new version.
|
|
4432
|
+
*/
|
|
4433
|
+
checkVersionUpdate() {
|
|
4434
|
+
const settingsPath = join(this.teammatesDir, "settings.json");
|
|
4435
|
+
let settings = {};
|
|
4436
|
+
try {
|
|
4437
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
4438
|
+
}
|
|
4439
|
+
catch {
|
|
4440
|
+
// No settings file or invalid JSON
|
|
4441
|
+
}
|
|
4442
|
+
const previous = settings.cliVersion ?? "";
|
|
4443
|
+
const current = PKG_VERSION;
|
|
4444
|
+
if (previous === current)
|
|
4445
|
+
return null;
|
|
4446
|
+
return { previous, current };
|
|
4447
|
+
}
|
|
4448
|
+
/**
|
|
4449
|
+
* Persist the current CLI version to settings.json.
|
|
4450
|
+
* Called after all migration tasks complete (or immediately if no migration needed).
|
|
4451
|
+
*/
|
|
4452
|
+
commitVersionUpdate() {
|
|
4453
|
+
const settingsPath = join(this.teammatesDir, "settings.json");
|
|
4454
|
+
let settings = {};
|
|
4455
|
+
try {
|
|
4456
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
4457
|
+
}
|
|
4458
|
+
catch {
|
|
4459
|
+
// No settings file or invalid JSON — create one
|
|
4460
|
+
}
|
|
4461
|
+
const previous = settings.cliVersion ?? "";
|
|
4462
|
+
const current = PKG_VERSION;
|
|
4463
|
+
settings.cliVersion = current;
|
|
4464
|
+
if (!settings.version)
|
|
4465
|
+
settings.version = 1;
|
|
4466
|
+
try {
|
|
4467
|
+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
|
|
4468
|
+
}
|
|
4469
|
+
catch {
|
|
4470
|
+
/* write failed — non-fatal */
|
|
4471
|
+
}
|
|
4472
|
+
// Detect major/minor version change (not just patch)
|
|
4473
|
+
const [prevMajor, prevMinor] = previous.split(".").map(Number);
|
|
4474
|
+
const [curMajor, curMinor] = current.split(".").map(Number);
|
|
4475
|
+
const isMajorMinor = previous !== "" && (prevMajor !== curMajor || prevMinor !== curMinor);
|
|
4476
|
+
if (isMajorMinor) {
|
|
4477
|
+
this.feedLine(tp.accent(` ✔ Updated from v${previous} → v${current}`));
|
|
4478
|
+
this.feedLine();
|
|
4479
|
+
this.refreshView();
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
3986
4482
|
async cmdCopy() {
|
|
3987
4483
|
this.doCopy(); // copies entire session
|
|
3988
4484
|
}
|
|
@@ -4136,6 +4632,116 @@ Issues that can't be resolved unilaterally — they need input from other teamma
|
|
|
4136
4632
|
this.refreshView();
|
|
4137
4633
|
this.kickDrain();
|
|
4138
4634
|
}
|
|
4635
|
+
async cmdScript(argsStr) {
|
|
4636
|
+
const args = argsStr.trim();
|
|
4637
|
+
const scriptsDir = join(this.teammatesDir, this.selfName, "scripts");
|
|
4638
|
+
// /script (no args) — show usage
|
|
4639
|
+
if (!args) {
|
|
4640
|
+
this.feedLine();
|
|
4641
|
+
this.feedLine(tp.bold(" /script — write and run reusable scripts"));
|
|
4642
|
+
this.feedLine(tp.muted(` ${"─".repeat(50)}`));
|
|
4643
|
+
this.feedLine(concat(tp.accent(" /script list".padEnd(36)), tp.text("List saved scripts")));
|
|
4644
|
+
this.feedLine(concat(tp.accent(" /script run <name>".padEnd(36)), tp.text("Run an existing script")));
|
|
4645
|
+
this.feedLine(concat(tp.accent(" /script <description>".padEnd(36)), tp.text("Create and run a new script")));
|
|
4646
|
+
this.feedLine();
|
|
4647
|
+
this.feedLine(tp.muted(` Scripts are saved to ${scriptsDir}`));
|
|
4648
|
+
this.feedLine();
|
|
4649
|
+
this.refreshView();
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4652
|
+
// /script list — list saved scripts
|
|
4653
|
+
if (args === "list") {
|
|
4654
|
+
let files = [];
|
|
4655
|
+
try {
|
|
4656
|
+
files = readdirSync(scriptsDir).filter((f) => !f.startsWith("."));
|
|
4657
|
+
}
|
|
4658
|
+
catch {
|
|
4659
|
+
// directory doesn't exist yet
|
|
4660
|
+
}
|
|
4661
|
+
this.feedLine();
|
|
4662
|
+
if (files.length === 0) {
|
|
4663
|
+
this.feedLine(tp.muted(" No scripts saved yet."));
|
|
4664
|
+
this.feedLine(tp.muted(" Use /script <description> to create one."));
|
|
4665
|
+
}
|
|
4666
|
+
else {
|
|
4667
|
+
this.feedLine(tp.bold(" Saved scripts"));
|
|
4668
|
+
this.feedLine(tp.muted(` ${"─".repeat(50)}`));
|
|
4669
|
+
for (const f of files) {
|
|
4670
|
+
this.feedLine(concat(tp.accent(` ${f}`)));
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
this.feedLine();
|
|
4674
|
+
this.refreshView();
|
|
4675
|
+
return;
|
|
4676
|
+
}
|
|
4677
|
+
// /script run <name> — run an existing script
|
|
4678
|
+
if (args.startsWith("run ")) {
|
|
4679
|
+
const name = args.slice(4).trim();
|
|
4680
|
+
if (!name) {
|
|
4681
|
+
this.feedLine(tp.muted(" Usage: /script run <name>"));
|
|
4682
|
+
this.refreshView();
|
|
4683
|
+
return;
|
|
4684
|
+
}
|
|
4685
|
+
// Find the script file (try exact match, then with common extensions)
|
|
4686
|
+
const candidates = [
|
|
4687
|
+
name,
|
|
4688
|
+
`${name}.sh`,
|
|
4689
|
+
`${name}.ts`,
|
|
4690
|
+
`${name}.js`,
|
|
4691
|
+
`${name}.ps1`,
|
|
4692
|
+
`${name}.py`,
|
|
4693
|
+
];
|
|
4694
|
+
let scriptPath = null;
|
|
4695
|
+
for (const c of candidates) {
|
|
4696
|
+
const p = join(scriptsDir, c);
|
|
4697
|
+
if (existsSync(p)) {
|
|
4698
|
+
scriptPath = p;
|
|
4699
|
+
break;
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
if (!scriptPath) {
|
|
4703
|
+
this.feedLine(tp.warning(` Script not found: ${name}`));
|
|
4704
|
+
this.feedLine(tp.muted(" Use /script list to see available scripts."));
|
|
4705
|
+
this.refreshView();
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
const scriptContent = readFileSync(scriptPath, "utf-8");
|
|
4709
|
+
const task = `Run the following script located at ${scriptPath}:\n\n\`\`\`\n${scriptContent}\n\`\`\`\n\nExecute it and report the results. If it fails, diagnose the issue and fix it.`;
|
|
4710
|
+
this.taskQueue.push({
|
|
4711
|
+
type: "script",
|
|
4712
|
+
teammate: this.selfName,
|
|
4713
|
+
task,
|
|
4714
|
+
});
|
|
4715
|
+
this.feedLine(concat(tp.muted(" Running script "), tp.accent(basename(scriptPath)), tp.muted(" → "), tp.accent(`@${this.adapterName}`)));
|
|
4716
|
+
this.feedLine();
|
|
4717
|
+
this.refreshView();
|
|
4718
|
+
this.kickDrain();
|
|
4719
|
+
return;
|
|
4720
|
+
}
|
|
4721
|
+
// /script <description> — create and run a new script
|
|
4722
|
+
const task = [
|
|
4723
|
+
`The user wants a reusable script. Their request:`,
|
|
4724
|
+
``,
|
|
4725
|
+
args,
|
|
4726
|
+
``,
|
|
4727
|
+
`Instructions:`,
|
|
4728
|
+
`1. Write the script and save it to the scripts directory: ${scriptsDir}`,
|
|
4729
|
+
`2. Create the directory if it doesn't exist.`,
|
|
4730
|
+
`3. Choose a short, descriptive filename (kebab-case, with appropriate extension like .sh, .ts, .js, .py, .ps1).`,
|
|
4731
|
+
`4. Make the script executable if applicable.`,
|
|
4732
|
+
`5. Run the script and report the results.`,
|
|
4733
|
+
`6. If the script needs to be parameterized, use command-line arguments.`,
|
|
4734
|
+
].join("\n");
|
|
4735
|
+
this.taskQueue.push({
|
|
4736
|
+
type: "script",
|
|
4737
|
+
teammate: this.selfName,
|
|
4738
|
+
task,
|
|
4739
|
+
});
|
|
4740
|
+
this.feedLine(concat(tp.muted(" Script task → "), tp.accent(`@${this.adapterName}`)));
|
|
4741
|
+
this.feedLine();
|
|
4742
|
+
this.refreshView();
|
|
4743
|
+
this.kickDrain();
|
|
4744
|
+
}
|
|
4139
4745
|
async cmdTheme() {
|
|
4140
4746
|
const t = theme();
|
|
4141
4747
|
this.feedLine();
|