@aion0/forge 0.10.35 → 0.10.36
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/README.md +9 -0
- package/RELEASE_NOTES.md +10 -6
- package/lib/chat/tool-dispatcher.ts +189 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,14 @@
|
|
|
27
27
|
|
|
28
28
|
## Install
|
|
29
29
|
|
|
30
|
+
**One-liner** (macOS + Linux — installs node deps, tmux, claude code, then Forge):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
curl -fsSL https://raw.githubusercontent.com/aiwatching/forge/main/scripts/install-deps.sh | bash
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or manually:
|
|
37
|
+
|
|
30
38
|
```bash
|
|
31
39
|
npm install -g @aion0/forge
|
|
32
40
|
forge server start
|
|
@@ -35,6 +43,7 @@ forge server start
|
|
|
35
43
|
Open `http://localhost:8403`. First launch prompts you to set an admin password.
|
|
36
44
|
|
|
37
45
|
**Requirements:** Node.js ≥ 20, tmux, [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)
|
|
46
|
+
**Optional:** jq, glab, gh (used by some pipelines)
|
|
38
47
|
|
|
39
48
|
## What is Forge?
|
|
40
49
|
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
# Forge v0.10.
|
|
1
|
+
# Forge v0.10.36
|
|
2
2
|
|
|
3
|
-
Released: 2026-06-
|
|
3
|
+
Released: 2026-06-04
|
|
4
4
|
|
|
5
|
-
## Changes since v0.10.
|
|
5
|
+
## Changes since v0.10.35
|
|
6
|
+
|
|
7
|
+
### Documentation
|
|
8
|
+
- docs: README install section gets one-liner + optional deps line
|
|
6
9
|
|
|
7
10
|
### Other
|
|
8
|
-
-
|
|
9
|
-
-
|
|
11
|
+
- scripts: add install-deps.sh cross-platform installer
|
|
12
|
+
- feat(chat): add 5 schedule builtin tools (create/list/delete/run/update)
|
|
13
|
+
- feat(vscode-ext): add Chat + Schedules views + session switcher
|
|
10
14
|
|
|
11
15
|
|
|
12
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.
|
|
16
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.35...v0.10.36
|
|
@@ -337,6 +337,135 @@ const BUILTINS: Record<string, BuiltinHandler> = {
|
|
|
337
337
|
: content;
|
|
338
338
|
},
|
|
339
339
|
|
|
340
|
+
// ── Schedules CRUD ─────────────────────────────────────────
|
|
341
|
+
// All five direct in-process via lib/schedules/store — no HTTP, no auth.
|
|
342
|
+
// Use these instead of dispatch_task + curl: cleaner, no token shenanigans.
|
|
343
|
+
|
|
344
|
+
create_schedule: async (input) => {
|
|
345
|
+
const p = (input as any) || {};
|
|
346
|
+
const name = String(p.name || '').trim();
|
|
347
|
+
const workflow = String(p.workflow || p.body_ref || '').trim();
|
|
348
|
+
if (!name) return JSON.stringify({ ok: false, error: 'name is required' });
|
|
349
|
+
if (!workflow) return JSON.stringify({ ok: false, error: 'workflow (pipeline name) is required' });
|
|
350
|
+
|
|
351
|
+
// Trigger normalization: prefer every_minutes; accept at (once) or cron.
|
|
352
|
+
let schedule_kind: 'period' | 'once' | 'cron' = 'period';
|
|
353
|
+
let schedule_interval_minutes: number | undefined;
|
|
354
|
+
let schedule_at: string | null | undefined;
|
|
355
|
+
let schedule_cron: string | null | undefined;
|
|
356
|
+
if (p.every_minutes != null) {
|
|
357
|
+
schedule_kind = 'period';
|
|
358
|
+
schedule_interval_minutes = Number(p.every_minutes);
|
|
359
|
+
} else if (p.at) {
|
|
360
|
+
schedule_kind = 'once';
|
|
361
|
+
schedule_at = String(p.at);
|
|
362
|
+
} else if (p.cron) {
|
|
363
|
+
schedule_kind = 'cron';
|
|
364
|
+
schedule_cron = String(p.cron);
|
|
365
|
+
} else {
|
|
366
|
+
return JSON.stringify({ ok: false, error: 'one of every_minutes / at / cron is required' });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const { createSchedule, seedNextRunAt } = await import('../schedules/store');
|
|
370
|
+
try {
|
|
371
|
+
const s = createSchedule({
|
|
372
|
+
name,
|
|
373
|
+
body_kind: 'pipeline',
|
|
374
|
+
body_ref: workflow,
|
|
375
|
+
input: (p.input && typeof p.input === 'object') ? p.input : {},
|
|
376
|
+
skills: Array.isArray(p.skills) ? p.skills : undefined,
|
|
377
|
+
enabled: p.enabled !== false,
|
|
378
|
+
schedule_kind,
|
|
379
|
+
schedule_interval_minutes,
|
|
380
|
+
schedule_at: schedule_at ?? null,
|
|
381
|
+
schedule_cron: schedule_cron ?? null,
|
|
382
|
+
action_kind: p.action || 'none',
|
|
383
|
+
});
|
|
384
|
+
seedNextRunAt(s.id);
|
|
385
|
+
return JSON.stringify({
|
|
386
|
+
ok: true,
|
|
387
|
+
schedule_id: s.id,
|
|
388
|
+
name: s.name,
|
|
389
|
+
enabled: s.enabled,
|
|
390
|
+
kind: s.schedule_kind,
|
|
391
|
+
next_run_at: s.next_run_at,
|
|
392
|
+
message: `Schedule "${s.name}" created. ${schedule_kind === 'period' ? `Fires every ${schedule_interval_minutes} minutes.` : schedule_kind === 'once' ? `Fires once at ${schedule_at}.` : `Fires on cron "${schedule_cron}".`}`,
|
|
393
|
+
});
|
|
394
|
+
} catch (e: any) {
|
|
395
|
+
return JSON.stringify({ ok: false, error: e?.message || String(e) });
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
list_schedules: async () => {
|
|
400
|
+
const { listSchedules } = await import('../schedules/store');
|
|
401
|
+
const { decorateSchedule } = await import('../schedules/state');
|
|
402
|
+
const all = listSchedules().map(decorateSchedule);
|
|
403
|
+
return JSON.stringify({
|
|
404
|
+
schedules: all.map((s) => ({
|
|
405
|
+
id: s.id,
|
|
406
|
+
name: s.name,
|
|
407
|
+
enabled: s.enabled,
|
|
408
|
+
active_state: s.active_state,
|
|
409
|
+
schedule_kind: s.schedule_kind,
|
|
410
|
+
body_ref: s.body_ref,
|
|
411
|
+
next_run_at: s.next_run_at,
|
|
412
|
+
last_run_at: s.last_run_at,
|
|
413
|
+
})),
|
|
414
|
+
total: all.length,
|
|
415
|
+
});
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
delete_schedule: async (input) => {
|
|
419
|
+
const id = String((input as any)?.id || '').trim();
|
|
420
|
+
if (!id) return JSON.stringify({ ok: false, error: 'id is required' });
|
|
421
|
+
const { deleteSchedule } = await import('../schedules/store');
|
|
422
|
+
const ok = deleteSchedule(id);
|
|
423
|
+
return JSON.stringify({ ok, message: ok ? `Schedule ${id} deleted.` : `Schedule ${id} not found.` });
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
run_schedule_now: async (input) => {
|
|
427
|
+
const id = String((input as any)?.id || '').trim();
|
|
428
|
+
if (!id) return JSON.stringify({ ok: false, error: 'id is required' });
|
|
429
|
+
const { getSchedule } = await import('../schedules/store');
|
|
430
|
+
const s = getSchedule(id);
|
|
431
|
+
if (!s) return JSON.stringify({ ok: false, error: `Schedule ${id} not found` });
|
|
432
|
+
const { executeSchedule } = await import('../schedules/scheduler');
|
|
433
|
+
try {
|
|
434
|
+
const runId = await executeSchedule(s, 'manual');
|
|
435
|
+
return JSON.stringify({ ok: true, schedule_id: id, run_id: runId, message: `Schedule "${s.name}" fired. Run id: ${runId}.` });
|
|
436
|
+
} catch (e: any) {
|
|
437
|
+
return JSON.stringify({ ok: false, error: e?.message || String(e) });
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
update_schedule: async (input) => {
|
|
442
|
+
const p = (input as any) || {};
|
|
443
|
+
const id = String(p.id || '').trim();
|
|
444
|
+
if (!id) return JSON.stringify({ ok: false, error: 'id is required' });
|
|
445
|
+
const patch: Record<string, unknown> = {};
|
|
446
|
+
if (typeof p.enabled === 'boolean') patch.enabled = p.enabled;
|
|
447
|
+
if (typeof p.name === 'string') patch.name = p.name;
|
|
448
|
+
if (p.input && typeof p.input === 'object') patch.input = p.input;
|
|
449
|
+
if (Array.isArray(p.skills)) patch.skills = p.skills;
|
|
450
|
+
if (typeof p.every_minutes === 'number') {
|
|
451
|
+
patch.schedule_kind = 'period';
|
|
452
|
+
patch.schedule_interval_minutes = p.every_minutes;
|
|
453
|
+
} else if (typeof p.at === 'string') {
|
|
454
|
+
patch.schedule_kind = 'once';
|
|
455
|
+
patch.schedule_at = p.at;
|
|
456
|
+
} else if (typeof p.cron === 'string') {
|
|
457
|
+
patch.schedule_kind = 'cron';
|
|
458
|
+
patch.schedule_cron = p.cron;
|
|
459
|
+
}
|
|
460
|
+
if (Object.keys(patch).length === 0) {
|
|
461
|
+
return JSON.stringify({ ok: false, error: 'no fields to update (try enabled / name / input / skills / every_minutes / at / cron)' });
|
|
462
|
+
}
|
|
463
|
+
const { updateSchedule, seedNextRunAt } = await import('../schedules/store');
|
|
464
|
+
const ok = updateSchedule(id, patch as any);
|
|
465
|
+
if (ok && (patch.schedule_kind || patch.enabled === true)) seedNextRunAt(id);
|
|
466
|
+
return JSON.stringify({ ok, message: ok ? `Schedule ${id} updated.` : `Schedule ${id} not found.` });
|
|
467
|
+
},
|
|
468
|
+
|
|
340
469
|
// Namespace gating meta-tool. Connector tools (mantis.*, gitlab.*, etc.)
|
|
341
470
|
// are NOT in the active tools list by default — only their catalog entry
|
|
342
471
|
// is visible in the system prompt. Calling connector_open({name}) makes
|
|
@@ -471,6 +600,66 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
|
|
|
471
600
|
required: ['doc'],
|
|
472
601
|
},
|
|
473
602
|
},
|
|
603
|
+
{
|
|
604
|
+
name: 'create_schedule',
|
|
605
|
+
description: 'Create a recurring (or one-off) schedule that fires a Forge pipeline on a timer. NO HTTP, NO auth — runs in-process. Use this when the user says "every N minutes/hours" / "watch X" / "monitor Y" / "auto-run pipeline on schedule". REQUIRED args: name + workflow + ONE of {every_minutes, at, cron}. Returns { ok, schedule_id, next_run_at }.',
|
|
606
|
+
input_schema: {
|
|
607
|
+
type: 'object',
|
|
608
|
+
properties: {
|
|
609
|
+
name: { type: 'string', description: 'Human-readable name shown in the Schedules UI.' },
|
|
610
|
+
workflow: { type: 'string', description: 'Pipeline workflow name (file basename of flows/<name>.yaml). Run trigger_pipeline() with NO args first if unsure what names are available.' },
|
|
611
|
+
input: { type: 'object', description: 'Pipeline input fields. Same shape as trigger_pipeline.input. OMIT optional fields to use defaults.' },
|
|
612
|
+
skills: { type: 'array', items: { type: 'string' }, description: 'Skill names to inject into every Claude task this schedule spawns.' },
|
|
613
|
+
every_minutes: { type: 'number', description: 'Period in minutes (e.g. 60 = hourly). Most common trigger.' },
|
|
614
|
+
at: { type: 'string', description: 'ISO timestamp for one-shot run (e.g. "2026-06-05T09:00:00Z"). Mutually exclusive with every_minutes / cron.' },
|
|
615
|
+
cron: { type: 'string', description: 'Cron expression for complex schedules (e.g. "0 9 * * 1-5" = weekdays 9am). Mutually exclusive with every_minutes / at.' },
|
|
616
|
+
enabled: { type: 'boolean', description: 'Whether to start enabled. Default true.' },
|
|
617
|
+
action: { type: 'string', enum: ['none', 'chat', 'email', 'telegram'], description: 'Post-run notification action. Default "none".' },
|
|
618
|
+
},
|
|
619
|
+
required: ['name', 'workflow'],
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
name: 'list_schedules',
|
|
624
|
+
description: 'List all configured schedules with status (active_state: idle / running / last_failed / paused), kind, next_run_at, last_run_at. Use to find a schedule\'s id before update/delete/run.',
|
|
625
|
+
input_schema: { type: 'object', properties: {} },
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
name: 'delete_schedule',
|
|
629
|
+
description: 'Permanently delete a schedule by id. Cannot be undone. Find id via list_schedules first.',
|
|
630
|
+
input_schema: {
|
|
631
|
+
type: 'object',
|
|
632
|
+
properties: { id: { type: 'string', description: 'Schedule id from list_schedules.' } },
|
|
633
|
+
required: ['id'],
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: 'run_schedule_now',
|
|
638
|
+
description: 'Fire a schedule\'s configured pipeline immediately (manual trigger), regardless of when it would next fire on its timer. Returns the run_id.',
|
|
639
|
+
input_schema: {
|
|
640
|
+
type: 'object',
|
|
641
|
+
properties: { id: { type: 'string', description: 'Schedule id from list_schedules.' } },
|
|
642
|
+
required: ['id'],
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: 'update_schedule',
|
|
647
|
+
description: 'Patch fields on an existing schedule (enable/disable, rename, change input, swap trigger). Only the fields you pass are changed.',
|
|
648
|
+
input_schema: {
|
|
649
|
+
type: 'object',
|
|
650
|
+
properties: {
|
|
651
|
+
id: { type: 'string', description: 'Schedule id from list_schedules.' },
|
|
652
|
+
enabled: { type: 'boolean', description: 'true to enable, false to pause without deleting.' },
|
|
653
|
+
name: { type: 'string' },
|
|
654
|
+
input: { type: 'object', description: 'New pipeline input fields (replaces existing).' },
|
|
655
|
+
skills: { type: 'array', items: { type: 'string' } },
|
|
656
|
+
every_minutes: { type: 'number', description: 'Switch trigger to interval.' },
|
|
657
|
+
at: { type: 'string', description: 'Switch trigger to one-shot at this ISO time.' },
|
|
658
|
+
cron: { type: 'string', description: 'Switch trigger to cron expression.' },
|
|
659
|
+
},
|
|
660
|
+
required: ['id'],
|
|
661
|
+
},
|
|
662
|
+
},
|
|
474
663
|
{
|
|
475
664
|
name: 'connector_open',
|
|
476
665
|
description: 'Load a connector to make its tools (e.g. mantis.search_bugs, gitlab.list_my_todos) available for use. REQUIRED before calling any connector tool — the catalog block in the system prompt shows what each connector can do. Tools stay loaded only for the current user task; the next user message resets the open set, so re-open as needed.',
|
package/package.json
CHANGED