@aion0/forge 0.10.53 → 0.10.56
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/RELEASE_NOTES.md +14 -3
- package/app/api/activity/summary/route.ts +30 -0
- package/app/api/cache/route.ts +125 -41
- package/app/api/chat/sessions/[id]/abort/route.ts +14 -0
- package/app/api/chat/sessions/[id]/note/route.ts +16 -0
- package/app/api/files/[...path]/route.ts +94 -0
- package/app/api/scratch/[...path]/route.ts +5 -0
- package/app/chat/page.tsx +237 -36
- package/app/files/[...path]/page.tsx +22 -0
- package/components/Dashboard.tsx +82 -26
- package/components/PipelineView.tsx +40 -7
- package/components/ScratchViewer.tsx +14 -3
- package/lib/chat/agent-loop.ts +95 -2
- package/lib/chat/input-queue.ts +159 -0
- package/lib/chat/link-patterns.ts +28 -5
- package/lib/chat/tool-dispatcher.ts +270 -17
- package/lib/chat/turn-control.ts +109 -0
- package/lib/chat-standalone.ts +75 -21
- package/lib/help-docs/10-troubleshooting.md +16 -0
- package/lib/help-docs/17-connectors.md +19 -0
- package/lib/help-docs/25-chat-tools.md +125 -0
- package/lib/help-docs/CLAUDE.md +2 -0
- package/lib/init.ts +14 -0
- package/lib/pipeline.ts +11 -0
- package/lib/scratch-cleanup.ts +25 -16
- package/lib/task-manager.ts +30 -0
- package/package.json +1 -1
|
@@ -16,7 +16,18 @@ function extOf(p: string): string {
|
|
|
16
16
|
return m ? m[1].toLowerCase() : '';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export default function ScratchViewer({
|
|
19
|
+
export default function ScratchViewer({
|
|
20
|
+
path,
|
|
21
|
+
apiBase = '/api/scratch',
|
|
22
|
+
topLabel = 'scratch',
|
|
23
|
+
}: {
|
|
24
|
+
path: string;
|
|
25
|
+
/** API route prefix. /api/scratch resolves under <dataDir>/scratch/;
|
|
26
|
+
* /api/files resolves dataDir-wide. Default scratch for back-compat. */
|
|
27
|
+
apiBase?: string;
|
|
28
|
+
/** Tiny header tag shown next to the file path (e.g. "scratch", "file"). */
|
|
29
|
+
topLabel?: string;
|
|
30
|
+
}) {
|
|
20
31
|
const decoded = (() => {
|
|
21
32
|
try {
|
|
22
33
|
return decodeURIComponent(path);
|
|
@@ -25,7 +36,7 @@ export default function ScratchViewer({ path }: { path: string }) {
|
|
|
25
36
|
}
|
|
26
37
|
})();
|
|
27
38
|
const ext = extOf(decoded);
|
|
28
|
-
const rawUrl =
|
|
39
|
+
const rawUrl = `${apiBase}/${path}`;
|
|
29
40
|
const downloadUrl = `${rawUrl}?download=1`;
|
|
30
41
|
|
|
31
42
|
const [text, setText] = useState<string | null>(null);
|
|
@@ -67,7 +78,7 @@ export default function ScratchViewer({ path }: { path: string }) {
|
|
|
67
78
|
<div className="min-h-screen bg-[var(--bg-primary)] text-[var(--text-primary)]">
|
|
68
79
|
<header className="sticky top-0 z-10 flex items-center justify-between gap-2 px-4 py-2 border-b border-[var(--border)] bg-[var(--bg-secondary)]">
|
|
69
80
|
<div className="flex items-center gap-2 min-w-0">
|
|
70
|
-
<span className="text-[10px] uppercase tracking-wide text-[var(--text-secondary)]">
|
|
81
|
+
<span className="text-[10px] uppercase tracking-wide text-[var(--text-secondary)]">{topLabel}</span>
|
|
71
82
|
<span className="text-xs font-mono truncate" title={decoded}>
|
|
72
83
|
{decoded}
|
|
73
84
|
</span>
|
package/lib/chat/agent-loop.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { buildMemoryContext } from './build-memory-context';
|
|
|
28
28
|
import { buildReferencePromptSection } from './reference-prompt';
|
|
29
29
|
import { buildMemoryTools } from './memory-tools';
|
|
30
30
|
import { buildStartWatchTool } from '../watch/start-watch-tool';
|
|
31
|
+
import { beginTurn, endTurn, isAborted, consumeNotes } from './turn-control';
|
|
31
32
|
import { estimateTokens } from '../memory/token-estimate';
|
|
32
33
|
import {
|
|
33
34
|
listInstalledConnectors,
|
|
@@ -419,9 +420,16 @@ function buildSystemPrompt(
|
|
|
419
420
|
}
|
|
420
421
|
}
|
|
421
422
|
if (builtinDefs.length > 0) {
|
|
423
|
+
// Builtins are always-active (no connector_open gate) — list their
|
|
424
|
+
// FULL description so per-tool rules like "save_tmp_file: output
|
|
425
|
+
// user_message verbatim" / "dispatch_task: ASK before classifying
|
|
426
|
+
// a save as a task" reach the LLM here, not just buried in the
|
|
427
|
+
// tools schema. The connector catalog above can be terse because
|
|
428
|
+
// those tools only become live after connector_open; builtins
|
|
429
|
+
// never go through that gate, so terseness here loses real guidance.
|
|
422
430
|
lines.push('', 'Builtin tools (always available):');
|
|
423
431
|
for (const t of builtinDefs) {
|
|
424
|
-
lines.push(`- ${t.name}: ${t.description
|
|
432
|
+
lines.push(`- ${t.name}: ${t.description}`);
|
|
425
433
|
}
|
|
426
434
|
}
|
|
427
435
|
|
|
@@ -559,6 +567,19 @@ export interface RunTurnArgs {
|
|
|
559
567
|
|
|
560
568
|
export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?: string }> {
|
|
561
569
|
const cb = args.callbacks?.onEvent ?? (() => {});
|
|
570
|
+
|
|
571
|
+
// Claim the turn-control flag FIRST — before any early-return path
|
|
572
|
+
// (session-not-found, provider-error). The outer try/finally below
|
|
573
|
+
// guarantees endTurn runs in EVERY exit path, including early returns
|
|
574
|
+
// and uncaught throws, so the running flag never gets stuck true.
|
|
575
|
+
//
|
|
576
|
+
// The caller (enqueueChatInput) may have already claimed via
|
|
577
|
+
// tryBeginTurn — beginTurn is idempotent on running:true and doesn't
|
|
578
|
+
// wipe notes that may have arrived in the claim→here window.
|
|
579
|
+
beginTurn(args.sessionId);
|
|
580
|
+
|
|
581
|
+
try {
|
|
582
|
+
|
|
562
583
|
const session = getSession(args.sessionId);
|
|
563
584
|
if (!session) {
|
|
564
585
|
cb({ type: 'error', data: { error: `session not found: ${args.sessionId}` } });
|
|
@@ -754,10 +775,53 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
|
|
|
754
775
|
let lastStop = '';
|
|
755
776
|
let assistantBlocksAccum: ContentBlock[] = [];
|
|
756
777
|
|
|
778
|
+
// beginTurn already ran at function entry (see top of runTurn). The
|
|
779
|
+
// try/finally below is for the LLM-streaming error path — keep it
|
|
780
|
+
// so fetch errors still get fanned out as 'error' events. endTurn is
|
|
781
|
+
// handled by the OUTER try/finally so removing it from this inner
|
|
782
|
+
// finally is intentional (avoids redundant calls).
|
|
757
783
|
try {
|
|
758
784
|
while (iter < MAX_ITERATIONS) {
|
|
759
785
|
iter += 1;
|
|
760
786
|
|
|
787
|
+
// ── User intervention (turn-control) ────────────────────────
|
|
788
|
+
// Abort: stop the loop cleanly at this boundary with a sentinel so
|
|
789
|
+
// the turn doesn't just vanish. (The current in-flight LLM call /
|
|
790
|
+
// tool batch from the previous iteration has already settled here.)
|
|
791
|
+
if (isAborted(args.sessionId)) {
|
|
792
|
+
const stopMsg = appendMessage({
|
|
793
|
+
session_id: args.sessionId,
|
|
794
|
+
role: 'assistant',
|
|
795
|
+
blocks: [{ type: 'text', text: '⏹ Stopped by user.' } as TextBlock],
|
|
796
|
+
});
|
|
797
|
+
cb({ type: 'message_saved', message_id: stopMsg.id, data: stopMsg });
|
|
798
|
+
lastStop = 'aborted';
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
// Supplementary notes: splice any queued user notes in as a user
|
|
802
|
+
// message so THIS iteration's LLM call sees them. Safe after a
|
|
803
|
+
// tool_result message — the adapter emits that as a `tool` message,
|
|
804
|
+
// so a following `user` message doesn't collide on role.
|
|
805
|
+
// Drain notes — each becomes its own user message so the thread
|
|
806
|
+
// shows them in arrival order (matches the optimistic messages the
|
|
807
|
+
// client already rendered when the user hit Send). The first note
|
|
808
|
+
// carries a flag for the model so it knows this is a mid-task
|
|
809
|
+
// redirect, not ambient chat; subsequent notes go raw to avoid
|
|
810
|
+
// cluttering the visible thread.
|
|
811
|
+
const notes = consumeNotes(args.sessionId);
|
|
812
|
+
if (notes.length > 0) {
|
|
813
|
+
const FLAG = '[mid-task interjection — sent WHILE you were running tools. Treat as an authoritative redirect that overrides any plan you announced earlier (count, target, scope). Adjust on the very next step.]';
|
|
814
|
+
for (let i = 0; i < notes.length; i++) {
|
|
815
|
+
const text = i === 0 ? `${FLAG}\n\n${notes[i]}` : notes[i]!;
|
|
816
|
+
const noteMsg = appendMessage({
|
|
817
|
+
session_id: args.sessionId,
|
|
818
|
+
role: 'user',
|
|
819
|
+
blocks: [{ type: 'text', text } as TextBlock],
|
|
820
|
+
});
|
|
821
|
+
cb({ type: 'message_saved', message_id: noteMsg.id, data: noteMsg });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
761
825
|
// ── Recompute open set every iteration ──────────────────────
|
|
762
826
|
// Scan history (since last user text msg) + this turn's accumulated
|
|
763
827
|
// blocks → which connectors are open right now. Then filter tools.
|
|
@@ -885,10 +949,28 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
|
|
|
885
949
|
|
|
886
950
|
if (result.stopReason !== 'tool_use') break;
|
|
887
951
|
|
|
888
|
-
// Execute tool calls
|
|
952
|
+
// Execute tool calls. The LLM can emit several tool_use blocks per
|
|
953
|
+
// iteration (parallel batch). Without an in-batch abort check, a
|
|
954
|
+
// user who clicks Stop after the batch starts has to wait for ALL
|
|
955
|
+
// tools to finish before the loop top-check fires next iter — feels
|
|
956
|
+
// like Stop did nothing. So: between tools, if abort was requested,
|
|
957
|
+
// skip the remaining ones with synthetic "aborted" tool_results
|
|
958
|
+
// (the tool_use/tool_result pairing invariant must hold for the
|
|
959
|
+
// Anthropic API; an orphan tool_use rejects the next call).
|
|
889
960
|
const toolUses = result.content.filter((b): b is ToolUseBlock => b.type === 'tool_use');
|
|
890
961
|
const toolResults: ToolResultBlock[] = [];
|
|
891
962
|
for (const t of toolUses) {
|
|
963
|
+
if (isAborted(args.sessionId)) {
|
|
964
|
+
const block: ToolResultBlock = {
|
|
965
|
+
type: 'tool_result',
|
|
966
|
+
tool_use_id: t.id,
|
|
967
|
+
content: '⏹ Skipped — user requested stop.',
|
|
968
|
+
is_error: true,
|
|
969
|
+
};
|
|
970
|
+
toolResults.push(block);
|
|
971
|
+
cb({ type: 'tool_result', data: { tool_use_id: t.id, name: t.name, result: { content: block.content, is_error: true } } });
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
892
974
|
const r = await dispatchTool({ id: t.id, name: t.name, input: t.input }, { extraBuiltins: memHandlers, sessionId: args.sessionId });
|
|
893
975
|
const block: ToolResultBlock = {
|
|
894
976
|
type: 'tool_result',
|
|
@@ -961,4 +1043,15 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
|
|
|
961
1043
|
cb({ type: 'error', data: { error: msg } });
|
|
962
1044
|
return { ok: false, error: msg };
|
|
963
1045
|
}
|
|
1046
|
+
// Inner try (LLM streaming) ends here — no finally; the OUTER
|
|
1047
|
+
// finally below owns endTurn so it runs even when an early return
|
|
1048
|
+
// above (session-not-found / provider-error) skips the inner loop.
|
|
1049
|
+
|
|
1050
|
+
} finally {
|
|
1051
|
+
// Outer finally — runs on ALL exit paths, including the early
|
|
1052
|
+
// returns at session-not-found and provider-error, and any throw
|
|
1053
|
+
// anywhere in the function. Guarantees the turn-control running
|
|
1054
|
+
// flag is always cleared so future inputs can start fresh turns.
|
|
1055
|
+
endTurn(args.sessionId);
|
|
1056
|
+
}
|
|
964
1057
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat input queue — the single routing/merge layer for all external
|
|
3
|
+
* inputs to a chat session.
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists. Before, every input source had to re-implement
|
|
6
|
+
* the "if a turn is already running, merge into it instead of forking
|
|
7
|
+
* a new turn" gate (see handleMessagePost vs the watch runChat callback).
|
|
8
|
+
* Easy to forget — and forgetting it caused the 2026-06-09 bug where a
|
|
9
|
+
* watch firing mid-turn produced duplicate trigger_pipeline calls (commit
|
|
10
|
+
* 0d48569 patched that one path). User: "你应该把所有的消息输入别是 chat
|
|
11
|
+
* 放在一个队列通道中,然后合并处理".
|
|
12
|
+
*
|
|
13
|
+
* Now there's exactly ONE entry point. All callers — user POST /messages,
|
|
14
|
+
* watch on_done/on_fail injects, schedule action announcements, future
|
|
15
|
+
* Slack/IDE/cron sources — call enqueueChatInput. The function decides
|
|
16
|
+
* whether to (a) merge into the running turn as a note, (b) start a new
|
|
17
|
+
* turn, or (c) just persist+broadcast an announcement without invoking
|
|
18
|
+
* the LLM.
|
|
19
|
+
*
|
|
20
|
+
* Per-session queueing already lives in turn-control.ts (notes Map keyed
|
|
21
|
+
* by sessionId). When multi-conversation features land later, they just
|
|
22
|
+
* spawn more sessions — the same merge semantics apply per-session
|
|
23
|
+
* without code changes here.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { appendMessage } from './session-store';
|
|
27
|
+
import { runTurn, type AgentEvent } from './agent-loop';
|
|
28
|
+
import type { ContentBlock } from './types';
|
|
29
|
+
import { isTurnRunning, addNote, tryBeginTurn } from './turn-control';
|
|
30
|
+
|
|
31
|
+
/** Where the input came from. Pure metadata — used for logs + decisions
|
|
32
|
+
* about default behavior (e.g. watch text might want a tag prefix). */
|
|
33
|
+
export type InputSource = 'user' | 'watch' | 'schedule' | 'bridge' | 'mcp' | 'unknown';
|
|
34
|
+
|
|
35
|
+
export interface EnqueueOpts {
|
|
36
|
+
sessionId: string;
|
|
37
|
+
/** For 'turn' mode: the raw user-side text the agent should see.
|
|
38
|
+
* For 'announce' mode: optional shortcut — wrapped in [{type:'text'}]. */
|
|
39
|
+
text?: string;
|
|
40
|
+
/** For 'announce' mode: pre-formed message blocks. Wins over `text`. */
|
|
41
|
+
blocks?: ContentBlock[];
|
|
42
|
+
/** Announce role. Default 'assistant' — a system-emitted notice. */
|
|
43
|
+
role?: 'user' | 'assistant';
|
|
44
|
+
/**
|
|
45
|
+
* 'turn' — drive the agent. Either start a new turn or merge into
|
|
46
|
+
* an already-running one as a note (LLM sees on next
|
|
47
|
+
* iteration boundary). MOST CALLERS USE THIS.
|
|
48
|
+
* 'announce' — push a pre-formed message into the thread, no agent
|
|
49
|
+
* invocation. Used by schedule chat-action so its
|
|
50
|
+
* completion notice shows up without triggering a reply.
|
|
51
|
+
*/
|
|
52
|
+
mode: 'turn' | 'announce';
|
|
53
|
+
source: InputSource;
|
|
54
|
+
/** Sink for SSE events fanned out to subscribers. Required for 'turn'
|
|
55
|
+
* mode (or the UI sees no streaming). 'announce' uses it to push the
|
|
56
|
+
* message_saved event so open tabs render the new message live. */
|
|
57
|
+
onEvent?: (e: AgentEvent) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface EnqueueResult {
|
|
61
|
+
ok: boolean;
|
|
62
|
+
/** 'started' — fresh runTurn kicked off
|
|
63
|
+
* 'merged' — text queued as a note for the in-flight turn
|
|
64
|
+
* 'announced' — message persisted + broadcast, no turn
|
|
65
|
+
* 'rejected' — input invalid (empty text in turn mode, etc.) */
|
|
66
|
+
status: 'started' | 'merged' | 'announced' | 'rejected';
|
|
67
|
+
/** Populated by 'announced' — the persisted message's id, so the
|
|
68
|
+
* /inject HTTP response can return it (existing API contract). */
|
|
69
|
+
messageId?: string;
|
|
70
|
+
reason?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The single entry point. Routes by mode + isTurnRunning state.
|
|
75
|
+
*
|
|
76
|
+
* Sync (returns immediately). For 'turn' mode it kicks off runTurn in
|
|
77
|
+
* the background — caller doesn't await; events flow through onEvent.
|
|
78
|
+
*/
|
|
79
|
+
export function enqueueChatInput(opts: EnqueueOpts): EnqueueResult {
|
|
80
|
+
if (opts.mode === 'announce') {
|
|
81
|
+
// Persist the message without invoking the LLM. role defaults to
|
|
82
|
+
// 'assistant' (system-emitted notice). Caller supplies blocks
|
|
83
|
+
// directly or a plain text shortcut.
|
|
84
|
+
const blocks: ContentBlock[] = opts.blocks
|
|
85
|
+
?? [{ type: 'text', text: (opts.text ?? '') } as ContentBlock];
|
|
86
|
+
if (blocks.length === 0) {
|
|
87
|
+
return { ok: false, status: 'rejected', reason: 'announce requires blocks or text' };
|
|
88
|
+
}
|
|
89
|
+
const saved = appendMessage({
|
|
90
|
+
session_id: opts.sessionId,
|
|
91
|
+
role: opts.role || 'assistant',
|
|
92
|
+
blocks,
|
|
93
|
+
});
|
|
94
|
+
opts.onEvent?.({ type: 'message_saved', message_id: saved.id, data: saved });
|
|
95
|
+
return { ok: true, status: 'announced', messageId: saved.id };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 'turn' mode — agent must drive a response.
|
|
99
|
+
const text = (opts.text || '').trim();
|
|
100
|
+
if (!text) {
|
|
101
|
+
return { ok: false, status: 'rejected', reason: 'turn mode requires non-empty text' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// If a turn is already in flight on this session, queue the text as
|
|
105
|
+
// a note for that running turn. The loop splices each note in at the
|
|
106
|
+
// next iteration boundary (see agent-loop.ts:790). This is the merge
|
|
107
|
+
// semantic — replaces every caller having to gate isTurnRunning itself.
|
|
108
|
+
if (isTurnRunning(opts.sessionId)) {
|
|
109
|
+
const queued = addNote(opts.sessionId, text);
|
|
110
|
+
if (queued) return { ok: true, status: 'merged' };
|
|
111
|
+
// Tiny race: isTurnRunning was true but endTurn fired between the
|
|
112
|
+
// check and addNote. Fall through to start a fresh turn.
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Atomic claim — closes the gap between this check and the in-loop
|
|
116
|
+
// beginTurn() call inside runTurn. runTurn does ~100ms of async setup
|
|
117
|
+
// (appendMessage, listMessagesCapped, provider resolve, SSE pushes)
|
|
118
|
+
// BEFORE its beginTurn runs; without claiming here, a second input
|
|
119
|
+
// arriving during that window would also see isTurnRunning=false and
|
|
120
|
+
// fork another runTurn. Both then race on the same chat history and
|
|
121
|
+
// produce duplicate tool calls (observed 2026-06-10: watch + user
|
|
122
|
+
// input → two save_tmp_file calls, two LLM replies).
|
|
123
|
+
//
|
|
124
|
+
// If tryBeginTurn returns false, another caller claimed first within
|
|
125
|
+
// the same JS tick — merge as a note into THAT turn instead. Cheap +
|
|
126
|
+
// correct: addNote on a now-running session always succeeds.
|
|
127
|
+
const claimed = tryBeginTurn(opts.sessionId);
|
|
128
|
+
if (!claimed) {
|
|
129
|
+
addNote(opts.sessionId, text);
|
|
130
|
+
return { ok: true, status: 'merged' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Claimed — start a fresh one. Fire-
|
|
134
|
+
// and-forget; errors land on the onEvent stream as 'error' events.
|
|
135
|
+
// Two failure modes:
|
|
136
|
+
// (a) runTurn throws (rejected Promise) — caught by .catch.
|
|
137
|
+
// (b) runTurn resolves with {ok:false, error:'...'} — handled in
|
|
138
|
+
// the .then; this case happens in agent-loop's known-error
|
|
139
|
+
// paths (context budget exhausted / orphan tool-use trim /
|
|
140
|
+
// fetch failure with adapter detail). Missing this branch
|
|
141
|
+
// would leave the UI hanging with no error indicator.
|
|
142
|
+
const startedAt = Date.now();
|
|
143
|
+
const onEvent = opts.onEvent;
|
|
144
|
+
void runTurn({
|
|
145
|
+
sessionId: opts.sessionId,
|
|
146
|
+
userText: text,
|
|
147
|
+
callbacks: { onEvent: onEvent || (() => {}) },
|
|
148
|
+
}).then((r) => {
|
|
149
|
+
if (!r.ok) {
|
|
150
|
+
onEvent?.({ type: 'error', data: { error: r.error || 'turn failed' } });
|
|
151
|
+
}
|
|
152
|
+
const ms = Date.now() - startedAt;
|
|
153
|
+
console.log(`[chat] turn ${opts.sessionId.slice(0, 8)} (${opts.source}) done in ${ms}ms ok=${r.ok}`);
|
|
154
|
+
}).catch((err) => {
|
|
155
|
+
console.error(`[input-queue] runTurn threw (source=${opts.source}):`, (err as Error).message);
|
|
156
|
+
onEvent?.({ type: 'error', data: { error: (err as Error).message } });
|
|
157
|
+
});
|
|
158
|
+
return { ok: true, status: 'started' };
|
|
159
|
+
}
|
|
@@ -81,17 +81,40 @@ export const LINK_PATTERNS: LinkPattern[] = [
|
|
|
81
81
|
url: 'https://nvd.nist.gov/vuln/detail/{1}',
|
|
82
82
|
label: '{1}',
|
|
83
83
|
},
|
|
84
|
-
// Forge
|
|
85
|
-
// `scratch/foo.md`
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
84
|
+
// Forge-managed files inside <dataDir>/. LLMs emit:
|
|
85
|
+
// • `scratch/foo.md` — legacy task-workspace writes (kept under
|
|
86
|
+
// <dataDir>/scratch/, served by /scratch viewer)
|
|
87
|
+
// • `tmp/foo.md` — chat's save_tmp_file output (<dataDir>/tmp/)
|
|
88
|
+
// • `flows/x.yaml` — pipeline workflow definitions
|
|
89
|
+
// • `prompts/y.yaml` — schedule prompt bodies
|
|
90
|
+
// All four resolve through the /files/<dataDir-path> viewer (rendered
|
|
91
|
+
// markdown + download button), which fetches /api/files for raw bytes.
|
|
92
|
+
// Sensitive top-level items (encrypt key, sqlite DBs, log, token
|
|
93
|
+
// caches) are blocked at the API layer, so safe to be liberal here.
|
|
89
94
|
{
|
|
90
95
|
id: 'scratch-file',
|
|
91
96
|
regex: /\bscratch\/([\w\-./]+?\.(?:md|txt|json|yaml|yml|csv|log|html|pdf|png|jpg|jpeg|gif|svg))\b/gi,
|
|
92
97
|
url: '/scratch/{1}',
|
|
93
98
|
label: 'scratch/{1}',
|
|
94
99
|
},
|
|
100
|
+
{
|
|
101
|
+
id: 'tmp-file',
|
|
102
|
+
regex: /\btmp\/([\w\-./]+?\.(?:md|txt|json|yaml|yml|csv|log|html|pdf|png|jpg|jpeg|gif|svg))\b/gi,
|
|
103
|
+
url: '/files/tmp/{1}',
|
|
104
|
+
label: 'tmp/{1}',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'flows-file',
|
|
108
|
+
regex: /\bflows\/([\w\-./]+?\.(?:yaml|yml|json))\b/gi,
|
|
109
|
+
url: '/files/flows/{1}',
|
|
110
|
+
label: 'flows/{1}',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'prompts-file',
|
|
114
|
+
regex: /\bprompts\/([\w\-./]+?\.(?:yaml|yml|md|txt))\b/gi,
|
|
115
|
+
url: '/files/prompts/{1}',
|
|
116
|
+
label: 'prompts/{1}',
|
|
117
|
+
},
|
|
95
118
|
];
|
|
96
119
|
|
|
97
120
|
export interface CompiledPattern {
|