@shadowob/connector 1.1.3-dev.251
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 +661 -0
- package/README.md +157 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +477 -0
- package/dist/index.cjs +293 -0
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +267 -0
- package/hermes-shadowob-plugin/README.md +176 -0
- package/hermes-shadowob-plugin/__init__.py +8 -0
- package/hermes-shadowob-plugin/adapter.py +1572 -0
- package/hermes-shadowob-plugin/plugin.yaml +84 -0
- package/hermes-shadowob-plugin/requirements.txt +2 -0
- package/hermes-shadowob-plugin/shadow_sdk.py +479 -0
- package/hermes-shadowob-plugin/tests/test_adapter_env.py +159 -0
- package/hermes-shadowob-plugin/tests/test_shadow_sdk_helpers.py +25 -0
- package/package.json +57 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
5
|
+
if str(ROOT) not in sys.path:
|
|
6
|
+
sys.path.insert(0, str(ROOT))
|
|
7
|
+
|
|
8
|
+
import adapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_env_enablement_is_flat(monkeypatch):
|
|
12
|
+
monkeypatch.setenv('SHADOW_BASE_URL', 'https://shadow.example.com/api')
|
|
13
|
+
monkeypatch.setenv('SHADOW_TOKEN', 'tok')
|
|
14
|
+
monkeypatch.setenv('SHADOW_CHANNEL_IDS', 'c1,c2')
|
|
15
|
+
monkeypatch.setenv('SHADOW_HOME_CHANNEL', 'c3')
|
|
16
|
+
monkeypatch.setenv('SHADOW_MENTION_ONLY', 'true')
|
|
17
|
+
monkeypatch.setenv('SHADOW_AGENT_ID', 'agent-1')
|
|
18
|
+
monkeypatch.setenv('SHADOW_HEARTBEAT_INTERVAL_SECONDS', '15')
|
|
19
|
+
monkeypatch.setenv(
|
|
20
|
+
'SHADOW_SLASH_COMMANDS_JSON',
|
|
21
|
+
'[{"name":"demo","description":"Demo command"}]',
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
seed = adapter._env_enablement()
|
|
25
|
+
|
|
26
|
+
assert seed['base_url'] == 'https://shadow.example.com/api'
|
|
27
|
+
assert seed['token'] == 'tok'
|
|
28
|
+
assert seed['channel_ids'] == ['c1', 'c2', 'c3']
|
|
29
|
+
assert seed['mention_only'] is True
|
|
30
|
+
assert seed['agent_id'] == 'agent-1'
|
|
31
|
+
assert seed['heartbeat_interval_seconds'] == '15'
|
|
32
|
+
assert seed['slash_commands'][0]['name'] == 'demo'
|
|
33
|
+
assert seed['home_channel']['chat_id'] == 'c3'
|
|
34
|
+
assert 'extra' not in seed
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_check_requirements_allows_dynamic_remote_config_without_channel(monkeypatch):
|
|
38
|
+
monkeypatch.setenv('SHADOW_BASE_URL', 'https://shadow.example.com')
|
|
39
|
+
monkeypatch.setenv('SHADOW_TOKEN', 'tok')
|
|
40
|
+
monkeypatch.delenv('SHADOW_CHANNEL_IDS', raising=False)
|
|
41
|
+
monkeypatch.delenv('SHADOW_CHANNEL_ID', raising=False)
|
|
42
|
+
monkeypatch.delenv('SHADOW_HOME_CHANNEL', raising=False)
|
|
43
|
+
monkeypatch.delenv('SHADOW_SERVER_IDS', raising=False)
|
|
44
|
+
monkeypatch.delenv('SHADOW_AUTO_DISCOVER_CHANNELS', raising=False)
|
|
45
|
+
|
|
46
|
+
assert adapter.check_requirements() is True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_env_enablement_does_not_require_static_agent_or_channel(monkeypatch):
|
|
50
|
+
monkeypatch.setenv('SHADOW_BASE_URL', 'https://shadow.example.com')
|
|
51
|
+
monkeypatch.setenv('SHADOW_TOKEN', 'tok')
|
|
52
|
+
monkeypatch.delenv('SHADOW_CHANNEL_IDS', raising=False)
|
|
53
|
+
monkeypatch.delenv('SHADOW_CHANNEL_ID', raising=False)
|
|
54
|
+
monkeypatch.delenv('SHADOW_HOME_CHANNEL', raising=False)
|
|
55
|
+
monkeypatch.delenv('SHADOW_AGENT_ID', raising=False)
|
|
56
|
+
|
|
57
|
+
seed = adapter._env_enablement()
|
|
58
|
+
|
|
59
|
+
assert seed['base_url'] == 'https://shadow.example.com'
|
|
60
|
+
assert seed['token'] == 'tok'
|
|
61
|
+
assert 'channel_ids' not in seed
|
|
62
|
+
assert 'agent_id' not in seed
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_remote_config_entries_filter_listen_policy():
|
|
66
|
+
remote_config = {
|
|
67
|
+
'servers': [
|
|
68
|
+
{
|
|
69
|
+
'id': 'server-1',
|
|
70
|
+
'name': 'Server',
|
|
71
|
+
'slug': 'server',
|
|
72
|
+
'channels': [
|
|
73
|
+
{'id': 'listen-1', 'name': 'general', 'policy': {'listen': True}},
|
|
74
|
+
{'id': 'skip-1', 'name': 'quiet', 'policy': {'listen': False}},
|
|
75
|
+
],
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
entries = adapter._remote_listen_channel_entries(remote_config)
|
|
81
|
+
|
|
82
|
+
assert [entry[0] for entry in entries] == ['listen-1']
|
|
83
|
+
assert entries[0][1]['serverId'] == 'server-1'
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_slash_command_prompt_and_interactive_block():
|
|
87
|
+
commands = [
|
|
88
|
+
{
|
|
89
|
+
'name': 'deploy',
|
|
90
|
+
'aliases': ['ship'],
|
|
91
|
+
'description': 'Deploy something',
|
|
92
|
+
'body': 'Run the deployment workflow.',
|
|
93
|
+
'interaction': {
|
|
94
|
+
'kind': 'form',
|
|
95
|
+
'prompt': 'Choose a target',
|
|
96
|
+
'fields': [{'id': 'target', 'kind': 'text', 'label': 'Target'}],
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
match = adapter._slash_command_match('/ship prod', commands)
|
|
102
|
+
|
|
103
|
+
assert match is not None
|
|
104
|
+
assert match[1] == 'ship'
|
|
105
|
+
assert match[2] == 'prod'
|
|
106
|
+
prompt = adapter._format_slash_command_prompt('/ship prod', match)
|
|
107
|
+
assert 'Slash command /deploy was invoked.' in prompt
|
|
108
|
+
assert 'Run the deployment workflow.' in prompt
|
|
109
|
+
block = adapter._slash_interactive_block(match, 'message-1')
|
|
110
|
+
assert block['id'].endswith(':message-1')
|
|
111
|
+
assert block['kind'] == 'form'
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_interactive_response_text_appends_shadow_context():
|
|
115
|
+
message = {
|
|
116
|
+
'metadata': {
|
|
117
|
+
'interactiveResponse': {
|
|
118
|
+
'blockId': 'block-1',
|
|
119
|
+
'actionId': 'approve',
|
|
120
|
+
'value': 'yes',
|
|
121
|
+
'values': {'comment': 'ok'},
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
text = adapter._interactive_response_text('Clicked approve', message)
|
|
127
|
+
|
|
128
|
+
assert 'Clicked approve' in text
|
|
129
|
+
assert '[Shadow interactive response]' in text
|
|
130
|
+
assert 'actionId: approve' in text
|
|
131
|
+
assert '"comment": "ok"' in text
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_interactive_response_text_includes_source_followup_prompt():
|
|
135
|
+
message = {
|
|
136
|
+
'metadata': {
|
|
137
|
+
'interactiveResponse': {
|
|
138
|
+
'sourceMessageId': 'source-1',
|
|
139
|
+
'blockId': 'block-1',
|
|
140
|
+
'actionId': 'ok',
|
|
141
|
+
'value': 'ok',
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
source = {
|
|
146
|
+
'content': 'Choose an action',
|
|
147
|
+
'metadata': {
|
|
148
|
+
'interactive': {
|
|
149
|
+
'prompt': 'Confirm the action',
|
|
150
|
+
'responsePrompt': 'Reply exactly DONE.',
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
text = adapter._interactive_response_text('', message, source)
|
|
156
|
+
|
|
157
|
+
assert 'sourceMessage: Choose an action' in text
|
|
158
|
+
assert 'sourcePrompt: Confirm the action' in text
|
|
159
|
+
assert 'followUpInstruction: Reply exactly DONE.' in text
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
5
|
+
if str(ROOT) not in sys.path:
|
|
6
|
+
sys.path.insert(0, str(ROOT))
|
|
7
|
+
|
|
8
|
+
from shadow_sdk import normalize_base_url, parse_bool, split_csv
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_normalize_base_url_strips_api_suffix():
|
|
12
|
+
assert normalize_base_url('https://example.com/api') == 'https://example.com'
|
|
13
|
+
assert normalize_base_url('https://example.com/api/') == 'https://example.com'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_parse_bool():
|
|
17
|
+
assert parse_bool('true') is True
|
|
18
|
+
assert parse_bool('1') is True
|
|
19
|
+
assert parse_bool('off', True) is False
|
|
20
|
+
assert parse_bool(None, True) is True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_split_csv():
|
|
24
|
+
assert split_csv('a,b, c') == ['a', 'b', 'c']
|
|
25
|
+
assert split_csv(['a', '', 'b']) == ['a', 'b']
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shadowob/connector",
|
|
3
|
+
"version": "1.1.3-dev.251",
|
|
4
|
+
"description": "Shadow connector helpers for OpenClaw, Hermes Agent, and cc-connect",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"require": "./dist/index.cjs",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"bin": {
|
|
11
|
+
"shadowob-connector": "./dist/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"development": "./src/index.ts",
|
|
17
|
+
"import": "./dist/index.js",
|
|
18
|
+
"require": "./dist/index.cjs",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"hermes-shadowob-plugin",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"shadow",
|
|
29
|
+
"shadowob",
|
|
30
|
+
"openclaw",
|
|
31
|
+
"hermes-agent",
|
|
32
|
+
"cc-connect",
|
|
33
|
+
"connector"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/buggyblues/shadow",
|
|
39
|
+
"directory": "packages/connector"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.15.21",
|
|
43
|
+
"tsup": "^8.5.0",
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
|
+
"vitest": "^4.1.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"dev": "tsup --watch",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:watch": "vitest"
|
|
56
|
+
}
|
|
57
|
+
}
|