pikiclaw 0.3.39 → 0.3.40
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/agent/drivers/claude.js +57 -0
- package/dist/agent/drivers/codex.js +7 -1
- package/dist/agent/index.js +2 -0
- package/dist/bot/bot.js +83 -8
- package/dist/bot/commands.js +24 -4
- package/dist/dashboard/routes/sessions.js +1 -1
- package/dist/dashboard/session-control.js +75 -2
- package/package.json +1 -1
|
@@ -1618,6 +1618,63 @@ function getClaudeUsageFromTelemetry(home, model) {
|
|
|
1618
1618
|
const windows = [{ label: ageLabel, usedPercent: null, remainingPercent: null, resetAt, resetAfterSeconds, status }];
|
|
1619
1619
|
return { ok: true, agent: 'claude', source: 'telemetry', capturedAt: chosen.capturedAt, status, windows, error: null };
|
|
1620
1620
|
}
|
|
1621
|
+
function claudeSessionTranscriptPath(workdir, sessionId) {
|
|
1622
|
+
const home = getHome();
|
|
1623
|
+
if (!home || !workdir || !sessionId)
|
|
1624
|
+
return '';
|
|
1625
|
+
return path.join(home, '.claude', 'projects', encodePathAsDirName(workdir), `${sessionId}.jsonl`);
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Scan a claude session transcript for the latest native /goal state. Returns
|
|
1629
|
+
* null when no `goal_status` attachment is present.
|
|
1630
|
+
*/
|
|
1631
|
+
export function getClaudeNativeGoal(workdir, sessionId) {
|
|
1632
|
+
const file = claudeSessionTranscriptPath(workdir, sessionId);
|
|
1633
|
+
if (!file || !fs.existsSync(file))
|
|
1634
|
+
return null;
|
|
1635
|
+
// Goal status lines are tiny attachments. Walk the tail (1 MB) to find the
|
|
1636
|
+
// last one — tail covers all realistic session sizes without parsing every
|
|
1637
|
+
// line of a long transcript.
|
|
1638
|
+
const lines = readTailLines(file, 1024 * 1024);
|
|
1639
|
+
let latest = null;
|
|
1640
|
+
for (const raw of lines) {
|
|
1641
|
+
if (!raw || raw[0] !== '{')
|
|
1642
|
+
continue;
|
|
1643
|
+
// Cheap pre-filter so we only JSON.parse the relevant subset.
|
|
1644
|
+
if (!raw.includes('"goal_status"'))
|
|
1645
|
+
continue;
|
|
1646
|
+
try {
|
|
1647
|
+
const ev = JSON.parse(raw);
|
|
1648
|
+
const att = ev?.attachment;
|
|
1649
|
+
if (!att || att.type !== 'goal_status')
|
|
1650
|
+
continue;
|
|
1651
|
+
const condition = typeof att.condition === 'string' ? att.condition : '';
|
|
1652
|
+
const met = !!att.met;
|
|
1653
|
+
const ts = typeof ev.timestamp === 'string' ? Date.parse(ev.timestamp) : NaN;
|
|
1654
|
+
latest = {
|
|
1655
|
+
condition,
|
|
1656
|
+
met,
|
|
1657
|
+
status: met || !condition ? 'complete' : 'active',
|
|
1658
|
+
updatedAtMs: Number.isFinite(ts) ? ts : Date.now(),
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
catch { /* skip */ }
|
|
1662
|
+
}
|
|
1663
|
+
// After auto-clear (met:true) claude still leaves the goal_status line in the
|
|
1664
|
+
// transcript; pikiclaw treats "no active goal" as null so the bridge mirrors
|
|
1665
|
+
// the codex semantics where `goal_get` returns null after a clear.
|
|
1666
|
+
if (latest && latest.met)
|
|
1667
|
+
return null;
|
|
1668
|
+
return latest;
|
|
1669
|
+
}
|
|
1670
|
+
/** Build the user-prompt that triggers claude's native `/goal <condition>` slash command. */
|
|
1671
|
+
export function buildClaudeSetGoalPrompt(objective) {
|
|
1672
|
+
return `/goal ${objective.trim()}`;
|
|
1673
|
+
}
|
|
1674
|
+
/** Build the user-prompt that triggers claude's native `/goal clear` slash command. */
|
|
1675
|
+
export function buildClaudeClearGoalPrompt() {
|
|
1676
|
+
return '/goal clear';
|
|
1677
|
+
}
|
|
1621
1678
|
// ---------------------------------------------------------------------------
|
|
1622
1679
|
// Driver
|
|
1623
1680
|
// ---------------------------------------------------------------------------
|
|
@@ -115,7 +115,13 @@ export class CodexAppServer {
|
|
|
115
115
|
}
|
|
116
116
|
this.pending.clear();
|
|
117
117
|
});
|
|
118
|
-
|
|
118
|
+
// Declare experimentalApi so `thread/goal/*` is reachable. Codex 0.130+
|
|
119
|
+
// gates these RPCs behind that capability — without it, every goal call
|
|
120
|
+
// returns "requires experimentalApi capability".
|
|
121
|
+
this.call('initialize', {
|
|
122
|
+
clientInfo: { name: 'pikiclaw', version: '0.2.0' },
|
|
123
|
+
capabilities: { experimentalApi: true },
|
|
124
|
+
})
|
|
119
125
|
.then(resp => {
|
|
120
126
|
clearTimeout(timer);
|
|
121
127
|
if (resp.error) {
|
package/dist/agent/index.js
CHANGED
|
@@ -32,6 +32,8 @@ export { getProjectSkillPaths, initializeProjectSkills, listSkills, getGlobalSki
|
|
|
32
32
|
export { readGoal, writeGoal, clearGoal, setGoal, pauseGoal, resumeGoal, completeGoal, accountTurn, bumpContinuationCount, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, sessionGoalPath, DEFAULT_MAX_CONTINUATIONS, } from './goal.js';
|
|
33
33
|
// ── Re-export: native codex goal bridge ──────────────────────────────────────
|
|
34
34
|
export { setCodexGoal, getCodexGoal, clearCodexGoal, pauseCodexGoal, resumeCodexGoal, } from './drivers/codex.js';
|
|
35
|
+
// ── Re-export: native claude goal bridge ─────────────────────────────────────
|
|
36
|
+
export { getClaudeNativeGoal, buildClaudeSetGoalPrompt, buildClaudeClearGoalPrompt, } from './drivers/claude.js';
|
|
35
37
|
// ── Re-export: MCP extensions ───────────────────────────────────────────────
|
|
36
38
|
export { listAllMcpExtensions, addGlobalMcpExtension, removeGlobalMcpExtension, updateGlobalMcpExtension, addWorkspaceMcpExtension, removeWorkspaceMcpExtension, updateWorkspaceMcpExtension, loadGlobalMcpExtensions, loadWorkspaceMcpExtensions, getCatalogItems, getCatalogItem, buildInstalledConfigFromRecommended, checkMcpHealth, getCachedHealth, cacheHealth, } from './mcp/extensions.js';
|
|
37
39
|
export { getRecommendedMcpServers, getRecommendedMcpServer, getRecommendedSkillRepos, searchMcpServers, searchSkills as searchSkillRepos, } from './mcp/registry.js';
|
package/dist/bot/bot.js
CHANGED
|
@@ -7,7 +7,7 @@ import os from 'node:os';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { execSync, spawn } from 'node:child_process';
|
|
9
9
|
import { getActiveUserConfig, loadWorkspaces, onUserConfigChange, resolveUserWorkdir, setUserWorkdir, updateUserConfig } from '../core/config/user-config.js';
|
|
10
|
-
import { doStream, ensureManagedSession, findManagedThreadSession, findThreadSessionAcrossAgents, getSessionStoredConfig, getUsage, initializeProjectSkills, listAgents, resolveAgentModels, listSkills, stageSessionFiles, reconcileOrphanedRunningSessions, getAgentBoundModelId, setAgentBoundModelId, collapseSkillPrompt, readGoal, accountTurn, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, bumpContinuationCount, pauseGoal, resumeGoal, setGoal as setGoalState, clearGoal as clearGoalState, setCodexGoal, getCodexGoal, clearCodexGoal, pauseCodexGoal, resumeCodexGoal, isPendingSessionId, } from '../agent/index.js';
|
|
10
|
+
import { doStream, ensureManagedSession, findManagedThreadSession, findThreadSessionAcrossAgents, getSessionStoredConfig, getUsage, initializeProjectSkills, listAgents, resolveAgentModels, listSkills, stageSessionFiles, reconcileOrphanedRunningSessions, getAgentBoundModelId, setAgentBoundModelId, collapseSkillPrompt, readGoal, accountTurn, shouldContinueAfterTurn, renderContinuationPrompt, renderBudgetLimitPrompt, bumpContinuationCount, pauseGoal, resumeGoal, setGoal as setGoalState, clearGoal as clearGoalState, setCodexGoal, getCodexGoal, clearCodexGoal, pauseCodexGoal, resumeCodexGoal, getClaudeNativeGoal, buildClaudeSetGoalPrompt, buildClaudeClearGoalPrompt, isPendingSessionId, } from '../agent/index.js';
|
|
11
11
|
import { querySessions, querySessionTail, updateSession, } from './session-hub.js';
|
|
12
12
|
import { getDriver, hasDriver, allDriverIds } from '../agent/driver.js';
|
|
13
13
|
import { resolveGuiIntegrationConfig } from '../agent/mcp/bridge.js';
|
|
@@ -142,6 +142,18 @@ function normalizeFromCodex(goal) {
|
|
|
142
142
|
continuationCount: null,
|
|
143
143
|
};
|
|
144
144
|
}
|
|
145
|
+
function normalizeFromClaudeNative(goal) {
|
|
146
|
+
return {
|
|
147
|
+
source: 'claude',
|
|
148
|
+
objective: goal.condition,
|
|
149
|
+
// Native /goal exposes no pause/budget — it's either active or absent.
|
|
150
|
+
status: 'active',
|
|
151
|
+
tokenBudget: null,
|
|
152
|
+
tokensUsed: 0,
|
|
153
|
+
timeUsedSeconds: 0,
|
|
154
|
+
continuationCount: null,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
145
157
|
// ---------------------------------------------------------------------------
|
|
146
158
|
// Bot
|
|
147
159
|
// ---------------------------------------------------------------------------
|
|
@@ -1333,14 +1345,14 @@ export class Bot {
|
|
|
1333
1345
|
* tasks that get cancelled or errored auto-pause the goal so the loop does
|
|
1334
1346
|
* not silently resume on the user's next message.
|
|
1335
1347
|
*
|
|
1336
|
-
* Codex sessions short-circuit:
|
|
1337
|
-
* lifecycle (state machine
|
|
1338
|
-
* pikiclaw stays out to avoid a double loop. See setSessionGoal
|
|
1339
|
-
* bridge to codex's `thread/goal/*` RPC
|
|
1340
|
-
* goal
|
|
1348
|
+
* Codex and Claude sessions short-circuit: each runs its own native `/goal`
|
|
1349
|
+
* lifecycle (codex's app-server state machine; claude's in-process Stop
|
|
1350
|
+
* hook), so pikiclaw stays out to avoid a double loop. See setSessionGoal
|
|
1351
|
+
* et al — they bridge to codex's `thread/goal/*` RPC and to claude's
|
|
1352
|
+
* `/goal <condition>` slash command instead of writing pikiclaw's goal.json.
|
|
1341
1353
|
*/
|
|
1342
1354
|
maybeEnqueueGoalContinuation(session, opts, result) {
|
|
1343
|
-
if (session.agent === 'codex')
|
|
1355
|
+
if (session.agent === 'codex' || session.agent === 'claude')
|
|
1344
1356
|
return;
|
|
1345
1357
|
const sessionId = (result.sessionId || session.sessionId || '').trim();
|
|
1346
1358
|
if (!sessionId || isPendingSessionId(sessionId))
|
|
@@ -1420,6 +1432,12 @@ export class Bot {
|
|
|
1420
1432
|
const goal = await getCodexGoal(sessionId);
|
|
1421
1433
|
return goal ? normalizeFromCodex(goal) : null;
|
|
1422
1434
|
}
|
|
1435
|
+
if (agent === 'claude') {
|
|
1436
|
+
if (!sessionId || isPendingSessionId(sessionId))
|
|
1437
|
+
return null;
|
|
1438
|
+
const goal = getClaudeNativeGoal(workdir, sessionId);
|
|
1439
|
+
return goal ? normalizeFromClaudeNative(goal) : null;
|
|
1440
|
+
}
|
|
1423
1441
|
const goal = readGoal(workdir, agent, sessionId);
|
|
1424
1442
|
return goal ? normalizeFromPikiclaw(goal) : null;
|
|
1425
1443
|
}
|
|
@@ -1448,6 +1466,37 @@ export class Bot {
|
|
|
1448
1466
|
throw new Error('codex did not return a goal snapshot');
|
|
1449
1467
|
return normalizeFromCodex(goal);
|
|
1450
1468
|
}
|
|
1469
|
+
if (agent === 'claude') {
|
|
1470
|
+
if (!sessionId || isPendingSessionId(sessionId)) {
|
|
1471
|
+
throw new Error('claude session must exist before /goal — send a first message to create the transcript');
|
|
1472
|
+
}
|
|
1473
|
+
// Native /goal owns its own continuation engine (Stop hook). pikiclaw
|
|
1474
|
+
// just submits the slash command as the next task; claude internally
|
|
1475
|
+
// sets up the goal_status attachment, injects its meta directive, and
|
|
1476
|
+
// keeps looping until the Haiku completion check returns met. Token
|
|
1477
|
+
// budget is accepted in the API for shape parity with codex/portable
|
|
1478
|
+
// but ignored — claude native /goal has no budget concept.
|
|
1479
|
+
const objective = opts.objective.trim();
|
|
1480
|
+
if (!objective)
|
|
1481
|
+
throw new Error('objective must be non-empty');
|
|
1482
|
+
this.submitSessionTask({
|
|
1483
|
+
agent,
|
|
1484
|
+
sessionId,
|
|
1485
|
+
workdir,
|
|
1486
|
+
prompt: buildClaudeSetGoalPrompt(objective),
|
|
1487
|
+
chatId: opts.chatId,
|
|
1488
|
+
modelId: opts.modelId,
|
|
1489
|
+
thinkingEffort: opts.thinkingEffort,
|
|
1490
|
+
});
|
|
1491
|
+
// Return an optimistic snapshot — the actual goal_status attachment is
|
|
1492
|
+
// written by claude during the task; readers can poll getSessionGoal.
|
|
1493
|
+
return normalizeFromClaudeNative({
|
|
1494
|
+
condition: objective,
|
|
1495
|
+
status: 'active',
|
|
1496
|
+
met: false,
|
|
1497
|
+
updatedAtMs: Date.now(),
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1451
1500
|
const goal = setGoalState(workdir, agent, sessionId, {
|
|
1452
1501
|
objective: opts.objective,
|
|
1453
1502
|
tokenBudget: opts.tokenBudget ?? null,
|
|
@@ -1477,6 +1526,11 @@ export class Bot {
|
|
|
1477
1526
|
const goal = resp.goal ?? (await getCodexGoal(sessionId));
|
|
1478
1527
|
return goal ? normalizeFromCodex(goal) : null;
|
|
1479
1528
|
}
|
|
1529
|
+
if (agent === 'claude') {
|
|
1530
|
+
// Claude's native /goal exposes no pause/resume — only set and clear.
|
|
1531
|
+
// Surface a clear error so the IM layer can render a friendly message.
|
|
1532
|
+
throw new Error('Claude native /goal does not support pause/resume — only `/goal clear`. Re-issue `/goal <objective>` to start fresh.');
|
|
1533
|
+
}
|
|
1480
1534
|
const goal = pauseGoal(workdir, agent, sessionId);
|
|
1481
1535
|
return goal ? normalizeFromPikiclaw(goal) : null;
|
|
1482
1536
|
}
|
|
@@ -1490,6 +1544,9 @@ export class Bot {
|
|
|
1490
1544
|
const goal = resp.goal ?? (await getCodexGoal(sessionId));
|
|
1491
1545
|
return goal ? normalizeFromCodex(goal) : null;
|
|
1492
1546
|
}
|
|
1547
|
+
if (agent === 'claude') {
|
|
1548
|
+
throw new Error('Claude native /goal does not support pause/resume — re-issue `/goal <objective>` to start fresh.');
|
|
1549
|
+
}
|
|
1493
1550
|
const goal = resumeGoal(workdir, agent, sessionId);
|
|
1494
1551
|
if (!goal || goal.status !== 'active')
|
|
1495
1552
|
return goal ? normalizeFromPikiclaw(goal) : null;
|
|
@@ -1508,7 +1565,7 @@ export class Bot {
|
|
|
1508
1565
|
}
|
|
1509
1566
|
return normalizeFromPikiclaw(goal);
|
|
1510
1567
|
}
|
|
1511
|
-
async clearSessionGoal(workdir, agent, sessionId) {
|
|
1568
|
+
async clearSessionGoal(workdir, agent, sessionId, opts = {}) {
|
|
1512
1569
|
if (agent === 'codex') {
|
|
1513
1570
|
if (!sessionId || isPendingSessionId(sessionId))
|
|
1514
1571
|
return;
|
|
@@ -1517,6 +1574,24 @@ export class Bot {
|
|
|
1517
1574
|
throw new Error(resp.error);
|
|
1518
1575
|
return;
|
|
1519
1576
|
}
|
|
1577
|
+
if (agent === 'claude') {
|
|
1578
|
+
if (!sessionId || isPendingSessionId(sessionId))
|
|
1579
|
+
return;
|
|
1580
|
+
// Read goal-status first to avoid spawning a no-op turn when nothing is set.
|
|
1581
|
+
const existing = getClaudeNativeGoal(workdir, sessionId);
|
|
1582
|
+
if (!existing)
|
|
1583
|
+
return;
|
|
1584
|
+
this.submitSessionTask({
|
|
1585
|
+
agent,
|
|
1586
|
+
sessionId,
|
|
1587
|
+
workdir,
|
|
1588
|
+
prompt: buildClaudeClearGoalPrompt(),
|
|
1589
|
+
chatId: opts.chatId,
|
|
1590
|
+
modelId: opts.modelId,
|
|
1591
|
+
thinkingEffort: opts.thinkingEffort,
|
|
1592
|
+
});
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1520
1595
|
clearGoalState(workdir, agent, sessionId);
|
|
1521
1596
|
}
|
|
1522
1597
|
cancelTask(taskId) {
|
package/dist/bot/commands.js
CHANGED
|
@@ -72,8 +72,11 @@ export function getWorkspacesData(bot, chatId) {
|
|
|
72
72
|
* the IM renderer to send back. Returns null when there is no active session
|
|
73
73
|
* for the chat (caller renders its own "pick a session first" message).
|
|
74
74
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
75
|
+
* Per-agent routing:
|
|
76
|
+
* - codex → native `thread/goal/*` RPC (state machine + budget + pause/resume)
|
|
77
|
+
* - claude → native `/goal <condition>` slash command (Stop hook continuation,
|
|
78
|
+
* auto-clear on completion, no budget / no pause/resume)
|
|
79
|
+
* - others → pikiclaw's portable goal.json with continuation injection
|
|
77
80
|
*/
|
|
78
81
|
export async function handleGoalCommand(bot, chatId, rawArgs) {
|
|
79
82
|
const session = bot.selectedSession(chatId);
|
|
@@ -104,12 +107,17 @@ export async function handleGoalCommand(bot, chatId, rawArgs) {
|
|
|
104
107
|
return `Resumed goal: ${truncate(goal.objective, 80)}`;
|
|
105
108
|
}
|
|
106
109
|
if (lower === 'clear' || lower === 'cancel' || lower === 'stop') {
|
|
107
|
-
await bot.clearSessionGoal(workdir, agent, sessionId);
|
|
108
|
-
return
|
|
110
|
+
await bot.clearSessionGoal(workdir, agent, sessionId, { chatId });
|
|
111
|
+
return agent === 'claude'
|
|
112
|
+
? 'Submitted `/goal clear` to claude. (Native /goal auto-clears once the condition is met, so this is only needed to stop early.)'
|
|
113
|
+
: 'Cleared goal.';
|
|
109
114
|
}
|
|
110
115
|
const { objective, tokenBudget } = parseObjective(args);
|
|
111
116
|
if (!objective)
|
|
112
117
|
return 'Usage: /goal <objective> (or pause / resume / clear)';
|
|
118
|
+
if (agent === 'claude' && tokenBudget != null) {
|
|
119
|
+
return 'Claude native /goal does not support `budget=N` — drop the budget prefix. (Use a codex session if you need a token budget.)';
|
|
120
|
+
}
|
|
113
121
|
const goal = await bot.setSessionGoal(workdir, agent, sessionId, {
|
|
114
122
|
objective,
|
|
115
123
|
tokenBudget,
|
|
@@ -122,6 +130,12 @@ export async function handleGoalCommand(bot, chatId, rawArgs) {
|
|
|
122
130
|
'Send any message to trigger codex\'s native continuation loop. Each message resumes the thread and codex audits / continues until it marks the goal complete or hits the budget.',
|
|
123
131
|
].join('\n');
|
|
124
132
|
}
|
|
133
|
+
if (agent === 'claude') {
|
|
134
|
+
return [
|
|
135
|
+
`Goal set (claude native): ${truncate(goal.objective, 120)}`,
|
|
136
|
+
'Claude\'s in-process Stop hook keeps working until a Haiku judge confirms the condition is met, then auto-clears. Send `/goal clear` to stop early; `/goal` to inspect.',
|
|
137
|
+
].join('\n');
|
|
138
|
+
}
|
|
125
139
|
return `Goal set${budgetLabel}: ${truncate(goal.objective, 120)}\nThe agent will keep working until it audits the objective complete${goal.tokenBudget != null ? ' or exhausts the budget' : ''}.`;
|
|
126
140
|
}
|
|
127
141
|
catch (e) {
|
|
@@ -131,6 +145,12 @@ export async function handleGoalCommand(bot, chatId, rawArgs) {
|
|
|
131
145
|
function formatGoalStatusLine(goal, agent) {
|
|
132
146
|
if (!goal)
|
|
133
147
|
return 'No goal set for this session. Use `/goal <objective>` to set one.';
|
|
148
|
+
if (goal.source === 'claude') {
|
|
149
|
+
return [
|
|
150
|
+
`Goal: ${truncate(goal.objective, 200)}`,
|
|
151
|
+
`Status: ${goal.status} · claude native (Stop hook, auto-clears on completion)`,
|
|
152
|
+
].join('\n');
|
|
153
|
+
}
|
|
134
154
|
const budget = goal.tokenBudget != null
|
|
135
155
|
? `${goal.tokensUsed}/${goal.tokenBudget} tokens`
|
|
136
156
|
: `${goal.tokensUsed} tokens (no budget)`;
|
|
@@ -437,7 +437,7 @@ app.get('/api/session-hub/skills', (c) => {
|
|
|
437
437
|
app.post('/api/session-hub/session/send', async (c) => {
|
|
438
438
|
try {
|
|
439
439
|
const { workdir, agent, sessionId, prompt, model, effort, attachments, cleanup } = await parseSessionSendRequest(c);
|
|
440
|
-
const queued = queueDashboardSessionTask({
|
|
440
|
+
const queued = await queueDashboardSessionTask({
|
|
441
441
|
workdir,
|
|
442
442
|
agent,
|
|
443
443
|
sessionId,
|
|
@@ -2,10 +2,38 @@
|
|
|
2
2
|
* Public session task control surface for dashboard and API routes.
|
|
3
3
|
*/
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { getProjectSkillPaths, listSkills, stageSessionFiles, ensureManagedSession, getDriverCapabilities } from '../agent/index.js';
|
|
5
|
+
import { getProjectSkillPaths, listSkills, stageSessionFiles, ensureManagedSession, getDriverCapabilities, isPendingSessionId } from '../agent/index.js';
|
|
6
6
|
import { loadUserConfig } from '../core/config/user-config.js';
|
|
7
7
|
import { runtime } from './runtime.js';
|
|
8
8
|
const KNOWN_AGENTS = new Set(['claude', 'codex', 'gemini', 'hermes']);
|
|
9
|
+
/**
|
|
10
|
+
* Parse a `/goal[ args]` prompt typed in the dashboard chat box. Returns null
|
|
11
|
+
* when the prompt is not a goal slash command. Sub-commands mirror the IM
|
|
12
|
+
* `handleGoalCommand` semantics (set / clear / pause / resume / status).
|
|
13
|
+
*
|
|
14
|
+
* Routing /goal through the native bridge is the dashboard's analog of what
|
|
15
|
+
* channels/{telegram,feishu,weixin}/bot.ts do via `handleGoalCommand` — before
|
|
16
|
+
* this hook, dashboard /goal was matched by the legacy `goal` skill resolver
|
|
17
|
+
* and silently rewritten to "Read SKILL.md and execute", which bypassed both
|
|
18
|
+
* the claude native /goal slash command and codex's thread/goal RPC.
|
|
19
|
+
*/
|
|
20
|
+
function parseGoalSlash(prompt) {
|
|
21
|
+
const trimmed = prompt.trim();
|
|
22
|
+
const m = trimmed.match(/^\/goal(?:\s+([\s\S]*))?$/);
|
|
23
|
+
if (!m)
|
|
24
|
+
return null;
|
|
25
|
+
const args = (m[1] || '').trim();
|
|
26
|
+
if (!args)
|
|
27
|
+
return { action: 'status', objective: '' };
|
|
28
|
+
const lower = args.toLowerCase();
|
|
29
|
+
if (lower === 'clear' || lower === 'cancel' || lower === 'stop')
|
|
30
|
+
return { action: 'clear', objective: '' };
|
|
31
|
+
if (lower === 'pause')
|
|
32
|
+
return { action: 'pause', objective: '' };
|
|
33
|
+
if (lower === 'resume')
|
|
34
|
+
return { action: 'resume', objective: '' };
|
|
35
|
+
return { action: 'set', objective: args };
|
|
36
|
+
}
|
|
9
37
|
/**
|
|
10
38
|
* Resolve a `/skill-name [args]` prompt into the full skill execution prompt.
|
|
11
39
|
* Returns null if the prompt is not a skill invocation or the skill is not found.
|
|
@@ -33,7 +61,7 @@ function resolveSkillFromPrompt(workdir, prompt) {
|
|
|
33
61
|
const resolvedPrompt = `${workdirHint}Read the skill definition at \`${targetPath}\` and execute the instructions defined there.${extra}`;
|
|
34
62
|
return { resolvedPrompt, skillName: skill.name };
|
|
35
63
|
}
|
|
36
|
-
export function queueDashboardSessionTask(request) {
|
|
64
|
+
export async function queueDashboardSessionTask(request) {
|
|
37
65
|
const bot = runtime.getBotRef();
|
|
38
66
|
if (!bot)
|
|
39
67
|
return { ok: false, error: 'Bot is not running' };
|
|
@@ -48,6 +76,14 @@ export function queueDashboardSessionTask(request) {
|
|
|
48
76
|
const thinkingEffort = resolvedAgent === 'gemini'
|
|
49
77
|
? ''
|
|
50
78
|
: (typeof request.effort === 'string' ? request.effort.trim().toLowerCase() : '');
|
|
79
|
+
// /goal — route directly to the goal bridge (claude native slash, codex RPC,
|
|
80
|
+
// or portable goal.json for gemini/hermes). Must run BEFORE skill resolution
|
|
81
|
+
// so the legacy `goal` skill doesn't grab the prompt and rewrite it into a
|
|
82
|
+
// "Read SKILL.md" instruction.
|
|
83
|
+
const goalCmd = parseGoalSlash(request.prompt || '');
|
|
84
|
+
if (goalCmd && request.sessionId && !isPendingSessionId(request.sessionId)) {
|
|
85
|
+
return runDashboardGoalSlash(bot, resolvedAgent, request, goalCmd, modelId, thinkingEffort);
|
|
86
|
+
}
|
|
51
87
|
// Resolve /skill-name prompts into full skill execution prompts
|
|
52
88
|
let prompt = request.prompt;
|
|
53
89
|
const skillResult = prompt ? resolveSkillFromPrompt(request.workdir, prompt) : null;
|
|
@@ -84,6 +120,43 @@ export function queueDashboardSessionTask(request) {
|
|
|
84
120
|
...(thinkingEffort ? { thinkingEffort } : {}),
|
|
85
121
|
});
|
|
86
122
|
}
|
|
123
|
+
async function runDashboardGoalSlash(bot, agent, request, cmd, modelId, thinkingEffort) {
|
|
124
|
+
const opts = { chatId: 'dashboard', modelId: modelId || undefined, thinkingEffort: thinkingEffort || undefined };
|
|
125
|
+
const sessionKey = `${agent}:${request.sessionId}`;
|
|
126
|
+
// Synthetic task id — for set / clear / resume on agents that internally
|
|
127
|
+
// submit a follow-up task (claude native slash, portable continuation),
|
|
128
|
+
// the real task id is owned by submitSessionTask. The dashboard's SSE
|
|
129
|
+
// stream listener picks that up via session events; this id is just to
|
|
130
|
+
// give the HTTP caller a non-empty taskId field.
|
|
131
|
+
const taskId = `goal-${cmd.action}-${Date.now().toString(36)}`;
|
|
132
|
+
try {
|
|
133
|
+
if (cmd.action === 'status') {
|
|
134
|
+
const goal = await bot.getSessionGoal(request.workdir, agent, request.sessionId);
|
|
135
|
+
return { ok: true, taskId, sessionKey, queued: false, goal };
|
|
136
|
+
}
|
|
137
|
+
if (cmd.action === 'clear') {
|
|
138
|
+
await bot.clearSessionGoal(request.workdir, agent, request.sessionId, opts);
|
|
139
|
+
return { ok: true, taskId, sessionKey, queued: false };
|
|
140
|
+
}
|
|
141
|
+
if (cmd.action === 'pause') {
|
|
142
|
+
const goal = await bot.pauseSessionGoal(request.workdir, agent, request.sessionId);
|
|
143
|
+
return { ok: true, taskId, sessionKey, queued: false, goal };
|
|
144
|
+
}
|
|
145
|
+
if (cmd.action === 'resume') {
|
|
146
|
+
const goal = await bot.resumeSessionGoal(request.workdir, agent, request.sessionId, opts);
|
|
147
|
+
return { ok: true, taskId, sessionKey, queued: false, goal };
|
|
148
|
+
}
|
|
149
|
+
// set
|
|
150
|
+
const goal = await bot.setSessionGoal(request.workdir, agent, request.sessionId, {
|
|
151
|
+
objective: cmd.objective,
|
|
152
|
+
...opts,
|
|
153
|
+
});
|
|
154
|
+
return { ok: true, taskId, sessionKey, queued: true, goal };
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
return { ok: false, error: e?.message || String(e) };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
87
160
|
export function forkDashboardSessionTask(request) {
|
|
88
161
|
const bot = runtime.getBotRef();
|
|
89
162
|
if (!bot)
|
package/package.json
CHANGED