@grindxp/cli 0.1.8 → 0.1.9
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/index.js +766 -134
- package/dist/web/server/assets/{agent.functions-zpMkBrG3.js → agent.functions-BL3upUNr.js} +444 -47
- package/dist/web/server/assets/{data.functions-9hSsMFx_.js → data.functions-DZmdFOMQ.js} +2 -2
- package/dist/web/server/assets/{index-C09LXa7Z.js → index-B2ULpkv2.js} +3 -3
- package/dist/web/server/assets/{index-D31yYLCV.js → index-BGBMycx-.js} +3 -3
- package/dist/web/server/assets/{index-BDL7hA7T.js → index-BQUCDamI.js} +3 -3
- package/dist/web/server/assets/{index-D7z4dRpK.js → index-CB8UtTN8.js} +2 -2
- package/dist/web/server/assets/{index-CJ_-TSqN.js → index-DTB2dYCz.js} +2 -2
- package/dist/web/server/assets/{index-D2fMUSdJ.js → index-DfU25rnD.js} +2 -2
- package/dist/web/server/assets/{index-b30aLTKp.js → index-SHH7zSKt.js} +2 -2
- package/dist/web/server/assets/{router-1koL9I3U.js → router-CXyGzWDS.js} +5 -5
- package/dist/web/server/assets/{sessions-DOkG47Ex.js → sessions-UCWtijHE.js} +47 -12
- package/dist/web/server/assets/{token-W0NPKas8.js → token-DGoahKjI.js} +3 -3
- package/dist/web/server/assets/{token-util-DA5xS0pj.js → token-util-BopJPy-I.js} +1 -1
- package/dist/web/server/assets/{token-util-1cB5CD6M.js → token-util-Bw35afYM.js} +2 -2
- package/dist/web/server/assets/{vault.server-Ndu49yTf.js → vault.server-CscY5Z8e.js} +46 -45
- package/dist/web/server/server.js +9 -9
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { f as createQuest, h as completeQuest, i as getQuestById, u as updateQuestStatus,
|
|
2
|
-
import {
|
|
1
|
+
import { f as createQuest, h as completeQuest, i as getQuestById, u as updateQuestStatus, j as updateCompanionMode, k as listCompanionInsights, m as deleteCompanionInsight, n as findQuestByPrefix, l as listQuestsByUser, o as updateQuest, p as listQuestLogs, a as listSkillsByUser, g as getCompanionByUserId, q as updateCompanionUserContext, r as updateCompanionInsight, s as findCompanionInsightByContent, t as createCompanionInsight, c as createServerRpc, v as getConversationById, w as createConversation, x as appendMessage, y as getConversationMessages, z as storedToModelMessages } from "./sessions-UCWtijHE.js";
|
|
2
|
+
import { b as and, e as eq, s as signals, d as desc, f as forgeRules, c as forgeRuleSchema, h as createForgeRuleInputSchema, i as forgeRuns, j as signalSchema, k as forgeRunSchema, l as sql, m as createForgeRunInputSchema, Z as ZodFirstPartyTypeKind, o as objectType, n as numberType, p as stringType, q as arrayType, r as enumType, u as unionType, t as getOAuthToken, v as isTokenExpired, w as refreshOAuthToken, O as OAUTH_CONFIGS, x as xpForLevelThreshold, y as formatElapsed, A as ACTIVITY_BASE_XP, z as activityTypeSchema, B as questDifficultySchema, C as booleanType, D as recordType, E as unknownType, a as getUserById, F as readTimer, G as getElapsedMinutes, H as clearTimer, I as writeTimer, J as readGrindConfig, K as writeGrindConfig, L as users, M as quests, N as skills, P as gte, Q as questLogs, g as getVaultContext, R as getGrindConfig } from "./vault.server-CscY5Z8e.js";
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import os from "os";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { homedir } from "node:os";
|
|
7
8
|
import { c as createServerFn } from "../server.js";
|
|
8
9
|
function rowToForgeRule(row) {
|
|
9
10
|
return forgeRuleSchema.parse({
|
|
@@ -19,6 +20,18 @@ function rowToForgeRule(row) {
|
|
|
19
20
|
updatedAt: row.updatedAt
|
|
20
21
|
});
|
|
21
22
|
}
|
|
23
|
+
function rowToSignal(row) {
|
|
24
|
+
return signalSchema.parse({
|
|
25
|
+
id: row.id,
|
|
26
|
+
userId: row.userId,
|
|
27
|
+
source: row.source,
|
|
28
|
+
type: row.type,
|
|
29
|
+
confidence: row.confidence,
|
|
30
|
+
payload: row.payload,
|
|
31
|
+
detectedAt: row.detectedAt,
|
|
32
|
+
ingestedAt: row.ingestedAt
|
|
33
|
+
});
|
|
34
|
+
}
|
|
22
35
|
function rowToForgeRun(row) {
|
|
23
36
|
return forgeRunSchema.parse({
|
|
24
37
|
id: row.id,
|
|
@@ -98,6 +111,16 @@ async function deleteForgeRule(db, userId, ruleId) {
|
|
|
98
111
|
const rows = await db.delete(forgeRules).where(and(eq(forgeRules.id, ruleId), eq(forgeRules.userId, userId))).returning({ id: forgeRules.id });
|
|
99
112
|
return rows.length > 0;
|
|
100
113
|
}
|
|
114
|
+
async function listSignals(db, userId, options = {}) {
|
|
115
|
+
const { limit = 20, source } = options;
|
|
116
|
+
const where = source ? and(eq(signals.userId, userId), eq(signals.source, source)) : eq(signals.userId, userId);
|
|
117
|
+
const rows = await db.query.signals.findMany({
|
|
118
|
+
where,
|
|
119
|
+
orderBy: [desc(signals.detectedAt)],
|
|
120
|
+
limit
|
|
121
|
+
});
|
|
122
|
+
return rows.map(rowToSignal);
|
|
123
|
+
}
|
|
101
124
|
async function hasForgeRunByDedupe(db, ruleId, dedupeKey) {
|
|
102
125
|
const rows = await db.select({ value: sql`count(1)` }).from(forgeRuns).where(and(eq(forgeRuns.ruleId, ruleId), eq(forgeRuns.dedupeKey, dedupeKey)));
|
|
103
126
|
const count = rows[0]?.value ?? 0;
|
|
@@ -7164,8 +7187,8 @@ function requireGetVercelOidcToken() {
|
|
|
7164
7187
|
}
|
|
7165
7188
|
try {
|
|
7166
7189
|
const [{ getTokenPayload, isExpired }, { refreshToken }] = await Promise.all([
|
|
7167
|
-
await import("./token-util-
|
|
7168
|
-
await import("./token-
|
|
7190
|
+
await import("./token-util-Bw35afYM.js").then((n) => n.t),
|
|
7191
|
+
await import("./token-DGoahKjI.js").then((n) => n.t)
|
|
7169
7192
|
]);
|
|
7170
7193
|
if (!token || isExpired(getTokenPayload(token))) {
|
|
7171
7194
|
await refreshToken();
|
|
@@ -15441,7 +15464,7 @@ async function resolveModel(config2) {
|
|
|
15441
15464
|
const modelId = config2.model ?? DEFAULT_MODELS[provider];
|
|
15442
15465
|
switch (provider) {
|
|
15443
15466
|
case "anthropic": {
|
|
15444
|
-
const { createAnthropic } = await import("./index-
|
|
15467
|
+
const { createAnthropic } = await import("./index-B2ULpkv2.js");
|
|
15445
15468
|
if (config2.authType === "oauth") {
|
|
15446
15469
|
const client2 = createAnthropic({
|
|
15447
15470
|
authToken: "grind-oauth-managed",
|
|
@@ -15459,7 +15482,7 @@ async function resolveModel(config2) {
|
|
|
15459
15482
|
return client(modelId);
|
|
15460
15483
|
}
|
|
15461
15484
|
case "openai": {
|
|
15462
|
-
const { createOpenAI } = await import("./index-
|
|
15485
|
+
const { createOpenAI } = await import("./index-BQUCDamI.js");
|
|
15463
15486
|
if (config2.authType === "oauth") {
|
|
15464
15487
|
const client2 = createOpenAI({
|
|
15465
15488
|
apiKey: "openai-oauth-dummy",
|
|
@@ -15475,14 +15498,14 @@ async function resolveModel(config2) {
|
|
|
15475
15498
|
return client(modelId);
|
|
15476
15499
|
}
|
|
15477
15500
|
case "google": {
|
|
15478
|
-
const { createGoogleGenerativeAI } = await import("./index-
|
|
15501
|
+
const { createGoogleGenerativeAI } = await import("./index-BGBMycx-.js");
|
|
15479
15502
|
const client = createGoogleGenerativeAI({
|
|
15480
15503
|
...config2.apiKey ? { apiKey: config2.apiKey } : {}
|
|
15481
15504
|
});
|
|
15482
15505
|
return client(modelId);
|
|
15483
15506
|
}
|
|
15484
15507
|
case "ollama": {
|
|
15485
|
-
const { createOpenAI } = await import("./index-
|
|
15508
|
+
const { createOpenAI } = await import("./index-BQUCDamI.js");
|
|
15486
15509
|
const client = createOpenAI({
|
|
15487
15510
|
baseURL: config2.baseUrl ?? "http://localhost:11434/v1",
|
|
15488
15511
|
apiKey: "ollama"
|
|
@@ -15564,8 +15587,16 @@ TOOL USAGE:
|
|
|
15564
15587
|
- When asked whether integrations/channels are connected or available (Telegram, WhatsApp, Discord, Google Calendar), call get_integrations_status first. Do not guess.
|
|
15565
15588
|
- If the user asks to send or test a Telegram message, call send_telegram_message immediately. Never ask the user for their chat ID — it is resolved automatically.
|
|
15566
15589
|
- If send_telegram_message fails because no chat ID was found yet, tell the user to send any message to the bot from Telegram (not /start specifically) and offer to try again immediately after.
|
|
15567
|
-
- When the user asks to automate, schedule reminders, or set recurring workflows, use forge tools
|
|
15568
|
-
- Before updating or
|
|
15590
|
+
- When the user asks to automate, schedule reminders, or set recurring workflows, use forge tools directly — never tell the user to use the CLI manually.
|
|
15591
|
+
- Before updating, deleting, or running a specific rule, call list_forge_rules to confirm the target and read its xpImpact field.
|
|
15592
|
+
- xpImpact: false rules (notifications, reminders, monitors): act fully autonomously — no explanation needed beyond confirming what you did.
|
|
15593
|
+
- xpImpact: true rules (log-to-vault, update-skill): proceed autonomously and briefly mention in your reply that XP will be awarded automatically.
|
|
15594
|
+
- Deleting a rule is permanent — tell the user this before calling delete_forge_rule.
|
|
15595
|
+
- run-script rules execute shell scripts as automations — always show the full script in your reply when creating or updating one.
|
|
15596
|
+
- Use list_forge_runs to diagnose failures.
|
|
15597
|
+
- When the user names a specific calendar (anything other than 'primary'), always call list_calendars first to resolve the name to its id, then pass that id to create_calendar_event or get_calendar_events. Never assume the id — always look it up.
|
|
15598
|
+
- If list_calendars does not return the named calendar and the user wants to create it, call create_calendar first, then use the returned id immediately for any subsequent event creation.
|
|
15599
|
+
- Never ask the user for a calendar ID — always resolve it yourself via list_calendars.
|
|
15569
15600
|
- Keep responses concise. 1-3 sentences for simple actions. No walls of text.
|
|
15570
15601
|
|
|
15571
15602
|
WEB & FILE ACCESS:
|
|
@@ -15905,6 +15936,15 @@ async function listCalendars(serviceConfig) {
|
|
|
15905
15936
|
const data = await resp.json();
|
|
15906
15937
|
return data.items ?? [];
|
|
15907
15938
|
}
|
|
15939
|
+
async function createCalendar(serviceConfig, summary, timeZone) {
|
|
15940
|
+
const body = { summary };
|
|
15941
|
+
if (timeZone) body.timeZone = timeZone;
|
|
15942
|
+
const resp = await googleFetch(`${BASE$1}/calendars`, serviceConfig, {
|
|
15943
|
+
method: "POST",
|
|
15944
|
+
body: JSON.stringify(body)
|
|
15945
|
+
});
|
|
15946
|
+
return resp.json();
|
|
15947
|
+
}
|
|
15908
15948
|
const BASE = "https://gmail.googleapis.com/gmail/v1/users/me";
|
|
15909
15949
|
function extractHeader(headers, name16) {
|
|
15910
15950
|
return headers.find((h) => h.name.toLowerCase() === name16.toLowerCase())?.value ?? "";
|
|
@@ -16075,6 +16115,8 @@ async function executeForgeAction(db, userId, plan) {
|
|
|
16075
16115
|
return executeLogToVault(db, userId, plan);
|
|
16076
16116
|
case "send-notification":
|
|
16077
16117
|
return executeSendNotification(plan);
|
|
16118
|
+
case "run-script":
|
|
16119
|
+
return executeRunScript(plan);
|
|
16078
16120
|
default:
|
|
16079
16121
|
return {
|
|
16080
16122
|
status: "skipped",
|
|
@@ -16270,6 +16312,53 @@ async function executeSendNotification(plan) {
|
|
|
16270
16312
|
error: `Unsupported notification channel '${channel}'.`
|
|
16271
16313
|
};
|
|
16272
16314
|
}
|
|
16315
|
+
async function executeRunScript(plan) {
|
|
16316
|
+
const script = asString(plan.actionConfig.script);
|
|
16317
|
+
if (!script) {
|
|
16318
|
+
return {
|
|
16319
|
+
status: "failed",
|
|
16320
|
+
actionPayload: {},
|
|
16321
|
+
error: "run-script requires actionConfig.script."
|
|
16322
|
+
};
|
|
16323
|
+
}
|
|
16324
|
+
const timeoutMs = parsePositiveInt$1(plan.actionConfig.timeout) ?? 3e4;
|
|
16325
|
+
const rawWorkdir = asString(plan.actionConfig.workdir);
|
|
16326
|
+
const workdir = rawWorkdir ? rawWorkdir.replace(/^~/, homedir()) : void 0;
|
|
16327
|
+
const result = spawnSync("sh", ["-c", script], {
|
|
16328
|
+
encoding: "utf8",
|
|
16329
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
16330
|
+
timeout: timeoutMs,
|
|
16331
|
+
...workdir ? { cwd: workdir } : {}
|
|
16332
|
+
});
|
|
16333
|
+
const errCode = result.error?.code;
|
|
16334
|
+
if (errCode === "ENOENT" && workdir) {
|
|
16335
|
+
return {
|
|
16336
|
+
status: "failed",
|
|
16337
|
+
actionPayload: { script, exitCode: null },
|
|
16338
|
+
error: `Working directory does not exist: ${workdir}`
|
|
16339
|
+
};
|
|
16340
|
+
}
|
|
16341
|
+
if (errCode === "ETIMEDOUT") {
|
|
16342
|
+
return {
|
|
16343
|
+
status: "failed",
|
|
16344
|
+
actionPayload: { script, exitCode: null },
|
|
16345
|
+
error: `Script timed out after ${timeoutMs}ms.`
|
|
16346
|
+
};
|
|
16347
|
+
}
|
|
16348
|
+
if (result.status !== 0) {
|
|
16349
|
+
const stderr = result.stderr?.trim().slice(0, 500) ?? "";
|
|
16350
|
+
return {
|
|
16351
|
+
status: "failed",
|
|
16352
|
+
actionPayload: { script, exitCode: result.status },
|
|
16353
|
+
error: `Script exited ${result.status ?? "null"}${stderr ? `: ${stderr}` : ""}`
|
|
16354
|
+
};
|
|
16355
|
+
}
|
|
16356
|
+
const stdout = result.stdout?.trim().slice(0, 2e3) ?? "";
|
|
16357
|
+
return {
|
|
16358
|
+
status: "success",
|
|
16359
|
+
actionPayload: { script, exitCode: 0, ...stdout ? { stdout } : {} }
|
|
16360
|
+
};
|
|
16361
|
+
}
|
|
16273
16362
|
async function executeQueueQuest(db, userId, plan) {
|
|
16274
16363
|
const eventPayload = asRecord$1(plan.actionConfig.eventPayload);
|
|
16275
16364
|
const questId = asString(plan.actionConfig.questId) ?? asString(eventPayload?.questId) ?? asString(plan.actionConfig.targetQuestId);
|
|
@@ -16379,6 +16468,37 @@ function parseConfidence(value) {
|
|
|
16379
16468
|
if (Number.isNaN(value) || value < 0 || value > 1) return null;
|
|
16380
16469
|
return value;
|
|
16381
16470
|
}
|
|
16471
|
+
const TRUST_LEVEL_NAMES = ["Watcher", "Advisor", "Scribe", "Agent", "Sovereign"];
|
|
16472
|
+
const TOOL_TRUST_REQUIREMENTS = {
|
|
16473
|
+
// Lv.2 Scribe: can act on behalf of the user
|
|
16474
|
+
complete_quest: 2,
|
|
16475
|
+
abandon_quest: 2,
|
|
16476
|
+
activate_quest: 2,
|
|
16477
|
+
start_timer: 2,
|
|
16478
|
+
stop_timer: 2,
|
|
16479
|
+
update_companion_mode: 2,
|
|
16480
|
+
// Lv.3 Agent: can create and modify structure
|
|
16481
|
+
create_quest: 3,
|
|
16482
|
+
update_quest: 3,
|
|
16483
|
+
// Lv.4 Sovereign: destructive or sensitive operations
|
|
16484
|
+
delete_insight: 4
|
|
16485
|
+
// Note: forge operations are not gated by trust level — the AI reasons
|
|
16486
|
+
// autonomously using the xpImpact field returned by list_forge_rules.
|
|
16487
|
+
};
|
|
16488
|
+
function requireTrust(ctx, toolName) {
|
|
16489
|
+
const required2 = TOOL_TRUST_REQUIREMENTS[toolName];
|
|
16490
|
+
if (required2 === void 0) return { denied: false };
|
|
16491
|
+
const current = ctx.trustLevel ?? 0;
|
|
16492
|
+
if (current < required2) {
|
|
16493
|
+
const requiredName = TRUST_LEVEL_NAMES[required2] ?? `Lv.${required2}`;
|
|
16494
|
+
const currentName = TRUST_LEVEL_NAMES[current] ?? `Lv.${current}`;
|
|
16495
|
+
return {
|
|
16496
|
+
denied: true,
|
|
16497
|
+
error: `Action requires trust level ${required2} (${requiredName}). Current level: ${current} (${currentName}). Grant trust with: grindxp companion trust ${required2}`
|
|
16498
|
+
};
|
|
16499
|
+
}
|
|
16500
|
+
return { denied: false };
|
|
16501
|
+
}
|
|
16382
16502
|
const MAX_FETCH_SIZE = 50 * 1024;
|
|
16383
16503
|
const MAX_READ_SIZE = 50 * 1024;
|
|
16384
16504
|
const MAX_BASH_OUTPUT = 50 * 1024;
|
|
@@ -16435,6 +16555,38 @@ async function requirePermission(ctx, toolName, detail) {
|
|
|
16435
16555
|
if (reply === "deny") return { denied: true, error: "Permission denied by user" };
|
|
16436
16556
|
return { denied: false };
|
|
16437
16557
|
}
|
|
16558
|
+
function classifyGoogleError(err) {
|
|
16559
|
+
if (err instanceof GoogleNotConnectedError || err instanceof GoogleTokenExpiredError) {
|
|
16560
|
+
return "Google account not connected or session expired. Run `grindxp integrations connect google`.";
|
|
16561
|
+
}
|
|
16562
|
+
if (err instanceof GoogleApiError) {
|
|
16563
|
+
switch (err.status) {
|
|
16564
|
+
case 401:
|
|
16565
|
+
return "Google account disconnected. Run `grindxp integrations connect google`.";
|
|
16566
|
+
case 403:
|
|
16567
|
+
return "No write access to this calendar. Check the calendar's sharing settings.";
|
|
16568
|
+
case 404:
|
|
16569
|
+
return "Calendar or event not found. Use list_calendars to verify available calendar IDs.";
|
|
16570
|
+
case 409:
|
|
16571
|
+
return "Conflict — this event may already exist on the calendar.";
|
|
16572
|
+
case 410:
|
|
16573
|
+
return "Sync token expired — a full re-sync will happen on the next poll.";
|
|
16574
|
+
}
|
|
16575
|
+
if (err.status >= 500) {
|
|
16576
|
+
return "Google Calendar is temporarily unavailable. Try again in a moment.";
|
|
16577
|
+
}
|
|
16578
|
+
if (err.status === 400) {
|
|
16579
|
+
try {
|
|
16580
|
+
const parsed = JSON.parse(err.body);
|
|
16581
|
+
const msg = parsed?.error?.message;
|
|
16582
|
+
if (msg) return `Invalid request: ${msg}`;
|
|
16583
|
+
} catch {
|
|
16584
|
+
}
|
|
16585
|
+
return "Invalid request — check the calendar ID and date format.";
|
|
16586
|
+
}
|
|
16587
|
+
}
|
|
16588
|
+
return err instanceof Error ? err.message : String(err);
|
|
16589
|
+
}
|
|
16438
16590
|
async function extractText(html) {
|
|
16439
16591
|
let text2 = "";
|
|
16440
16592
|
let skip = false;
|
|
@@ -16593,16 +16745,6 @@ async function resolveTelegramChatId(ctx, token) {
|
|
|
16593
16745
|
return { chatId: candidate, source: "recent-signal" };
|
|
16594
16746
|
}
|
|
16595
16747
|
}
|
|
16596
|
-
const webhookActive = Boolean(
|
|
16597
|
-
ctx.config?.gateway?.telegramWebhookSecret ?? freshConfig?.gateway?.telegramWebhookSecret
|
|
16598
|
-
);
|
|
16599
|
-
if (webhookActive) {
|
|
16600
|
-
return {
|
|
16601
|
-
chatId: null,
|
|
16602
|
-
source: "none",
|
|
16603
|
-
detail: "Send any message to your Telegram bot and I'll respond automatically. The chat ID will be captured on first contact."
|
|
16604
|
-
};
|
|
16605
|
-
}
|
|
16606
16748
|
const updatesResponse = await fetch(
|
|
16607
16749
|
`https://api.telegram.org/bot${token}/getUpdates?limit=50&timeout=0`,
|
|
16608
16750
|
{
|
|
@@ -16668,10 +16810,17 @@ function persistTelegramDefaultChatId(ctx, chatId) {
|
|
|
16668
16810
|
telegramDefaultChatId: chatId
|
|
16669
16811
|
}
|
|
16670
16812
|
};
|
|
16671
|
-
|
|
16813
|
+
const onDisk = readGrindConfig();
|
|
16814
|
+
if (!onDisk?.gateway) return;
|
|
16815
|
+
writeGrindConfig({ ...onDisk, gateway: { ...onDisk.gateway, telegramDefaultChatId: chatId } });
|
|
16672
16816
|
}
|
|
16673
16817
|
const FORGE_TRIGGER_TYPES = ["cron", "event", "signal", "webhook", "manual"];
|
|
16674
|
-
const FORGE_ACTION_TYPES = [
|
|
16818
|
+
const FORGE_ACTION_TYPES = [
|
|
16819
|
+
"queue-quest",
|
|
16820
|
+
"log-to-vault",
|
|
16821
|
+
"send-notification",
|
|
16822
|
+
"run-script"
|
|
16823
|
+
];
|
|
16675
16824
|
const FORGE_NOTIFICATION_CHANNELS = ["console", "telegram", "webhook", "whatsapp"];
|
|
16676
16825
|
const FORGE_WEBHOOK_CHANNEL_TAGS = ["webhook", "telegram", "discord", "whatsapp"];
|
|
16677
16826
|
const SIMPLE_CRON_FIELD_REGEX = /^[\d*/,\-]+$/;
|
|
@@ -16982,6 +17131,37 @@ async function normalizeForgeRuleDefinition(ctx, input) {
|
|
|
16982
17131
|
}
|
|
16983
17132
|
break;
|
|
16984
17133
|
}
|
|
17134
|
+
case "run-script": {
|
|
17135
|
+
const script = asNonEmptyString(actionConfig.script);
|
|
17136
|
+
if (!script) {
|
|
17137
|
+
return {
|
|
17138
|
+
ok: false,
|
|
17139
|
+
error: "run-script requires actionConfig.script (shell command to execute)."
|
|
17140
|
+
};
|
|
17141
|
+
}
|
|
17142
|
+
actionConfig.script = script;
|
|
17143
|
+
if (actionConfig.timeout !== void 0) {
|
|
17144
|
+
const timeout = parsePositiveInt(actionConfig.timeout);
|
|
17145
|
+
if (!timeout) {
|
|
17146
|
+
return {
|
|
17147
|
+
ok: false,
|
|
17148
|
+
error: "run-script actionConfig.timeout must be a positive integer (milliseconds)."
|
|
17149
|
+
};
|
|
17150
|
+
}
|
|
17151
|
+
actionConfig.timeout = timeout;
|
|
17152
|
+
}
|
|
17153
|
+
if (actionConfig.workdir !== void 0) {
|
|
17154
|
+
const workdir = asNonEmptyString(actionConfig.workdir);
|
|
17155
|
+
if (!workdir) {
|
|
17156
|
+
return {
|
|
17157
|
+
ok: false,
|
|
17158
|
+
error: "run-script actionConfig.workdir must be a non-empty string when provided."
|
|
17159
|
+
};
|
|
17160
|
+
}
|
|
17161
|
+
actionConfig.workdir = workdir;
|
|
17162
|
+
}
|
|
17163
|
+
break;
|
|
17164
|
+
}
|
|
16985
17165
|
}
|
|
16986
17166
|
return {
|
|
16987
17167
|
ok: true,
|
|
@@ -17049,7 +17229,7 @@ function createGrindTools(ctx) {
|
|
|
17049
17229
|
const calendars = await listCalendars(googleConfig);
|
|
17050
17230
|
return { ok: true, calendars, count: calendars.length };
|
|
17051
17231
|
} catch (err) {
|
|
17052
|
-
return { ok: false, error:
|
|
17232
|
+
return { ok: false, error: classifyGoogleError(err) };
|
|
17053
17233
|
}
|
|
17054
17234
|
}
|
|
17055
17235
|
}),
|
|
@@ -17145,12 +17325,12 @@ function createGrindTools(ctx) {
|
|
|
17145
17325
|
});
|
|
17146
17326
|
return { ok: true, events: result.events, count: result.events.length };
|
|
17147
17327
|
} catch (err) {
|
|
17148
|
-
return { ok: false, error:
|
|
17328
|
+
return { ok: false, error: classifyGoogleError(err) };
|
|
17149
17329
|
}
|
|
17150
17330
|
}
|
|
17151
17331
|
}),
|
|
17152
17332
|
create_calendar_event: tool({
|
|
17153
|
-
description: "Create a new event in Google Calendar.
|
|
17333
|
+
description: "Create a new event in Google Calendar. If the user names a specific calendar (anything other than 'primary'), call list_calendars first to resolve its id, then pass that id as calendarId. If the calendar does not exist yet, call create_calendar first, then use the returned id.",
|
|
17154
17334
|
inputSchema: objectType({
|
|
17155
17335
|
summary: stringType().min(1).max(500).describe("Event title"),
|
|
17156
17336
|
startDateTime: stringType().describe("Start time (ISO 8601, e.g. 2026-02-21T09:00:00)"),
|
|
@@ -17159,7 +17339,10 @@ function createGrindTools(ctx) {
|
|
|
17159
17339
|
location: stringType().optional().describe("Event location"),
|
|
17160
17340
|
attendees: arrayType(stringType().email()).optional().describe("List of attendee email addresses"),
|
|
17161
17341
|
allDay: booleanType().optional().describe("If true, treats startDateTime/endDateTime as dates (YYYY-MM-DD)"),
|
|
17162
|
-
timeZone: stringType().optional().describe("IANA timezone name (e.g. America/New_York)")
|
|
17342
|
+
timeZone: stringType().optional().describe("IANA timezone name (e.g. America/New_York)"),
|
|
17343
|
+
calendarId: stringType().optional().describe(
|
|
17344
|
+
"Calendar ID to create the event in. Use list_calendars to resolve a calendar name to its id. Defaults to the primary calendar."
|
|
17345
|
+
)
|
|
17163
17346
|
}),
|
|
17164
17347
|
execute: async ({
|
|
17165
17348
|
summary,
|
|
@@ -17169,7 +17352,8 @@ function createGrindTools(ctx) {
|
|
|
17169
17352
|
location,
|
|
17170
17353
|
attendees,
|
|
17171
17354
|
allDay,
|
|
17172
|
-
timeZone
|
|
17355
|
+
timeZone,
|
|
17356
|
+
calendarId
|
|
17173
17357
|
}) => {
|
|
17174
17358
|
const googleConfig = ctx.config?.services?.google;
|
|
17175
17359
|
if (!googleConfig) {
|
|
@@ -17179,19 +17363,45 @@ function createGrindTools(ctx) {
|
|
|
17179
17363
|
};
|
|
17180
17364
|
}
|
|
17181
17365
|
try {
|
|
17182
|
-
const event = await createCalendarEvent(
|
|
17183
|
-
|
|
17184
|
-
|
|
17185
|
-
|
|
17186
|
-
|
|
17187
|
-
|
|
17188
|
-
|
|
17189
|
-
|
|
17190
|
-
|
|
17191
|
-
|
|
17366
|
+
const event = await createCalendarEvent(
|
|
17367
|
+
googleConfig,
|
|
17368
|
+
{
|
|
17369
|
+
summary,
|
|
17370
|
+
startDateTime,
|
|
17371
|
+
endDateTime,
|
|
17372
|
+
...description ? { description } : {},
|
|
17373
|
+
...location ? { location } : {},
|
|
17374
|
+
...attendees ? { attendees } : {},
|
|
17375
|
+
...allDay ? { allDay } : {},
|
|
17376
|
+
...timeZone ? { timeZone } : {}
|
|
17377
|
+
},
|
|
17378
|
+
calendarId ?? "primary"
|
|
17379
|
+
);
|
|
17192
17380
|
return { ok: true, event };
|
|
17193
17381
|
} catch (err) {
|
|
17194
|
-
return { ok: false, error:
|
|
17382
|
+
return { ok: false, error: classifyGoogleError(err) };
|
|
17383
|
+
}
|
|
17384
|
+
}
|
|
17385
|
+
}),
|
|
17386
|
+
create_calendar: tool({
|
|
17387
|
+
description: "Create a new Google Calendar. After creation, use the returned id with create_calendar_event to add events to it. Use this when the user asks to create a calendar that does not yet exist in their list.",
|
|
17388
|
+
inputSchema: objectType({
|
|
17389
|
+
summary: stringType().min(1).max(255).describe("Calendar name (e.g. 'Work', 'God', 'Study')"),
|
|
17390
|
+
timeZone: stringType().optional().describe("IANA timezone name for the calendar (e.g. America/Sao_Paulo)")
|
|
17391
|
+
}),
|
|
17392
|
+
execute: async ({ summary, timeZone }) => {
|
|
17393
|
+
const googleConfig = ctx.config?.services?.google;
|
|
17394
|
+
if (!googleConfig) {
|
|
17395
|
+
return {
|
|
17396
|
+
ok: false,
|
|
17397
|
+
error: "Google account not connected. Run `grindxp integrations connect google`."
|
|
17398
|
+
};
|
|
17399
|
+
}
|
|
17400
|
+
try {
|
|
17401
|
+
const calendar = await createCalendar(googleConfig, summary, timeZone);
|
|
17402
|
+
return { ok: true, calendar };
|
|
17403
|
+
} catch (err) {
|
|
17404
|
+
return { ok: false, error: classifyGoogleError(err) };
|
|
17195
17405
|
}
|
|
17196
17406
|
}
|
|
17197
17407
|
}),
|
|
@@ -17244,7 +17454,7 @@ function createGrindTools(ctx) {
|
|
|
17244
17454
|
);
|
|
17245
17455
|
return { ok: true, event };
|
|
17246
17456
|
} catch (err) {
|
|
17247
|
-
return { ok: false, error:
|
|
17457
|
+
return { ok: false, error: classifyGoogleError(err) };
|
|
17248
17458
|
}
|
|
17249
17459
|
}
|
|
17250
17460
|
}),
|
|
@@ -17266,7 +17476,7 @@ function createGrindTools(ctx) {
|
|
|
17266
17476
|
await deleteCalendarEvent(googleConfig, eventId, calendarId ?? "primary");
|
|
17267
17477
|
return { ok: true, eventId };
|
|
17268
17478
|
} catch (err) {
|
|
17269
|
-
return { ok: false, error:
|
|
17479
|
+
return { ok: false, error: classifyGoogleError(err) };
|
|
17270
17480
|
}
|
|
17271
17481
|
}
|
|
17272
17482
|
}),
|
|
@@ -17387,7 +17597,7 @@ function createGrindTools(ctx) {
|
|
|
17387
17597
|
}
|
|
17388
17598
|
}),
|
|
17389
17599
|
list_forge_rules: tool({
|
|
17390
|
-
description: "List forge automation rules.
|
|
17600
|
+
description: "List forge automation rules. Each rule includes xpImpact: true/false — use this to decide how to communicate the action to the user. Always call this before updating, deleting, or running a specific rule.",
|
|
17391
17601
|
inputSchema: objectType({
|
|
17392
17602
|
enabledOnly: booleanType().optional().describe("When true, return only enabled rules."),
|
|
17393
17603
|
includeRecentRuns: booleanType().default(true).describe("When true, include recent execution history."),
|
|
@@ -17409,6 +17619,7 @@ function createGrindTools(ctx) {
|
|
|
17409
17619
|
enabled: rule.enabled,
|
|
17410
17620
|
triggerType: rule.triggerType,
|
|
17411
17621
|
actionType: rule.actionType,
|
|
17622
|
+
xpImpact: ["log-to-vault", "update-skill"].includes(rule.actionType),
|
|
17412
17623
|
triggerConfig: redactForgeValue(rule.triggerConfig),
|
|
17413
17624
|
actionConfig: redactForgeValue(rule.actionConfig),
|
|
17414
17625
|
updatedAt: rule.updatedAt
|
|
@@ -17483,14 +17694,14 @@ function createGrindTools(ctx) {
|
|
|
17483
17694
|
}
|
|
17484
17695
|
}),
|
|
17485
17696
|
create_forge_rule: tool({
|
|
17486
|
-
description: "Create a forge automation rule
|
|
17697
|
+
description: "Create a forge automation rule (queue-quest, log-to-vault, send-notification, run-script). send-notification and queue-quest have no XP impact — create autonomously. log-to-vault auto-awards XP — create it and mention that in your reply. run-script executes a shell script — always include the full script in your reply so the user can verify it.",
|
|
17487
17698
|
inputSchema: objectType({
|
|
17488
17699
|
name: stringType().min(2).max(128).describe("Human-readable rule name."),
|
|
17489
17700
|
triggerType: enumType(FORGE_TRIGGER_TYPES).describe("Trigger type (cron, event, signal, webhook, manual)."),
|
|
17490
17701
|
triggerConfig: recordType(stringType(), unknownType()).default({}).describe("Trigger configuration object."),
|
|
17491
17702
|
actionType: enumType(FORGE_ACTION_TYPES).describe("Action type (queue-quest, log-to-vault, send-notification)."),
|
|
17492
17703
|
actionConfig: recordType(stringType(), unknownType()).default({}).describe(
|
|
17493
|
-
"Action configuration object. send-notification: required 'message' (static string) or 'script' (shell command whose stdout becomes the message), plus 'channel' (telegram/console/webhook/whatsapp) and channel credentials. queue-quest: required 'questId'. log-to-vault: required 'activityType', 'durationMinutes'."
|
|
17704
|
+
"Action configuration object. send-notification: required 'message' (static string) or 'script' (shell command whose stdout becomes the message), plus 'channel' (telegram/console/webhook/whatsapp) and channel credentials. queue-quest: required 'questId'. log-to-vault: required 'activityType', 'durationMinutes'. run-script: required 'script' (shell command), optional 'timeout' (ms, default 30000), optional 'workdir' (supports ~)."
|
|
17494
17705
|
),
|
|
17495
17706
|
enabled: booleanType().default(true).describe("Whether the rule starts enabled.")
|
|
17496
17707
|
}),
|
|
@@ -17531,7 +17742,7 @@ function createGrindTools(ctx) {
|
|
|
17531
17742
|
}
|
|
17532
17743
|
}),
|
|
17533
17744
|
update_forge_rule: tool({
|
|
17534
|
-
description: "Update a forge rule by ID prefix or name.
|
|
17745
|
+
description: "Update a forge rule by ID prefix or name. Call list_forge_rules first to confirm the target. Act autonomously — no permission needed unless changing to a log-to-vault action, in which case mention the XP impact in your reply.",
|
|
17535
17746
|
inputSchema: objectType({
|
|
17536
17747
|
ruleSearch: stringType().min(1).describe("Rule ID prefix or rule name substring."),
|
|
17537
17748
|
name: stringType().min(2).max(128).optional().describe("New rule name."),
|
|
@@ -17601,6 +17812,7 @@ function createGrindTools(ctx) {
|
|
|
17601
17812
|
nextTriggerConfig = normalized.triggerConfig;
|
|
17602
17813
|
nextActionConfig = normalized.actionConfig;
|
|
17603
17814
|
}
|
|
17815
|
+
actionType ?? rule.actionType;
|
|
17604
17816
|
const updated = await updateForgeRule(ctx.db, ctx.userId, rule.id, {
|
|
17605
17817
|
...name16 !== void 0 ? { name: name16 } : {},
|
|
17606
17818
|
...triggerType !== void 0 ? { triggerType } : {},
|
|
@@ -17630,7 +17842,7 @@ function createGrindTools(ctx) {
|
|
|
17630
17842
|
}
|
|
17631
17843
|
}),
|
|
17632
17844
|
delete_forge_rule: tool({
|
|
17633
|
-
description: "Delete a forge rule by ID prefix or name.
|
|
17845
|
+
description: "Delete a forge rule by ID prefix or name. This is permanent — warn the user before calling this. Run history is also removed.",
|
|
17634
17846
|
inputSchema: objectType({
|
|
17635
17847
|
ruleSearch: stringType().min(1).describe("Rule ID prefix or rule name substring.")
|
|
17636
17848
|
}),
|
|
@@ -17639,6 +17851,12 @@ function createGrindTools(ctx) {
|
|
|
17639
17851
|
if (!rule) {
|
|
17640
17852
|
return { ok: false, error: `No forge rule matching "${ruleSearch}".` };
|
|
17641
17853
|
}
|
|
17854
|
+
const perm = await requirePermission(
|
|
17855
|
+
ctx,
|
|
17856
|
+
"delete_forge_rule",
|
|
17857
|
+
`Permanently delete forge rule "${rule.name}"?`
|
|
17858
|
+
);
|
|
17859
|
+
if (perm.denied) return { ok: false, error: "Deletion cancelled." };
|
|
17642
17860
|
const deleted = await deleteForgeRule(ctx.db, ctx.userId, rule.id);
|
|
17643
17861
|
if (!deleted) {
|
|
17644
17862
|
return { ok: false, error: "Failed to delete forge rule." };
|
|
@@ -17655,7 +17873,7 @@ function createGrindTools(ctx) {
|
|
|
17655
17873
|
}
|
|
17656
17874
|
}),
|
|
17657
17875
|
run_forge_rule: tool({
|
|
17658
|
-
description: "Run a forge rule immediately by ID prefix or name.
|
|
17876
|
+
description: "Run a forge rule immediately by ID prefix or name. Check xpImpact from list_forge_rules first: if false (notifications, reminders), run autonomously; if true (log-to-vault), run and mention the XP award in your reply.",
|
|
17659
17877
|
inputSchema: objectType({
|
|
17660
17878
|
ruleSearch: stringType().min(1).describe("Rule ID prefix or rule name substring."),
|
|
17661
17879
|
eventPayload: recordType(stringType(), unknownType()).optional().describe("Optional payload passed as event context for this run-now execution."),
|
|
@@ -17763,6 +17981,8 @@ function createGrindTools(ctx) {
|
|
|
17763
17981
|
baseXp: numberType().int().positive().default(10).describe("Base XP before multipliers. 10=small, 25=medium, 50=large, 100=epic")
|
|
17764
17982
|
}),
|
|
17765
17983
|
execute: async ({ title, description, type, difficulty, skillTags, baseXp }) => {
|
|
17984
|
+
const trust = requireTrust(ctx, "create_quest");
|
|
17985
|
+
if (trust.denied) return { error: trust.error };
|
|
17766
17986
|
const active = await listQuestsByUser(ctx.db, ctx.userId, ["active"]);
|
|
17767
17987
|
if (active.length >= 5) {
|
|
17768
17988
|
return { error: "Max 5 active quests. Complete or abandon one first." };
|
|
@@ -17798,6 +18018,8 @@ function createGrindTools(ctx) {
|
|
|
17798
18018
|
durationMinutes: numberType().int().positive().optional().describe("Duration in minutes if timer proof")
|
|
17799
18019
|
}),
|
|
17800
18020
|
execute: async ({ questSearch, proofType, durationMinutes }) => {
|
|
18021
|
+
const trust = requireTrust(ctx, "complete_quest");
|
|
18022
|
+
if (trust.denied) return { error: trust.error };
|
|
17801
18023
|
const quest = await findQuestByPrefix(ctx.db, ctx.userId, questSearch);
|
|
17802
18024
|
if (!quest) return { error: `No quest matching "${questSearch}"` };
|
|
17803
18025
|
if (quest.status === "completed") return { error: "Quest already completed" };
|
|
@@ -17834,6 +18056,8 @@ function createGrindTools(ctx) {
|
|
|
17834
18056
|
questSearch: stringType().describe("Quest ID prefix or title substring")
|
|
17835
18057
|
}),
|
|
17836
18058
|
execute: async ({ questSearch }) => {
|
|
18059
|
+
const trust = requireTrust(ctx, "abandon_quest");
|
|
18060
|
+
if (trust.denied) return { error: trust.error };
|
|
17837
18061
|
const quest = await findQuestByPrefix(ctx.db, ctx.userId, questSearch);
|
|
17838
18062
|
if (!quest) return { error: `No quest matching "${questSearch}"` };
|
|
17839
18063
|
if (quest.status !== "active") return { error: `Quest is ${quest.status}, not active` };
|
|
@@ -17851,6 +18075,8 @@ function createGrindTools(ctx) {
|
|
|
17851
18075
|
questSearch: stringType().describe("Quest ID prefix or title substring")
|
|
17852
18076
|
}),
|
|
17853
18077
|
execute: async ({ questSearch }) => {
|
|
18078
|
+
const trust = requireTrust(ctx, "start_timer");
|
|
18079
|
+
if (trust.denied) return { error: trust.error };
|
|
17854
18080
|
const existing = readTimer(ctx.timerPath);
|
|
17855
18081
|
if (existing) {
|
|
17856
18082
|
const elapsed = formatElapsed(existing.startedAt);
|
|
@@ -17876,6 +18102,8 @@ function createGrindTools(ctx) {
|
|
|
17876
18102
|
complete: booleanType().default(false).describe("Whether to also complete the quest with timer-duration proof")
|
|
17877
18103
|
}),
|
|
17878
18104
|
execute: async ({ complete }) => {
|
|
18105
|
+
const trust = requireTrust(ctx, "stop_timer");
|
|
18106
|
+
if (trust.denied) return { error: trust.error };
|
|
17879
18107
|
const timer = readTimer(ctx.timerPath);
|
|
17880
18108
|
if (!timer) return { error: "No timer running" };
|
|
17881
18109
|
const elapsed = getElapsedMinutes(timer.startedAt);
|
|
@@ -18611,6 +18839,174 @@ ${normalized}` : normalized;
|
|
|
18611
18839
|
return { pattern, matches, totalMatches: matches.length, truncated };
|
|
18612
18840
|
}
|
|
18613
18841
|
}),
|
|
18842
|
+
list_skills: tool({
|
|
18843
|
+
description: "List all skills with current XP, level, and category. Use this to see the full skill breakdown before making quest suggestions or updates.",
|
|
18844
|
+
inputSchema: objectType({}),
|
|
18845
|
+
execute: async () => {
|
|
18846
|
+
const allSkills = await listSkillsByUser(ctx.db, ctx.userId);
|
|
18847
|
+
return allSkills.map((s) => ({
|
|
18848
|
+
id: s.id.slice(0, 8),
|
|
18849
|
+
name: s.name,
|
|
18850
|
+
category: s.category,
|
|
18851
|
+
xp: s.xp,
|
|
18852
|
+
level: s.level
|
|
18853
|
+
}));
|
|
18854
|
+
}
|
|
18855
|
+
}),
|
|
18856
|
+
list_quest_logs: tool({
|
|
18857
|
+
description: "List recent quest completion history. Returns completions with XP earned, duration, and proof type. Use this to see what the user has actually done recently.",
|
|
18858
|
+
inputSchema: objectType({
|
|
18859
|
+
limit: numberType().int().min(1).max(100).default(20).describe("Maximum number of logs to return"),
|
|
18860
|
+
sinceDaysAgo: numberType().int().min(1).max(90).optional().describe("Only return logs from this many days ago")
|
|
18861
|
+
}),
|
|
18862
|
+
execute: async ({ limit, sinceDaysAgo }) => {
|
|
18863
|
+
const since = sinceDaysAgo ? Date.now() - sinceDaysAgo * 24 * 60 * 60 * 1e3 : void 0;
|
|
18864
|
+
const logs = await listQuestLogs(ctx.db, ctx.userId, {
|
|
18865
|
+
limit,
|
|
18866
|
+
...since ? { since } : {}
|
|
18867
|
+
});
|
|
18868
|
+
const questIds = [...new Set(logs.map((l) => l.questId))];
|
|
18869
|
+
const questTitles = {};
|
|
18870
|
+
for (const qid of questIds) {
|
|
18871
|
+
const q = await getQuestById(ctx.db, qid);
|
|
18872
|
+
if (q) questTitles[qid] = q.title;
|
|
18873
|
+
}
|
|
18874
|
+
return logs.map((l) => ({
|
|
18875
|
+
questTitle: questTitles[l.questId] ?? l.questId.slice(0, 8),
|
|
18876
|
+
completedAt: new Date(l.completedAt).toLocaleDateString(),
|
|
18877
|
+
xpEarned: l.xpEarned,
|
|
18878
|
+
durationMinutes: l.durationMinutes ?? null,
|
|
18879
|
+
proofType: l.proofType,
|
|
18880
|
+
streakDay: l.streakDay
|
|
18881
|
+
}));
|
|
18882
|
+
}
|
|
18883
|
+
}),
|
|
18884
|
+
list_signals: tool({
|
|
18885
|
+
description: "List recently detected signals (git commits, file changes, process observations, webhook events). Use this to understand what the user has been doing passively.",
|
|
18886
|
+
inputSchema: objectType({
|
|
18887
|
+
limit: numberType().int().min(1).max(50).default(20).describe("Maximum number of signals to return"),
|
|
18888
|
+
source: enumType(["git", "file", "process", "webhook"]).optional().describe("Filter by signal source")
|
|
18889
|
+
}),
|
|
18890
|
+
execute: async ({ limit, source }) => {
|
|
18891
|
+
const sigs = await listSignals(ctx.db, ctx.userId, {
|
|
18892
|
+
limit,
|
|
18893
|
+
...source ? { source } : {}
|
|
18894
|
+
});
|
|
18895
|
+
return sigs.map((s) => ({
|
|
18896
|
+
source: s.source,
|
|
18897
|
+
type: s.type,
|
|
18898
|
+
confidence: s.confidence,
|
|
18899
|
+
detectedAt: new Date(s.detectedAt).toLocaleString(),
|
|
18900
|
+
payload: s.payload
|
|
18901
|
+
}));
|
|
18902
|
+
}
|
|
18903
|
+
}),
|
|
18904
|
+
update_quest: tool({
|
|
18905
|
+
description: "Update quest details: title, description, difficulty, type, baseXp, skillTags, or schedule. Does NOT change quest status — use abandon_quest or complete_quest for that.",
|
|
18906
|
+
inputSchema: objectType({
|
|
18907
|
+
questSearch: stringType().describe("Quest ID prefix or title substring"),
|
|
18908
|
+
title: stringType().min(1).max(256).optional().describe("New quest title"),
|
|
18909
|
+
description: stringType().max(2e3).nullable().optional().describe("New description"),
|
|
18910
|
+
type: enumType(["daily", "weekly", "epic", "bounty", "chain", "ritual"]).optional().describe("New quest type"),
|
|
18911
|
+
difficulty: enumType(["easy", "medium", "hard", "epic"]).optional().describe("New difficulty"),
|
|
18912
|
+
skillTags: arrayType(stringType()).optional().describe("Replace skill tags"),
|
|
18913
|
+
baseXp: numberType().int().positive().optional().describe("New base XP (before multipliers)"),
|
|
18914
|
+
scheduleCron: stringType().nullable().optional().describe("New cron schedule (null to remove)")
|
|
18915
|
+
}).refine(
|
|
18916
|
+
(v) => v.title !== void 0 || v.description !== void 0 || v.type !== void 0 || v.difficulty !== void 0 || v.skillTags !== void 0 || v.baseXp !== void 0 || v.scheduleCron !== void 0,
|
|
18917
|
+
{ message: "Provide at least one field to update." }
|
|
18918
|
+
),
|
|
18919
|
+
execute: async ({
|
|
18920
|
+
questSearch,
|
|
18921
|
+
title,
|
|
18922
|
+
description,
|
|
18923
|
+
type,
|
|
18924
|
+
difficulty,
|
|
18925
|
+
skillTags,
|
|
18926
|
+
baseXp,
|
|
18927
|
+
scheduleCron
|
|
18928
|
+
}) => {
|
|
18929
|
+
const trust = requireTrust(ctx, "update_quest");
|
|
18930
|
+
if (trust.denied) return { error: trust.error };
|
|
18931
|
+
const quest = await findQuestByPrefix(ctx.db, ctx.userId, questSearch);
|
|
18932
|
+
if (!quest) return { error: `No quest matching "${questSearch}"` };
|
|
18933
|
+
const updated = await updateQuest(ctx.db, quest.id, ctx.userId, {
|
|
18934
|
+
...title !== void 0 ? { title } : {},
|
|
18935
|
+
...description !== void 0 ? { description } : {},
|
|
18936
|
+
...type !== void 0 ? { type } : {},
|
|
18937
|
+
...difficulty !== void 0 ? { difficulty } : {},
|
|
18938
|
+
...skillTags !== void 0 ? { skillTags } : {},
|
|
18939
|
+
...baseXp !== void 0 ? { baseXp } : {},
|
|
18940
|
+
...scheduleCron !== void 0 ? { scheduleCron } : {}
|
|
18941
|
+
});
|
|
18942
|
+
if (!updated) return { error: "Failed to update quest." };
|
|
18943
|
+
return {
|
|
18944
|
+
ok: true,
|
|
18945
|
+
id: updated.id.slice(0, 8),
|
|
18946
|
+
title: updated.title,
|
|
18947
|
+
type: updated.type,
|
|
18948
|
+
difficulty: updated.difficulty,
|
|
18949
|
+
skillTags: updated.skillTags,
|
|
18950
|
+
baseXp: updated.baseXp
|
|
18951
|
+
};
|
|
18952
|
+
}
|
|
18953
|
+
}),
|
|
18954
|
+
activate_quest: tool({
|
|
18955
|
+
description: "Move a quest from 'available' to 'active'. Use when the user is ready to start working on a quest that isn't active yet. Subject to the 5-quest active limit.",
|
|
18956
|
+
inputSchema: objectType({
|
|
18957
|
+
questSearch: stringType().describe("Quest ID prefix or title substring")
|
|
18958
|
+
}),
|
|
18959
|
+
execute: async ({ questSearch }) => {
|
|
18960
|
+
const trust = requireTrust(ctx, "activate_quest");
|
|
18961
|
+
if (trust.denied) return { error: trust.error };
|
|
18962
|
+
const quest = await findQuestByPrefix(ctx.db, ctx.userId, questSearch);
|
|
18963
|
+
if (!quest) return { error: `No quest matching "${questSearch}"` };
|
|
18964
|
+
if (quest.status === "active") return { error: "Quest is already active." };
|
|
18965
|
+
if (quest.status === "completed")
|
|
18966
|
+
return { error: "Quest is completed — cannot reactivate." };
|
|
18967
|
+
if (quest.status === "abandoned")
|
|
18968
|
+
return { error: "Quest is abandoned — cannot reactivate." };
|
|
18969
|
+
const active = await listQuestsByUser(ctx.db, ctx.userId, ["active"]);
|
|
18970
|
+
if (active.length >= 5) {
|
|
18971
|
+
return { error: "Max 5 active quests. Complete or abandon one first." };
|
|
18972
|
+
}
|
|
18973
|
+
await updateQuestStatus(ctx.db, quest.id, ctx.userId, "active");
|
|
18974
|
+
return { ok: true, quest: quest.title, status: "active" };
|
|
18975
|
+
}
|
|
18976
|
+
}),
|
|
18977
|
+
delete_insight: tool({
|
|
18978
|
+
description: "Delete a stored companion insight. Only AI-observed insights can be deleted. User-stated insights are permanent and cannot be removed by the companion.",
|
|
18979
|
+
inputSchema: objectType({
|
|
18980
|
+
insightId: stringType().min(1).describe("Insight ID (full UUID or short ID prefix)")
|
|
18981
|
+
}),
|
|
18982
|
+
execute: async ({ insightId }) => {
|
|
18983
|
+
const trust = requireTrust(ctx, "delete_insight");
|
|
18984
|
+
if (trust.denied) return { error: trust.error };
|
|
18985
|
+
const insights = await listCompanionInsights(ctx.db, ctx.userId, 200);
|
|
18986
|
+
const match = insights.find((i) => i.id === insightId || i.id.startsWith(insightId));
|
|
18987
|
+
if (!match) return { error: `No insight matching "${insightId}"` };
|
|
18988
|
+
if (match.source === "user-stated") {
|
|
18989
|
+
return {
|
|
18990
|
+
error: "Cannot delete user-stated insights. These are facts you provided — they can only be updated, not removed."
|
|
18991
|
+
};
|
|
18992
|
+
}
|
|
18993
|
+
const deleted = await deleteCompanionInsight(ctx.db, match.id, ctx.userId);
|
|
18994
|
+
if (!deleted) return { error: "Failed to delete insight." };
|
|
18995
|
+
return { ok: true, deleted: true, content: match.content };
|
|
18996
|
+
}
|
|
18997
|
+
}),
|
|
18998
|
+
update_companion_mode: tool({
|
|
18999
|
+
description: "Change the companion operating mode. 'suggest' = propose only, 'assist' = act on explicit requests, 'auto' = act proactively.",
|
|
19000
|
+
inputSchema: objectType({
|
|
19001
|
+
mode: enumType(["off", "suggest", "assist", "auto"]).describe("New companion mode")
|
|
19002
|
+
}),
|
|
19003
|
+
execute: async ({ mode }) => {
|
|
19004
|
+
const trust = requireTrust(ctx, "update_companion_mode");
|
|
19005
|
+
if (trust.denied) return { error: trust.error };
|
|
19006
|
+
const updated = await updateCompanionMode(ctx.db, ctx.userId, mode);
|
|
19007
|
+
return { ok: true, mode: updated.mode };
|
|
19008
|
+
}
|
|
19009
|
+
}),
|
|
18614
19010
|
bash: tool({
|
|
18615
19011
|
description: "Execute a shell command. Returns stdout, stderr, and exit code. Both stdout and stderr are capped at 50KB.",
|
|
18616
19012
|
inputSchema: objectType({
|
|
@@ -19009,7 +19405,8 @@ const streamMessage = createServerFn({
|
|
|
19009
19405
|
db,
|
|
19010
19406
|
userId,
|
|
19011
19407
|
timerPath,
|
|
19012
|
-
config: config2
|
|
19408
|
+
config: config2,
|
|
19409
|
+
trustLevel: companionRow?.trustLevel ?? 0
|
|
19013
19410
|
};
|
|
19014
19411
|
const promptCtxBase = {
|
|
19015
19412
|
user,
|