opencode-routines 0.1.0
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/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +15383 -0
- package/dist/tui.d.ts +61 -0
- package/dist/tui.js +210 -0
- package/package.json +63 -0
package/dist/tui.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
type TuiApi = {
|
|
2
|
+
route: {
|
|
3
|
+
current: {
|
|
4
|
+
name: string;
|
|
5
|
+
params?: Record<string, any>;
|
|
6
|
+
};
|
|
7
|
+
navigate?: (name: string, params?: Record<string, unknown>) => void;
|
|
8
|
+
};
|
|
9
|
+
client: {
|
|
10
|
+
session: {
|
|
11
|
+
prompt: (input: any) => Promise<unknown>;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
ui: {
|
|
15
|
+
dialog: {
|
|
16
|
+
replace: (render: () => any) => void;
|
|
17
|
+
clear: () => void;
|
|
18
|
+
};
|
|
19
|
+
DialogAlert: (props: {
|
|
20
|
+
title: string;
|
|
21
|
+
message: string;
|
|
22
|
+
}) => any;
|
|
23
|
+
DialogPrompt: (props: {
|
|
24
|
+
title: string;
|
|
25
|
+
placeholder?: string;
|
|
26
|
+
onConfirm?: (value: string) => void;
|
|
27
|
+
}) => any;
|
|
28
|
+
DialogSelect: (props: {
|
|
29
|
+
title: string;
|
|
30
|
+
options: Array<{
|
|
31
|
+
title: string;
|
|
32
|
+
value: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
footer?: string;
|
|
35
|
+
}>;
|
|
36
|
+
onSelect?: (option: {
|
|
37
|
+
value: string;
|
|
38
|
+
}) => void;
|
|
39
|
+
}) => any;
|
|
40
|
+
toast: (input: {
|
|
41
|
+
variant?: "info" | "success" | "warning" | "error";
|
|
42
|
+
title?: string;
|
|
43
|
+
message: string;
|
|
44
|
+
}) => void;
|
|
45
|
+
};
|
|
46
|
+
lifecycle: {
|
|
47
|
+
onDispose: (fn: () => void) => void;
|
|
48
|
+
};
|
|
49
|
+
keymap: {
|
|
50
|
+
registerLayer: (input: {
|
|
51
|
+
commands: any[];
|
|
52
|
+
bindings?: any[];
|
|
53
|
+
}) => unknown;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
type TuiPlugin = (api: TuiApi, options?: Record<string, unknown>, meta?: Record<string, unknown>) => Promise<void>;
|
|
57
|
+
declare const _default: {
|
|
58
|
+
id: string;
|
|
59
|
+
tui: TuiPlugin;
|
|
60
|
+
};
|
|
61
|
+
export default _default;
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/tui.ts
|
|
18
|
+
var loops = new Map;
|
|
19
|
+
function activeSessionID(api) {
|
|
20
|
+
const route = api.route.current;
|
|
21
|
+
if (route.name !== "session")
|
|
22
|
+
return;
|
|
23
|
+
return typeof route.params?.sessionID === "string" ? route.params.sessionID : undefined;
|
|
24
|
+
}
|
|
25
|
+
function parseDurationSeconds(input) {
|
|
26
|
+
const match = input.trim().match(/^(\d+)(s|m|h)$/i);
|
|
27
|
+
if (!match)
|
|
28
|
+
return;
|
|
29
|
+
const value = Number(match[1]);
|
|
30
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
31
|
+
return;
|
|
32
|
+
const unit = match[2].toLowerCase();
|
|
33
|
+
if (unit === "s")
|
|
34
|
+
return value;
|
|
35
|
+
if (unit === "m")
|
|
36
|
+
return value * 60;
|
|
37
|
+
return value * 3600;
|
|
38
|
+
}
|
|
39
|
+
function parseLoop(input) {
|
|
40
|
+
const trimmed = input.trim();
|
|
41
|
+
if (!trimmed)
|
|
42
|
+
return;
|
|
43
|
+
const [first, ...rest] = trimmed.split(/\s+/);
|
|
44
|
+
const intervalSeconds = parseDurationSeconds(first ?? "");
|
|
45
|
+
if (intervalSeconds === undefined)
|
|
46
|
+
return { prompt: trimmed };
|
|
47
|
+
const prompt = rest.join(" ").trim();
|
|
48
|
+
if (!prompt)
|
|
49
|
+
return;
|
|
50
|
+
return { intervalSeconds, prompt };
|
|
51
|
+
}
|
|
52
|
+
function loopID() {
|
|
53
|
+
return `loop_${Math.random().toString(16).slice(2, 10)}`;
|
|
54
|
+
}
|
|
55
|
+
async function submitPrompt(api, loop) {
|
|
56
|
+
loop.fires += 1;
|
|
57
|
+
await api.client.session.prompt({
|
|
58
|
+
path: { sessionID: loop.sessionID },
|
|
59
|
+
body: {
|
|
60
|
+
parts: [{ type: "text", text: loop.prompt }]
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function scheduleFixed(api, loop) {
|
|
65
|
+
if (!loop.intervalSeconds)
|
|
66
|
+
return;
|
|
67
|
+
loop.timer = setTimeout(async () => {
|
|
68
|
+
if (!loops.has(loop.id))
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
await submitPrompt(api, loop);
|
|
72
|
+
scheduleFixed(api, loop);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
loops.delete(loop.id);
|
|
75
|
+
api.ui.toast({
|
|
76
|
+
variant: "error",
|
|
77
|
+
title: "Loop stopped",
|
|
78
|
+
message: error instanceof Error ? error.message : String(error)
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}, loop.intervalSeconds * 1000);
|
|
82
|
+
}
|
|
83
|
+
function stopLoop(id) {
|
|
84
|
+
const loop = loops.get(id);
|
|
85
|
+
if (!loop)
|
|
86
|
+
return false;
|
|
87
|
+
if (loop.timer)
|
|
88
|
+
clearTimeout(loop.timer);
|
|
89
|
+
loops.delete(id);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function showLoops(api) {
|
|
93
|
+
const items = [...loops.values()];
|
|
94
|
+
if (items.length === 0) {
|
|
95
|
+
api.ui.dialog.replace(() => api.ui.DialogAlert({
|
|
96
|
+
title: "Loops",
|
|
97
|
+
message: "No active loops."
|
|
98
|
+
}));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
api.ui.dialog.replace(() => api.ui.DialogSelect({
|
|
102
|
+
title: "Active loops",
|
|
103
|
+
options: items.map((loop) => ({
|
|
104
|
+
title: `${loop.id} \xB7 ${loop.mode}${loop.intervalSeconds ? ` \xB7 every ${loop.intervalSeconds}s` : ""}`,
|
|
105
|
+
value: loop.id,
|
|
106
|
+
description: loop.prompt,
|
|
107
|
+
footer: `${loop.fires} fires \xB7 session ${loop.sessionID}`
|
|
108
|
+
})),
|
|
109
|
+
onSelect: (option) => {
|
|
110
|
+
stopLoop(String(option.value));
|
|
111
|
+
api.ui.toast({ variant: "success", message: `Stopped ${String(option.value)}` });
|
|
112
|
+
api.ui.dialog.clear();
|
|
113
|
+
}
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
var tui = async (api) => {
|
|
117
|
+
api.lifecycle.onDispose(() => {
|
|
118
|
+
for (const id of [...loops.keys()])
|
|
119
|
+
stopLoop(id);
|
|
120
|
+
});
|
|
121
|
+
api.keymap.registerLayer({
|
|
122
|
+
commands: [
|
|
123
|
+
{
|
|
124
|
+
name: "routines.loop",
|
|
125
|
+
title: "Start same-session loop",
|
|
126
|
+
category: "Scheduler",
|
|
127
|
+
namespace: "palette",
|
|
128
|
+
slashName: "loop",
|
|
129
|
+
run() {
|
|
130
|
+
const sessionID = activeSessionID(api);
|
|
131
|
+
if (!sessionID) {
|
|
132
|
+
api.ui.toast({ variant: "warning", message: "Open a session before starting /loop." });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
api.ui.dialog.replace(() => api.ui.DialogPrompt({
|
|
136
|
+
title: "Start loop",
|
|
137
|
+
placeholder: "5m /babysit-prs or /babysit-prs",
|
|
138
|
+
onConfirm: (value) => {
|
|
139
|
+
const parsed = parseLoop(value);
|
|
140
|
+
if (!parsed) {
|
|
141
|
+
api.ui.toast({ variant: "warning", message: "Usage: /loop 5m <prompt> or /loop <prompt>" });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const loop = {
|
|
145
|
+
id: loopID(),
|
|
146
|
+
sessionID,
|
|
147
|
+
prompt: parsed.prompt,
|
|
148
|
+
mode: parsed.intervalSeconds === undefined ? "dynamic" : "fixed",
|
|
149
|
+
intervalSeconds: parsed.intervalSeconds,
|
|
150
|
+
createdAt: new Date().toISOString(),
|
|
151
|
+
fires: 0
|
|
152
|
+
};
|
|
153
|
+
loops.set(loop.id, loop);
|
|
154
|
+
if (loop.mode === "fixed")
|
|
155
|
+
scheduleFixed(api, loop);
|
|
156
|
+
else
|
|
157
|
+
submitPrompt(api, loop);
|
|
158
|
+
api.ui.toast({
|
|
159
|
+
variant: "success",
|
|
160
|
+
title: "Loop started",
|
|
161
|
+
message: loop.mode === "fixed" ? `${loop.id} every ${loop.intervalSeconds}s` : `${loop.id} dynamic mode; use ScheduleWakeup to continue`
|
|
162
|
+
});
|
|
163
|
+
api.ui.dialog.clear();
|
|
164
|
+
}
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "routines.loops",
|
|
170
|
+
title: "List active loops",
|
|
171
|
+
category: "Scheduler",
|
|
172
|
+
namespace: "palette",
|
|
173
|
+
slashName: "loops",
|
|
174
|
+
run() {
|
|
175
|
+
showLoops(api);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "routines.stop_loop",
|
|
180
|
+
title: "Stop a same-session loop",
|
|
181
|
+
category: "Scheduler",
|
|
182
|
+
namespace: "palette",
|
|
183
|
+
slashName: "stop-loop",
|
|
184
|
+
run() {
|
|
185
|
+
showLoops(api);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "routines.schedule_standalone_session",
|
|
190
|
+
title: "Create standalone scheduled session",
|
|
191
|
+
category: "Scheduler",
|
|
192
|
+
namespace: "palette",
|
|
193
|
+
slashName: "schedule-standalone-session",
|
|
194
|
+
run() {
|
|
195
|
+
api.ui.dialog.replace(() => api.ui.DialogAlert({
|
|
196
|
+
title: "Standalone schedules",
|
|
197
|
+
message: "Use the ScheduleCreate tool (or natural language like 'create a standalone scheduled run...') to create durable OS-backed standalone sessions. The ambiguous /schedule command is intentionally not registered."
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
var tui_default = {
|
|
205
|
+
id: "opencode-routines-tui",
|
|
206
|
+
tui
|
|
207
|
+
};
|
|
208
|
+
export {
|
|
209
|
+
tui_default as default
|
|
210
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-routines",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode routines: same-session loops, cron prompts, and host-backed standalone scheduled agents",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./tui": {
|
|
17
|
+
"types": "./dist/tui.d.ts",
|
|
18
|
+
"import": "./dist/tui.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "bun build src/index.ts src/tui.ts --outdir dist --target bun --format esm && tsc --emitDeclarationOnly",
|
|
23
|
+
"clean": "rm -rf dist",
|
|
24
|
+
"prepublishOnly": "bun run clean && bun run build",
|
|
25
|
+
"release:patch": "npm version patch && npm publish",
|
|
26
|
+
"release:minor": "npm version minor && npm publish",
|
|
27
|
+
"release:major": "npm version major && npm publish",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "npm run build && node scripts/validate-routines-surface.mjs"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"opencode",
|
|
33
|
+
"plugin",
|
|
34
|
+
"routines",
|
|
35
|
+
"scheduler",
|
|
36
|
+
"loop",
|
|
37
|
+
"cron",
|
|
38
|
+
"launchd",
|
|
39
|
+
"systemd",
|
|
40
|
+
"automation",
|
|
41
|
+
"jobs"
|
|
42
|
+
],
|
|
43
|
+
"author": "Emilio Esposito",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/EmilioEsposito/opencode-routines.git"
|
|
48
|
+
},
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/EmilioEsposito/opencode-routines/issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/EmilioEsposito/opencode-routines#readme",
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@opencode-ai/plugin": "^1.0.162"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"bun-types": "latest",
|
|
58
|
+
"typescript": "^5.7.3"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@opencode-ai/plugin": ">=1.0.0"
|
|
62
|
+
}
|
|
63
|
+
}
|