@2en/clawly-plugins 1.4.1 → 1.6.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.
@@ -0,0 +1,88 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {compareSemVer, isUpdateAvailable, parseSemVer} from './semver'
3
+
4
+ describe('parseSemVer', () => {
5
+ test('parses simple version', () => {
6
+ const v = parseSemVer('1.2.3')
7
+ expect(v).toEqual({major: 1, minor: 2, patch: 3, prerelease: undefined, raw: '1.2.3'})
8
+ })
9
+
10
+ test('parses version with v prefix', () => {
11
+ const v = parseSemVer('v1.2.3')
12
+ expect(v).toEqual({major: 1, minor: 2, patch: 3, prerelease: undefined, raw: 'v1.2.3'})
13
+ })
14
+
15
+ test('parses version with prerelease', () => {
16
+ const v = parseSemVer('1.2.3-beta.1')
17
+ expect(v).toEqual({major: 1, minor: 2, patch: 3, prerelease: 'beta.1', raw: '1.2.3-beta.1'})
18
+ })
19
+
20
+ test('returns null for invalid input', () => {
21
+ expect(parseSemVer('')).toBeNull()
22
+ expect(parseSemVer('abc')).toBeNull()
23
+ expect(parseSemVer('1.2')).toBeNull()
24
+ expect(parseSemVer('1.2.3.4')).toBeNull()
25
+ })
26
+
27
+ test('trims whitespace', () => {
28
+ const v = parseSemVer(' 1.0.0 ')
29
+ expect(v?.major).toBe(1)
30
+ })
31
+ })
32
+
33
+ describe('compareSemVer', () => {
34
+ test('equal versions return 0', () => {
35
+ expect(compareSemVer('1.0.0', '1.0.0')).toBe(0)
36
+ })
37
+
38
+ test('major difference', () => {
39
+ expect(compareSemVer('2.0.0', '1.0.0')).toBe(1)
40
+ expect(compareSemVer('1.0.0', '2.0.0')).toBe(-1)
41
+ })
42
+
43
+ test('minor difference', () => {
44
+ expect(compareSemVer('1.1.0', '1.0.0')).toBe(1)
45
+ expect(compareSemVer('1.0.0', '1.1.0')).toBe(-1)
46
+ })
47
+
48
+ test('patch difference', () => {
49
+ expect(compareSemVer('1.0.1', '1.0.0')).toBe(1)
50
+ expect(compareSemVer('1.0.0', '1.0.1')).toBe(-1)
51
+ })
52
+
53
+ test('release > prerelease at same version', () => {
54
+ expect(compareSemVer('1.0.0', '1.0.0-beta.1')).toBe(1)
55
+ expect(compareSemVer('1.0.0-beta.1', '1.0.0')).toBe(-1)
56
+ })
57
+
58
+ test('two prereleases at same version return 0', () => {
59
+ expect(compareSemVer('1.0.0-alpha', '1.0.0-beta')).toBe(0)
60
+ })
61
+
62
+ test('returns null for invalid input', () => {
63
+ expect(compareSemVer('abc', '1.0.0')).toBeNull()
64
+ expect(compareSemVer('1.0.0', 'abc')).toBeNull()
65
+ })
66
+ })
67
+
68
+ describe('isUpdateAvailable', () => {
69
+ test('returns true when latest > current', () => {
70
+ expect(isUpdateAvailable('1.0.0', '1.1.0')).toBe(true)
71
+ expect(isUpdateAvailable('1.0.0', '2.0.0')).toBe(true)
72
+ expect(isUpdateAvailable('1.5.0', '1.5.1')).toBe(true)
73
+ })
74
+
75
+ test('returns false when latest <= current', () => {
76
+ expect(isUpdateAvailable('1.0.0', '1.0.0')).toBe(false)
77
+ expect(isUpdateAvailable('2.0.0', '1.0.0')).toBe(false)
78
+ })
79
+
80
+ test('returns false when latest is prerelease', () => {
81
+ expect(isUpdateAvailable('1.0.0', '2.0.0-beta.1')).toBe(false)
82
+ })
83
+
84
+ test('returns false for invalid input', () => {
85
+ expect(isUpdateAvailable('abc', '1.0.0')).toBe(false)
86
+ expect(isUpdateAvailable('1.0.0', 'abc')).toBe(false)
87
+ })
88
+ })
package/lib/semver.ts ADDED
@@ -0,0 +1,46 @@
1
+ export interface SemVer {
2
+ major: number
3
+ minor: number
4
+ patch: number
5
+ prerelease: string | undefined
6
+ raw: string
7
+ }
8
+
9
+ const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/
10
+
11
+ export function parseSemVer(version: string): SemVer | null {
12
+ const m = version.trim().match(SEMVER_RE)
13
+ if (!m) return null
14
+ return {
15
+ major: Number(m[1]),
16
+ minor: Number(m[2]),
17
+ patch: Number(m[3]),
18
+ prerelease: m[4] ?? undefined,
19
+ raw: version.trim(),
20
+ }
21
+ }
22
+
23
+ export function compareSemVer(a: string, b: string): -1 | 0 | 1 | null {
24
+ const pa = parseSemVer(a)
25
+ const pb = parseSemVer(b)
26
+ if (!pa || !pb) return null
27
+
28
+ for (const key of ['major', 'minor', 'patch'] as const) {
29
+ if (pa[key] > pb[key]) return 1
30
+ if (pa[key] < pb[key]) return -1
31
+ }
32
+
33
+ // Both equal in major.minor.patch — compare prerelease
34
+ // A release (no prerelease) is greater than a prerelease
35
+ if (!pa.prerelease && pb.prerelease) return 1
36
+ if (pa.prerelease && !pb.prerelease) return -1
37
+
38
+ return 0
39
+ }
40
+
41
+ export function isUpdateAvailable(current: string, latest: string): boolean {
42
+ const pl = parseSemVer(latest)
43
+ if (!pl || pl.prerelease) return false // latest must be stable
44
+ const cmp = compareSemVer(latest, current)
45
+ return cmp === 1
46
+ }
@@ -0,0 +1,71 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {stripCliLogs} from './stripCliLogs'
3
+
4
+ describe('stripCliLogs', () => {
5
+ test('strips leading log lines', () => {
6
+ const input = [
7
+ '11:40:09 [plugins] echo: registered /clawly_echo command',
8
+ '11:40:09 [plugins] tool: registered clawly_is_user_online agent tool',
9
+ '11:40:09 [plugins] tool: registered clawly_send_app_push agent tool',
10
+ '[{"id":"clawly-plugins","version":"1.5.0"}]',
11
+ ].join('\n')
12
+ expect(stripCliLogs(input)).toBe('[{"id":"clawly-plugins","version":"1.5.0"}]')
13
+ })
14
+
15
+ test('returns unchanged when no log lines', () => {
16
+ const input = '{"ok":true}'
17
+ expect(stripCliLogs(input)).toBe('{"ok":true}')
18
+ })
19
+
20
+ test('handles single-digit hour', () => {
21
+ const input = '9:05:01 [core] init\n{"data":1}'
22
+ expect(stripCliLogs(input)).toBe('{"data":1}')
23
+ })
24
+
25
+ test('returns empty string when all lines are logs', () => {
26
+ const input = '11:00:00 [a] foo\n12:00:00 [b] bar'
27
+ expect(stripCliLogs(input)).toBe('')
28
+ })
29
+
30
+ test('preserves multiline JSON after logs', () => {
31
+ const input = ['11:40:09 [plugins] loading', '{', ' "version": "1.0.0"', '}'].join('\n')
32
+ expect(stripCliLogs(input)).toBe('{\n "version": "1.0.0"\n}')
33
+ })
34
+
35
+ test('handles empty input', () => {
36
+ expect(stripCliLogs('')).toBe('')
37
+ })
38
+
39
+ test('does not strip lines that merely contain a timestamp mid-line', () => {
40
+ const input = 'some prefix 11:40:09 [x] y\n{"ok":true}'
41
+ expect(stripCliLogs(input)).toBe('some prefix 11:40:09 [x] y\n{"ok":true}')
42
+ })
43
+
44
+ test('strips ANSI color codes from log lines', () => {
45
+ const input = [
46
+ '\x1b[35m12:46:24 [plugins] presence: registered clawly.isOnline method',
47
+ '\x1b[35m12:46:24 [plugins] registered clawly.plugins.version',
48
+ '[{"id":"clawly-plugins","version":"1.5.0"}]',
49
+ ].join('\n')
50
+ expect(stripCliLogs(input)).toBe('[{"id":"clawly-plugins","version":"1.5.0"}]')
51
+ })
52
+
53
+ test('strips log lines without timestamps (after ANSI stripping)', () => {
54
+ const input = [
55
+ '\x1b[35m[plugins] echo: registered /clawly_echo command',
56
+ '\x1b[35m[plugins] tool: registered clawly_send_app_push agent tool',
57
+ '[{"id":"clawly-plugins"}]',
58
+ ].join('\n')
59
+ expect(stripCliLogs(input)).toBe('[{"id":"clawly-plugins"}]')
60
+ })
61
+
62
+ test('strips plain log lines without timestamps', () => {
63
+ const input = '[plugins] Loaded clawly-plugins plugin.\n{"ok":true}'
64
+ expect(stripCliLogs(input)).toBe('{"ok":true}')
65
+ })
66
+
67
+ test('preserves JSON arrays (not confused with log prefixes)', () => {
68
+ const input = '[{"id":"test"}]'
69
+ expect(stripCliLogs(input)).toBe('[{"id":"test"}]')
70
+ })
71
+ })
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Strip leading log lines from `openclaw ... --json` output.
3
+ *
4
+ * OpenClaw CLI may emit log lines (with or without ANSI colors / timestamps)
5
+ * before the actual JSON payload:
6
+ *
7
+ * 11:40:09 [plugins] echo: registered /clawly_echo command
8
+ * \x1b[35m[plugins] tool: registered clawly_send_app_push agent tool
9
+ * [{"id":"clawly-plugins", ...}]
10
+ *
11
+ * This function strips ANSI escape sequences, drops every line that looks
12
+ * like a log prefix (`HH:MM:SS [tag] ...` or `[tag] ...`), and returns the
13
+ * remaining text (the JSON body).
14
+ */
15
+
16
+ const ANSI_RE = /\x1b\[[0-9;]*m/g
17
+ const LOG_LINE_RE = /^(\d{1,2}:\d{2}:\d{2} )?\[\w[\w-]*\] /
18
+
19
+ export function stripCliLogs(output: string): string {
20
+ const cleaned = output.replace(ANSI_RE, '')
21
+ const lines = cleaned.split('\n')
22
+ const firstNonLog = lines.findIndex((line) => !LOG_LINE_RE.test(line))
23
+ if (firstNonLog === -1) return ''
24
+ return lines.slice(firstNonLog).join('\n').trim()
25
+ }
@@ -1,12 +1,51 @@
1
1
  {
2
2
  "id": "clawly-plugins",
3
3
  "name": "Clawly Plugins",
4
- "description": "Clawly utility RPC methods (clawly.*): file access, presence, push notifications, and agent messaging.",
4
+ "description": "Clawly utility RPC methods (clawly.*): file access, presence, push notifications, agent messaging, memory browser, and ClawHub CLI bridge.",
5
5
  "version": "0.2.0",
6
+ "uiHints": {
7
+ "memoryDir": {
8
+ "label": "Memory directory",
9
+ "help": "Absolute path to the memory directory containing .md files. Defaults to <OPENCLAW_WORKSPACE>/memory.",
10
+ "placeholder": "/data/memory"
11
+ },
12
+ "bin": {
13
+ "label": "clawhub binary",
14
+ "help": "Command to run (must be in PATH on the gateway host).",
15
+ "placeholder": "clawhub"
16
+ },
17
+ "defaultDir": {
18
+ "label": "Default skills dir",
19
+ "help": "Maps to `clawhub --dir` when not provided per-call.",
20
+ "placeholder": "skills"
21
+ },
22
+ "defaultNoInput": {
23
+ "label": "Default no-input",
24
+ "help": "If true, adds `--no-input` unless the tool call overrides it.",
25
+ "advanced": true
26
+ },
27
+ "defaultTimeoutMs": {
28
+ "label": "Default timeout (ms)",
29
+ "help": "Default command timeout for most tool calls.",
30
+ "advanced": true
31
+ },
32
+ "configPath": {
33
+ "label": "CLAWHUB_CONFIG_PATH",
34
+ "help": "Where the ClawHub CLI stores auth/config. Defaults to `<OPENCLAW_STATE_DIR>/clawhub/config.json`.",
35
+ "advanced": true,
36
+ "placeholder": "~/.openclaw/clawhub/config.json"
37
+ }
38
+ },
6
39
  "configSchema": {
7
40
  "type": "object",
8
41
  "additionalProperties": false,
9
42
  "properties": {
43
+ "memoryDir": { "type": "string", "minLength": 1 },
44
+ "bin": { "type": "string", "minLength": 1 },
45
+ "defaultDir": { "type": "string", "minLength": 1 },
46
+ "defaultNoInput": { "type": "boolean" },
47
+ "defaultTimeoutMs": { "type": "number", "minimum": 1000 },
48
+ "configPath": { "type": "string" },
10
49
  "gatewayBaseUrl": { "type": "string" },
11
50
  "gatewayToken": { "type": "string" }
12
51
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2en/clawly-plugins",
3
- "version": "1.4.1",
3
+ "version": "1.6.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -12,6 +12,9 @@
12
12
  "zx": "npm:zx@8.8.5-lite"
13
13
  },
14
14
  "files": [
15
+ "command",
16
+ "gateway",
17
+ "lib",
15
18
  "tools",
16
19
  "index.ts",
17
20
  "calendar.ts",
@@ -20,11 +23,6 @@
20
23
  "email.ts",
21
24
  "gateway-fetch.ts",
22
25
  "outbound.ts",
23
- "echo.ts",
24
- "presence.ts",
25
- "notification.ts",
26
- "agent-send.ts",
27
- "tools.ts",
28
26
  "openclaw.plugin.json"
29
27
  ],
30
28
  "publishConfig": {
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type {PluginApi} from '../index'
8
- import {isClientOnline} from '../presence'
8
+ import {isClientOnline} from '../gateway/presence'
9
9
 
10
10
  const TOOL_NAME = 'clawly_is_user_online'
11
11
 
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type {PluginApi} from '../index'
10
- import {sendPushNotification} from '../notification'
10
+ import {sendPushNotification} from '../gateway/notification'
11
11
 
12
12
  const TOOL_NAME = 'clawly_send_app_push'
13
13
 
package/tools/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type {PluginApi} from '../index'
2
+ import {registerIsUserOnlineTool} from './clawly-is-user-online'
3
+ import {registerSendAppPushTool} from './clawly-send-app-push'
4
+
5
+ export function registerTools(api: PluginApi) {
6
+ registerIsUserOnlineTool(api)
7
+ registerSendAppPushTool(api)
8
+ }
package/tools.ts DELETED
@@ -1,2 +0,0 @@
1
- export {registerIsUserOnlineTool} from './tools/clawly-is-user-online'
2
- export {registerSendAppPushTool} from './tools/clawly-send-app-push'