@purpleraven/hits 0.2.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/AGENTS.md +298 -0
- package/LICENSE +190 -0
- package/README.md +336 -0
- package/bin/hits.js +56 -0
- package/config/schema.json +94 -0
- package/config/settings.yaml +102 -0
- package/data/dev_handover.yaml +143 -0
- package/hits_core/__init__.py +9 -0
- package/hits_core/ai/__init__.py +11 -0
- package/hits_core/ai/compressor.py +86 -0
- package/hits_core/ai/llm_client.py +65 -0
- package/hits_core/ai/slm_filter.py +126 -0
- package/hits_core/api/__init__.py +3 -0
- package/hits_core/api/routes/__init__.py +8 -0
- package/hits_core/api/routes/auth.py +211 -0
- package/hits_core/api/routes/handover.py +117 -0
- package/hits_core/api/routes/health.py +8 -0
- package/hits_core/api/routes/knowledge.py +177 -0
- package/hits_core/api/routes/node.py +121 -0
- package/hits_core/api/routes/work_log.py +174 -0
- package/hits_core/api/server.py +181 -0
- package/hits_core/auth/__init__.py +21 -0
- package/hits_core/auth/dependencies.py +61 -0
- package/hits_core/auth/manager.py +368 -0
- package/hits_core/auth/middleware.py +69 -0
- package/hits_core/collector/__init__.py +18 -0
- package/hits_core/collector/ai_session_collector.py +118 -0
- package/hits_core/collector/base.py +73 -0
- package/hits_core/collector/daemon.py +94 -0
- package/hits_core/collector/git_collector.py +177 -0
- package/hits_core/collector/hits_action_collector.py +110 -0
- package/hits_core/collector/shell_collector.py +178 -0
- package/hits_core/main.py +36 -0
- package/hits_core/mcp/__init__.py +20 -0
- package/hits_core/mcp/server.py +429 -0
- package/hits_core/models/__init__.py +18 -0
- package/hits_core/models/node.py +56 -0
- package/hits_core/models/tree.py +68 -0
- package/hits_core/models/work_log.py +64 -0
- package/hits_core/models/workflow.py +92 -0
- package/hits_core/platform/__init__.py +5 -0
- package/hits_core/platform/actions.py +225 -0
- package/hits_core/service/__init__.py +6 -0
- package/hits_core/service/handover_service.py +382 -0
- package/hits_core/service/knowledge_service.py +172 -0
- package/hits_core/service/tree_service.py +105 -0
- package/hits_core/storage/__init__.py +11 -0
- package/hits_core/storage/base.py +84 -0
- package/hits_core/storage/file_store.py +314 -0
- package/hits_core/storage/redis_store.py +123 -0
- package/hits_web/dist/assets/index-Bgx7F6m6.css +1 -0
- package/hits_web/dist/assets/index-D1B5E67G.js +3 -0
- package/hits_web/dist/index.html +16 -0
- package/package.json +60 -0
- package/requirements-core.txt +7 -0
- package/requirements.txt +1 -0
- package/run.sh +271 -0
- package/server.js +234 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Node entity model for knowledge tree."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NodeLayer(str, Enum):
|
|
10
|
+
WHY = "why"
|
|
11
|
+
HOW = "how"
|
|
12
|
+
WHAT = "what"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NodeType(str, Enum):
|
|
16
|
+
STANDARD = "standard"
|
|
17
|
+
NEGATIVE_PATH = "negative_path"
|
|
18
|
+
DECISION = "decision"
|
|
19
|
+
ACTION = "action"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Node(BaseModel):
|
|
23
|
+
id: str = Field(..., description="Unique node identifier")
|
|
24
|
+
layer: NodeLayer = Field(..., description="Tree layer (why/how/what)")
|
|
25
|
+
title: str = Field(..., description="Node display title")
|
|
26
|
+
description: Optional[str] = Field(default=None, description="Detailed description")
|
|
27
|
+
node_type: NodeType = Field(default=NodeType.STANDARD, description="Node type")
|
|
28
|
+
|
|
29
|
+
parent_id: Optional[str] = Field(default=None, description="Parent node ID")
|
|
30
|
+
children_ids: list[str] = Field(default_factory=list, description="Child node IDs")
|
|
31
|
+
|
|
32
|
+
action: Optional[str] = Field(default=None, description="Executable action (URL, command, etc.)")
|
|
33
|
+
action_type: Optional[str] = Field(default=None, description="Action type: url, shell, app")
|
|
34
|
+
|
|
35
|
+
metadata: dict = Field(default_factory=dict, description="Additional metadata")
|
|
36
|
+
tokens_saved: int = Field(default=0, description="Estimated tokens saved by compression")
|
|
37
|
+
|
|
38
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
39
|
+
updated_at: datetime = Field(default_factory=datetime.now)
|
|
40
|
+
|
|
41
|
+
class Config:
|
|
42
|
+
use_enum_values = True
|
|
43
|
+
|
|
44
|
+
def is_root(self) -> bool:
|
|
45
|
+
return self.parent_id is None
|
|
46
|
+
|
|
47
|
+
def is_negative_path(self) -> bool:
|
|
48
|
+
return self.node_type == NodeType.NEGATIVE_PATH
|
|
49
|
+
|
|
50
|
+
def add_child(self, child_id: str) -> None:
|
|
51
|
+
if child_id not in self.children_ids:
|
|
52
|
+
self.children_ids.append(child_id)
|
|
53
|
+
|
|
54
|
+
def remove_child(self, child_id: str) -> None:
|
|
55
|
+
if child_id in self.children_ids:
|
|
56
|
+
self.children_ids.remove(child_id)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Knowledge tree structure with Why-How-What hierarchy."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from .node import Node, NodeLayer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class KnowledgeTree(BaseModel):
|
|
10
|
+
id: str = Field(..., description="Tree unique identifier")
|
|
11
|
+
name: str = Field(..., description="Tree name")
|
|
12
|
+
description: Optional[str] = Field(default=None, description="Tree description")
|
|
13
|
+
|
|
14
|
+
root_ids: list[str] = Field(default_factory=list, description="Root node IDs (WHY layer)")
|
|
15
|
+
nodes: dict[str, Node] = Field(default_factory=dict, description="All nodes by ID")
|
|
16
|
+
|
|
17
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
18
|
+
updated_at: datetime = Field(default_factory=datetime.now)
|
|
19
|
+
|
|
20
|
+
def add_node(self, node: Node) -> None:
|
|
21
|
+
self.nodes[node.id] = node
|
|
22
|
+
if node.is_root() and node.id not in self.root_ids:
|
|
23
|
+
self.root_ids.append(node.id)
|
|
24
|
+
elif node.parent_id and node.parent_id in self.nodes:
|
|
25
|
+
self.nodes[node.parent_id].add_child(node.id)
|
|
26
|
+
|
|
27
|
+
def remove_node(self, node_id: str) -> Optional[Node]:
|
|
28
|
+
if node_id not in self.nodes:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
node = self.nodes.pop(node_id)
|
|
32
|
+
|
|
33
|
+
if node_id in self.root_ids:
|
|
34
|
+
self.root_ids.remove(node_id)
|
|
35
|
+
|
|
36
|
+
if node.parent_id and node.parent_id in self.nodes:
|
|
37
|
+
self.nodes[node.parent_id].remove_child(node_id)
|
|
38
|
+
|
|
39
|
+
for child_id in node.children_ids[:]:
|
|
40
|
+
self.remove_node(child_id)
|
|
41
|
+
|
|
42
|
+
return node
|
|
43
|
+
|
|
44
|
+
def get_node(self, node_id: str) -> Optional[Node]:
|
|
45
|
+
return self.nodes.get(node_id)
|
|
46
|
+
|
|
47
|
+
def get_children(self, node_id: str) -> list[Node]:
|
|
48
|
+
node = self.get_node(node_id)
|
|
49
|
+
if not node:
|
|
50
|
+
return []
|
|
51
|
+
return [self.nodes[cid] for cid in node.children_ids if cid in self.nodes]
|
|
52
|
+
|
|
53
|
+
def get_path(self, node_id: str) -> list[Node]:
|
|
54
|
+
path = []
|
|
55
|
+
current = self.get_node(node_id)
|
|
56
|
+
while current:
|
|
57
|
+
path.insert(0, current)
|
|
58
|
+
current = self.get_node(current.parent_id) if current.parent_id else None
|
|
59
|
+
return path
|
|
60
|
+
|
|
61
|
+
def get_nodes_by_layer(self, layer: NodeLayer) -> list[Node]:
|
|
62
|
+
return [n for n in self.nodes.values() if n.layer == layer]
|
|
63
|
+
|
|
64
|
+
def get_negative_paths(self) -> list[Node]:
|
|
65
|
+
return [n for n in self.nodes.values() if n.is_negative_path()]
|
|
66
|
+
|
|
67
|
+
def total_tokens_saved(self) -> int:
|
|
68
|
+
return sum(n.tokens_saved for n in self.nodes.values())
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Work log model for recording user activities."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WorkLogSource(str, Enum):
|
|
10
|
+
GIT = "git"
|
|
11
|
+
SHELL = "shell"
|
|
12
|
+
LINK_CLICK = "link_click"
|
|
13
|
+
SHELL_EXEC = "shell_exec"
|
|
14
|
+
AI_SESSION = "ai_session"
|
|
15
|
+
MANUAL = "manual"
|
|
16
|
+
BROWSER_HISTORY = "browser_history"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WorkLogResultType(str, Enum):
|
|
20
|
+
COMMIT = "commit"
|
|
21
|
+
PR = "pr"
|
|
22
|
+
FILE = "file"
|
|
23
|
+
URL = "url"
|
|
24
|
+
COMMAND = "command"
|
|
25
|
+
NONE = "none"
|
|
26
|
+
AI_RESPONSE = "ai_response"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class WorkLog(BaseModel):
|
|
30
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
31
|
+
|
|
32
|
+
id: str = Field(..., description="Unique log identifier")
|
|
33
|
+
|
|
34
|
+
source: WorkLogSource = Field(..., description="Where this log came from")
|
|
35
|
+
|
|
36
|
+
request_text: Optional[str] = Field(default=None, description="Original request/prompt")
|
|
37
|
+
request_by: Optional[str] = Field(default=None, description="Who made the request")
|
|
38
|
+
|
|
39
|
+
performed_by: str = Field(..., description="Who/what performed the action")
|
|
40
|
+
performed_at: datetime = Field(default_factory=datetime.now)
|
|
41
|
+
|
|
42
|
+
result_type: WorkLogResultType = Field(default=WorkLogResultType.NONE)
|
|
43
|
+
result_ref: Optional[str] = Field(default=None, description="Reference to result (commit hash, URL, etc)")
|
|
44
|
+
result_data: Optional[dict] = Field(default=None, description="Additional result metadata")
|
|
45
|
+
|
|
46
|
+
context: Optional[str] = Field(default=None, description="Why this action was taken")
|
|
47
|
+
tags: list[str] = Field(default_factory=list, description="Tags for search")
|
|
48
|
+
|
|
49
|
+
project_path: Optional[str] = Field(default=None, description="Project directory if applicable")
|
|
50
|
+
|
|
51
|
+
node_id: Optional[str] = Field(default=None, description="Linked knowledge node ID")
|
|
52
|
+
category: Optional[str] = Field(default=None, description="Category name from HITS UI")
|
|
53
|
+
|
|
54
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
55
|
+
|
|
56
|
+
def has_result(self) -> bool:
|
|
57
|
+
return self.result_type != WorkLogResultType.NONE and self.result_ref is not None
|
|
58
|
+
|
|
59
|
+
def get_summary(self) -> str:
|
|
60
|
+
if self.request_text:
|
|
61
|
+
return self.request_text[:100]
|
|
62
|
+
if self.result_ref:
|
|
63
|
+
return f"[{self.result_type}] {self.result_ref[:50]}"
|
|
64
|
+
return f"[{self.source}] {self.performed_by}"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Workflow model for causal relationships between nodes."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StepType(str, Enum):
|
|
10
|
+
TRIGGER = "trigger"
|
|
11
|
+
DECISION = "decision"
|
|
12
|
+
ACTION = "action"
|
|
13
|
+
OUTCOME = "outcome"
|
|
14
|
+
FAILURE = "failure"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WorkflowStep(BaseModel):
|
|
18
|
+
id: str = Field(..., description="Step unique identifier")
|
|
19
|
+
name: str = Field(..., description="Step name")
|
|
20
|
+
step_type: StepType = Field(default=StepType.ACTION, description="Step type")
|
|
21
|
+
node_id: Optional[str] = Field(default=None, description="Linked knowledge node ID")
|
|
22
|
+
description: Optional[str] = Field(default=None, description="Step description")
|
|
23
|
+
|
|
24
|
+
next_steps: list[str] = Field(default_factory=list, description="Next step IDs")
|
|
25
|
+
condition: Optional[str] = Field(default=None, description="Condition for branching")
|
|
26
|
+
|
|
27
|
+
estimated_tokens: int = Field(default=0, description="Estimated tokens for this step")
|
|
28
|
+
|
|
29
|
+
class Config:
|
|
30
|
+
use_enum_values = True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Workflow(BaseModel):
|
|
34
|
+
id: str = Field(..., description="Workflow unique identifier")
|
|
35
|
+
name: str = Field(..., description="Workflow name")
|
|
36
|
+
description: Optional[str] = Field(default=None, description="Workflow description")
|
|
37
|
+
|
|
38
|
+
entry_step_id: Optional[str] = Field(default=None, description="Entry point step ID")
|
|
39
|
+
steps: dict[str, WorkflowStep] = Field(default_factory=dict, description="All steps by ID")
|
|
40
|
+
|
|
41
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
42
|
+
updated_at: datetime = Field(default_factory=datetime.now)
|
|
43
|
+
|
|
44
|
+
def add_step(self, step: WorkflowStep, is_entry: bool = False) -> None:
|
|
45
|
+
self.steps[step.id] = step
|
|
46
|
+
if is_entry or not self.entry_step_id:
|
|
47
|
+
self.entry_step_id = step.id
|
|
48
|
+
|
|
49
|
+
def remove_step(self, step_id: str) -> Optional[WorkflowStep]:
|
|
50
|
+
if step_id not in self.steps:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
step = self.steps.pop(step_id)
|
|
54
|
+
|
|
55
|
+
if self.entry_step_id == step_id:
|
|
56
|
+
self.entry_step_id = None
|
|
57
|
+
|
|
58
|
+
for s in self.steps.values():
|
|
59
|
+
if step_id in s.next_steps:
|
|
60
|
+
s.next_steps.remove(step_id)
|
|
61
|
+
|
|
62
|
+
return step
|
|
63
|
+
|
|
64
|
+
def get_step(self, step_id: str) -> Optional[WorkflowStep]:
|
|
65
|
+
return self.steps.get(step_id)
|
|
66
|
+
|
|
67
|
+
def get_next_steps(self, step_id: str) -> list[WorkflowStep]:
|
|
68
|
+
step = self.get_step(step_id)
|
|
69
|
+
if not step:
|
|
70
|
+
return []
|
|
71
|
+
return [self.steps[sid] for sid in step.next_steps if sid in self.steps]
|
|
72
|
+
|
|
73
|
+
def get_execution_order(self) -> list[WorkflowStep]:
|
|
74
|
+
if not self.entry_step_id:
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
visited = set()
|
|
78
|
+
order = []
|
|
79
|
+
|
|
80
|
+
def dfs(step_id: str):
|
|
81
|
+
if step_id in visited or step_id not in self.steps:
|
|
82
|
+
return
|
|
83
|
+
visited.add(step_id)
|
|
84
|
+
order.append(self.steps[step_id])
|
|
85
|
+
for next_id in self.steps[step_id].next_steps:
|
|
86
|
+
dfs(next_id)
|
|
87
|
+
|
|
88
|
+
dfs(self.entry_step_id)
|
|
89
|
+
return order
|
|
90
|
+
|
|
91
|
+
def total_estimated_tokens(self) -> int:
|
|
92
|
+
return sum(s.estimated_tokens for s in self.steps.values())
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Cross-platform utilities without GUI dependencies."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import webbrowser
|
|
7
|
+
from typing import Optional, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_wsl() -> bool:
|
|
11
|
+
"""Detect if running under Windows Subsystem for Linux."""
|
|
12
|
+
if sys.platform != "linux":
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
with open("/proc/version", "r") as f:
|
|
17
|
+
version = f.read().lower()
|
|
18
|
+
return "microsoft" in version or "wsl" in version
|
|
19
|
+
except Exception:
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def detect_terminal_emulator() -> Optional[str]:
|
|
24
|
+
"""Detect available terminal emulator on Linux."""
|
|
25
|
+
terminals = [
|
|
26
|
+
("wt", "wt.exe"), # Windows Terminal via WSL
|
|
27
|
+
("gnome-terminal", "gnome-terminal"),
|
|
28
|
+
("konsole", "konsole"),
|
|
29
|
+
("xfce4-terminal", "xfce4-terminal"),
|
|
30
|
+
("xterm", "xterm"),
|
|
31
|
+
("alacritty", "alacritty"),
|
|
32
|
+
("kitty", "kitty"),
|
|
33
|
+
("terminator", "terminator"),
|
|
34
|
+
("urxvt", "urxvt"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
for name, cmd in terminals:
|
|
38
|
+
if cmd == "wt.exe" and is_wsl():
|
|
39
|
+
try:
|
|
40
|
+
subprocess.run(["which", "wt.exe"], capture_output=True, check=True)
|
|
41
|
+
return "wt"
|
|
42
|
+
except Exception:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
subprocess.run(["which", cmd], capture_output=True, check=True)
|
|
47
|
+
return name
|
|
48
|
+
except Exception:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_wsl_windows_path(linux_path: str) -> str:
|
|
55
|
+
"""Convert Linux path to Windows path for WSL."""
|
|
56
|
+
if not is_wsl():
|
|
57
|
+
return linux_path
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
result = subprocess.run(
|
|
61
|
+
["wslpath", "-w", linux_path],
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
check=True
|
|
65
|
+
)
|
|
66
|
+
return result.stdout.strip()
|
|
67
|
+
except Exception:
|
|
68
|
+
return linux_path
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class PlatformAction:
|
|
72
|
+
_is_wsl = is_wsl()
|
|
73
|
+
_terminal_cache: Optional[str] = None
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def _get_terminal(cls) -> Optional[str]:
|
|
77
|
+
if cls._terminal_cache is None:
|
|
78
|
+
cls._terminal_cache = detect_terminal_emulator()
|
|
79
|
+
return cls._terminal_cache
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def open_url(url: str) -> bool:
|
|
83
|
+
"""Open URL in default browser - works on all platforms."""
|
|
84
|
+
try:
|
|
85
|
+
if is_wsl():
|
|
86
|
+
try:
|
|
87
|
+
win_url = get_wsl_windows_path(url) if url.startswith("/") else url
|
|
88
|
+
subprocess.run(["cmd.exe", "/c", "start", win_url], check=True)
|
|
89
|
+
return True
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
webbrowser.open(url)
|
|
94
|
+
return True
|
|
95
|
+
except Exception:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def run_shell(cls, command: str, terminal: bool = True) -> bool:
|
|
100
|
+
"""Execute shell command, optionally in a new terminal."""
|
|
101
|
+
try:
|
|
102
|
+
if not terminal:
|
|
103
|
+
subprocess.Popen(command, shell=True)
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
if sys.platform == "win32":
|
|
107
|
+
return cls._run_shell_windows(command)
|
|
108
|
+
elif sys.platform == "darwin":
|
|
109
|
+
return cls._run_shell_macos(command)
|
|
110
|
+
else:
|
|
111
|
+
return cls._run_shell_linux(command)
|
|
112
|
+
except Exception:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def _run_shell_windows(cls, command: str) -> bool:
|
|
117
|
+
"""Run shell command on Windows."""
|
|
118
|
+
term = os.environ.get("TERM_PROGRAM", "")
|
|
119
|
+
|
|
120
|
+
if term == "vscode" or "WT_SESSION" in os.environ:
|
|
121
|
+
subprocess.Popen(["wt", "cmd", "/k", command])
|
|
122
|
+
else:
|
|
123
|
+
subprocess.Popen(["cmd", "/c", "start", "cmd", "/k", command])
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def _run_shell_macos(cls, command: str) -> bool:
|
|
128
|
+
"""Run shell command on macOS."""
|
|
129
|
+
script = f'tell application "Terminal" to do script "{command}"'
|
|
130
|
+
subprocess.Popen(["osascript", "-e", script])
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def _run_shell_linux(cls, command: str) -> bool:
|
|
135
|
+
"""Run shell command on Linux (including WSL)."""
|
|
136
|
+
if cls._is_wsl:
|
|
137
|
+
try:
|
|
138
|
+
subprocess.run(["which", "wt.exe"], capture_output=True, check=True)
|
|
139
|
+
subprocess.Popen([
|
|
140
|
+
"wt.exe", "-d", ".",
|
|
141
|
+
"bash", "-c", f"{command}; echo; read -p 'Press Enter to close...'"
|
|
142
|
+
])
|
|
143
|
+
return True
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
terminal = cls._get_terminal()
|
|
148
|
+
|
|
149
|
+
if terminal == "gnome-terminal":
|
|
150
|
+
subprocess.Popen([
|
|
151
|
+
"gnome-terminal", "--", "bash", "-c",
|
|
152
|
+
f"{command}; echo; read -p 'Press Enter to close...'"
|
|
153
|
+
])
|
|
154
|
+
elif terminal == "konsole":
|
|
155
|
+
subprocess.Popen([
|
|
156
|
+
"konsole", "-e", "bash", "-c",
|
|
157
|
+
f"{command}; echo; read -p 'Press Enter to close...'"
|
|
158
|
+
])
|
|
159
|
+
elif terminal == "xfce4-terminal":
|
|
160
|
+
subprocess.Popen([
|
|
161
|
+
"xfce4-terminal", "-e", f"bash -c '{command}; echo; read -p \"Press Enter to close...\"'"
|
|
162
|
+
])
|
|
163
|
+
elif terminal == "alacritty":
|
|
164
|
+
subprocess.Popen([
|
|
165
|
+
"alacritty", "-e", "bash", "-c",
|
|
166
|
+
f"{command}; echo; read -p 'Press Enter to close...'"
|
|
167
|
+
])
|
|
168
|
+
elif terminal == "kitty":
|
|
169
|
+
subprocess.Popen([
|
|
170
|
+
"kitty", "bash", "-c",
|
|
171
|
+
f"{command}; echo; read -p 'Press Enter to close...'"
|
|
172
|
+
])
|
|
173
|
+
elif terminal in ("xterm", "urxvt"):
|
|
174
|
+
subprocess.Popen([
|
|
175
|
+
terminal, "-e", "bash", "-c",
|
|
176
|
+
f"{command}; echo; read -p 'Press Enter to close...'"
|
|
177
|
+
])
|
|
178
|
+
else:
|
|
179
|
+
subprocess.Popen([
|
|
180
|
+
"x-terminal-emulator", "-e", "bash", "-c",
|
|
181
|
+
f"{command}; echo; read -p 'Press Enter to close...'"
|
|
182
|
+
])
|
|
183
|
+
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def launch_app(cls, app_path: str) -> bool:
|
|
188
|
+
"""Launch an application."""
|
|
189
|
+
try:
|
|
190
|
+
if sys.platform == "win32":
|
|
191
|
+
subprocess.Popen(["cmd", "/c", "start", "", app_path], shell=True)
|
|
192
|
+
elif sys.platform == "darwin":
|
|
193
|
+
subprocess.Popen(["open", app_path])
|
|
194
|
+
elif cls._is_wsl:
|
|
195
|
+
win_path = get_wsl_windows_path(app_path)
|
|
196
|
+
subprocess.Popen(["cmd.exe", "/c", "start", "", win_path])
|
|
197
|
+
else:
|
|
198
|
+
subprocess.Popen([app_path])
|
|
199
|
+
return True
|
|
200
|
+
except Exception:
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def execute(action_type: str, action: str) -> bool:
|
|
205
|
+
"""Execute action based on type."""
|
|
206
|
+
action_type = action_type.lower().strip()
|
|
207
|
+
|
|
208
|
+
if action_type == "url":
|
|
209
|
+
return PlatformAction.open_url(action)
|
|
210
|
+
elif action_type == "shell":
|
|
211
|
+
return PlatformAction.run_shell(action)
|
|
212
|
+
elif action_type == "app":
|
|
213
|
+
return PlatformAction.launch_app(action)
|
|
214
|
+
else:
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_platform_info() -> dict:
|
|
219
|
+
"""Get current platform information."""
|
|
220
|
+
return {
|
|
221
|
+
"system": sys.platform,
|
|
222
|
+
"is_wsl": is_wsl(),
|
|
223
|
+
"terminal": detect_terminal_emulator(),
|
|
224
|
+
"python": sys.version,
|
|
225
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Business services for tree and workflow management."""
|
|
2
|
+
|
|
3
|
+
from .tree_service import TreeService
|
|
4
|
+
from .knowledge_service import KnowledgeService, KnowledgeNode, KnowledgeCategory
|
|
5
|
+
|
|
6
|
+
__all__ = ["TreeService", "KnowledgeService", "KnowledgeNode", "KnowledgeCategory"]
|