opencode-claw 0.1.0 → 0.2.1
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 +3 -2
- package/dist/channels/router.d.ts +1 -1
- package/dist/channels/router.js +108 -33
- package/dist/channels/slack.js +3 -0
- package/dist/channels/telegram.js +3 -0
- package/dist/channels/types.d.ts +2 -0
- package/dist/channels/whatsapp.js +12 -0
- package/dist/cli.js +22 -4
- package/dist/config/schema.d.ts +33 -0
- package/dist/config/schema.js +7 -0
- package/dist/cron/scheduler.d.ts +1 -1
- package/dist/cron/scheduler.js +5 -25
- package/dist/index.js +1 -1
- package/dist/sessions/manager.d.ts +1 -1
- package/dist/sessions/manager.js +2 -2
- package/dist/sessions/prompt.d.ts +13 -0
- package/dist/sessions/prompt.js +118 -0
- package/dist/wizard/clack-prompter.d.ts +2 -0
- package/dist/wizard/clack-prompter.js +53 -0
- package/dist/wizard/onboarding.d.ts +2 -0
- package/dist/wizard/onboarding.js +209 -0
- package/dist/wizard/prompts.d.ts +31 -0
- package/dist/wizard/prompts.js +6 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -51,8 +51,7 @@ opencode-claw
|
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
53
|
# 1. Create a config file in your project directory
|
|
54
|
-
|
|
55
|
-
mv opencode-claw.example.json opencode-claw.json
|
|
54
|
+
npx opencode-claw --init
|
|
56
55
|
|
|
57
56
|
# 2. Edit opencode-claw.json with your tokens and preferences
|
|
58
57
|
# (see Configuration section below)
|
|
@@ -61,6 +60,8 @@ mv opencode-claw.example.json opencode-claw.json
|
|
|
61
60
|
npx opencode-claw
|
|
62
61
|
```
|
|
63
62
|
|
|
63
|
+
Or create `opencode-claw.json` manually — see the [example config](https://github.com/jinkoso/opencode-claw/blob/main/opencode-claw.example.json) for all available options.
|
|
64
|
+
|
|
64
65
|
The service starts an OpenCode server, connects your configured channels, initializes the memory system, and begins listening for messages.
|
|
65
66
|
|
|
66
67
|
## Configuration
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
1
|
+
import type { OpencodeClient } from "@opencode-ai/sdk/v2";
|
|
2
2
|
import type { Config } from "../config/types.js";
|
|
3
3
|
import type { SessionManager } from "../sessions/manager.js";
|
|
4
4
|
import type { Logger } from "../utils/logger.js";
|
package/dist/channels/router.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildSessionKey } from "../sessions/manager.js";
|
|
2
|
+
import { promptStreaming } from "../sessions/prompt.js";
|
|
2
3
|
function allowlist(config, channel) {
|
|
3
4
|
const ch = config.channels[channel];
|
|
4
5
|
if (!ch)
|
|
@@ -17,16 +18,10 @@ function rejection(config, channel) {
|
|
|
17
18
|
}
|
|
18
19
|
function checkAllowlist(config, msg) {
|
|
19
20
|
const list = allowlist(config, msg.channel);
|
|
20
|
-
if (!list)
|
|
21
|
+
if (!list || list.length === 0)
|
|
21
22
|
return true;
|
|
22
23
|
return list.includes(msg.peerId);
|
|
23
24
|
}
|
|
24
|
-
function extractText(parts) {
|
|
25
|
-
return parts
|
|
26
|
-
.filter((p) => p.type === "text" && typeof p.text === "string")
|
|
27
|
-
.map((p) => p.text)
|
|
28
|
-
.join("\n\n");
|
|
29
|
-
}
|
|
30
25
|
function parseCommand(text) {
|
|
31
26
|
const trimmed = text.trim();
|
|
32
27
|
if (!trimmed.startsWith("/"))
|
|
@@ -42,8 +37,13 @@ const HELP_TEXT = `Available commands:
|
|
|
42
37
|
/sessions — List your sessions
|
|
43
38
|
/current — Show current session
|
|
44
39
|
/fork — Fork current session into a new one
|
|
40
|
+
/cancel — Abort the currently running agent
|
|
45
41
|
/help — Show this help`;
|
|
46
|
-
|
|
42
|
+
// peerKey uniquely identifies a peer within a channel for active-stream tracking
|
|
43
|
+
function peerKey(channel, peerId) {
|
|
44
|
+
return `${channel}:${peerId}`;
|
|
45
|
+
}
|
|
46
|
+
async function handleCommand(cmd, msg, deps, activeStreams) {
|
|
47
47
|
const key = buildSessionKey(msg.channel, msg.peerId, msg.threadId);
|
|
48
48
|
const prefix = `${msg.channel}:${msg.peerId}`;
|
|
49
49
|
switch (cmd.name) {
|
|
@@ -79,8 +79,7 @@ async function handleCommand(cmd, msg, deps) {
|
|
|
79
79
|
if (!current)
|
|
80
80
|
return "No active session to fork.";
|
|
81
81
|
const result = await deps.client.session.fork({
|
|
82
|
-
|
|
83
|
-
body: {},
|
|
82
|
+
sessionID: current,
|
|
84
83
|
});
|
|
85
84
|
if (!result.data)
|
|
86
85
|
return "Fork failed: no data returned.";
|
|
@@ -88,6 +87,16 @@ async function handleCommand(cmd, msg, deps) {
|
|
|
88
87
|
await deps.sessions.switchSession(key, forked);
|
|
89
88
|
return `Forked into new session: ${forked}`;
|
|
90
89
|
}
|
|
90
|
+
case "cancel": {
|
|
91
|
+
const pk = peerKey(msg.channel, msg.peerId);
|
|
92
|
+
const sessionId = activeStreams.get(pk);
|
|
93
|
+
if (!sessionId)
|
|
94
|
+
return "No agent is currently running.";
|
|
95
|
+
const result = await deps.client.session.abort({ sessionID: sessionId });
|
|
96
|
+
const aborted = result.data ?? false;
|
|
97
|
+
deps.logger.info("router: session aborted by user", { sessionId, aborted });
|
|
98
|
+
return aborted ? "Agent aborted." : "Abort request sent (agent may already be done).";
|
|
99
|
+
}
|
|
91
100
|
case "help": {
|
|
92
101
|
return HELP_TEXT;
|
|
93
102
|
}
|
|
@@ -96,7 +105,7 @@ async function handleCommand(cmd, msg, deps) {
|
|
|
96
105
|
}
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
|
-
async function routeMessage(msg, deps) {
|
|
108
|
+
async function routeMessage(msg, deps, activeStreams, pendingQuestions) {
|
|
100
109
|
const adapter = deps.adapters.get(msg.channel);
|
|
101
110
|
if (!adapter) {
|
|
102
111
|
deps.logger.warn("router: no adapter for channel", { channel: msg.channel });
|
|
@@ -117,7 +126,7 @@ async function routeMessage(msg, deps) {
|
|
|
117
126
|
// Command interception
|
|
118
127
|
const cmd = parseCommand(msg.text);
|
|
119
128
|
if (cmd) {
|
|
120
|
-
const reply = await handleCommand(cmd, msg, deps);
|
|
129
|
+
const reply = await handleCommand(cmd, msg, deps, activeStreams);
|
|
121
130
|
await adapter.send(msg.peerId, { text: reply, replyToId: msg.replyToId });
|
|
122
131
|
return;
|
|
123
132
|
}
|
|
@@ -125,38 +134,91 @@ async function routeMessage(msg, deps) {
|
|
|
125
134
|
const key = buildSessionKey(msg.channel, msg.peerId, msg.threadId);
|
|
126
135
|
const sessionId = await deps.sessions.resolveSession(key);
|
|
127
136
|
deps.logger.debug("router: prompting session", { sessionId, channel: msg.channel });
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
const pk = peerKey(msg.channel, msg.peerId);
|
|
138
|
+
activeStreams.set(pk, sessionId);
|
|
139
|
+
// Start typing indicator
|
|
140
|
+
if (adapter.sendTyping) {
|
|
141
|
+
await adapter.sendTyping(msg.peerId).catch(() => { });
|
|
142
|
+
}
|
|
143
|
+
const progressEnabled = deps.config.router.progress.enabled;
|
|
144
|
+
function formatQuestion(request) {
|
|
145
|
+
const lines = ["❓ The agent has a question:"];
|
|
146
|
+
for (const q of request.questions) {
|
|
147
|
+
lines.push("");
|
|
148
|
+
if (q.header)
|
|
149
|
+
lines.push(`**${q.header}**`);
|
|
150
|
+
lines.push(q.question);
|
|
151
|
+
if (q.options && q.options.length > 0) {
|
|
152
|
+
for (let i = 0; i < q.options.length; i++) {
|
|
153
|
+
const opt = q.options[i];
|
|
154
|
+
if (opt) {
|
|
155
|
+
lines.push(` ${i + 1}. ${opt.label}${opt.description ? ` — ${opt.description}` : ""}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (q.multiple)
|
|
160
|
+
lines.push("(You can pick multiple — separate with commas)");
|
|
161
|
+
}
|
|
162
|
+
lines.push("");
|
|
163
|
+
lines.push("Reply with your answer:");
|
|
164
|
+
return lines.join("\n");
|
|
165
|
+
}
|
|
166
|
+
function waitForUserReply(questionTimeoutMs) {
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const timer = setTimeout(() => {
|
|
169
|
+
pendingQuestions.delete(pk);
|
|
170
|
+
reject(new Error("question_timeout"));
|
|
171
|
+
}, questionTimeoutMs);
|
|
172
|
+
pendingQuestions.set(pk, { resolve, timeout: timer });
|
|
135
173
|
});
|
|
136
174
|
}
|
|
175
|
+
const progress = progressEnabled
|
|
176
|
+
? {
|
|
177
|
+
onToolRunning: (_tool, title) => adapter.send(msg.peerId, {
|
|
178
|
+
text: `🔧 ${title}...`,
|
|
179
|
+
replyToId: msg.replyToId,
|
|
180
|
+
}),
|
|
181
|
+
onHeartbeat: async () => {
|
|
182
|
+
if (adapter.sendTyping) {
|
|
183
|
+
await adapter.sendTyping(msg.peerId).catch(() => { });
|
|
184
|
+
}
|
|
185
|
+
await adapter.send(msg.peerId, { text: "⏳ Still working..." });
|
|
186
|
+
},
|
|
187
|
+
onQuestion: async (request) => {
|
|
188
|
+
const text = formatQuestion(request);
|
|
189
|
+
await adapter.send(msg.peerId, { text });
|
|
190
|
+
const userReply = await waitForUserReply(deps.timeoutMs);
|
|
191
|
+
return request.questions.map(() => [userReply]);
|
|
192
|
+
},
|
|
193
|
+
toolThrottleMs: deps.config.router.progress.toolThrottleMs,
|
|
194
|
+
heartbeatMs: deps.config.router.progress.heartbeatMs,
|
|
195
|
+
}
|
|
196
|
+
: undefined;
|
|
197
|
+
let reply;
|
|
198
|
+
try {
|
|
199
|
+
reply = await promptStreaming(deps.client, sessionId, msg.text, deps.timeoutMs, deps.logger, progress);
|
|
200
|
+
}
|
|
137
201
|
catch (err) {
|
|
138
|
-
|
|
139
|
-
if (controller.signal.aborted) {
|
|
140
|
-
deps.logger.warn("router: session prompt timed out", {
|
|
141
|
-
sessionId,
|
|
142
|
-
timeoutMs: deps.timeoutMs,
|
|
143
|
-
});
|
|
202
|
+
if (err instanceof Error && err.message === "timeout") {
|
|
144
203
|
await adapter.send(msg.peerId, {
|
|
145
204
|
text: "Request timed out. The agent took too long to respond.",
|
|
146
205
|
replyToId: msg.replyToId,
|
|
147
206
|
});
|
|
148
207
|
return;
|
|
149
208
|
}
|
|
209
|
+
if (err instanceof Error && err.message === "aborted") {
|
|
210
|
+
// Already notified via /cancel reply; nothing more to send
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
150
213
|
throw err;
|
|
151
214
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
215
|
+
finally {
|
|
216
|
+
activeStreams.delete(pk);
|
|
217
|
+
pendingQuestions.delete(pk);
|
|
218
|
+
if (adapter.stopTyping) {
|
|
219
|
+
await adapter.stopTyping(msg.peerId).catch(() => { });
|
|
220
|
+
}
|
|
157
221
|
}
|
|
158
|
-
// Extract text parts from response
|
|
159
|
-
const reply = extractText(result.data.parts);
|
|
160
222
|
if (!reply) {
|
|
161
223
|
deps.logger.warn("router: empty response from agent", { sessionId });
|
|
162
224
|
await adapter.send(msg.peerId, { text: "(empty response)" });
|
|
@@ -165,9 +227,22 @@ async function routeMessage(msg, deps) {
|
|
|
165
227
|
await adapter.send(msg.peerId, { text: reply, replyToId: msg.replyToId });
|
|
166
228
|
}
|
|
167
229
|
export function createRouter(deps) {
|
|
230
|
+
// Tracks which sessionId is currently streaming for each channel:peerId pair
|
|
231
|
+
const activeStreams = new Map();
|
|
232
|
+
// Tracks pending question resolvers — when agent asks a question, user's next message resolves it
|
|
233
|
+
const pendingQuestions = new Map();
|
|
168
234
|
async function handler(msg) {
|
|
169
235
|
try {
|
|
170
|
-
|
|
236
|
+
// Check if this message is a reply to a pending question
|
|
237
|
+
const pk = peerKey(msg.channel, msg.peerId);
|
|
238
|
+
const pending = pendingQuestions.get(pk);
|
|
239
|
+
if (pending) {
|
|
240
|
+
clearTimeout(pending.timeout);
|
|
241
|
+
pendingQuestions.delete(pk);
|
|
242
|
+
pending.resolve(msg.text);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
await routeMessage(msg, deps, activeStreams, pendingQuestions);
|
|
171
246
|
}
|
|
172
247
|
catch (err) {
|
|
173
248
|
deps.logger.error("router: unhandled error", {
|
package/dist/channels/slack.js
CHANGED
|
@@ -72,6 +72,9 @@ export function createTelegramAdapter(config, logger) {
|
|
|
72
72
|
reply_parameters: message.replyToId ? { message_id: Number(message.replyToId) } : undefined,
|
|
73
73
|
});
|
|
74
74
|
},
|
|
75
|
+
async sendTyping(peerId) {
|
|
76
|
+
await bot.api.sendChatAction(Number(peerId), "typing");
|
|
77
|
+
},
|
|
75
78
|
status() {
|
|
76
79
|
return state;
|
|
77
80
|
},
|
package/dist/channels/types.d.ts
CHANGED
|
@@ -23,5 +23,7 @@ export type ChannelAdapter = {
|
|
|
23
23
|
start(handler: InboundMessageHandler): Promise<void>;
|
|
24
24
|
stop(): Promise<void>;
|
|
25
25
|
send(peerId: string, message: OutboundMessage): Promise<void>;
|
|
26
|
+
sendTyping?(peerId: string): Promise<void>;
|
|
27
|
+
stopTyping?(peerId: string): Promise<void>;
|
|
26
28
|
status(): ChannelStatus;
|
|
27
29
|
};
|
|
@@ -129,6 +129,18 @@ export function createWhatsAppAdapter(config, logger) {
|
|
|
129
129
|
const jid = `${peerId}@s.whatsapp.net`;
|
|
130
130
|
await sock.sendMessage(jid, { text: message.text });
|
|
131
131
|
},
|
|
132
|
+
async sendTyping(peerId) {
|
|
133
|
+
if (!sock)
|
|
134
|
+
return;
|
|
135
|
+
const jid = `${peerId}@s.whatsapp.net`;
|
|
136
|
+
await sock.sendPresenceUpdate("composing", jid);
|
|
137
|
+
},
|
|
138
|
+
async stopTyping(peerId) {
|
|
139
|
+
if (!sock)
|
|
140
|
+
return;
|
|
141
|
+
const jid = `${peerId}@s.whatsapp.net`;
|
|
142
|
+
await sock.sendPresenceUpdate("paused", jid);
|
|
143
|
+
},
|
|
132
144
|
status() {
|
|
133
145
|
return state;
|
|
134
146
|
},
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { main } from "./index.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
});
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
if (args.includes("--init")) {
|
|
5
|
+
const { runOnboardingWizard } = await import("./wizard/onboarding.js");
|
|
6
|
+
const { createClackPrompter } = await import("./wizard/clack-prompter.js");
|
|
7
|
+
const { WizardCancelledError } = await import("./wizard/prompts.js");
|
|
8
|
+
const prompter = createClackPrompter();
|
|
9
|
+
try {
|
|
10
|
+
await runOnboardingWizard(prompter);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
if (err instanceof WizardCancelledError) {
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
main().catch((err) => {
|
|
21
|
+
console.error("Fatal:", err);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
|
24
|
+
}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -310,10 +310,33 @@ export declare const configSchema: z.ZodObject<{
|
|
|
310
310
|
}>>;
|
|
311
311
|
router: z.ZodDefault<z.ZodObject<{
|
|
312
312
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
313
|
+
progress: z.ZodDefault<z.ZodObject<{
|
|
314
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
315
|
+
toolThrottleMs: z.ZodDefault<z.ZodNumber>;
|
|
316
|
+
heartbeatMs: z.ZodDefault<z.ZodNumber>;
|
|
317
|
+
}, "strip", z.ZodTypeAny, {
|
|
318
|
+
enabled: boolean;
|
|
319
|
+
toolThrottleMs: number;
|
|
320
|
+
heartbeatMs: number;
|
|
321
|
+
}, {
|
|
322
|
+
enabled?: boolean | undefined;
|
|
323
|
+
toolThrottleMs?: number | undefined;
|
|
324
|
+
heartbeatMs?: number | undefined;
|
|
325
|
+
}>>;
|
|
313
326
|
}, "strip", z.ZodTypeAny, {
|
|
314
327
|
timeoutMs: number;
|
|
328
|
+
progress: {
|
|
329
|
+
enabled: boolean;
|
|
330
|
+
toolThrottleMs: number;
|
|
331
|
+
heartbeatMs: number;
|
|
332
|
+
};
|
|
315
333
|
}, {
|
|
316
334
|
timeoutMs?: number | undefined;
|
|
335
|
+
progress?: {
|
|
336
|
+
enabled?: boolean | undefined;
|
|
337
|
+
toolThrottleMs?: number | undefined;
|
|
338
|
+
heartbeatMs?: number | undefined;
|
|
339
|
+
} | undefined;
|
|
317
340
|
}>>;
|
|
318
341
|
}, "strip", z.ZodTypeAny, {
|
|
319
342
|
opencode: {
|
|
@@ -375,6 +398,11 @@ export declare const configSchema: z.ZodObject<{
|
|
|
375
398
|
};
|
|
376
399
|
router: {
|
|
377
400
|
timeoutMs: number;
|
|
401
|
+
progress: {
|
|
402
|
+
enabled: boolean;
|
|
403
|
+
toolThrottleMs: number;
|
|
404
|
+
heartbeatMs: number;
|
|
405
|
+
};
|
|
378
406
|
};
|
|
379
407
|
cron?: {
|
|
380
408
|
enabled: boolean;
|
|
@@ -478,5 +506,10 @@ export declare const configSchema: z.ZodObject<{
|
|
|
478
506
|
} | undefined;
|
|
479
507
|
router?: {
|
|
480
508
|
timeoutMs?: number | undefined;
|
|
509
|
+
progress?: {
|
|
510
|
+
enabled?: boolean | undefined;
|
|
511
|
+
toolThrottleMs?: number | undefined;
|
|
512
|
+
heartbeatMs?: number | undefined;
|
|
513
|
+
} | undefined;
|
|
481
514
|
} | undefined;
|
|
482
515
|
}>;
|
package/dist/config/schema.js
CHANGED
|
@@ -103,6 +103,13 @@ export const configSchema = z.object({
|
|
|
103
103
|
router: z
|
|
104
104
|
.object({
|
|
105
105
|
timeoutMs: z.number().int().min(1000).default(300_000),
|
|
106
|
+
progress: z
|
|
107
|
+
.object({
|
|
108
|
+
enabled: z.boolean().default(true),
|
|
109
|
+
toolThrottleMs: z.number().int().min(1000).default(5_000),
|
|
110
|
+
heartbeatMs: z.number().int().min(10_000).default(60_000),
|
|
111
|
+
})
|
|
112
|
+
.default({}),
|
|
106
113
|
})
|
|
107
114
|
.default({}),
|
|
108
115
|
});
|
package/dist/cron/scheduler.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
1
|
+
import type { OpencodeClient } from "@opencode-ai/sdk/v2";
|
|
2
2
|
import type { CronConfig } from "../config/types.js";
|
|
3
3
|
import type { OutboxWriter } from "../outbox/writer.js";
|
|
4
4
|
import type { Logger } from "../utils/logger.js";
|
package/dist/cron/scheduler.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import cron from "node-cron";
|
|
2
|
-
|
|
3
|
-
return parts
|
|
4
|
-
.filter((p) => p.type === "text" && typeof p.text === "string")
|
|
5
|
-
.map((p) => p.text)
|
|
6
|
-
.join("\n\n");
|
|
7
|
-
}
|
|
2
|
+
import { promptStreaming } from "../sessions/prompt.js";
|
|
8
3
|
export function createCronScheduler(deps) {
|
|
9
4
|
const jobs = new Map();
|
|
10
5
|
const running = new Set();
|
|
@@ -18,39 +13,24 @@ export function createCronScheduler(deps) {
|
|
|
18
13
|
deps.logger.info(`cron: firing job "${job.id}"`, { schedule: job.schedule });
|
|
19
14
|
try {
|
|
20
15
|
const session = await deps.client.session.create({
|
|
21
|
-
|
|
16
|
+
title,
|
|
22
17
|
});
|
|
23
18
|
if (!session.data)
|
|
24
19
|
throw new Error("session.create returned no data");
|
|
25
20
|
const sessionId = session.data.id;
|
|
26
21
|
deps.logger.debug(`cron: job "${job.id}" session created`, { sessionId });
|
|
27
|
-
// session.prompt() is synchronous — blocks until the agent finishes.
|
|
28
|
-
// Wrap with AbortSignal timeout for safety.
|
|
29
22
|
const timeout = job.timeoutMs ?? deps.config.defaultTimeoutMs;
|
|
30
|
-
|
|
31
|
-
const timer = setTimeout(() => controller.abort(), timeout);
|
|
32
|
-
let result;
|
|
23
|
+
let text;
|
|
33
24
|
try {
|
|
34
|
-
|
|
35
|
-
path: { id: sessionId },
|
|
36
|
-
body: { parts: [{ type: "text", text: job.prompt }] },
|
|
37
|
-
});
|
|
25
|
+
text = await promptStreaming(deps.client, sessionId, job.prompt, timeout, deps.logger);
|
|
38
26
|
}
|
|
39
27
|
catch (err) {
|
|
40
|
-
if (
|
|
28
|
+
if (err instanceof Error && err.message === "timeout") {
|
|
41
29
|
deps.logger.warn(`cron: job "${job.id}" timed out after ${timeout}ms`);
|
|
42
30
|
return;
|
|
43
31
|
}
|
|
44
32
|
throw err;
|
|
45
33
|
}
|
|
46
|
-
finally {
|
|
47
|
-
clearTimeout(timer);
|
|
48
|
-
}
|
|
49
|
-
if (!result.data) {
|
|
50
|
-
deps.logger.warn(`cron: job "${job.id}" returned no data`);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const text = extractText(result.data.parts);
|
|
54
34
|
deps.logger.info(`cron: job "${job.id}" completed`, {
|
|
55
35
|
sessionId,
|
|
56
36
|
responseLength: text.length,
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dirname, resolve } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { createOpencode } from "@opencode-ai/sdk";
|
|
3
|
+
import { createOpencode } from "@opencode-ai/sdk/v2";
|
|
4
4
|
import { createRouter } from "./channels/router.js";
|
|
5
5
|
import { createSlackAdapter } from "./channels/slack.js";
|
|
6
6
|
import { createTelegramAdapter } from "./channels/telegram.js";
|
package/dist/sessions/manager.js
CHANGED
|
@@ -14,7 +14,7 @@ export function createSessionManager(client, config, map, logger) {
|
|
|
14
14
|
if (existing)
|
|
15
15
|
return existing;
|
|
16
16
|
const session = await client.session.create({
|
|
17
|
-
|
|
17
|
+
title: title ?? key,
|
|
18
18
|
});
|
|
19
19
|
if (!session.data)
|
|
20
20
|
throw new Error("session.create returned no data");
|
|
@@ -30,7 +30,7 @@ export function createSessionManager(client, config, map, logger) {
|
|
|
30
30
|
}
|
|
31
31
|
async function newSession(key, title) {
|
|
32
32
|
const session = await client.session.create({
|
|
33
|
-
|
|
33
|
+
title: title ?? `New session ${new Date().toISOString()}`,
|
|
34
34
|
});
|
|
35
35
|
if (!session.data)
|
|
36
36
|
throw new Error("session.create returned no data");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { OpencodeClient, QuestionRequest } from "@opencode-ai/sdk/v2";
|
|
2
|
+
import type { Logger } from "../utils/logger.js";
|
|
3
|
+
export type ToolProgressCallback = (tool: string, title: string) => Promise<void>;
|
|
4
|
+
export type HeartbeatCallback = () => Promise<void>;
|
|
5
|
+
export type QuestionCallback = (question: QuestionRequest) => Promise<Array<Array<string>>>;
|
|
6
|
+
export type ProgressOptions = {
|
|
7
|
+
onToolRunning?: ToolProgressCallback;
|
|
8
|
+
onHeartbeat?: HeartbeatCallback;
|
|
9
|
+
onQuestion?: QuestionCallback;
|
|
10
|
+
toolThrottleMs?: number;
|
|
11
|
+
heartbeatMs?: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function promptStreaming(client: OpencodeClient, sessionId: string, promptText: string, timeoutMs: number, logger: Logger, progress?: ProgressOptions): Promise<string>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export async function promptStreaming(client, sessionId, promptText, timeoutMs, logger, progress) {
|
|
2
|
+
const { stream } = await client.event.subscribe();
|
|
3
|
+
const controller = new AbortController();
|
|
4
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
5
|
+
const textParts = new Map();
|
|
6
|
+
const notifiedTools = new Set();
|
|
7
|
+
let lastToolNotifyTime = 0;
|
|
8
|
+
let lastActivityTime = Date.now();
|
|
9
|
+
let heartbeatTimer;
|
|
10
|
+
const toolThrottleMs = progress?.toolThrottleMs ?? 5_000;
|
|
11
|
+
const heartbeatMs = progress?.heartbeatMs ?? 60_000;
|
|
12
|
+
if (progress?.onHeartbeat && heartbeatMs > 0) {
|
|
13
|
+
const onHeartbeat = progress.onHeartbeat;
|
|
14
|
+
heartbeatTimer = setInterval(() => {
|
|
15
|
+
const elapsed = Date.now() - lastActivityTime;
|
|
16
|
+
if (elapsed >= heartbeatMs) {
|
|
17
|
+
onHeartbeat().catch(() => { });
|
|
18
|
+
lastActivityTime = Date.now();
|
|
19
|
+
}
|
|
20
|
+
}, heartbeatMs);
|
|
21
|
+
}
|
|
22
|
+
function touchActivity() {
|
|
23
|
+
lastActivityTime = Date.now();
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
await client.session.promptAsync({
|
|
27
|
+
sessionID: sessionId,
|
|
28
|
+
parts: [{ type: "text", text: promptText }],
|
|
29
|
+
});
|
|
30
|
+
for await (const raw of stream) {
|
|
31
|
+
if (controller.signal.aborted) {
|
|
32
|
+
throw new Error("timeout");
|
|
33
|
+
}
|
|
34
|
+
const event = raw;
|
|
35
|
+
if (event.type === "message.part.delta") {
|
|
36
|
+
const { sessionID, partID, delta } = event.properties;
|
|
37
|
+
if (sessionID !== sessionId)
|
|
38
|
+
continue;
|
|
39
|
+
const prev = textParts.get(partID) ?? "";
|
|
40
|
+
textParts.set(partID, prev + delta);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (event.type === "message.part.updated") {
|
|
44
|
+
const { part } = event.properties;
|
|
45
|
+
if (part.sessionID !== sessionId)
|
|
46
|
+
continue;
|
|
47
|
+
if (part.type === "text" && part.text) {
|
|
48
|
+
textParts.set(part.id, part.text);
|
|
49
|
+
}
|
|
50
|
+
if (part.type === "tool" && part.state.status === "running" && progress?.onToolRunning) {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
if (!notifiedTools.has(part.callID) && now - lastToolNotifyTime >= toolThrottleMs) {
|
|
53
|
+
notifiedTools.add(part.callID);
|
|
54
|
+
lastToolNotifyTime = now;
|
|
55
|
+
const title = "title" in part.state && part.state.title ? part.state.title : part.tool;
|
|
56
|
+
await progress.onToolRunning(part.tool, title).catch(() => { });
|
|
57
|
+
touchActivity();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (event.type === "question.asked") {
|
|
63
|
+
const request = event.properties;
|
|
64
|
+
if (request.sessionID !== sessionId)
|
|
65
|
+
continue;
|
|
66
|
+
if (progress?.onQuestion) {
|
|
67
|
+
try {
|
|
68
|
+
const answers = await progress.onQuestion(request);
|
|
69
|
+
await client.question.reply({
|
|
70
|
+
requestID: request.id,
|
|
71
|
+
answers,
|
|
72
|
+
});
|
|
73
|
+
touchActivity();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
await client.question.reject({ requestID: request.id });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
await client.question.reject({ requestID: request.id });
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (event.type === "session.error") {
|
|
85
|
+
const { sessionID, error } = event.properties;
|
|
86
|
+
if (sessionID && sessionID !== sessionId)
|
|
87
|
+
continue;
|
|
88
|
+
if (error && "name" in error && error.name === "MessageAbortedError") {
|
|
89
|
+
throw new Error("aborted");
|
|
90
|
+
}
|
|
91
|
+
const msg = error && "data" in error && typeof error.data.message === "string"
|
|
92
|
+
? error.data.message
|
|
93
|
+
: "unknown session error";
|
|
94
|
+
throw new Error(msg);
|
|
95
|
+
}
|
|
96
|
+
if (event.type === "session.idle") {
|
|
97
|
+
if (event.properties.sessionID !== sessionId)
|
|
98
|
+
continue;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
if (controller.signal.aborted || (err instanceof Error && err.message === "timeout")) {
|
|
105
|
+
logger.warn("prompt: session timed out", { sessionId, timeoutMs });
|
|
106
|
+
throw new Error("timeout");
|
|
107
|
+
}
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
if (heartbeatTimer)
|
|
113
|
+
clearInterval(heartbeatTimer);
|
|
114
|
+
await stream.return(undefined);
|
|
115
|
+
}
|
|
116
|
+
const parts = [...textParts.values()];
|
|
117
|
+
return parts.join("");
|
|
118
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { cancel, confirm, intro, isCancel, note, outro, select, text } from "@clack/prompts";
|
|
2
|
+
import { WizardCancelledError } from "./prompts.js";
|
|
3
|
+
function guardCancel(value) {
|
|
4
|
+
if (isCancel(value)) {
|
|
5
|
+
cancel("Setup cancelled.");
|
|
6
|
+
throw new WizardCancelledError();
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
export function createClackPrompter() {
|
|
11
|
+
return {
|
|
12
|
+
async intro(title) {
|
|
13
|
+
intro(title);
|
|
14
|
+
},
|
|
15
|
+
async outro(message) {
|
|
16
|
+
outro(message);
|
|
17
|
+
},
|
|
18
|
+
async note(message, title) {
|
|
19
|
+
note(message, title);
|
|
20
|
+
},
|
|
21
|
+
async select(params) {
|
|
22
|
+
const result = await select({
|
|
23
|
+
message: params.message,
|
|
24
|
+
options: params.options,
|
|
25
|
+
initialValue: params.initialValue,
|
|
26
|
+
});
|
|
27
|
+
return guardCancel(result);
|
|
28
|
+
},
|
|
29
|
+
async text(params) {
|
|
30
|
+
const result = await text({
|
|
31
|
+
message: params.message,
|
|
32
|
+
initialValue: params.initialValue,
|
|
33
|
+
placeholder: params.placeholder,
|
|
34
|
+
validate: params.validate
|
|
35
|
+
? (value) => {
|
|
36
|
+
const fn = params.validate;
|
|
37
|
+
if (!fn)
|
|
38
|
+
return undefined;
|
|
39
|
+
return fn(value ?? "");
|
|
40
|
+
}
|
|
41
|
+
: undefined,
|
|
42
|
+
});
|
|
43
|
+
return guardCancel(result);
|
|
44
|
+
},
|
|
45
|
+
async confirm(params) {
|
|
46
|
+
const result = await confirm({
|
|
47
|
+
message: params.message,
|
|
48
|
+
initialValue: params.initialValue,
|
|
49
|
+
});
|
|
50
|
+
return guardCancel(result);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { access, writeFile } from "node:fs/promises";
|
|
2
|
+
const ALL_CAPS_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
3
|
+
function resolveTokenValue(input) {
|
|
4
|
+
const trimmed = input.trim();
|
|
5
|
+
if (trimmed.startsWith("${") && trimmed.endsWith("}"))
|
|
6
|
+
return trimmed;
|
|
7
|
+
if (trimmed.startsWith("$"))
|
|
8
|
+
return `\${${trimmed.slice(1)}}`;
|
|
9
|
+
if (ALL_CAPS_RE.test(trimmed))
|
|
10
|
+
return `\${${trimmed}}`;
|
|
11
|
+
return trimmed;
|
|
12
|
+
}
|
|
13
|
+
function splitAllowlist(raw) {
|
|
14
|
+
return raw
|
|
15
|
+
.split(",")
|
|
16
|
+
.map((s) => s.trim())
|
|
17
|
+
.filter((s) => s.length > 0);
|
|
18
|
+
}
|
|
19
|
+
async function collectTelegramConfig(p) {
|
|
20
|
+
const rawToken = await p.text({
|
|
21
|
+
message: "Telegram bot token (paste value or env var name like TELEGRAM_BOT_TOKEN):",
|
|
22
|
+
placeholder: "TELEGRAM_BOT_TOKEN",
|
|
23
|
+
validate: (v) => (v.trim().length === 0 ? "Required" : undefined),
|
|
24
|
+
});
|
|
25
|
+
const botToken = resolveTokenValue(rawToken);
|
|
26
|
+
const rawAllowlist = await p.text({
|
|
27
|
+
message: "Allowed Telegram usernames (comma-separated, leave blank for none):",
|
|
28
|
+
placeholder: "alice,bob",
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
enabled: true,
|
|
32
|
+
botToken,
|
|
33
|
+
allowlist: splitAllowlist(rawAllowlist),
|
|
34
|
+
mode: "polling",
|
|
35
|
+
rejectionBehavior: "ignore",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function collectSlackConfig(p) {
|
|
39
|
+
const rawBotToken = await p.text({
|
|
40
|
+
message: "Slack bot token (xoxb-... or env var name like SLACK_BOT_TOKEN):",
|
|
41
|
+
placeholder: "SLACK_BOT_TOKEN",
|
|
42
|
+
validate: (v) => (v.trim().length === 0 ? "Required" : undefined),
|
|
43
|
+
});
|
|
44
|
+
const botToken = resolveTokenValue(rawBotToken);
|
|
45
|
+
const rawAppToken = await p.text({
|
|
46
|
+
message: "Slack app token (xapp-... or env var name like SLACK_APP_TOKEN):",
|
|
47
|
+
placeholder: "SLACK_APP_TOKEN",
|
|
48
|
+
validate: (v) => (v.trim().length === 0 ? "Required" : undefined),
|
|
49
|
+
});
|
|
50
|
+
const appToken = resolveTokenValue(rawAppToken);
|
|
51
|
+
return {
|
|
52
|
+
enabled: true,
|
|
53
|
+
botToken,
|
|
54
|
+
appToken,
|
|
55
|
+
mode: "socket",
|
|
56
|
+
rejectionBehavior: "ignore",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function collectWhatsAppConfig(p) {
|
|
60
|
+
const rawAllowlist = await p.text({
|
|
61
|
+
message: "Allowed phone numbers (comma-separated with country code, e.g. 5511999887766):",
|
|
62
|
+
placeholder: "5511999887766,441234567890",
|
|
63
|
+
validate: (v) => (v.trim().length === 0 ? "Required" : undefined),
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
enabled: true,
|
|
67
|
+
allowlist: splitAllowlist(rawAllowlist),
|
|
68
|
+
authDir: "./data/whatsapp/auth",
|
|
69
|
+
debounceMs: 1000,
|
|
70
|
+
rejectionBehavior: "ignore",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function collectChannels(p) {
|
|
74
|
+
const channels = {};
|
|
75
|
+
const configured = new Set();
|
|
76
|
+
const channelOptions = [
|
|
77
|
+
{ value: "telegram", label: "Telegram", hint: "requires bot token" },
|
|
78
|
+
{ value: "slack", label: "Slack", hint: "requires bot + app token" },
|
|
79
|
+
{ value: "whatsapp", label: "WhatsApp", hint: "scan QR on first run" },
|
|
80
|
+
{ value: "skip", label: "Skip — no channels now" },
|
|
81
|
+
];
|
|
82
|
+
let configureMore = true;
|
|
83
|
+
while (configureMore) {
|
|
84
|
+
const available = channelOptions.filter((o) => o.value === "skip" || !configured.has(o.value));
|
|
85
|
+
const choice = await p.select({
|
|
86
|
+
message: "Which channel would you like to configure?",
|
|
87
|
+
options: available,
|
|
88
|
+
});
|
|
89
|
+
if (choice === "skip")
|
|
90
|
+
break;
|
|
91
|
+
const channelId = choice;
|
|
92
|
+
if (channelId === "telegram") {
|
|
93
|
+
channels.telegram = await collectTelegramConfig(p);
|
|
94
|
+
configured.add("telegram");
|
|
95
|
+
}
|
|
96
|
+
else if (channelId === "slack") {
|
|
97
|
+
channels.slack = await collectSlackConfig(p);
|
|
98
|
+
configured.add("slack");
|
|
99
|
+
}
|
|
100
|
+
else if (channelId === "whatsapp") {
|
|
101
|
+
channels.whatsapp = await collectWhatsAppConfig(p);
|
|
102
|
+
configured.add("whatsapp");
|
|
103
|
+
}
|
|
104
|
+
const allChannels = ["telegram", "slack", "whatsapp"];
|
|
105
|
+
const remaining = allChannels.filter((c) => !configured.has(c));
|
|
106
|
+
if (remaining.length === 0)
|
|
107
|
+
break;
|
|
108
|
+
configureMore = await p.confirm({
|
|
109
|
+
message: "Configure another channel?",
|
|
110
|
+
initialValue: false,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return channels;
|
|
114
|
+
}
|
|
115
|
+
async function collectMemoryConfig(p) {
|
|
116
|
+
const backend = await p.select({
|
|
117
|
+
message: "Memory backend:",
|
|
118
|
+
options: [
|
|
119
|
+
{ value: "txt", label: "Text files (simple, zero deps)" },
|
|
120
|
+
{ value: "openviking", label: "OpenViking (semantic search)" },
|
|
121
|
+
],
|
|
122
|
+
initialValue: "txt",
|
|
123
|
+
});
|
|
124
|
+
if (backend === "openviking") {
|
|
125
|
+
const url = await p.text({
|
|
126
|
+
message: "OpenViking URL:",
|
|
127
|
+
initialValue: "http://localhost:8100",
|
|
128
|
+
validate: (v) => {
|
|
129
|
+
try {
|
|
130
|
+
new URL(v);
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return "Must be a valid URL";
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const fallback = await p.confirm({
|
|
139
|
+
message: "Fall back to text files if OpenViking is unreachable?",
|
|
140
|
+
initialValue: true,
|
|
141
|
+
});
|
|
142
|
+
return { backend: "openviking", openviking: { url, fallback } };
|
|
143
|
+
}
|
|
144
|
+
return { backend: "txt" };
|
|
145
|
+
}
|
|
146
|
+
export async function runOnboardingWizard(p) {
|
|
147
|
+
const configPath = "./opencode-claw.json";
|
|
148
|
+
let existingConfigFound = false;
|
|
149
|
+
try {
|
|
150
|
+
await access(configPath);
|
|
151
|
+
existingConfigFound = true;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// file doesn't exist — proceed
|
|
155
|
+
}
|
|
156
|
+
if (existingConfigFound) {
|
|
157
|
+
const overwrite = await p.confirm({
|
|
158
|
+
message: "opencode-claw.json already exists. Overwrite it?",
|
|
159
|
+
initialValue: false,
|
|
160
|
+
});
|
|
161
|
+
if (!overwrite) {
|
|
162
|
+
await p.outro("Setup cancelled. Existing config unchanged.");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
await p.intro("opencode-claw setup");
|
|
167
|
+
const channels = await collectChannels(p);
|
|
168
|
+
const memory = await collectMemoryConfig(p);
|
|
169
|
+
const portRaw = await p.text({
|
|
170
|
+
message: "OpenCode server port (0 = random):",
|
|
171
|
+
initialValue: "0",
|
|
172
|
+
validate: (v) => {
|
|
173
|
+
const n = Number(v);
|
|
174
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535)
|
|
175
|
+
return "Must be an integer 0–65535";
|
|
176
|
+
return undefined;
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
const port = Number(portRaw);
|
|
180
|
+
const enableCron = await p.confirm({
|
|
181
|
+
message: "Enable cron job scheduling?",
|
|
182
|
+
initialValue: false,
|
|
183
|
+
});
|
|
184
|
+
if (enableCron) {
|
|
185
|
+
await p.note("Add jobs to the config file manually after setup.\nSee opencode-claw.example.json for the cron job schema.", "Cron jobs");
|
|
186
|
+
}
|
|
187
|
+
const channelSummary = Object.keys(channels).length === 0
|
|
188
|
+
? " No channels configured"
|
|
189
|
+
: Object.keys(channels)
|
|
190
|
+
.map((c) => ` - ${c}`)
|
|
191
|
+
.join("\n");
|
|
192
|
+
const memorySummary = memory.backend === "openviking"
|
|
193
|
+
? ` openviking (${memory.openviking.url})`
|
|
194
|
+
: " txt (./data/memory)";
|
|
195
|
+
await p.note([
|
|
196
|
+
`Channels:\n${channelSummary}`,
|
|
197
|
+
`Memory: ${memorySummary}`,
|
|
198
|
+
`OpenCode port: ${port}`,
|
|
199
|
+
`Cron: ${enableCron ? "enabled" : "disabled"}`,
|
|
200
|
+
].join("\n"), "Config summary");
|
|
201
|
+
const config = {
|
|
202
|
+
opencode: { port },
|
|
203
|
+
memory,
|
|
204
|
+
channels,
|
|
205
|
+
...(enableCron ? { cron: { enabled: true, defaultTimeoutMs: 300000, jobs: [] } } : {}),
|
|
206
|
+
};
|
|
207
|
+
await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
208
|
+
await p.outro("Config written to opencode-claw.json. Run 'npx opencode-claw' to start.");
|
|
209
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type WizardSelectOption<T = string> = {
|
|
2
|
+
value: T;
|
|
3
|
+
label: string;
|
|
4
|
+
hint?: string;
|
|
5
|
+
};
|
|
6
|
+
export type WizardSelectParams<T = string> = {
|
|
7
|
+
message: string;
|
|
8
|
+
options: Array<WizardSelectOption<T>>;
|
|
9
|
+
initialValue?: T;
|
|
10
|
+
};
|
|
11
|
+
export type WizardTextParams = {
|
|
12
|
+
message: string;
|
|
13
|
+
initialValue?: string;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
validate?: (value: string) => string | undefined;
|
|
16
|
+
};
|
|
17
|
+
export type WizardConfirmParams = {
|
|
18
|
+
message: string;
|
|
19
|
+
initialValue?: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type WizardPrompter = {
|
|
22
|
+
intro: (title: string) => Promise<void>;
|
|
23
|
+
outro: (message: string) => Promise<void>;
|
|
24
|
+
note: (message: string, title?: string) => Promise<void>;
|
|
25
|
+
select: <T>(params: WizardSelectParams<T>) => Promise<T>;
|
|
26
|
+
text: (params: WizardTextParams) => Promise<string>;
|
|
27
|
+
confirm: (params: WizardConfirmParams) => Promise<boolean>;
|
|
28
|
+
};
|
|
29
|
+
export declare class WizardCancelledError extends Error {
|
|
30
|
+
constructor();
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-claw",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Wrap OpenCode with persistent memory, messaging channels, and cron jobs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"node": ">=20"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
|
-
"start": "bun run src/
|
|
50
|
+
"start": "bun run src/cli.ts",
|
|
51
51
|
"build": "tsc --build",
|
|
52
52
|
"prepublishOnly": "npm run build",
|
|
53
53
|
"typecheck": "bun x tsc --noEmit",
|
|
@@ -58,12 +58,13 @@
|
|
|
58
58
|
"test:e2e": "bun test test/"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@
|
|
62
|
-
"@opencode-ai/plugin": "^1.2.10",
|
|
63
|
-
"grammy": "^1.35.0",
|
|
61
|
+
"@clack/prompts": "^1.0.1",
|
|
64
62
|
"@grammyjs/runner": "^2.0.3",
|
|
63
|
+
"@opencode-ai/plugin": "^1.2.10",
|
|
64
|
+
"@opencode-ai/sdk": "^1.2.10",
|
|
65
65
|
"@slack/bolt": "^4.3.0",
|
|
66
66
|
"@whiskeysockets/baileys": "^6.7.16",
|
|
67
|
+
"grammy": "^1.35.0",
|
|
67
68
|
"node-cron": "^3.0.3",
|
|
68
69
|
"zod": "^3.24.0"
|
|
69
70
|
},
|