@insta-dev01/intclaw 1.0.11 → 1.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.
Files changed (49) hide show
  1. package/LICENSE +1 -1
  2. package/README.en.md +424 -0
  3. package/README.md +365 -164
  4. package/index.ts +28 -0
  5. package/openclaw.plugin.json +12 -41
  6. package/package.json +69 -40
  7. package/src/channel.ts +557 -0
  8. package/src/config/accounts.ts +230 -0
  9. package/src/config/schema.ts +144 -0
  10. package/src/core/connection.ts +733 -0
  11. package/src/core/message-handler.ts +1268 -0
  12. package/src/core/provider.ts +106 -0
  13. package/src/core/state.ts +54 -0
  14. package/src/directory.ts +95 -0
  15. package/src/gateway-methods.ts +237 -0
  16. package/src/onboarding.ts +387 -0
  17. package/src/policy.ts +19 -0
  18. package/src/probe.ts +213 -0
  19. package/src/reply-dispatcher.ts +674 -0
  20. package/src/runtime.ts +7 -0
  21. package/src/sdk/helpers.ts +317 -0
  22. package/src/sdk/types.ts +515 -0
  23. package/src/secret-input.ts +19 -0
  24. package/src/services/media/audio.ts +54 -0
  25. package/src/services/media/chunk-upload.ts +293 -0
  26. package/src/services/media/common.ts +154 -0
  27. package/src/services/media/file.ts +70 -0
  28. package/src/services/media/image.ts +67 -0
  29. package/src/services/media/index.ts +10 -0
  30. package/src/services/media/video.ts +162 -0
  31. package/src/services/media.ts +1134 -0
  32. package/src/services/messaging/index.ts +16 -0
  33. package/src/services/messaging/send.ts +137 -0
  34. package/src/services/messaging.ts +800 -0
  35. package/src/targets.ts +45 -0
  36. package/src/types/index.ts +52 -0
  37. package/src/utils/agent.ts +63 -0
  38. package/src/utils/async.ts +51 -0
  39. package/src/utils/constants.ts +9 -0
  40. package/src/utils/http-client.ts +84 -0
  41. package/src/utils/index.ts +8 -0
  42. package/src/utils/logger.ts +78 -0
  43. package/src/utils/session.ts +118 -0
  44. package/src/utils/token.ts +94 -0
  45. package/src/utils/utils-legacy.ts +506 -0
  46. package/.env.example +0 -11
  47. package/skills/intclaw_matrix/SKILL.md +0 -20
  48. package/src/channel/intclaw_channel.js +0 -155
  49. package/src/index.js +0 -23
@@ -0,0 +1,230 @@
1
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId , normalizeResolvedSecretInputString, normalizeSecretInputString } from "../sdk/helpers.ts";
2
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
3
+ import type {
4
+ IntclawConfig,
5
+ IntclawAccountConfig,
6
+ IntclawDefaultAccountSelectionSource,
7
+ ResolvedIntclawAccount,
8
+ } from "../types/index.ts";
9
+
10
+ /**
11
+ * List all configured account IDs from the accounts field.
12
+ */
13
+ function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
14
+ const accounts = (cfg.channels?.["intclaw-connector"] as IntclawConfig)?.accounts;
15
+ if (!accounts || typeof accounts !== "object") {
16
+ return [];
17
+ }
18
+ return Object.keys(accounts).filter(Boolean);
19
+ }
20
+
21
+ /**
22
+ * List all IntClaw account IDs.
23
+ * If no accounts are configured, returns [DEFAULT_ACCOUNT_ID] for backward compatibility.
24
+ */
25
+ export function listIntclawAccountIds(cfg: ClawdbotConfig): string[] {
26
+ const ids = listConfiguredAccountIds(cfg);
27
+ if (ids.length === 0) {
28
+ // Backward compatibility: no accounts configured, use default
29
+ return [DEFAULT_ACCOUNT_ID];
30
+ }
31
+ return [...ids].toSorted((a, b) => a.localeCompare(b));
32
+ }
33
+
34
+ /**
35
+ * Resolve the default account selection and its source.
36
+ */
37
+ export function resolveDefaultIntclawAccountSelection(cfg: ClawdbotConfig): {
38
+ accountId: string;
39
+ source: IntclawDefaultAccountSelectionSource;
40
+ } {
41
+ const preferredRaw = (cfg.channels?.["intclaw-connector"] as IntclawConfig | undefined)?.defaultAccount?.trim();
42
+ const preferred = preferredRaw ? normalizeAccountId(preferredRaw) : undefined;
43
+ if (preferred) {
44
+ return {
45
+ accountId: preferred,
46
+ source: "explicit-default",
47
+ };
48
+ }
49
+ const ids = listIntclawAccountIds(cfg);
50
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) {
51
+ return {
52
+ accountId: DEFAULT_ACCOUNT_ID,
53
+ source: "mapped-default",
54
+ };
55
+ }
56
+ return {
57
+ accountId: ids[0] ?? DEFAULT_ACCOUNT_ID,
58
+ source: "fallback",
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Resolve the default account ID.
64
+ */
65
+ export function resolveDefaultIntclawAccountId(cfg: ClawdbotConfig): string {
66
+ return resolveDefaultIntclawAccountSelection(cfg).accountId;
67
+ }
68
+
69
+ /**
70
+ * Get the raw account-specific config.
71
+ */
72
+ function resolveAccountConfig(
73
+ cfg: ClawdbotConfig,
74
+ accountId: string,
75
+ ): IntclawAccountConfig | undefined {
76
+ const accounts = (cfg.channels?.["intclaw-connector"] as IntclawConfig)?.accounts;
77
+ if (!accounts || typeof accounts !== "object") {
78
+ return undefined;
79
+ }
80
+ return accounts[accountId];
81
+ }
82
+
83
+ /**
84
+ * Merge top-level config with account-specific config.
85
+ * Account-specific fields override top-level fields.
86
+ */
87
+ function mergeIntclawAccountConfig(cfg: ClawdbotConfig, accountId: string): IntclawConfig {
88
+ const intclawCfg = cfg.channels?.["intclaw-connector"] as IntclawConfig | undefined;
89
+
90
+ // Extract base config (exclude accounts field to avoid recursion)
91
+ const { accounts: _ignored, defaultAccount: _ignoredDefaultAccount, ...base } = intclawCfg ?? {};
92
+
93
+ // Get account-specific overrides
94
+ const account = resolveAccountConfig(cfg, accountId) ?? {};
95
+
96
+ // Merge: account config overrides base config
97
+ return { ...base, ...account } as IntclawConfig;
98
+ }
99
+
100
+ /**
101
+ * Resolve IntClaw credentials from a config.
102
+ */
103
+ export function resolveIntclawCredentials(cfg?: IntclawConfig): {
104
+ clientId: string;
105
+ clientSecret: string;
106
+ } | null;
107
+ export function resolveIntclawCredentials(
108
+ cfg: IntclawConfig | undefined,
109
+ options: { allowUnresolvedSecretRef?: boolean },
110
+ ): {
111
+ clientId: string;
112
+ clientSecret: string;
113
+ } | null;
114
+ export function resolveIntclawCredentials(
115
+ cfg?: IntclawConfig,
116
+ options?: { allowUnresolvedSecretRef?: boolean },
117
+ ): {
118
+ clientId: string;
119
+ clientSecret: string;
120
+ } | null {
121
+ const normalizeString = (value: unknown): string | undefined => {
122
+ if (typeof value === "number") {
123
+ return String(value);
124
+ }
125
+ if (typeof value !== "string") {
126
+ return undefined;
127
+ }
128
+ const trimmed = value.trim();
129
+ return trimmed ? trimmed : undefined;
130
+ };
131
+
132
+ const resolveSecretLike = (value: unknown, path: string): string | undefined => {
133
+ // Missing credential: treat as not configured (no exception).
134
+ // This path is used in non-onboarding contexts (e.g. channel listing/status),
135
+ // so we must not throw when credentials are absent.
136
+ if (value === undefined || value === null) {
137
+ return undefined;
138
+ }
139
+
140
+ const asString = normalizeString(value);
141
+ if (asString) {
142
+ return asString;
143
+ }
144
+
145
+ // In relaxed/onboarding paths only: allow direct env SecretRef reads for UX.
146
+ // Default resolution path must preserve unresolved-ref diagnostics/policy semantics.
147
+ if (options?.allowUnresolvedSecretRef && typeof value === "object" && value !== null) {
148
+ const rec = value as Record<string, unknown>;
149
+ const source = normalizeString(rec.source)?.toLowerCase();
150
+ const id = normalizeString(rec.id);
151
+ if (source === "env" && id) {
152
+ const envValue = normalizeString(process.env[id]);
153
+ if (envValue) {
154
+ return envValue;
155
+ }
156
+ }
157
+ }
158
+
159
+ if (options?.allowUnresolvedSecretRef) {
160
+ return normalizeSecretInputString(value);
161
+ }
162
+ return normalizeResolvedSecretInputString({ value, path });
163
+ };
164
+
165
+ const clientId = resolveSecretLike(cfg?.clientId, "channels.intclaw-connector.clientId");
166
+ const clientSecret = resolveSecretLike(cfg?.clientSecret, "channels.intclaw-connector.clientSecret");
167
+
168
+ if (!clientId || !clientSecret) {
169
+ return null;
170
+ }
171
+ return {
172
+ clientId,
173
+ clientSecret,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Resolve a complete IntClaw account with merged config.
179
+ */
180
+ export function resolveIntclawAccount(params: {
181
+ cfg: ClawdbotConfig;
182
+ accountId?: string | null;
183
+ }): ResolvedIntclawAccount {
184
+ const hasExplicitAccountId =
185
+ typeof params.accountId === "string" && params.accountId.trim() !== "";
186
+ const defaultSelection = hasExplicitAccountId
187
+ ? null
188
+ : resolveDefaultIntclawAccountSelection(params.cfg);
189
+ const accountId = hasExplicitAccountId
190
+ ? normalizeAccountId(params.accountId ?? "")
191
+ : (defaultSelection?.accountId ?? DEFAULT_ACCOUNT_ID);
192
+ const selectionSource = hasExplicitAccountId
193
+ ? "explicit"
194
+ : (defaultSelection?.source ?? "fallback");
195
+ const intclawCfg = params.cfg.channels?.["intclaw-connector"] as IntclawConfig | undefined;
196
+
197
+ // Base enabled state (top-level)
198
+ const baseEnabled = intclawCfg?.enabled !== false;
199
+
200
+ // Merge configs
201
+ const merged = mergeIntclawAccountConfig(params.cfg, accountId);
202
+
203
+ // Account-level enabled state
204
+ const accountEnabled = merged.enabled !== false;
205
+ const enabled = baseEnabled && accountEnabled;
206
+
207
+ // Resolve credentials from merged config
208
+ const creds = resolveIntclawCredentials(merged);
209
+ const accountName = (merged as IntclawAccountConfig).name;
210
+
211
+ return {
212
+ accountId,
213
+ selectionSource,
214
+ enabled,
215
+ configured: Boolean(creds),
216
+ name: typeof accountName === "string" ? accountName.trim() || undefined : undefined,
217
+ clientId: creds?.clientId,
218
+ clientSecret: creds?.clientSecret,
219
+ config: merged,
220
+ };
221
+ }
222
+
223
+ /**
224
+ * List all enabled and configured accounts.
225
+ */
226
+ export function listEnabledIntclawAccounts(cfg: ClawdbotConfig): ResolvedIntclawAccount[] {
227
+ return listIntclawAccountIds(cfg)
228
+ .map((accountId) => resolveIntclawAccount({ cfg, accountId }))
229
+ .filter((account) => account.enabled && account.configured);
230
+ }
@@ -0,0 +1,144 @@
1
+ import { normalizeAccountId } from "../sdk/helpers.ts";
2
+ import { z } from "zod";
3
+ export { z };
4
+ import { buildSecretInputSchema, hasConfiguredSecretInput } from "../secret-input.ts";
5
+
6
+ const DmPolicySchema = z.enum(["open", "pairing", "allowlist"]);
7
+ const GroupPolicySchema = z.enum(["open", "allowlist", "disabled"]);
8
+
9
+ const ToolPolicySchema = z
10
+ .object({
11
+ allow: z.array(z.string()).optional(),
12
+ deny: z.array(z.string()).optional(),
13
+ })
14
+ .strict()
15
+ .optional();
16
+
17
+ /**
18
+ * Group session scope for routing IntClaw group messages.
19
+ * - "group" (default): one session per group chat
20
+ * - "group_sender": one session per (group + sender)
21
+ */
22
+ const GroupSessionScopeSchema = z
23
+ .enum(["group", "group_sender"])
24
+ .optional();
25
+
26
+ /**
27
+ * Intclaw tools configuration.
28
+ * Controls which tool categories are enabled.
29
+ */
30
+ const IntclawToolsConfigSchema = z
31
+ .object({
32
+ docs: z.boolean().optional(), // Document operations (default: true)
33
+ media: z.boolean().optional(), // Media upload operations (default: true)
34
+ })
35
+ .strict()
36
+ .optional();
37
+
38
+ export const IntclawGroupSchema = z
39
+ .object({
40
+ requireMention: z.boolean().optional(),
41
+ tools: ToolPolicySchema,
42
+ enabled: z.boolean().optional(),
43
+ allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
44
+ systemPrompt: z.string().optional(),
45
+ groupSessionScope: GroupSessionScopeSchema,
46
+ })
47
+ .strict();
48
+
49
+ const IntclawSharedConfigShape = {
50
+ dmPolicy: DmPolicySchema.optional(),
51
+ allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
52
+ groupPolicy: GroupPolicySchema.optional(),
53
+ groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
54
+ requireMention: z.boolean().optional(),
55
+ groups: z.record(z.string(), IntclawGroupSchema.optional()).optional(),
56
+ historyLimit: z.number().int().min(0).optional(),
57
+ dmHistoryLimit: z.number().int().min(0).optional(),
58
+ textChunkLimit: z.number().int().positive().optional(),
59
+ mediaMaxMb: z.number().positive().optional(),
60
+ tools: IntclawToolsConfigSchema,
61
+ typingIndicator: z.boolean().optional(),
62
+ resolveSenderNames: z.boolean().optional(),
63
+ separateSessionByConversation: z.boolean().optional(),
64
+ sharedMemoryAcrossConversations: z.boolean().optional(),
65
+ groupSessionScope: GroupSessionScopeSchema,
66
+ asyncMode: z.boolean().optional(),
67
+ ackText: z.string().optional(),
68
+ endpoint: z.string().optional(), // DWClient gateway endpoint
69
+ debug: z.boolean().optional(), // DWClient debug mode
70
+ };
71
+
72
+ /**
73
+ * Per-account configuration.
74
+ * All fields are optional - missing fields inherit from top-level config.
75
+ */
76
+ export const IntclawAccountConfigSchema = z
77
+ .object({
78
+ enabled: z.boolean().optional(),
79
+ name: z.string().optional(), // Display name for this account
80
+ clientId: z.union([z.string(), z.number()]).optional(),
81
+ clientSecret: buildSecretInputSchema().optional(),
82
+ ...IntclawSharedConfigShape,
83
+ })
84
+ .strict();
85
+
86
+ export const IntclawConfigSchema = z
87
+ .object({
88
+ enabled: z.boolean().optional(),
89
+ defaultAccount: z.string().optional(),
90
+ // Top-level credentials (backward compatible for single-account mode)
91
+ clientId: z.union([z.string(), z.number()]).optional(),
92
+ clientSecret: buildSecretInputSchema().optional(),
93
+ enableMediaUpload: z.boolean().optional(),
94
+ systemPrompt: z.string().optional(),
95
+ ...IntclawSharedConfigShape,
96
+ dmPolicy: DmPolicySchema.optional().default("open"),
97
+ groupPolicy: GroupPolicySchema.optional().default("open"),
98
+ requireMention: z.boolean().optional().default(true),
99
+ separateSessionByConversation: z.boolean().optional().default(true),
100
+ sharedMemoryAcrossConversations: z.boolean().optional().default(false),
101
+ groupSessionScope: GroupSessionScopeSchema.optional().default("group"),
102
+ // Multi-account configuration
103
+ accounts: z.record(z.string(), IntclawAccountConfigSchema.optional()).optional(),
104
+ })
105
+ .strict()
106
+ .superRefine((value, ctx) => {
107
+ const defaultAccount = value.defaultAccount?.trim();
108
+ if (defaultAccount && value.accounts && Object.keys(value.accounts).length > 0) {
109
+ const normalizedDefaultAccount = normalizeAccountId(defaultAccount);
110
+ if (!Object.prototype.hasOwnProperty.call(value.accounts, normalizedDefaultAccount)) {
111
+ ctx.addIssue({
112
+ code: z.ZodIssueCode.custom,
113
+ path: ["defaultAccount"],
114
+ message: `channels.intclaw-connector.defaultAccount="${defaultAccount}" does not match a configured account key`,
115
+ });
116
+ }
117
+ }
118
+
119
+ // Validate dmPolicy and allowFrom consistency
120
+ if (value.dmPolicy === "allowlist") {
121
+ const allowFrom = value.allowFrom ?? [];
122
+ if (allowFrom.length === 0) {
123
+ ctx.addIssue({
124
+ code: z.ZodIssueCode.custom,
125
+ path: ["allowFrom"],
126
+ message:
127
+ 'channels.intclaw-connector.dmPolicy="allowlist" requires channels.intclaw-connector.allowFrom to contain at least one entry',
128
+ });
129
+ }
130
+ }
131
+
132
+ // Validate groupPolicy and groupAllowFrom consistency
133
+ if (value.groupPolicy === "allowlist") {
134
+ const groupAllowFrom = value.groupAllowFrom ?? [];
135
+ if (groupAllowFrom.length === 0) {
136
+ ctx.addIssue({
137
+ code: z.ZodIssueCode.custom,
138
+ path: ["groupAllowFrom"],
139
+ message:
140
+ 'channels.intclaw-connector.groupPolicy="allowlist" requires channels.intclaw-connector.groupAllowFrom to contain at least one entry',
141
+ });
142
+ }
143
+ }
144
+ });