@codyswann/lisa 4.33.4 → 4.33.6
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/dist/configs/eslint/expo.d.ts.map +1 -1
- package/dist/configs/eslint/expo.js +4 -7
- package/dist/configs/eslint/expo.js.map +1 -1
- package/dist/configs/eslint/view-gate.d.ts +42 -0
- package/dist/configs/eslint/view-gate.d.ts.map +1 -0
- package/dist/configs/eslint/view-gate.js +110 -0
- package/dist/configs/eslint/view-gate.js.map +1 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +8 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +20 -9
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/eslint-plugin-component-structure/README.md +85 -2
- package/eslint-plugin-component-structure/__tests__/no-hooks-in-view.test.js +229 -0
- package/eslint-plugin-component-structure/__tests__/no-return-in-view.test.js +185 -0
- package/eslint-plugin-component-structure/__tests__/plugin-index.test.js +8 -1
- package/eslint-plugin-component-structure/index.js +4 -1
- package/eslint-plugin-component-structure/lib/view-file-scope.js +62 -0
- package/eslint-plugin-component-structure/rules/no-hooks-in-view.js +101 -0
- package/eslint-plugin-component-structure/rules/no-return-in-view.js +133 -62
- package/expo/copy-overwrite/eslint.expo.ts +88 -1
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/skills/container-view-pattern/SKILL.md +80 -24
- package/plugins/lisa-expo/.codex-plugin/skills/container-view-pattern/scripts/validate_component.py +166 -43
- package/plugins/lisa-expo/skills/container-view-pattern/SKILL.md +80 -24
- package/plugins/lisa-expo/skills/container-view-pattern/scripts/validate_component.py +166 -43
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-agy/skills/container-view-pattern/SKILL.md +80 -24
- package/plugins/lisa-expo-agy/skills/container-view-pattern/scripts/validate_component.py +166 -43
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/skills/container-view-pattern/SKILL.md +80 -24
- package/plugins/lisa-expo-copilot/skills/container-view-pattern/scripts/validate_component.py +166 -43
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/skills/container-view-pattern/SKILL.md +80 -24
- package/plugins/lisa-expo-cursor/skills/container-view-pattern/scripts/validate_component.py +166 -43
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/expo/skills/container-view-pattern/SKILL.md +80 -24
- package/plugins/src/expo/skills/container-view-pattern/scripts/validate_component.py +166 -43
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +414 -6
- package/typescript/create-only/.github/workflows/review-evidence.yml +78 -3
package/plugins/lisa-expo-cursor/skills/container-view-pattern/scripts/validate_component.py
CHANGED
|
@@ -10,11 +10,29 @@ This script validates that a component directory follows the Container/View patt
|
|
|
10
10
|
- Has index.tsx with correct export
|
|
11
11
|
- View uses memo() wrapper
|
|
12
12
|
- View has displayName
|
|
13
|
-
- View
|
|
13
|
+
- View is an arrow function with an expression body (declaration form is a
|
|
14
|
+
violation, not an alternative spelling)
|
|
15
|
+
- View calls no hooks — matched by SHAPE (`use` + uppercase), never by a list of
|
|
16
|
+
names, because the call that motivated this check was a project-local custom
|
|
17
|
+
hook that no list would have contained
|
|
18
|
+
|
|
19
|
+
One check reports as an ADVISORY rather than an error: "Container renders JSX
|
|
20
|
+
besides the View". It is a whole-file text scan that cannot tell a returned JSX
|
|
21
|
+
tree from JSX inside a callback, so a Container holding a legitimate `renderItem`
|
|
22
|
+
trips it — and moving a `renderItem` out of a View is one of the sanctioned fixes
|
|
23
|
+
for the View hook ban. It prints and does not fail.
|
|
24
|
+
|
|
25
|
+
This is a fast pre-check, not the gate. ESLint reads the AST of every file;
|
|
26
|
+
this reads one directory with regexes. When they disagree, ESLint is right.
|
|
14
27
|
|
|
15
28
|
Usage:
|
|
16
29
|
python3 validate_component.py <path-to-component-directory>
|
|
17
30
|
|
|
31
|
+
The script ships inside the Lisa plugin, not in a consumer's `.claude/skills/`,
|
|
32
|
+
so invoke it through the plugin root:
|
|
33
|
+
|
|
34
|
+
python3 "${CLAUDE_PLUGIN_ROOT:-node_modules/@codyswann/lisa/plugins/lisa-expo}/skills/container-view-pattern/scripts/validate_component.py" <path>
|
|
35
|
+
|
|
18
36
|
Example:
|
|
19
37
|
python3 validate_component.py features/player-kanban/components/AddColumnButton
|
|
20
38
|
"""
|
|
@@ -25,21 +43,118 @@ import sys
|
|
|
25
43
|
from pathlib import Path
|
|
26
44
|
|
|
27
45
|
|
|
28
|
-
|
|
46
|
+
HOOK_CALL = re.compile(r"\b(use[A-Z]\w*)\s*\(")
|
|
47
|
+
"""Every hook call, by SHAPE.
|
|
48
|
+
|
|
49
|
+
`use` followed by an uppercase letter is React's own definition of a hook name,
|
|
50
|
+
so this matches `useState`, `useMemo`, and every project-local custom hook
|
|
51
|
+
alike. It is a shape and never a list: a list is what let
|
|
52
|
+
`useCreateNoteQuickActionEnabled()` through.
|
|
53
|
+
|
|
54
|
+
It also matches hook calls inside comments and strings. That is the accepted
|
|
55
|
+
cost of a regex pre-check; ESLint's `component-structure/no-hooks-in-view` reads
|
|
56
|
+
the AST and is the gate.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _arrow_body_is_block(source: str, start: int) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Whether the arrow beginning at `start` has a block body.
|
|
63
|
+
|
|
64
|
+
Walks to the first top-level `=>` instead of matching a parameter list with
|
|
65
|
+
`\\([^)]*\\)`, which fails on any nested paren — a destructured parameter with
|
|
66
|
+
a default, a type annotation carrying a function type, or a parenthesised
|
|
67
|
+
union.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
source: The View file's text.
|
|
71
|
+
start: Index just past the `const <Name>View` binding.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True when the arrow's body opens with `{`.
|
|
75
|
+
"""
|
|
76
|
+
depth = 0
|
|
77
|
+
index = start
|
|
78
|
+
while index < len(source) - 1:
|
|
79
|
+
char = source[index]
|
|
80
|
+
if char in "([{":
|
|
81
|
+
depth += 1
|
|
82
|
+
elif char in ")]}":
|
|
83
|
+
depth -= 1
|
|
84
|
+
elif char == ";" and depth == 0:
|
|
85
|
+
return False
|
|
86
|
+
elif depth == 0 and source[index : index + 2] == "=>":
|
|
87
|
+
rest = source[index + 2 :].lstrip()
|
|
88
|
+
return rest.startswith("{")
|
|
89
|
+
index += 1
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def view_form_errors(component_name: str, view_content: str) -> list[str]:
|
|
94
|
+
"""
|
|
95
|
+
Errors about the FORM of the View component.
|
|
96
|
+
|
|
97
|
+
The requirement is an arrow function with an expression body. A function
|
|
98
|
+
declaration cannot have one, so declaration form is banned by construction
|
|
99
|
+
rather than by scanning for the word "function" — which would fire on JSDoc
|
|
100
|
+
prose, on local render helpers, and on disable-comment justifications, none
|
|
101
|
+
of which are the View component.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
component_name: The component directory's name, without the View suffix.
|
|
105
|
+
view_content: The View file's text.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Zero or one error message.
|
|
109
|
+
"""
|
|
110
|
+
name = f"{component_name}View"
|
|
111
|
+
expected = f"const {name} = (props) => (...)"
|
|
112
|
+
|
|
113
|
+
if re.search(rf"\bfunction\s+{re.escape(name)}\b", view_content):
|
|
114
|
+
return [
|
|
115
|
+
f"View must be an arrow function with an expression body: {expected}. "
|
|
116
|
+
"A function declaration cannot have one, so it always carries a statement list."
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
function_expression = re.search(
|
|
120
|
+
rf"\bconst\s+{re.escape(name)}\b[^=]*=\s*function\b", view_content
|
|
121
|
+
)
|
|
122
|
+
if function_expression:
|
|
123
|
+
return [
|
|
124
|
+
f"View must be an arrow function with an expression body: {expected}. "
|
|
125
|
+
"A function expression cannot have one."
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
binding = re.search(rf"\bconst\s+{re.escape(name)}\b", view_content)
|
|
129
|
+
if binding and _arrow_body_is_block(view_content, binding.end()):
|
|
130
|
+
return [
|
|
131
|
+
"View should use arrow function shorthand: () => (...) instead of () => { return (...) }"
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def validate_component(component_path: str) -> tuple[bool, list[str], list[str]]:
|
|
29
138
|
"""
|
|
30
139
|
Validate a component directory follows Container/View pattern.
|
|
31
140
|
|
|
141
|
+
Errors and advisories are separated because one check here cannot decide
|
|
142
|
+
what it reports on — see the Container JSX scan below. A check that cannot
|
|
143
|
+
tell a violation from a legitimate shape must say so rather than fail the
|
|
144
|
+
run; a confident wrong verdict is worse than a flagged uncertainty.
|
|
145
|
+
|
|
32
146
|
Args:
|
|
33
147
|
component_path: Path to the component directory
|
|
34
148
|
|
|
35
149
|
Returns:
|
|
36
|
-
Tuple of (is_valid, list of error messages)
|
|
150
|
+
Tuple of (is_valid, list of error messages, list of advisory messages)
|
|
37
151
|
"""
|
|
38
152
|
errors = []
|
|
153
|
+
advisories = []
|
|
39
154
|
component_dir = Path(component_path)
|
|
40
155
|
|
|
41
156
|
if not component_dir.is_dir():
|
|
42
|
-
return False, [f"Path is not a directory: {component_path}"]
|
|
157
|
+
return False, [f"Path is not a directory: {component_path}"], advisories
|
|
43
158
|
|
|
44
159
|
component_name = component_dir.name
|
|
45
160
|
|
|
@@ -92,39 +207,24 @@ def validate_component(component_path: str) -> tuple[bool, list[str]]:
|
|
|
92
207
|
if not re.search(display_name_pattern, view_content):
|
|
93
208
|
errors.append(f"View should have displayName: {component_name}View.displayName = \"{component_name}View\"")
|
|
94
209
|
|
|
95
|
-
# Check
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
# Exclude memo import check
|
|
114
|
-
if hook_pattern == r"\buse[A-Z]\w+\s*\(":
|
|
115
|
-
# More specific check to avoid false positives
|
|
116
|
-
if re.search(r"\buseState\s*\(", view_content):
|
|
117
|
-
errors.append("View should not contain useState - move to Container")
|
|
118
|
-
break
|
|
119
|
-
if re.search(r"\buseEffect\s*\(", view_content):
|
|
120
|
-
errors.append("View should not contain useEffect - move to Container")
|
|
121
|
-
break
|
|
122
|
-
if re.search(r"\buseMemo\s*\(", view_content):
|
|
123
|
-
errors.append("View should not contain useMemo - move to Container")
|
|
124
|
-
break
|
|
125
|
-
if re.search(r"\buseCallback\s*\(", view_content):
|
|
126
|
-
errors.append("View should not contain useCallback - move to Container")
|
|
127
|
-
break
|
|
210
|
+
# Check the View component's form.
|
|
211
|
+
#
|
|
212
|
+
# The old check was `const {name}View = ([^)]*) => {` — arrow-only, and
|
|
213
|
+
# therefore blind to `function {name}View(...) {`, independently
|
|
214
|
+
# reproducing the exact defect the ESLint rule carried. It also broke on
|
|
215
|
+
# any parameter list containing a nested paren.
|
|
216
|
+
errors.extend(view_form_errors(component_name, view_content))
|
|
217
|
+
|
|
218
|
+
# Check for hooks in View (they should be in Container).
|
|
219
|
+
#
|
|
220
|
+
# One generic shape match, deliberately. The previous version listed
|
|
221
|
+
# `\buse[A-Z]\w+\s*\(` first and then narrowed to four hardcoded names
|
|
222
|
+
# and `break`ed, so the general pattern was UNREACHABLE — dead code that
|
|
223
|
+
# read as coverage. A project-local hook such as
|
|
224
|
+
# `useCreateNoteQuickActionEnabled()` matched the pattern that was never
|
|
225
|
+
# evaluated and none of the four that were.
|
|
226
|
+
for hook in sorted(set(HOOK_CALL.findall(view_content))):
|
|
227
|
+
errors.append(f"View should not contain {hook} - move to Container")
|
|
128
228
|
|
|
129
229
|
# Validate Container file
|
|
130
230
|
if container_file.exists():
|
|
@@ -140,14 +240,31 @@ def validate_component(component_path: str) -> tuple[bool, list[str]]:
|
|
|
140
240
|
if not re.search(return_view_pattern, container_content):
|
|
141
241
|
errors.append(f"Container should return <{component_name}View />")
|
|
142
242
|
|
|
143
|
-
#
|
|
144
|
-
#
|
|
243
|
+
# ADVISORY, not an error — and deliberately so.
|
|
244
|
+
#
|
|
245
|
+
# "Container should ONLY render View" is a naive `<([A-Z]\w*)` scan over
|
|
246
|
+
# the ENTIRE container file. It cannot tell a component's returned JSX
|
|
247
|
+
# tree from JSX inside a callback, so a Container holding a perfectly
|
|
248
|
+
# legitimate `renderItem` — `useCallback(({item}) => <Row … />, […])` —
|
|
249
|
+
# trips it.
|
|
250
|
+
#
|
|
251
|
+
# That matters now that Views may not call hooks: moving a renderItem
|
|
252
|
+
# callback to the Container is one of the two sanctioned fixes, and
|
|
253
|
+
# Containers ARE allowed to hold logic, including a callback that
|
|
254
|
+
# returns JSX. Left as an error, this check would reject the very
|
|
255
|
+
# migration the View rules ask for.
|
|
256
|
+
#
|
|
257
|
+
# So it reports and does not fail. Scoping it to the returned tree needs
|
|
258
|
+
# a parser, which a regex pre-check is not; until then, saying plainly
|
|
259
|
+
# what it cannot distinguish beats a confident wrong verdict.
|
|
145
260
|
all_jsx_pattern = r"<([A-Z][a-zA-Z0-9]*)"
|
|
146
261
|
jsx_matches = re.findall(all_jsx_pattern, container_content)
|
|
147
262
|
other_components = [m for m in jsx_matches if m != f"{component_name}View"]
|
|
148
263
|
if other_components:
|
|
149
|
-
|
|
150
|
-
f"Container
|
|
264
|
+
advisories.append(
|
|
265
|
+
f"Container renders JSX besides {component_name}View: {', '.join(sorted(set(other_components)))}. "
|
|
266
|
+
"This is a whole-file text scan and cannot tell a returned JSX tree from JSX inside a callback, "
|
|
267
|
+
"so a legitimate renderItem callback lands here too. Review it; this does not fail validation."
|
|
151
268
|
)
|
|
152
269
|
|
|
153
270
|
# Check for extra files
|
|
@@ -178,7 +295,7 @@ def validate_component(component_path: str) -> tuple[bool, list[str]]:
|
|
|
178
295
|
if not is_allowed and item.is_file():
|
|
179
296
|
errors.append(f"Unexpected file in component directory: {item.name}")
|
|
180
297
|
|
|
181
|
-
return len(errors) == 0, errors
|
|
298
|
+
return len(errors) == 0, errors, advisories
|
|
182
299
|
|
|
183
300
|
|
|
184
301
|
def main():
|
|
@@ -193,7 +310,13 @@ def main():
|
|
|
193
310
|
print(f"Validating component: {component_path}")
|
|
194
311
|
print("-" * 50)
|
|
195
312
|
|
|
196
|
-
is_valid, errors = validate_component(component_path)
|
|
313
|
+
is_valid, errors, advisories = validate_component(component_path)
|
|
314
|
+
|
|
315
|
+
if advisories:
|
|
316
|
+
print("Advisories (reviewed by a human, not a failure):")
|
|
317
|
+
for advisory in advisories:
|
|
318
|
+
print(f" ~ {advisory}")
|
|
319
|
+
print("-" * 50)
|
|
197
320
|
|
|
198
321
|
if is_valid:
|
|
199
322
|
print("Component follows Container/View pattern")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.33.
|
|
3
|
+
"version": "4.33.6",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.33.
|
|
3
|
+
"version": "4.33.6",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.33.
|
|
3
|
+
"version": "4.33.6",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.33.
|
|
3
|
+
"version": "4.33.6",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.33.
|
|
3
|
+
"version": "4.33.6",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|