@aion0/forge 0.10.84 → 0.10.86

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 CHANGED
@@ -1,11 +1,11 @@
1
- # Forge v0.10.84
1
+ # Forge v0.10.86
2
2
 
3
- Released: 2026-06-15
3
+ Released: 2026-06-16
4
4
 
5
- ## Changes since v0.10.83
5
+ ## Changes since v0.10.85
6
6
 
7
7
  ### Other
8
- - fix(auth): logout redirects to current origin, not localhost:8403
8
+ - fix(schedules): pause/resume + extractor openai support
9
9
 
10
10
 
11
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.83...v0.10.84
11
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.85...v0.10.86
@@ -116,29 +116,36 @@ export async function PATCH(req: Request, { params }: { params: Promise<{ id: st
116
116
  }
117
117
  patch.action_config = body.action_config;
118
118
  }
119
- const effectiveActionKind = patch.action_kind ?? existing.action_kind;
120
- const effectiveActionConfig = patch.action_config ?? existing.action_config ?? {};
121
- if (effectiveActionKind === 'chat') {
122
- const sid = typeof effectiveActionConfig.session_id === 'string' ? effectiveActionConfig.session_id.trim() : '';
123
- if (!sid) return NextResponse.json({ error: 'action_kind=chat requires action_config.session_id' }, { status: 400 });
124
- }
125
- if (effectiveActionKind === 'email') {
126
- const toRaw = effectiveActionConfig.to;
127
- const to = Array.isArray(toRaw)
128
- ? toRaw.filter((x) => typeof x === 'string' && x.trim())
129
- : (typeof toRaw === 'string' && toRaw.trim() ? [toRaw] : []);
130
- if (to.length === 0) {
131
- return NextResponse.json({ error: 'action_kind=email requires action_config.to (recipient address or array)' }, { status: 400 });
119
+ // Cross-field validation runs ONLY for the section being patched.
120
+ // Toggling unrelated fields (e.g. `enabled` from the Pause/Resume button)
121
+ // must not re-validate sections the user isn't touching — a schedule with a
122
+ // pre-existing quirk in its action_config would otherwise refuse to pause.
123
+ if (patch.action_kind !== undefined || patch.action_config !== undefined) {
124
+ const effectiveActionKind = patch.action_kind ?? existing.action_kind;
125
+ const effectiveActionConfig = patch.action_config ?? existing.action_config ?? {};
126
+ if (effectiveActionKind === 'chat') {
127
+ const sid = typeof effectiveActionConfig.session_id === 'string' ? effectiveActionConfig.session_id.trim() : '';
128
+ if (!sid) return NextResponse.json({ error: 'action_kind=chat requires action_config.session_id' }, { status: 400 });
129
+ }
130
+ if (effectiveActionKind === 'email') {
131
+ const toRaw = effectiveActionConfig.to;
132
+ const to = Array.isArray(toRaw)
133
+ ? toRaw.filter((x) => typeof x === 'string' && x.trim())
134
+ : (typeof toRaw === 'string' && toRaw.trim() ? [toRaw] : []);
135
+ if (to.length === 0) {
136
+ return NextResponse.json({ error: 'action_kind=email requires action_config.to (recipient address or array)' }, { status: 400 });
137
+ }
132
138
  }
133
139
  }
134
140
 
135
- // Cross-field consistency
136
- const effectiveKind = patch.schedule_kind ?? existing.schedule_kind;
137
- if (effectiveKind === 'cron' && !(patch.schedule_cron ?? existing.schedule_cron)) {
138
- return NextResponse.json({ error: 'schedule_cron is required when schedule_kind=cron' }, { status: 400 });
139
- }
140
- if (effectiveKind === 'once' && !(patch.schedule_at ?? existing.schedule_at)) {
141
- return NextResponse.json({ error: 'schedule_at is required when schedule_kind=once' }, { status: 400 });
141
+ if (patch.schedule_kind !== undefined || patch.schedule_cron !== undefined || patch.schedule_at !== undefined) {
142
+ const effectiveKind = patch.schedule_kind ?? existing.schedule_kind;
143
+ if (effectiveKind === 'cron' && !(patch.schedule_cron ?? existing.schedule_cron)) {
144
+ return NextResponse.json({ error: 'schedule_cron is required when schedule_kind=cron' }, { status: 400 });
145
+ }
146
+ if (effectiveKind === 'once' && !(patch.schedule_at ?? existing.schedule_at)) {
147
+ return NextResponse.json({ error: 'schedule_at is required when schedule_kind=once' }, { status: 400 });
148
+ }
142
149
  }
143
150
 
144
151
  updateSchedule(id, patch);
@@ -19,6 +19,8 @@ import { inferAdapter, pickApiKey, pickBaseUrl } from '@/lib/chat/agent-loop';
19
19
  import { makeAnthropicClient } from '@/lib/chat/llm/anthropic';
20
20
  import { listAgents } from '@/lib/agents';
21
21
  import { scanProjects } from '@/lib/projects';
22
+ import { createOpenAI } from '@ai-sdk/openai';
23
+ import { generateText } from 'ai';
22
24
 
23
25
  interface Extracted {
24
26
  name: string;
@@ -116,15 +118,9 @@ export async function POST(req: Request) {
116
118
  ? profiles[preferredId]
117
119
  : candidates[0]![1];
118
120
  const adapter = inferAdapter(profile.provider);
119
- if (adapter !== 'anthropic') {
120
- return NextResponse.json({
121
- ok: false,
122
- error: 'extractor currently supports anthropic profiles only (openai is a follow-up)',
123
- }, { status: 400 });
124
- }
125
121
  const apiKey = pickApiKey(profile, adapter);
126
122
  const baseUrl = pickBaseUrl(profile, adapter);
127
- const model = profile.model || 'claude-sonnet-4-6';
123
+ const model = profile.model || (adapter === 'anthropic' ? 'claude-sonnet-4-6' : 'gpt-4o-mini');
128
124
 
129
125
  const cliAgents = listAgents().filter((a: any) => a.backendType !== 'api');
130
126
  const availableAgentIds = cliAgents.map((a: any) => a.id);
@@ -134,15 +130,26 @@ export async function POST(req: Request) {
134
130
 
135
131
  let raw = '';
136
132
  try {
137
- const client = makeAnthropicClient(apiKey, baseUrl);
138
- const resp = await client.messages.create({
139
- model,
140
- max_tokens: 1500,
141
- system,
142
- messages: [{ role: 'user', content: text }],
143
- });
144
- for (const block of resp.content) {
145
- if (block.type === 'text') raw += block.text;
133
+ if (adapter === 'anthropic') {
134
+ const client = makeAnthropicClient(apiKey, baseUrl);
135
+ const resp = await client.messages.create({
136
+ model,
137
+ max_tokens: 1500,
138
+ system,
139
+ messages: [{ role: 'user', content: text }],
140
+ });
141
+ for (const block of resp.content) {
142
+ if (block.type === 'text') raw += block.text;
143
+ }
144
+ } else {
145
+ // OpenAI / OpenAI-compatible — non-streaming one-shot via @ai-sdk/openai.
146
+ const provider = createOpenAI({ apiKey, baseURL: baseUrl });
147
+ const resp = await generateText({
148
+ model: provider.chat(model),
149
+ system,
150
+ messages: [{ role: 'user', content: text }],
151
+ });
152
+ raw = resp.text;
146
153
  }
147
154
  } catch (e: any) {
148
155
  return NextResponse.json({