@joshualelon/clawdbot-skill-flow 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 +265 -0
- package/clawdbot.plugin.json +8 -0
- package/index.ts +65 -0
- package/package.json +76 -0
- package/src/commands/flow-create.ts +69 -0
- package/src/commands/flow-delete.ts +36 -0
- package/src/commands/flow-list.ts +33 -0
- package/src/commands/flow-start.ts +56 -0
- package/src/commands/flow-step.ts +101 -0
- package/src/engine/executor.ts +109 -0
- package/src/engine/renderer.ts +119 -0
- package/src/engine/transitions.ts +160 -0
- package/src/examples/onboarding.json +41 -0
- package/src/examples/pushups.json +49 -0
- package/src/examples/survey.json +41 -0
- package/src/state/flow-store.ts +158 -0
- package/src/state/history-store.ts +76 -0
- package/src/state/session-store.ts +155 -0
- package/src/types.ts +67 -0
- package/src/validation.ts +119 -0
- package/types.d.ts +36 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow metadata storage with file locking for concurrent access
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { promises as fs } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import lockfile from "lockfile";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
|
10
|
+
import type { FlowMetadata } from "../types.js";
|
|
11
|
+
import { FlowMetadataSchema } from "../validation.js";
|
|
12
|
+
|
|
13
|
+
const lock = promisify(lockfile.lock);
|
|
14
|
+
const unlock = promisify(lockfile.unlock);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the flows directory path
|
|
18
|
+
*/
|
|
19
|
+
function getFlowsDir(api: ClawdbotPluginApi): string {
|
|
20
|
+
const stateDir = api.runtime.state.resolveStateDir();
|
|
21
|
+
return path.join(stateDir, "flows");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the flow directory path
|
|
26
|
+
*/
|
|
27
|
+
function getFlowDir(api: ClawdbotPluginApi, flowName: string): string {
|
|
28
|
+
return path.join(getFlowsDir(api), flowName);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the flow metadata file path
|
|
33
|
+
*/
|
|
34
|
+
function getFlowMetadataPath(
|
|
35
|
+
api: ClawdbotPluginApi,
|
|
36
|
+
flowName: string
|
|
37
|
+
): string {
|
|
38
|
+
return path.join(getFlowDir(api, flowName), "metadata.json");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load a flow by name
|
|
43
|
+
*/
|
|
44
|
+
export async function loadFlow(
|
|
45
|
+
api: ClawdbotPluginApi,
|
|
46
|
+
flowName: string
|
|
47
|
+
): Promise<FlowMetadata | null> {
|
|
48
|
+
const metadataPath = getFlowMetadataPath(api, flowName);
|
|
49
|
+
const lockPath = `${metadataPath}.lock`;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await lock(lockPath, { wait: 5000, retries: 3 });
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const content = await fs.readFile(metadataPath, "utf-8");
|
|
56
|
+
const data = JSON.parse(content);
|
|
57
|
+
return FlowMetadataSchema.parse(data);
|
|
58
|
+
} finally {
|
|
59
|
+
await unlock(lockPath);
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
api.logger.error(`Failed to load flow ${flowName}:`, error);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Save a flow (atomic write with locking)
|
|
72
|
+
*/
|
|
73
|
+
export async function saveFlow(
|
|
74
|
+
api: ClawdbotPluginApi,
|
|
75
|
+
flow: FlowMetadata
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const flowDir = getFlowDir(api, flow.name);
|
|
78
|
+
const metadataPath = getFlowMetadataPath(api, flow.name);
|
|
79
|
+
const lockPath = `${metadataPath}.lock`;
|
|
80
|
+
const tempPath = `${metadataPath}.tmp`;
|
|
81
|
+
|
|
82
|
+
// Validate flow metadata
|
|
83
|
+
FlowMetadataSchema.parse(flow);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// Ensure flow directory exists
|
|
87
|
+
await fs.mkdir(flowDir, { recursive: true });
|
|
88
|
+
|
|
89
|
+
await lock(lockPath, { wait: 5000, retries: 3 });
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Write to temp file
|
|
93
|
+
await fs.writeFile(tempPath, JSON.stringify(flow, null, 2), "utf-8");
|
|
94
|
+
|
|
95
|
+
// Atomic rename
|
|
96
|
+
await fs.rename(tempPath, metadataPath);
|
|
97
|
+
} finally {
|
|
98
|
+
await unlock(lockPath);
|
|
99
|
+
|
|
100
|
+
// Cleanup temp file if it still exists
|
|
101
|
+
try {
|
|
102
|
+
await fs.unlink(tempPath);
|
|
103
|
+
} catch {
|
|
104
|
+
// Ignore errors
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
api.logger.error(`Failed to save flow ${flow.name}:`, error);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* List all flows
|
|
115
|
+
*/
|
|
116
|
+
export async function listFlows(
|
|
117
|
+
api: ClawdbotPluginApi
|
|
118
|
+
): Promise<FlowMetadata[]> {
|
|
119
|
+
const flowsDir = getFlowsDir(api);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await fs.mkdir(flowsDir, { recursive: true });
|
|
123
|
+
const entries = await fs.readdir(flowsDir, { withFileTypes: true });
|
|
124
|
+
|
|
125
|
+
const flows: FlowMetadata[] = [];
|
|
126
|
+
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
const flow = await loadFlow(api, entry.name);
|
|
130
|
+
if (flow) {
|
|
131
|
+
flows.push(flow);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return flows;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
api.logger.error("Failed to list flows:", error);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Delete a flow
|
|
145
|
+
*/
|
|
146
|
+
export async function deleteFlow(
|
|
147
|
+
api: ClawdbotPluginApi,
|
|
148
|
+
flowName: string
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
const flowDir = getFlowDir(api, flowName);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await fs.rm(flowDir, { recursive: true, force: true });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
api.logger.error(`Failed to delete flow ${flowName}:`, error);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONL append-only history log for completed flows
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { promises as fs } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
|
8
|
+
import type { FlowSession } from "../types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get history file path for a flow
|
|
12
|
+
*/
|
|
13
|
+
function getHistoryPath(api: ClawdbotPluginApi, flowName: string): string {
|
|
14
|
+
const stateDir = api.runtime.state.resolveStateDir();
|
|
15
|
+
return path.join(stateDir, "flows", flowName, "history.jsonl");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Save completed flow session to history (fire-and-forget)
|
|
20
|
+
*/
|
|
21
|
+
export async function saveFlowHistory(
|
|
22
|
+
api: ClawdbotPluginApi,
|
|
23
|
+
session: FlowSession
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const historyPath = getHistoryPath(api, session.flowName);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Ensure directory exists
|
|
29
|
+
await fs.mkdir(path.dirname(historyPath), { recursive: true });
|
|
30
|
+
|
|
31
|
+
// Create history entry
|
|
32
|
+
const entry = {
|
|
33
|
+
...session,
|
|
34
|
+
completedAt: Date.now(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Append to JSONL file
|
|
38
|
+
const line = JSON.stringify(entry) + "\n";
|
|
39
|
+
await fs.appendFile(historyPath, line, "utf-8");
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// Log error but don't throw (fire-and-forget)
|
|
42
|
+
api.logger.error(
|
|
43
|
+
`Failed to save history for flow ${session.flowName}:`,
|
|
44
|
+
error
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read history for a flow (for analytics/debugging)
|
|
51
|
+
* @public - For future analytics/debugging features
|
|
52
|
+
*/
|
|
53
|
+
export async function readFlowHistory(
|
|
54
|
+
api: ClawdbotPluginApi,
|
|
55
|
+
flowName: string
|
|
56
|
+
): Promise<Array<FlowSession & { completedAt: number }>> {
|
|
57
|
+
const historyPath = getHistoryPath(api, flowName);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const content = await fs.readFile(historyPath, "utf-8");
|
|
61
|
+
const lines = content.trim().split("\n");
|
|
62
|
+
|
|
63
|
+
return lines
|
|
64
|
+
.filter((line) => line.length > 0)
|
|
65
|
+
.map((line) => JSON.parse(line));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
api.logger.error(
|
|
71
|
+
`Failed to read history for flow ${flowName}:`,
|
|
72
|
+
error
|
|
73
|
+
);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory session storage with automatic timeout cleanup
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { FlowSession } from "../types.js";
|
|
6
|
+
|
|
7
|
+
// Session timeout: 30 minutes
|
|
8
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
// Cleanup interval: 5 minutes
|
|
11
|
+
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
// In-memory session store
|
|
14
|
+
const sessions = new Map<string, FlowSession>();
|
|
15
|
+
|
|
16
|
+
// Cleanup timer
|
|
17
|
+
let cleanupTimer: NodeJS.Timeout | null = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate session key
|
|
21
|
+
*/
|
|
22
|
+
export function getSessionKey(senderId: string, flowName: string): string {
|
|
23
|
+
return `${senderId}-${flowName}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a new session
|
|
28
|
+
*/
|
|
29
|
+
export function createSession(params: {
|
|
30
|
+
flowName: string;
|
|
31
|
+
currentStepId: string;
|
|
32
|
+
senderId: string;
|
|
33
|
+
channel: string;
|
|
34
|
+
}): FlowSession {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
const session: FlowSession = {
|
|
37
|
+
...params,
|
|
38
|
+
variables: {},
|
|
39
|
+
startedAt: now,
|
|
40
|
+
lastActivityAt: now,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const key = getSessionKey(params.senderId, params.flowName);
|
|
44
|
+
sessions.set(key, session);
|
|
45
|
+
|
|
46
|
+
// Start cleanup timer if not already running
|
|
47
|
+
if (!cleanupTimer) {
|
|
48
|
+
cleanupTimer = setInterval(cleanupExpiredSessions, CLEANUP_INTERVAL_MS);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return session;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get session by key (returns null if expired)
|
|
56
|
+
*/
|
|
57
|
+
export function getSession(key: string): FlowSession | null {
|
|
58
|
+
const session = sessions.get(key);
|
|
59
|
+
|
|
60
|
+
if (!session) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check if expired
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
if (now - session.lastActivityAt > SESSION_TIMEOUT_MS) {
|
|
67
|
+
sessions.delete(key);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return session;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Update session (merges partial updates)
|
|
76
|
+
*/
|
|
77
|
+
export function updateSession(
|
|
78
|
+
key: string,
|
|
79
|
+
patch: Partial<FlowSession>
|
|
80
|
+
): FlowSession | null {
|
|
81
|
+
const session = getSession(key);
|
|
82
|
+
|
|
83
|
+
if (!session) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const updated: FlowSession = {
|
|
88
|
+
...session,
|
|
89
|
+
...patch,
|
|
90
|
+
lastActivityAt: Date.now(),
|
|
91
|
+
// Merge variables instead of replacing
|
|
92
|
+
variables: patch.variables
|
|
93
|
+
? { ...session.variables, ...patch.variables }
|
|
94
|
+
: session.variables,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
sessions.set(key, updated);
|
|
98
|
+
return updated;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Delete session
|
|
103
|
+
*/
|
|
104
|
+
export function deleteSession(key: string): void {
|
|
105
|
+
sessions.delete(key);
|
|
106
|
+
|
|
107
|
+
// Stop cleanup timer if no sessions remain
|
|
108
|
+
if (sessions.size === 0 && cleanupTimer) {
|
|
109
|
+
clearInterval(cleanupTimer);
|
|
110
|
+
cleanupTimer = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Cleanup expired sessions
|
|
116
|
+
*/
|
|
117
|
+
function cleanupExpiredSessions(): void {
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
const keysToDelete: string[] = [];
|
|
120
|
+
|
|
121
|
+
for (const [key, session] of sessions.entries()) {
|
|
122
|
+
if (now - session.lastActivityAt > SESSION_TIMEOUT_MS) {
|
|
123
|
+
keysToDelete.push(key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const key of keysToDelete) {
|
|
128
|
+
sessions.delete(key);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Stop timer if no sessions remain
|
|
132
|
+
if (sessions.size === 0 && cleanupTimer) {
|
|
133
|
+
clearInterval(cleanupTimer);
|
|
134
|
+
cleanupTimer = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get all active sessions (for debugging)
|
|
140
|
+
* @public - For debugging and monitoring
|
|
141
|
+
*/
|
|
142
|
+
export function getAllSessions(): FlowSession[] {
|
|
143
|
+
return Array.from(sessions.values());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Clear all sessions (for testing)
|
|
148
|
+
*/
|
|
149
|
+
export function clearAllSessions(): void {
|
|
150
|
+
sessions.clear();
|
|
151
|
+
if (cleanupTimer) {
|
|
152
|
+
clearInterval(cleanupTimer);
|
|
153
|
+
cleanupTimer = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for skill-flow plugin
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type ValidationType = "number" | "email" | "phone";
|
|
6
|
+
|
|
7
|
+
export interface Button {
|
|
8
|
+
text: string;
|
|
9
|
+
value: string | number;
|
|
10
|
+
next?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FlowStep {
|
|
14
|
+
id: string;
|
|
15
|
+
message: string;
|
|
16
|
+
buttons?: Array<Button | string | number>;
|
|
17
|
+
next?: string;
|
|
18
|
+
capture?: string;
|
|
19
|
+
validate?: ValidationType;
|
|
20
|
+
condition?: {
|
|
21
|
+
variable: string;
|
|
22
|
+
equals?: string | number;
|
|
23
|
+
greaterThan?: number;
|
|
24
|
+
lessThan?: number;
|
|
25
|
+
contains?: string;
|
|
26
|
+
next: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FlowMetadata {
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
version: string;
|
|
34
|
+
author?: string;
|
|
35
|
+
steps: FlowStep[];
|
|
36
|
+
triggers?: {
|
|
37
|
+
manual?: boolean;
|
|
38
|
+
cron?: string;
|
|
39
|
+
event?: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface FlowSession {
|
|
44
|
+
flowName: string;
|
|
45
|
+
currentStepId: string;
|
|
46
|
+
senderId: string;
|
|
47
|
+
channel: string;
|
|
48
|
+
variables: Record<string, string | number>;
|
|
49
|
+
startedAt: number;
|
|
50
|
+
lastActivityAt: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TransitionResult {
|
|
54
|
+
nextStepId?: string;
|
|
55
|
+
variables: Record<string, string | number>;
|
|
56
|
+
complete: boolean;
|
|
57
|
+
error?: string;
|
|
58
|
+
message?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ReplyPayload {
|
|
62
|
+
text: string;
|
|
63
|
+
buttons?: Array<{
|
|
64
|
+
text: string;
|
|
65
|
+
callback_data: string;
|
|
66
|
+
}[]>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod validation schemas for flow definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { Button } from "./types.js";
|
|
7
|
+
|
|
8
|
+
// Validation type enum
|
|
9
|
+
const ValidationTypeSchema = z.enum(["number", "email", "phone"]);
|
|
10
|
+
|
|
11
|
+
// Button schema - can be string, number, or full object
|
|
12
|
+
const ButtonValueSchema = z.union([
|
|
13
|
+
z.string(),
|
|
14
|
+
z.number(),
|
|
15
|
+
z.object({
|
|
16
|
+
text: z.string(),
|
|
17
|
+
value: z.union([z.string(), z.number()]),
|
|
18
|
+
next: z.string().optional(),
|
|
19
|
+
}),
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
// Conditional branching schema
|
|
23
|
+
const ConditionSchema = z.object({
|
|
24
|
+
variable: z.string(),
|
|
25
|
+
equals: z.union([z.string(), z.number()]).optional(),
|
|
26
|
+
greaterThan: z.number().optional(),
|
|
27
|
+
lessThan: z.number().optional(),
|
|
28
|
+
contains: z.string().optional(),
|
|
29
|
+
next: z.string(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Flow step schema
|
|
33
|
+
const FlowStepSchema = z.object({
|
|
34
|
+
id: z.string(),
|
|
35
|
+
message: z.string(),
|
|
36
|
+
buttons: z.array(ButtonValueSchema).optional(),
|
|
37
|
+
next: z.string().optional(),
|
|
38
|
+
capture: z.string().optional(),
|
|
39
|
+
validate: ValidationTypeSchema.optional(),
|
|
40
|
+
condition: ConditionSchema.optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Trigger schema
|
|
44
|
+
const TriggerSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
manual: z.boolean().optional(),
|
|
47
|
+
cron: z.string().optional(),
|
|
48
|
+
event: z.string().optional(),
|
|
49
|
+
})
|
|
50
|
+
.optional();
|
|
51
|
+
|
|
52
|
+
// Complete flow metadata schema
|
|
53
|
+
export const FlowMetadataSchema = z.object({
|
|
54
|
+
name: z.string(),
|
|
55
|
+
description: z.string(),
|
|
56
|
+
version: z.string(),
|
|
57
|
+
author: z.string().optional(),
|
|
58
|
+
steps: z.array(FlowStepSchema).min(1),
|
|
59
|
+
triggers: TriggerSchema,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Normalize button input to Button object
|
|
64
|
+
*/
|
|
65
|
+
export function normalizeButton(
|
|
66
|
+
btn: string | number | Button,
|
|
67
|
+
_index: number
|
|
68
|
+
): Button {
|
|
69
|
+
if (typeof btn === "string") {
|
|
70
|
+
return {
|
|
71
|
+
text: btn,
|
|
72
|
+
value: btn,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (typeof btn === "number") {
|
|
76
|
+
return {
|
|
77
|
+
text: String(btn),
|
|
78
|
+
value: btn,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return btn;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validate input based on validation type
|
|
86
|
+
*/
|
|
87
|
+
export function validateInput(
|
|
88
|
+
input: string,
|
|
89
|
+
validationType: "number" | "email" | "phone"
|
|
90
|
+
): { valid: boolean; error?: string } {
|
|
91
|
+
switch (validationType) {
|
|
92
|
+
case "number": {
|
|
93
|
+
const num = Number(input);
|
|
94
|
+
if (isNaN(num)) {
|
|
95
|
+
return { valid: false, error: "Please enter a valid number" };
|
|
96
|
+
}
|
|
97
|
+
return { valid: true };
|
|
98
|
+
}
|
|
99
|
+
case "email": {
|
|
100
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
101
|
+
if (!emailRegex.test(input)) {
|
|
102
|
+
return { valid: false, error: "Please enter a valid email address" };
|
|
103
|
+
}
|
|
104
|
+
return { valid: true };
|
|
105
|
+
}
|
|
106
|
+
case "phone": {
|
|
107
|
+
const phoneRegex = /^\+?[\d\s()-]{10,}$/;
|
|
108
|
+
if (!phoneRegex.test(input)) {
|
|
109
|
+
return {
|
|
110
|
+
valid: false,
|
|
111
|
+
error: "Please enter a valid phone number",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { valid: true };
|
|
115
|
+
}
|
|
116
|
+
default:
|
|
117
|
+
return { valid: true };
|
|
118
|
+
}
|
|
119
|
+
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for external modules without types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
declare module "lockfile" {
|
|
6
|
+
export function lock(
|
|
7
|
+
path: string,
|
|
8
|
+
opts: { wait?: number; retries?: number },
|
|
9
|
+
callback: (err: Error | null) => void
|
|
10
|
+
): void;
|
|
11
|
+
|
|
12
|
+
export function unlock(path: string, callback: (err: Error | null) => void): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare module "clawdbot/plugin-sdk" {
|
|
16
|
+
export interface ClawdbotPluginApi {
|
|
17
|
+
logger: {
|
|
18
|
+
info(...args: any[]): void;
|
|
19
|
+
error(...args: any[]): void;
|
|
20
|
+
warn(...args: any[]): void;
|
|
21
|
+
debug(...args: any[]): void;
|
|
22
|
+
};
|
|
23
|
+
runtime: {
|
|
24
|
+
state: {
|
|
25
|
+
resolveStateDir(): string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
registerCommand(config: {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
acceptsArgs: boolean;
|
|
32
|
+
requireAuth: boolean;
|
|
33
|
+
handler: (args: any) => Promise<any>;
|
|
34
|
+
}): void;
|
|
35
|
+
}
|
|
36
|
+
}
|