refactor: tidy directive parsing + queue status
This commit is contained in:
@@ -43,7 +43,7 @@ Text + native (when enabled):
|
|||||||
- `/reasoning on|off|stream` (alias: `/reason`; `stream` = Telegram draft only)
|
- `/reasoning on|off|stream` (alias: `/reason`; `stream` = Telegram draft only)
|
||||||
- `/elevated on|off` (alias: `/elev`)
|
- `/elevated on|off` (alias: `/elev`)
|
||||||
- `/model <name>` (or `/<alias>` from `agent.models.*.alias`)
|
- `/model <name>` (or `/<alias>` from `agent.models.*.alias`)
|
||||||
- `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`)
|
- `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`; send `/queue` to see current settings)
|
||||||
|
|
||||||
Text-only:
|
Text-only:
|
||||||
- `/compact [instructions]` (see [/concepts/compaction](/concepts/compaction))
|
- `/compact [instructions]` (see [/concepts/compaction](/concepts/compaction))
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { normalizeCommandBody } from "./commands-registry.js";
|
||||||
|
|
||||||
export type GroupActivationMode = "mention" | "always";
|
export type GroupActivationMode = "mention" | "always";
|
||||||
|
|
||||||
export function normalizeGroupActivation(
|
export function normalizeGroupActivation(
|
||||||
@@ -16,11 +18,9 @@ export function parseActivationCommand(raw?: string): {
|
|||||||
if (!raw) return { hasCommand: false };
|
if (!raw) return { hasCommand: false };
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return { hasCommand: false };
|
if (!trimmed) return { hasCommand: false };
|
||||||
const match = trimmed.match(
|
const normalized = normalizeCommandBody(trimmed);
|
||||||
/^\/activation(?:\s*:\s*([a-zA-Z]+)?\s*|\s+([a-zA-Z]+)\s*)?$/i,
|
const match = normalized.match(/^\/activation(?:\s+([a-zA-Z]+))?\s*$/i);
|
||||||
);
|
|
||||||
if (!match) return { hasCommand: false };
|
if (!match) return { hasCommand: false };
|
||||||
const token = match[1] ?? match[2];
|
const mode = normalizeGroupActivation(match[1]);
|
||||||
const mode = normalizeGroupActivation(token);
|
|
||||||
return { hasCommand: true, mode };
|
return { hasCommand: true, mode };
|
||||||
}
|
}
|
||||||
|
|||||||
154
src/auto-reply/reply.directive.parse.test.ts
Normal file
154
src/auto-reply/reply.directive.parse.test.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
extractElevatedDirective,
|
||||||
|
extractQueueDirective,
|
||||||
|
extractReasoningDirective,
|
||||||
|
extractReplyToTag,
|
||||||
|
extractThinkDirective,
|
||||||
|
extractVerboseDirective,
|
||||||
|
} from "./reply.js";
|
||||||
|
|
||||||
|
describe("directive parsing", () => {
|
||||||
|
it("ignores verbose directive inside URL", () => {
|
||||||
|
const body = "https://x.com/verioussmith/status/1997066835133669687";
|
||||||
|
const res = extractVerboseDirective(body);
|
||||||
|
expect(res.hasDirective).toBe(false);
|
||||||
|
expect(res.cleaned).toBe(body);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores typoed /verioussmith", () => {
|
||||||
|
const body = "/verioussmith";
|
||||||
|
const res = extractVerboseDirective(body);
|
||||||
|
expect(res.hasDirective).toBe(false);
|
||||||
|
expect(res.cleaned).toBe(body.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores think directive inside URL", () => {
|
||||||
|
const body = "see https://example.com/path/thinkstuff";
|
||||||
|
const res = extractThinkDirective(body);
|
||||||
|
expect(res.hasDirective).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches verbose with leading space", () => {
|
||||||
|
const res = extractVerboseDirective(" please /verbose on now");
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.verboseLevel).toBe("on");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches reasoning directive", () => {
|
||||||
|
const res = extractReasoningDirective("/reasoning on please");
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.reasoningLevel).toBe("on");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches reasoning stream directive", () => {
|
||||||
|
const res = extractReasoningDirective("/reasoning stream please");
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.reasoningLevel).toBe("stream");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches elevated with leading space", () => {
|
||||||
|
const res = extractElevatedDirective(" please /elevated on now");
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.elevatedLevel).toBe("on");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches think at start of line", () => {
|
||||||
|
const res = extractThinkDirective("/think:high run slow");
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.thinkLevel).toBe("high");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not match /think followed by extra letters", () => {
|
||||||
|
// e.g. someone typing "/think" + extra letter "hink"
|
||||||
|
const res = extractThinkDirective("/thinkstuff");
|
||||||
|
expect(res.hasDirective).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches /think with no argument", () => {
|
||||||
|
const res = extractThinkDirective("/think");
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.thinkLevel).toBeUndefined();
|
||||||
|
expect(res.rawLevel).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches /t with no argument", () => {
|
||||||
|
const res = extractThinkDirective("/t");
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.thinkLevel).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches think with no argument and consumes colon", () => {
|
||||||
|
const res = extractThinkDirective("/think:");
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.thinkLevel).toBeUndefined();
|
||||||
|
expect(res.rawLevel).toBeUndefined();
|
||||||
|
expect(res.cleaned).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches verbose with no argument", () => {
|
||||||
|
const res = extractVerboseDirective("/verbose:");
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.verboseLevel).toBeUndefined();
|
||||||
|
expect(res.rawLevel).toBeUndefined();
|
||||||
|
expect(res.cleaned).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches reasoning with no argument", () => {
|
||||||
|
const res = extractReasoningDirective("/reasoning:");
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.reasoningLevel).toBeUndefined();
|
||||||
|
expect(res.rawLevel).toBeUndefined();
|
||||||
|
expect(res.cleaned).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches elevated with no argument", () => {
|
||||||
|
const res = extractElevatedDirective("/elevated:");
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.elevatedLevel).toBeUndefined();
|
||||||
|
expect(res.rawLevel).toBeUndefined();
|
||||||
|
expect(res.cleaned).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches queue directive", () => {
|
||||||
|
const res = extractQueueDirective("please /queue interrupt now");
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.queueMode).toBe("interrupt");
|
||||||
|
expect(res.queueReset).toBe(false);
|
||||||
|
expect(res.cleaned).toBe("please now");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses queue options and modes", () => {
|
||||||
|
const res = extractQueueDirective(
|
||||||
|
"please /queue steer+backlog debounce:2s cap:5 drop:summarize now",
|
||||||
|
);
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.queueMode).toBe("steer-backlog");
|
||||||
|
expect(res.debounceMs).toBe(2000);
|
||||||
|
expect(res.cap).toBe(5);
|
||||||
|
expect(res.dropPolicy).toBe("summarize");
|
||||||
|
expect(res.cleaned).toBe("please now");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts reply_to_current tag", () => {
|
||||||
|
const res = extractReplyToTag("ok [[reply_to_current]]", "msg-1");
|
||||||
|
expect(res.replyToId).toBe("msg-1");
|
||||||
|
expect(res.cleaned).toBe("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts reply_to id tag", () => {
|
||||||
|
const res = extractReplyToTag("see [[reply_to:12345]] now", "msg-1");
|
||||||
|
expect(res.replyToId).toBe("12345");
|
||||||
|
expect(res.cleaned).toBe("see now");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves newlines when stripping reply tags", () => {
|
||||||
|
const res = extractReplyToTag(
|
||||||
|
"line 1\nline 2 [[reply_to_current]]\n\nline 3",
|
||||||
|
"msg-2",
|
||||||
|
);
|
||||||
|
expect(res.replyToId).toBe("msg-2");
|
||||||
|
expect(res.cleaned).toBe("line 1\nline 2\n\nline 3");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,15 +12,7 @@ import {
|
|||||||
saveSessionStore,
|
saveSessionStore,
|
||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import { drainSystemEvents } from "../infra/system-events.js";
|
import { drainSystemEvents } from "../infra/system-events.js";
|
||||||
import {
|
import { getReplyFromConfig } from "./reply.js";
|
||||||
extractElevatedDirective,
|
|
||||||
extractQueueDirective,
|
|
||||||
extractReasoningDirective,
|
|
||||||
extractReplyToTag,
|
|
||||||
extractThinkDirective,
|
|
||||||
extractVerboseDirective,
|
|
||||||
getReplyFromConfig,
|
|
||||||
} from "./reply.js";
|
|
||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
vi.mock("../agents/pi-embedded.js", () => ({
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||||
@@ -60,7 +52,7 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("directive parsing", () => {
|
describe("directive behavior", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
vi.mocked(loadModelCatalog).mockResolvedValue([
|
vi.mocked(loadModelCatalog).mockResolvedValue([
|
||||||
@@ -74,127 +66,6 @@ describe("directive parsing", () => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores verbose directive inside URL", () => {
|
|
||||||
const body = "https://x.com/verioussmith/status/1997066835133669687";
|
|
||||||
const res = extractVerboseDirective(body);
|
|
||||||
expect(res.hasDirective).toBe(false);
|
|
||||||
expect(res.cleaned).toBe(body);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ignores typoed /verioussmith", () => {
|
|
||||||
const body = "/verioussmith";
|
|
||||||
const res = extractVerboseDirective(body);
|
|
||||||
expect(res.hasDirective).toBe(false);
|
|
||||||
expect(res.cleaned).toBe(body.trim());
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ignores think directive inside URL", () => {
|
|
||||||
const body = "see https://example.com/path/thinkstuff";
|
|
||||||
const res = extractThinkDirective(body);
|
|
||||||
expect(res.hasDirective).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches verbose with leading space", () => {
|
|
||||||
const res = extractVerboseDirective(" please /verbose on now");
|
|
||||||
expect(res.hasDirective).toBe(true);
|
|
||||||
expect(res.verboseLevel).toBe("on");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches reasoning directive", () => {
|
|
||||||
const res = extractReasoningDirective("/reasoning on please");
|
|
||||||
expect(res.hasDirective).toBe(true);
|
|
||||||
expect(res.reasoningLevel).toBe("on");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches reasoning stream directive", () => {
|
|
||||||
const res = extractReasoningDirective("/reasoning stream please");
|
|
||||||
expect(res.hasDirective).toBe(true);
|
|
||||||
expect(res.reasoningLevel).toBe("stream");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches elevated with leading space", () => {
|
|
||||||
const res = extractElevatedDirective(" please /elevated on now");
|
|
||||||
expect(res.hasDirective).toBe(true);
|
|
||||||
expect(res.elevatedLevel).toBe("on");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches think at start of line", () => {
|
|
||||||
const res = extractThinkDirective("/think:high run slow");
|
|
||||||
expect(res.hasDirective).toBe(true);
|
|
||||||
expect(res.thinkLevel).toBe("high");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not match /think followed by extra letters", () => {
|
|
||||||
// e.g. someone typing "/think" + extra letter "hink"
|
|
||||||
const res = extractThinkDirective("/thinkstuff");
|
|
||||||
expect(res.hasDirective).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches /think with no argument", () => {
|
|
||||||
const res = extractThinkDirective("/think");
|
|
||||||
expect(res.hasDirective).toBe(true);
|
|
||||||
expect(res.thinkLevel).toBeUndefined();
|
|
||||||
expect(res.rawLevel).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches /t with no argument", () => {
|
|
||||||
const res = extractThinkDirective("/t");
|
|
||||||
expect(res.hasDirective).toBe(true);
|
|
||||||
expect(res.thinkLevel).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches think with no argument and consumes colon", () => {
|
|
||||||
const res = extractThinkDirective("/think:");
|
|
||||||
expect(res.hasDirective).toBe(true);
|
|
||||||
expect(res.thinkLevel).toBeUndefined();
|
|
||||||
expect(res.rawLevel).toBeUndefined();
|
|
||||||
expect(res.cleaned).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches verbose with no argument", () => {
|
|
||||||
const res = extractVerboseDirective("/verbose:");
|
|
||||||
expect(res.hasDirective).toBe(true);
|
|
||||||
expect(res.verboseLevel).toBeUndefined();
|
|
||||||
expect(res.rawLevel).toBeUndefined();
|
|
||||||
expect(res.cleaned).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches reasoning with no argument", () => {
|
|
||||||
const res = extractReasoningDirective("/reasoning:");
|
|
||||||
expect(res.hasDirective).toBe(true);
|
|
||||||
expect(res.reasoningLevel).toBeUndefined();
|
|
||||||
expect(res.rawLevel).toBeUndefined();
|
|
||||||
expect(res.cleaned).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches elevated with no argument", () => {
|
|
||||||
const res = extractElevatedDirective("/elevated:");
|
|
||||||
expect(res.hasDirective).toBe(true);
|
|
||||||
expect(res.elevatedLevel).toBeUndefined();
|
|
||||||
expect(res.rawLevel).toBeUndefined();
|
|
||||||
expect(res.cleaned).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("matches queue directive", () => {
|
|
||||||
const res = extractQueueDirective("please /queue interrupt now");
|
|
||||||
expect(res.hasDirective).toBe(true);
|
|
||||||
expect(res.queueMode).toBe("interrupt");
|
|
||||||
expect(res.queueReset).toBe(false);
|
|
||||||
expect(res.cleaned).toBe("please now");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("parses queue options and modes", () => {
|
|
||||||
const res = extractQueueDirective(
|
|
||||||
"please /queue steer+backlog debounce:2s cap:5 drop:summarize now",
|
|
||||||
);
|
|
||||||
expect(res.hasDirective).toBe(true);
|
|
||||||
expect(res.queueMode).toBe("steer-backlog");
|
|
||||||
expect(res.debounceMs).toBe(2000);
|
|
||||||
expect(res.cap).toBe(5);
|
|
||||||
expect(res.dropPolicy).toBe("summarize");
|
|
||||||
expect(res.cleaned).toBe("please now");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps reserved command aliases from matching after trimming", async () => {
|
it("keeps reserved command aliases from matching after trimming", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
@@ -254,6 +125,44 @@ describe("directive parsing", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows current queue settings when /queue has no arguments", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/queue",
|
||||||
|
From: "+1222",
|
||||||
|
To: "+1222",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
agent: {
|
||||||
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
|
routing: {
|
||||||
|
queue: {
|
||||||
|
mode: "collect",
|
||||||
|
debounceMs: 1500,
|
||||||
|
cap: 9,
|
||||||
|
drop: "summarize",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
whatsapp: { allowFrom: ["*"] },
|
||||||
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain(
|
||||||
|
"Current queue settings: mode=collect, debounce=1500ms, cap=9, drop=summarize.",
|
||||||
|
);
|
||||||
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("shows current think level when /think has no argument", async () => {
|
it("shows current think level when /think has no argument", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
@@ -299,27 +208,6 @@ describe("directive parsing", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("extracts reply_to_current tag", () => {
|
|
||||||
const res = extractReplyToTag("ok [[reply_to_current]]", "msg-1");
|
|
||||||
expect(res.replyToId).toBe("msg-1");
|
|
||||||
expect(res.cleaned).toBe("ok");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("extracts reply_to id tag", () => {
|
|
||||||
const res = extractReplyToTag("see [[reply_to:12345]] now", "msg-1");
|
|
||||||
expect(res.replyToId).toBe("12345");
|
|
||||||
expect(res.cleaned).toBe("see now");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("preserves newlines when stripping reply tags", () => {
|
|
||||||
const res = extractReplyToTag(
|
|
||||||
"line 1\nline 2 [[reply_to_current]]\n\nline 3",
|
|
||||||
"msg-2",
|
|
||||||
);
|
|
||||||
expect(res.replyToId).toBe("msg-2");
|
|
||||||
expect(res.cleaned).toBe("line 1\nline 2\n\nline 3");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("strips reply tags and maps reply_to_current to MessageSid", async () => {
|
it("strips reply tags and maps reply_to_current to MessageSid", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import {
|
|||||||
extractQueueDirective,
|
extractQueueDirective,
|
||||||
type QueueDropPolicy,
|
type QueueDropPolicy,
|
||||||
type QueueMode,
|
type QueueMode,
|
||||||
|
resolveQueueSettings,
|
||||||
} from "./queue.js";
|
} from "./queue.js";
|
||||||
|
|
||||||
const SYSTEM_MARK = "⚙️";
|
const SYSTEM_MARK = "⚙️";
|
||||||
@@ -328,6 +329,7 @@ export async function handleDirectiveOnly(params: {
|
|||||||
allowedModelKeys,
|
allowedModelKeys,
|
||||||
allowedModelCatalog,
|
allowedModelCatalog,
|
||||||
resetModelOverride,
|
resetModelOverride,
|
||||||
|
provider,
|
||||||
initialModelLabel,
|
initialModelLabel,
|
||||||
formatModelSwitchEvent,
|
formatModelSwitchEvent,
|
||||||
currentThinkLevel,
|
currentThinkLevel,
|
||||||
@@ -433,6 +435,33 @@ export async function handleDirectiveOnly(params: {
|
|||||||
return { text: "elevated is not available right now." };
|
return { text: "elevated is not available right now." };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
directives.hasQueueDirective &&
|
||||||
|
!directives.queueMode &&
|
||||||
|
!directives.queueReset &&
|
||||||
|
!directives.hasQueueOptions &&
|
||||||
|
directives.rawQueueMode === undefined &&
|
||||||
|
directives.rawDebounce === undefined &&
|
||||||
|
directives.rawCap === undefined &&
|
||||||
|
directives.rawDrop === undefined
|
||||||
|
) {
|
||||||
|
const settings = resolveQueueSettings({
|
||||||
|
cfg: params.cfg,
|
||||||
|
provider,
|
||||||
|
sessionEntry,
|
||||||
|
});
|
||||||
|
const debounceLabel =
|
||||||
|
typeof settings.debounceMs === "number"
|
||||||
|
? `${settings.debounceMs}ms`
|
||||||
|
: "default";
|
||||||
|
const capLabel =
|
||||||
|
typeof settings.cap === "number" ? String(settings.cap) : "default";
|
||||||
|
const dropLabel = settings.dropPolicy ?? "default";
|
||||||
|
return {
|
||||||
|
text: `Current queue settings: mode=${settings.mode}, debounce=${debounceLabel}, cap=${capLabel}, drop=${dropLabel}.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const queueModeInvalid =
|
const queueModeInvalid =
|
||||||
directives.hasQueueDirective &&
|
directives.hasQueueDirective &&
|
||||||
!directives.queueMode &&
|
!directives.queueMode &&
|
||||||
|
|||||||
@@ -9,6 +9,81 @@ import {
|
|||||||
type VerboseLevel,
|
type VerboseLevel,
|
||||||
} from "../thinking.js";
|
} from "../thinking.js";
|
||||||
|
|
||||||
|
type ExtractedLevel<T> = {
|
||||||
|
cleaned: string;
|
||||||
|
level?: T;
|
||||||
|
rawLevel?: string;
|
||||||
|
hasDirective: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const escapeRegExp = (value: string) =>
|
||||||
|
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
|
||||||
|
const matchLevelDirective = (
|
||||||
|
body: string,
|
||||||
|
names: string[],
|
||||||
|
): { start: number; end: number; rawLevel?: string } | null => {
|
||||||
|
const namePattern = names.map(escapeRegExp).join("|");
|
||||||
|
const match = body.match(
|
||||||
|
new RegExp(`(?:^|\\s)\\/(?:${namePattern})(?=$|\\s|:)`, "i"),
|
||||||
|
);
|
||||||
|
if (!match || match.index === undefined) return null;
|
||||||
|
const start = match.index;
|
||||||
|
let end = match.index + match[0].length;
|
||||||
|
let i = end;
|
||||||
|
while (i < body.length && /\s/.test(body[i])) i += 1;
|
||||||
|
if (body[i] === ":") {
|
||||||
|
i += 1;
|
||||||
|
while (i < body.length && /\s/.test(body[i])) i += 1;
|
||||||
|
}
|
||||||
|
const argStart = i;
|
||||||
|
while (i < body.length && /[A-Za-z-]/.test(body[i])) i += 1;
|
||||||
|
const rawLevel = i > argStart ? body.slice(argStart, i) : undefined;
|
||||||
|
end = i;
|
||||||
|
return { start, end, rawLevel };
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractLevelDirective = <T>(
|
||||||
|
body: string,
|
||||||
|
names: string[],
|
||||||
|
normalize: (raw?: string) => T | undefined,
|
||||||
|
): ExtractedLevel<T> => {
|
||||||
|
const match = matchLevelDirective(body, names);
|
||||||
|
if (!match) {
|
||||||
|
return { cleaned: body.trim(), hasDirective: false };
|
||||||
|
}
|
||||||
|
const rawLevel = match.rawLevel;
|
||||||
|
const level = normalize(rawLevel);
|
||||||
|
const cleaned = body
|
||||||
|
.slice(0, match.start)
|
||||||
|
.concat(body.slice(match.end))
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
return {
|
||||||
|
cleaned,
|
||||||
|
level,
|
||||||
|
rawLevel,
|
||||||
|
hasDirective: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractSimpleDirective = (
|
||||||
|
body: string,
|
||||||
|
names: string[],
|
||||||
|
): { cleaned: string; hasDirective: boolean } => {
|
||||||
|
const namePattern = names.map(escapeRegExp).join("|");
|
||||||
|
const match = body.match(
|
||||||
|
new RegExp(`(?:^|\\s)\\/(?:${namePattern})(?=$|\\s|:)(?:\\s*:\\s*)?`, "i"),
|
||||||
|
);
|
||||||
|
const cleaned = match
|
||||||
|
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
|
||||||
|
: body.trim();
|
||||||
|
return {
|
||||||
|
cleaned,
|
||||||
|
hasDirective: Boolean(match),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function extractThinkDirective(body?: string): {
|
export function extractThinkDirective(body?: string): {
|
||||||
cleaned: string;
|
cleaned: string;
|
||||||
thinkLevel?: ThinkLevel;
|
thinkLevel?: ThinkLevel;
|
||||||
@@ -16,19 +91,16 @@ export function extractThinkDirective(body?: string): {
|
|||||||
hasDirective: boolean;
|
hasDirective: boolean;
|
||||||
} {
|
} {
|
||||||
if (!body) return { cleaned: "", hasDirective: false };
|
if (!body) return { cleaned: "", hasDirective: false };
|
||||||
// Match with optional argument - require word boundary via lookahead after keyword
|
const extracted = extractLevelDirective(
|
||||||
const match = body.match(
|
body,
|
||||||
/(?:^|\s)\/(?:thinking|think|t)(?=$|\s|:)(?:\s*:?\s*(?:([a-zA-Z-]+)\b)?)?/i,
|
["thinking", "think", "t"],
|
||||||
|
normalizeThinkLevel,
|
||||||
);
|
);
|
||||||
const thinkLevel = normalizeThinkLevel(match?.[1]);
|
|
||||||
const cleaned = match
|
|
||||||
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
|
|
||||||
: body.trim();
|
|
||||||
return {
|
return {
|
||||||
cleaned,
|
cleaned: extracted.cleaned,
|
||||||
thinkLevel,
|
thinkLevel: extracted.level,
|
||||||
rawLevel: match?.[1],
|
rawLevel: extracted.rawLevel,
|
||||||
hasDirective: !!match,
|
hasDirective: extracted.hasDirective,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,18 +111,16 @@ export function extractVerboseDirective(body?: string): {
|
|||||||
hasDirective: boolean;
|
hasDirective: boolean;
|
||||||
} {
|
} {
|
||||||
if (!body) return { cleaned: "", hasDirective: false };
|
if (!body) return { cleaned: "", hasDirective: false };
|
||||||
const match = body.match(
|
const extracted = extractLevelDirective(
|
||||||
/(?:^|\s)\/(?:verbose|v)(?=$|\s|:)(?:\s*:?\s*(?:([a-zA-Z-]+)\b)?)?/i,
|
body,
|
||||||
|
["verbose", "v"],
|
||||||
|
normalizeVerboseLevel,
|
||||||
);
|
);
|
||||||
const verboseLevel = normalizeVerboseLevel(match?.[1]);
|
|
||||||
const cleaned = match
|
|
||||||
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
|
|
||||||
: body.trim();
|
|
||||||
return {
|
return {
|
||||||
cleaned,
|
cleaned: extracted.cleaned,
|
||||||
verboseLevel,
|
verboseLevel: extracted.level,
|
||||||
rawLevel: match?.[1],
|
rawLevel: extracted.rawLevel,
|
||||||
hasDirective: !!match,
|
hasDirective: extracted.hasDirective,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,18 +131,16 @@ export function extractElevatedDirective(body?: string): {
|
|||||||
hasDirective: boolean;
|
hasDirective: boolean;
|
||||||
} {
|
} {
|
||||||
if (!body) return { cleaned: "", hasDirective: false };
|
if (!body) return { cleaned: "", hasDirective: false };
|
||||||
const match = body.match(
|
const extracted = extractLevelDirective(
|
||||||
/(?:^|\s)\/(?:elevated|elev)(?=$|\s|:)(?:\s*:?\s*(?:([a-zA-Z-]+)\b)?)?/i,
|
body,
|
||||||
|
["elevated", "elev"],
|
||||||
|
normalizeElevatedLevel,
|
||||||
);
|
);
|
||||||
const elevatedLevel = normalizeElevatedLevel(match?.[1]);
|
|
||||||
const cleaned = match
|
|
||||||
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
|
|
||||||
: body.trim();
|
|
||||||
return {
|
return {
|
||||||
cleaned,
|
cleaned: extracted.cleaned,
|
||||||
elevatedLevel,
|
elevatedLevel: extracted.level,
|
||||||
rawLevel: match?.[1],
|
rawLevel: extracted.rawLevel,
|
||||||
hasDirective: !!match,
|
hasDirective: extracted.hasDirective,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,18 +151,16 @@ export function extractReasoningDirective(body?: string): {
|
|||||||
hasDirective: boolean;
|
hasDirective: boolean;
|
||||||
} {
|
} {
|
||||||
if (!body) return { cleaned: "", hasDirective: false };
|
if (!body) return { cleaned: "", hasDirective: false };
|
||||||
const match = body.match(
|
const extracted = extractLevelDirective(
|
||||||
/(?:^|\s)\/(?:reasoning|reason)(?=$|\s|:)(?:\s*:?\s*(?:([a-zA-Z-]+)\b)?)?/i,
|
body,
|
||||||
|
["reasoning", "reason"],
|
||||||
|
normalizeReasoningLevel,
|
||||||
);
|
);
|
||||||
const reasoningLevel = normalizeReasoningLevel(match?.[1]);
|
|
||||||
const cleaned = match
|
|
||||||
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
|
|
||||||
: body.trim();
|
|
||||||
return {
|
return {
|
||||||
cleaned,
|
cleaned: extracted.cleaned,
|
||||||
reasoningLevel,
|
reasoningLevel: extracted.level,
|
||||||
rawLevel: match?.[1],
|
rawLevel: extracted.rawLevel,
|
||||||
hasDirective: !!match,
|
hasDirective: extracted.hasDirective,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,14 +169,7 @@ export function extractStatusDirective(body?: string): {
|
|||||||
hasDirective: boolean;
|
hasDirective: boolean;
|
||||||
} {
|
} {
|
||||||
if (!body) return { cleaned: "", hasDirective: false };
|
if (!body) return { cleaned: "", hasDirective: false };
|
||||||
const match = body.match(/(?:^|\s)\/status(?=$|\s|:)(?:\s*:\s*)?/i);
|
return extractSimpleDirective(body, ["status"]);
|
||||||
const cleaned = match
|
|
||||||
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
|
|
||||||
: body.trim();
|
|
||||||
return {
|
|
||||||
cleaned,
|
|
||||||
hasDirective: !!match,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel };
|
export type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel };
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { normalizeCommandBody } from "./commands-registry.js";
|
||||||
|
|
||||||
export type SendPolicyOverride = "allow" | "deny";
|
export type SendPolicyOverride = "allow" | "deny";
|
||||||
|
|
||||||
export function normalizeSendPolicyOverride(
|
export function normalizeSendPolicyOverride(
|
||||||
@@ -17,11 +19,10 @@ export function parseSendPolicyCommand(raw?: string): {
|
|||||||
if (!raw) return { hasCommand: false };
|
if (!raw) return { hasCommand: false };
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return { hasCommand: false };
|
if (!trimmed) return { hasCommand: false };
|
||||||
const match = trimmed.match(
|
const normalized = normalizeCommandBody(trimmed);
|
||||||
/^\/send(?:\s*:\s*([a-zA-Z]+)?\s*|\s+([a-zA-Z]+)\s*)?$/i,
|
const match = normalized.match(/^\/send(?:\s+([a-zA-Z]+))?\s*$/i);
|
||||||
);
|
|
||||||
if (!match) return { hasCommand: false };
|
if (!match) return { hasCommand: false };
|
||||||
const token = (match[1] ?? match[2])?.trim().toLowerCase();
|
const token = match[1]?.trim().toLowerCase();
|
||||||
if (!token) return { hasCommand: true };
|
if (!token) return { hasCommand: true };
|
||||||
if (token === "inherit" || token === "default" || token === "reset") {
|
if (token === "inherit" || token === "default" || token === "reset") {
|
||||||
return { hasCommand: true, mode: "inherit" };
|
return { hasCommand: true, mode: "inherit" };
|
||||||
|
|||||||
Reference in New Issue
Block a user