wolli 0.0.3 → 0.0.5

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.
Files changed (33) hide show
  1. package/README.md +23 -24
  2. package/built-in/plugins/discord/discord-chat.ts +47 -84
  3. package/built-in/plugins/discord/index.ts +139 -97
  4. package/built-in/plugins/discord/package.json +2 -2
  5. package/built-in/plugins/github/README.md +148 -0
  6. package/built-in/plugins/github/github-api.test.ts +260 -0
  7. package/built-in/plugins/github/github-api.ts +491 -0
  8. package/built-in/plugins/github/github-chat.ts +216 -0
  9. package/built-in/plugins/github/github-review.ts +102 -0
  10. package/built-in/plugins/github/github-workspace.ts +84 -0
  11. package/built-in/plugins/github/index.ts +795 -0
  12. package/built-in/plugins/github/package.json +14 -0
  13. package/built-in/plugins/scheduler/cron.ts +131 -0
  14. package/built-in/plugins/scheduler/index.ts +147 -151
  15. package/built-in/plugins/scheduler/package.json +3 -2
  16. package/built-in/plugins/scheduler/scheduler-due.ts +22 -0
  17. package/built-in/plugins/telegram/README.md +3 -3
  18. package/built-in/plugins/telegram/index.ts +177 -139
  19. package/built-in/plugins/telegram/package.json +2 -2
  20. package/built-in/plugins/telegram/telegram-chat.ts +79 -155
  21. package/dist/cli.js +6208 -6355
  22. package/docs/hooks.md +103 -0
  23. package/docs/index.md +10 -8
  24. package/docs/integrations.md +78 -663
  25. package/docs/introduction.md +95 -0
  26. package/docs/plugins.md +43 -36
  27. package/docs/providers.md +63 -0
  28. package/docs/sdk.md +42 -47
  29. package/docs/tools.md +81 -0
  30. package/docs/workflows.md +170 -0
  31. package/package.json +1 -1
  32. package/built-in/plugins/scheduler/scheduler-chat.ts +0 -164
  33. package/docs/extensions.md +0 -2331
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "wolli-integration-github",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "wolli": {
7
+ "integrations": ["./index.ts"],
8
+ "workflows": ["./github-chat.ts"],
9
+ "tools": ["./github-review.ts"]
10
+ },
11
+ "peerDependencies": {
12
+ "wolli": "*"
13
+ }
14
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Cron tool — lets the agent schedule its own prompts (add / list / update / remove / run)
3
+ * by driving the scheduler integration's CRUD actions. `add` snapshots the calling
4
+ * session's tags onto the job (`originTags`), so when the job fires, `scheduler-due.ts`
5
+ * wakes the session that surface belongs to and its reply returns there with no
6
+ * scheduler-side special-casing.
7
+ */
8
+
9
+ import { defineTool } from "wolli";
10
+ import { Type } from "typebox";
11
+ import scheduler from "./index.ts";
12
+
13
+ const CronParams = Type.Object({
14
+ action: Type.Union([
15
+ Type.Literal("add"),
16
+ Type.Literal("list"),
17
+ Type.Literal("update"),
18
+ Type.Literal("remove"),
19
+ Type.Literal("run"),
20
+ ]),
21
+ prompt: Type.Optional(Type.String({ description: "What to run (the woken session's first message)." })),
22
+ name: Type.Optional(Type.String({ description: "Human label for the job." })),
23
+ at: Type.Optional(Type.Number({ description: "One-shot run time, epoch ms." })),
24
+ everyMs: Type.Optional(Type.Number({ description: "Fixed interval in ms." })),
25
+ cron: Type.Optional(Type.String({ description: "Cron expression (5/6-field)." })),
26
+ tz: Type.Optional(Type.String({ description: "Timezone for the cron expression (host local if omitted)." })),
27
+ id: Type.Optional(Type.String({ description: "Job id, for update / remove / run." })),
28
+ enabled: Type.Optional(Type.Boolean({ description: "Enable or disable the job (update)." })),
29
+ });
30
+
31
+ /** The fields of a job this tool reads back from `listJobs` (the integration owns the full shape). */
32
+ interface Job {
33
+ id: string;
34
+ name?: string;
35
+ schedule: { kind: "at"; at: number } | { kind: "every"; everyMs: number } | { kind: "cron"; expr: string; tz?: string };
36
+ enabled: boolean;
37
+ nextRunAt: number;
38
+ }
39
+
40
+ /** Map the tool's flat `at`/`everyMs`/`cron` fields onto the integration's `Schedule` union. */
41
+ function buildSchedule(p: { at?: number; everyMs?: number; cron?: string; tz?: string }): Job["schedule"] | undefined {
42
+ if (p.at !== undefined) return { kind: "at", at: p.at };
43
+ if (p.everyMs !== undefined) return { kind: "every", everyMs: p.everyMs };
44
+ if (p.cron !== undefined) return { kind: "cron", expr: p.cron, tz: p.tz };
45
+ return undefined;
46
+ }
47
+
48
+ function describeSchedule(schedule: Job["schedule"]): string {
49
+ switch (schedule.kind) {
50
+ case "at":
51
+ return `at ${new Date(schedule.at).toISOString()}`;
52
+ case "every":
53
+ return `every ${schedule.everyMs}ms`;
54
+ case "cron":
55
+ return `cron "${schedule.expr}"${schedule.tz ? ` (${schedule.tz})` : ""}`;
56
+ }
57
+ }
58
+
59
+ function text(message: string, details: unknown) {
60
+ return { content: [{ type: "text" as const, text: message }], details };
61
+ }
62
+
63
+ export default defineTool({
64
+ name: "cron",
65
+ label: "Cron",
66
+ description:
67
+ "Schedule prompts to run later. Actions: add (prompt + at/everyMs/cron), list, update (id), remove (id), run (id).",
68
+ parameters: CronParams,
69
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
70
+ const sched = ctx.integration(scheduler);
71
+ try {
72
+ switch (params.action) {
73
+ case "add": {
74
+ if (!params.prompt) return text("Error: prompt is required to add a job.", { error: "prompt required" });
75
+ const schedule = buildSchedule(params);
76
+ if (!schedule) {
77
+ return text("Error: provide one of at, everyMs, or cron.", { error: "schedule required" });
78
+ }
79
+ // Snapshot the scheduling session's tags so the fired result returns to this surface.
80
+ const result = (await sched.addJob({
81
+ prompt: params.prompt,
82
+ name: params.name,
83
+ schedule,
84
+ originTags: ctx.session.getTags(),
85
+ })) as { id: string; nextRunAt: number };
86
+ return text(
87
+ `Scheduled job ${result.id} — ${describeSchedule(schedule)} — next ${new Date(result.nextRunAt).toISOString()}.`,
88
+ result,
89
+ );
90
+ }
91
+ case "list": {
92
+ const result = (await sched.listJobs({})) as { jobs: Job[] };
93
+ const body = result.jobs.length
94
+ ? result.jobs
95
+ .map((j) => {
96
+ const label = j.name ? `${j.name} ` : "";
97
+ const state = j.enabled ? `next ${new Date(j.nextRunAt).toISOString()}` : "disabled";
98
+ return `${j.id} ${label}— ${describeSchedule(j.schedule)} — ${state}`;
99
+ })
100
+ .join("\n")
101
+ : "No scheduled jobs.";
102
+ return text(body, result);
103
+ }
104
+ case "update": {
105
+ if (!params.id) return text("Error: id is required to update a job.", { error: "id required" });
106
+ const result = await sched.updateJob({
107
+ id: params.id,
108
+ prompt: params.prompt,
109
+ name: params.name,
110
+ schedule: buildSchedule(params),
111
+ enabled: params.enabled,
112
+ });
113
+ return text(`Updated job ${params.id}.`, result);
114
+ }
115
+ case "remove": {
116
+ if (!params.id) return text("Error: id is required to remove a job.", { error: "id required" });
117
+ const result = (await sched.removeJob({ id: params.id })) as { removed: boolean };
118
+ return text(result.removed ? `Removed job ${params.id}.` : `No job ${params.id}.`, result);
119
+ }
120
+ case "run": {
121
+ if (!params.id) return text("Error: id is required to run a job.", { error: "id required" });
122
+ const result = await sched.runJob({ id: params.id });
123
+ return text(`Job ${params.id} will run on the next tick.`, result);
124
+ }
125
+ }
126
+ } catch (err) {
127
+ const message = err instanceof Error ? err.message : String(err);
128
+ return text(`Error: ${message}`, { error: message });
129
+ }
130
+ },
131
+ });
@@ -4,13 +4,12 @@
4
4
  * This integration owns the jobs and the wake loop: it persists jobs in `ctx.store`
5
5
  * (one file at `~/.wolli/agents/<name>/store/scheduler.json`), ticks a coarse timer,
6
6
  * and emits a `due` event when a job's time arrives. It does not touch sessions or the
7
- * agent; the paired extension (`scheduler-chat.ts`) registers the agent-facing `cron`
8
- * tool and, on `due`, wakes a session. See `INTEGRATION.md` for the producer-vs-mapping
9
- * split.
7
+ * agent; the `cron` tool (`cron.ts`) drives the CRUD actions and the `scheduler-due.ts`
8
+ * workflow, on `due`, wakes the session the job was scheduled from.
10
9
  *
11
10
  * Jobs are scheduled by the agent through the `cron` tool, which calls the CRUD actions
12
- * below. The scheduler has no secret — onboarding just writes an empty `scheduler.default`
13
- * account so `run()` starts.
11
+ * below. The scheduler has no secret — onboarding just writes an empty `scheduler`
12
+ * account record so `run()` starts.
14
13
  *
15
14
  * ## Guarantees
16
15
  * - At-most-once: a tick advances a job's `nextRunAt` (or disables a one-shot) and
@@ -21,7 +20,7 @@
21
20
  */
22
21
 
23
22
  import { randomUUID } from "node:crypto";
24
- import type { IntegrationOnboardContext, IntegrationsAPI, KeyValueStore } from "@opsyhq/wolli";
23
+ import { defineIntegration, type IntegrationOnboardContext, type KeyValueStore } from "wolli";
25
24
  import { Cron } from "croner";
26
25
  import { type Static, Type } from "typebox";
27
26
 
@@ -80,165 +79,162 @@ async function onboard(ctx: IntegrationOnboardContext): Promise<Record<string, u
80
79
  return {};
81
80
  }
82
81
 
83
- export default function (wolli: IntegrationsAPI) {
84
- wolli.registerIntegration({
85
- name: "scheduler",
86
- account: Type.Object({
87
- /** Wake interval in ms; defaults to 60s. */
88
- tickMs: Type.Optional(Type.Number()),
82
+ export default defineIntegration({
83
+ account: Type.Object({
84
+ /** Wake interval in ms; defaults to 60s. */
85
+ tickMs: Type.Optional(Type.Number()),
86
+ }),
87
+ events: {
88
+ due: Type.Object({
89
+ id: Type.String(),
90
+ prompt: Type.String(),
91
+ originTags: Type.Optional(Type.Record(Type.String(), Type.String())),
92
+ name: Type.Optional(Type.String()),
89
93
  }),
90
- events: {
91
- due: Type.Object({
92
- id: Type.String(),
94
+ },
95
+ onboard,
96
+ actions: {
97
+ addJob: {
98
+ description: "Schedule a new job from a prompt and a schedule (at / every / cron).",
99
+ parameters: Type.Object({
93
100
  prompt: Type.String(),
94
- originTags: Type.Optional(Type.Record(Type.String(), Type.String())),
95
101
  name: Type.Optional(Type.String()),
102
+ schedule: Schedule,
103
+ originTags: Type.Optional(Type.Record(Type.String(), Type.String())),
96
104
  }),
97
- },
98
- onboard,
99
- actions: {
100
- addJob: {
101
- description: "Schedule a new job from a prompt and a schedule (at / every / cron).",
102
- parameters: Type.Object({
103
- prompt: Type.String(),
104
- name: Type.Optional(Type.String()),
105
- schedule: Schedule,
106
- originTags: Type.Optional(Type.Record(Type.String(), Type.String())),
107
- }),
108
- execute: async (params, ctx) => {
109
- const p = params as {
110
- prompt: string;
111
- name?: string;
112
- schedule: Schedule;
113
- originTags?: Record<string, string>;
114
- };
115
- const now = Date.now();
116
- const seeded = computeNextRunAt(p.schedule, now);
117
- const job: Job = {
118
- id: randomUUID(),
119
- name: p.name,
120
- prompt: p.prompt,
121
- schedule: p.schedule,
122
- enabled: seeded !== null,
123
- originTags: p.originTags,
124
- nextRunAt: seeded ?? 0,
125
- };
126
- const jobs = loadJobs(ctx.store);
127
- jobs[job.id] = job;
128
- saveJobs(ctx.store, jobs);
129
- return { id: job.id, nextRunAt: job.nextRunAt };
130
- },
105
+ execute: async (params, ctx) => {
106
+ const p = params as {
107
+ prompt: string;
108
+ name?: string;
109
+ schedule: Schedule;
110
+ originTags?: Record<string, string>;
111
+ };
112
+ const now = Date.now();
113
+ const seeded = computeNextRunAt(p.schedule, now);
114
+ const job: Job = {
115
+ id: randomUUID(),
116
+ name: p.name,
117
+ prompt: p.prompt,
118
+ schedule: p.schedule,
119
+ enabled: seeded !== null,
120
+ originTags: p.originTags,
121
+ nextRunAt: seeded ?? 0,
122
+ };
123
+ const jobs = loadJobs(ctx.store);
124
+ jobs[job.id] = job;
125
+ saveJobs(ctx.store, jobs);
126
+ return { id: job.id, nextRunAt: job.nextRunAt };
131
127
  },
132
- listJobs: {
133
- description: "List all scheduled jobs.",
134
- parameters: Type.Object({}),
135
- execute: async (_params, ctx) => {
136
- return { jobs: Object.values(loadJobs(ctx.store)) };
137
- },
128
+ },
129
+ listJobs: {
130
+ description: "List all scheduled jobs.",
131
+ parameters: Type.Object({}),
132
+ execute: async (_params, ctx) => {
133
+ return { jobs: Object.values(loadJobs(ctx.store)) };
138
134
  },
139
- updateJob: {
140
- description: "Update a job by id; recomputes the next run when the schedule changes.",
141
- parameters: Type.Object({
142
- id: Type.String(),
143
- prompt: Type.Optional(Type.String()),
144
- name: Type.Optional(Type.String()),
145
- schedule: Type.Optional(Schedule),
146
- enabled: Type.Optional(Type.Boolean()),
147
- }),
148
- execute: async (params, ctx) => {
149
- const p = params as {
150
- id: string;
151
- prompt?: string;
152
- name?: string;
153
- schedule?: Schedule;
154
- enabled?: boolean;
155
- };
156
- const jobs = loadJobs(ctx.store);
157
- const job = jobs[p.id];
158
- if (!job) throw new Error(`unknown job '${p.id}'`);
135
+ },
136
+ updateJob: {
137
+ description: "Update a job by id; recomputes the next run when the schedule changes.",
138
+ parameters: Type.Object({
139
+ id: Type.String(),
140
+ prompt: Type.Optional(Type.String()),
141
+ name: Type.Optional(Type.String()),
142
+ schedule: Type.Optional(Schedule),
143
+ enabled: Type.Optional(Type.Boolean()),
144
+ }),
145
+ execute: async (params, ctx) => {
146
+ const p = params as {
147
+ id: string;
148
+ prompt?: string;
149
+ name?: string;
150
+ schedule?: Schedule;
151
+ enabled?: boolean;
152
+ };
153
+ const jobs = loadJobs(ctx.store);
154
+ const job = jobs[p.id];
155
+ if (!job) throw new Error(`unknown job '${p.id}'`);
159
156
 
160
- if (p.prompt !== undefined) job.prompt = p.prompt;
161
- if (p.name !== undefined) job.name = p.name;
162
- if (p.enabled !== undefined) job.enabled = p.enabled;
163
- if (p.schedule !== undefined) {
164
- job.schedule = p.schedule;
165
- const next = computeNextRunAt(p.schedule, Date.now());
166
- job.nextRunAt = next ?? 0;
167
- if (next === null) job.enabled = false;
168
- }
157
+ if (p.prompt !== undefined) job.prompt = p.prompt;
158
+ if (p.name !== undefined) job.name = p.name;
159
+ if (p.enabled !== undefined) job.enabled = p.enabled;
160
+ if (p.schedule !== undefined) {
161
+ job.schedule = p.schedule;
162
+ const next = computeNextRunAt(p.schedule, Date.now());
163
+ job.nextRunAt = next ?? 0;
164
+ if (next === null) job.enabled = false;
165
+ }
169
166
 
170
- saveJobs(ctx.store, jobs);
171
- return { job };
172
- },
167
+ saveJobs(ctx.store, jobs);
168
+ return { job };
173
169
  },
174
- removeJob: {
175
- description: "Delete a job by id.",
176
- parameters: Type.Object({ id: Type.String() }),
177
- execute: async (params, ctx) => {
178
- const { id } = params as { id: string };
179
- const jobs = loadJobs(ctx.store);
180
- const removed = id in jobs;
181
- delete jobs[id];
182
- saveJobs(ctx.store, jobs);
183
- return { removed };
184
- },
170
+ },
171
+ removeJob: {
172
+ description: "Delete a job by id.",
173
+ parameters: Type.Object({ id: Type.String() }),
174
+ execute: async (params, ctx) => {
175
+ const { id } = params as { id: string };
176
+ const jobs = loadJobs(ctx.store);
177
+ const removed = id in jobs;
178
+ delete jobs[id];
179
+ saveJobs(ctx.store, jobs);
180
+ return { removed };
185
181
  },
186
- runJob: {
187
- description: "Run a job on the next tick (sets it due immediately).",
188
- parameters: Type.Object({ id: Type.String() }),
189
- execute: async (params, ctx) => {
190
- const { id } = params as { id: string };
191
- const jobs = loadJobs(ctx.store);
192
- const job = jobs[id];
193
- if (!job) throw new Error(`unknown job '${id}'`);
194
- job.enabled = true;
195
- job.nextRunAt = 0;
196
- saveJobs(ctx.store, jobs);
197
- return { id, nextRunAt: job.nextRunAt };
198
- },
182
+ },
183
+ runJob: {
184
+ description: "Run a job on the next tick (sets it due immediately).",
185
+ parameters: Type.Object({ id: Type.String() }),
186
+ execute: async (params, ctx) => {
187
+ const { id } = params as { id: string };
188
+ const jobs = loadJobs(ctx.store);
189
+ const job = jobs[id];
190
+ if (!job) throw new Error(`unknown job '${id}'`);
191
+ job.enabled = true;
192
+ job.nextRunAt = 0;
193
+ saveJobs(ctx.store, jobs);
194
+ return { id, nextRunAt: job.nextRunAt };
199
195
  },
200
196
  },
201
- run(ctx) {
202
- const account = ctx.account as SchedulerAccount;
203
- const tickMs = account.tickMs ?? DEFAULT_TICK_MS;
197
+ },
198
+ run(ctx) {
199
+ const account = ctx.account as SchedulerAccount;
200
+ const tickMs = account.tickMs ?? DEFAULT_TICK_MS;
204
201
 
205
- const tick = (): void => {
206
- const now = Date.now();
207
- const jobs = loadJobs(ctx.store);
208
- const due: Job[] = [];
209
- for (const job of Object.values(jobs)) {
210
- if (!job.enabled || job.nextRunAt > now) continue;
211
- job.lastRunAt = now;
212
- if (job.schedule.kind === "at") {
213
- job.enabled = false; // one-shot
214
- } else {
215
- const next = computeNextRunAt(job.schedule, now);
216
- if (next === null) job.enabled = false;
217
- else job.nextRunAt = next;
218
- }
219
- due.push(job);
202
+ const tick = (): void => {
203
+ const now = Date.now();
204
+ const jobs = loadJobs(ctx.store);
205
+ const due: Job[] = [];
206
+ for (const job of Object.values(jobs)) {
207
+ if (!job.enabled || job.nextRunAt > now) continue;
208
+ job.lastRunAt = now;
209
+ if (job.schedule.kind === "at") {
210
+ job.enabled = false; // one-shot
211
+ } else {
212
+ const next = computeNextRunAt(job.schedule, now);
213
+ if (next === null) job.enabled = false;
214
+ else job.nextRunAt = next;
220
215
  }
221
- if (due.length === 0) return;
216
+ due.push(job);
217
+ }
218
+ if (due.length === 0) return;
222
219
 
223
- // Persist the advanced state before emitting so a crash right after an emit never re-fires.
224
- saveJobs(ctx.store, jobs);
225
- for (const job of due) {
226
- ctx.emit("due", {
227
- id: job.id,
228
- prompt: job.prompt,
229
- originTags: job.originTags,
230
- name: job.name,
231
- });
232
- }
233
- };
220
+ // Persist the advanced state before emitting so a crash right after an emit never re-fires.
221
+ saveJobs(ctx.store, jobs);
222
+ for (const job of due) {
223
+ ctx.emit("due", {
224
+ id: job.id,
225
+ prompt: job.prompt,
226
+ originTags: job.originTags,
227
+ name: job.name,
228
+ });
229
+ }
230
+ };
234
231
 
235
- // One catch-up tick on start: each overdue job fires once (recompute-from-now), not N replays.
236
- tick();
237
- const timer = setInterval(tick, tickMs);
232
+ // One catch-up tick on start: each overdue job fires once (recompute-from-now), not N replays.
233
+ tick();
234
+ const timer = setInterval(tick, tickMs);
238
235
 
239
- const dispose = () => clearInterval(timer);
240
- ctx.signal.addEventListener("abort", dispose);
241
- return dispose;
242
- },
243
- });
244
- }
236
+ const dispose = () => clearInterval(timer);
237
+ ctx.signal.addEventListener("abort", dispose);
238
+ return dispose;
239
+ },
240
+ });
@@ -5,12 +5,13 @@
5
5
  "type": "module",
6
6
  "wolli": {
7
7
  "integrations": ["./index.ts"],
8
- "extensions": ["./scheduler-chat.ts"]
8
+ "workflows": ["./scheduler-due.ts"],
9
+ "tools": ["./cron.ts"]
9
10
  },
10
11
  "dependencies": {
11
12
  "croner": "10.0.1"
12
13
  },
13
14
  "peerDependencies": {
14
- "@opsyhq/wolli": "*"
15
+ "wolli": "*"
15
16
  }
16
17
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Scheduler due routing — on a `due` event, runs the job's prompt as a turn in the session
3
+ * it was scheduled from (the newest match for the job's origin tags). A telegram-tagged
4
+ * origin means telegram's own `agent_end` ships the reply back to that chat; no
5
+ * scheduler-side channel handling. If no session matches (the origin was pruned), a fresh
6
+ * one is created carrying the SAME origin tags so it stays bound to that surface — never
7
+ * an untagged session, which would deliver nowhere.
8
+ */
9
+
10
+ import scheduler from "./index.ts";
11
+
12
+ export const due = scheduler.on("due", async (job, ctx) => {
13
+ const originTags = job.originTags ?? {};
14
+ const [match] = await ctx.agent.findSessions(originTags);
15
+ const session = match
16
+ ? await ctx.agent.openSession(match.id)
17
+ : await ctx.agent.createSession({
18
+ setup: (s) => s.appendTags(originTags),
19
+ });
20
+ // followUp queues cleanly if a turn is in flight.
21
+ await session.sendUserMessage(job.prompt, { deliverAs: "followUp" });
22
+ });
@@ -33,13 +33,13 @@ response.
33
33
 
34
34
  ## Commands
35
35
 
36
- These commands are registered as the bot's command menu via BotFather on startup and
37
- are handled by the integration directly, not forwarded to the model:
36
+ These commands are registered as the bot's command menu at producer startup and are
37
+ handled locally by the inbound routing workflow, not forwarded to the model:
38
38
 
39
39
  | Command | Action |
40
40
  |---------|--------|
41
41
  | `/new` | Start a fresh session for this chat. The new session becomes the active one; the previous session stays addressable but new messages route to the new one. |
42
- | `/status` | Show the current session name and the model used in the last reply. |
42
+ | `/status` | Show the current session name and its current model. |
43
43
  | `/help` | List the available commands. |
44
44
 
45
45
  Any other `/command` returns `Unknown command: /<name>. Try /help.`