oomi-ai 0.2.17 → 0.2.19

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 (47) hide show
  1. package/README.md +243 -200
  2. package/agent_instructions.md +214 -184
  3. package/bin/oomi-ai.js +4024 -3441
  4. package/bin/sessionBridgeState.js +78 -78
  5. package/lib/channelPluginClient.js +119 -0
  6. package/lib/personaApiClient.js +253 -0
  7. package/lib/personaJobExecutor.js +115 -0
  8. package/lib/personaJobPoller.js +112 -0
  9. package/lib/personaRuntimeProcess.js +152 -0
  10. package/lib/scaffold.js +108 -0
  11. package/lib/template.js +45 -0
  12. package/openclaw.extension.js +602 -602
  13. package/openclaw.plugin.json +17 -17
  14. package/package.json +67 -65
  15. package/skills/oomi/SKILL.md +191 -191
  16. package/skills/oomi/agent_instructions.md +80 -80
  17. package/skills/oomi/config.json +2 -2
  18. package/skills/oomi/scripts/get_avatar_capabilities.py +40 -40
  19. package/skills/oomi/scripts/get_data.py +49 -49
  20. package/skills/oomi/scripts/install_agent_instructions.py +78 -78
  21. package/skills/oomi/scripts/send_goal.py +53 -53
  22. package/skills/oomi/scripts/sync.py +46 -46
  23. package/skills/oomi/setup.py +41 -41
  24. package/templates/persona-app/.env.example +8 -0
  25. package/templates/persona-app/README.md +35 -0
  26. package/templates/persona-app/eslint.config.js +28 -0
  27. package/templates/persona-app/index.html +18 -0
  28. package/templates/persona-app/oomi.runtime.json +13 -0
  29. package/templates/persona-app/package.json +42 -0
  30. package/templates/persona-app/persona/brief.md +14 -0
  31. package/templates/persona-app/persona.json +14 -0
  32. package/templates/persona-app/public/manifest.webmanifest +8 -0
  33. package/templates/persona-app/public/oomi.health.json +6 -0
  34. package/templates/persona-app/src/App.css +180 -0
  35. package/templates/persona-app/src/App.tsx +14 -0
  36. package/templates/persona-app/src/index.css +32 -0
  37. package/templates/persona-app/src/main.tsx +10 -0
  38. package/templates/persona-app/src/pages/HomePage.tsx +73 -0
  39. package/templates/persona-app/src/pages/ScenePage.tsx +18 -0
  40. package/templates/persona-app/src/persona/config.ts +6 -0
  41. package/templates/persona-app/src/persona/notes.ts +5 -0
  42. package/templates/persona-app/src/vite-env.d.ts +3 -0
  43. package/templates/persona-app/template.json +13 -0
  44. package/templates/persona-app/tsconfig.app.json +23 -0
  45. package/templates/persona-app/tsconfig.json +7 -0
  46. package/templates/persona-app/tsconfig.node.json +21 -0
  47. package/templates/persona-app/vite.config.ts +18 -0
@@ -1,80 +1,80 @@
1
- # Oomi Avatar Control
2
-
3
- Before using avatar commands, call `get_avatar_capabilities` and prefer canonical values.
4
- Inline tags are stripped from user-visible text.
5
-
6
- Use inline tags like:
7
- - [anim:Waving], [anim:Walking], [anim:Idle], [anim:Sitting Idle]
8
- - [face:happy], [face:sad], [face:surprised], [face:focused], [face:gentle], [face:thinking]
9
- - [gesture:nod], [gesture:think], [gesture:shrug], [gesture:wave], [gesture:bow]
10
- - [look:camera], [look:left], [look:right], [look:up], [look:down]
11
-
12
- Aliases allowed (if needed): wave -> Waving, walk -> Walking, idle -> Idle, sit/sitting -> Sitting Idle
13
-
14
- ## Oomi Connect Flow
15
-
16
- If the user asks to connect OpenClaw to Oomi voice/web:
17
-
18
- 1. Refresh bootstrap/update first:
19
- - `curl -fsSL https://www.oomi.ai/install.sh | bash`
20
- 2. Ensure `oomi-ai` is installed (`pnpm add -g oomi-ai@latest`).
21
- - Fallback: `npm install -g oomi-ai@latest`
22
- 3. Pair device token without starting legacy bridge:
23
- - `oomi openclaw pair --app-url https://www.oomi.ai --no-start`
24
- 4. Install OpenClaw plugin:
25
- - `openclaw plugins install oomi-ai@latest`
26
- 5. Configure channel account (`channels.oomi.accounts.default`) from:
27
- - `oomi openclaw plugin --show-secrets --backend-url https://api.oomi.ai`
28
- 6. Restart OpenClaw.
29
- 7. Ensure exactly one bridge worker is running (singleton handler):
30
- - `oomi openclaw bridge ensure --detach`
31
- - If stale/disconnected: `oomi openclaw bridge restart --detach`
32
- - On macOS, prefer supervised mode: `oomi openclaw bridge service install`
33
- 8. If user provides an app URL (for example "Connect yourself to Oomi. Use app URL <URL>."):
34
- - Use that URL directly in the pair command.
35
- - Example: `oomi openclaw pair --app-url <URL> --no-start`
36
- 9. Return this exact result format to the user:
37
- - `Oomi Connect Ready`
38
- - `Auth Link: ...`
39
- - No extra narrative text before or after those lines.
40
-
41
- Primary UX requirement:
42
- - Never ask the user to enter raw pairing identifiers.
43
- - The user should open the auth link, complete sign-in, and land directly in connected chat.
44
- - Return `/connect/<invite-token>` auth links only.
45
-
46
- Do not ask users to paste gateway IP/token/password when managed connect is available.
47
-
48
- ## Hidden Speech Payload
49
-
50
- For managed voice turns, keep visible assistant chat text natural and user-facing.
51
- Do not put spoken-style tags like `[happy]`, `[sad]`, or `[excited]` into visible chat text.
52
-
53
- When the runtime supports it, voice turns may include a hidden speech sidecar on the assistant message:
54
-
55
- ```json
56
- {
57
- "metadata": {
58
- "spoken": {
59
- "text": "Speech-optimized text for TTS only.",
60
- "instructions": "Speak with upbeat, warm excitement and slightly rising intonation.",
61
- "style": {
62
- "emotion": "excited",
63
- "energy": "medium_high"
64
- }
65
- }
66
- }
67
- }
68
- ```
69
-
70
- Rules:
71
- - visible `content` remains the source of truth for Oomi chat rendering
72
- - for managed voice replies, include `metadata.spoken` when delivery benefits from cleaner phrasing or explicit speaking guidance
73
- - `metadata.spoken.text` is for backend TTS only
74
- - `metadata.spoken.language` should be one of the supported Qwen language values such as `English`
75
- - `metadata.spoken.segments` can carry bounded per-segment prosody for pace, pitch, volume, and pause timing
76
- - `metadata.spoken.instructions` should be natural-language guidance, not raw bracket tags
77
- - `metadata.spoken.style` is optional metadata for debugging or future mapping
78
- - if no hidden speech sidecar exists, Oomi falls back to speaking the visible assistant text
79
- - if you omit `metadata.spoken`, the plugin now synthesizes a bounded hidden fallback from visible assistant text
80
- - visible chat text is never rewritten by the plugin
1
+ # Oomi Avatar Control
2
+
3
+ Before using avatar commands, call `get_avatar_capabilities` and prefer canonical values.
4
+ Inline tags are stripped from user-visible text.
5
+
6
+ Use inline tags like:
7
+ - [anim:Waving], [anim:Walking], [anim:Idle], [anim:Sitting Idle]
8
+ - [face:happy], [face:sad], [face:surprised], [face:focused], [face:gentle], [face:thinking]
9
+ - [gesture:nod], [gesture:think], [gesture:shrug], [gesture:wave], [gesture:bow]
10
+ - [look:camera], [look:left], [look:right], [look:up], [look:down]
11
+
12
+ Aliases allowed (if needed): wave -> Waving, walk -> Walking, idle -> Idle, sit/sitting -> Sitting Idle
13
+
14
+ ## Oomi Connect Flow
15
+
16
+ If the user asks to connect OpenClaw to Oomi voice/web:
17
+
18
+ 1. Refresh bootstrap/update first:
19
+ - `curl -fsSL https://www.oomi.ai/install.sh | bash`
20
+ 2. Ensure `oomi-ai` is installed (`pnpm add -g oomi-ai@latest`).
21
+ - Fallback: `npm install -g oomi-ai@latest`
22
+ 3. Pair device token without starting legacy bridge:
23
+ - `oomi openclaw pair --app-url https://www.oomi.ai --no-start`
24
+ 4. Install OpenClaw plugin:
25
+ - `openclaw plugins install oomi-ai@latest`
26
+ 5. Configure channel account (`channels.oomi.accounts.default`) from:
27
+ - `oomi openclaw plugin --show-secrets --backend-url https://api.oomi.ai`
28
+ 6. Restart OpenClaw.
29
+ 7. Ensure exactly one bridge worker is running (singleton handler):
30
+ - `oomi openclaw bridge ensure --detach`
31
+ - If stale/disconnected: `oomi openclaw bridge restart --detach`
32
+ - On macOS, prefer supervised mode: `oomi openclaw bridge service install`
33
+ 8. If user provides an app URL (for example "Connect yourself to Oomi. Use app URL <URL>."):
34
+ - Use that URL directly in the pair command.
35
+ - Example: `oomi openclaw pair --app-url <URL> --no-start`
36
+ 9. Return this exact result format to the user:
37
+ - `Oomi Connect Ready`
38
+ - `Auth Link: ...`
39
+ - No extra narrative text before or after those lines.
40
+
41
+ Primary UX requirement:
42
+ - Never ask the user to enter raw pairing identifiers.
43
+ - The user should open the auth link, complete sign-in, and land directly in connected chat.
44
+ - Return `/connect/<invite-token>` auth links only.
45
+
46
+ Do not ask users to paste gateway IP/token/password when managed connect is available.
47
+
48
+ ## Hidden Speech Payload
49
+
50
+ For managed voice turns, keep visible assistant chat text natural and user-facing.
51
+ Do not put spoken-style tags like `[happy]`, `[sad]`, or `[excited]` into visible chat text.
52
+
53
+ When the runtime supports it, voice turns may include a hidden speech sidecar on the assistant message:
54
+
55
+ ```json
56
+ {
57
+ "metadata": {
58
+ "spoken": {
59
+ "text": "Speech-optimized text for TTS only.",
60
+ "instructions": "Speak with upbeat, warm excitement and slightly rising intonation.",
61
+ "style": {
62
+ "emotion": "excited",
63
+ "energy": "medium_high"
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ Rules:
71
+ - visible `content` remains the source of truth for Oomi chat rendering
72
+ - for managed voice replies, include `metadata.spoken` when delivery benefits from cleaner phrasing or explicit speaking guidance
73
+ - `metadata.spoken.text` is for backend TTS only
74
+ - `metadata.spoken.language` should be one of the supported Qwen language values such as `English`
75
+ - `metadata.spoken.segments` can carry bounded per-segment prosody for pace, pitch, volume, and pause timing
76
+ - `metadata.spoken.instructions` should be natural-language guidance, not raw bracket tags
77
+ - `metadata.spoken.style` is optional metadata for debugging or future mapping
78
+ - if no hidden speech sidecar exists, Oomi falls back to speaking the visible assistant text
79
+ - if you omit `metadata.spoken`, the plugin now synthesizes a bounded hidden fallback from visible assistant text
80
+ - visible chat text is never rewritten by the plugin
@@ -1,3 +1,3 @@
1
- {
2
- "api_url": "http://localhost:3000/api/skill"
1
+ {
2
+ "api_url": "http://localhost:3000/api/skill"
3
3
  }
@@ -1,40 +1,40 @@
1
- import urllib.request
2
- import urllib.error
3
- import json
4
- import os
5
- import sys
6
-
7
- # Load config
8
- try:
9
- config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.json')
10
- with open(config_path, 'r') as f:
11
- config = json.load(f)
12
- BASE_URL = config.get('api_url', 'http://localhost:3000/api/skill')
13
- except:
14
- BASE_URL = 'http://localhost:3000/api/skill'
15
-
16
- API_URL = f"{BASE_URL}/avatar-capabilities"
17
-
18
- def get_avatar_capabilities():
19
- try:
20
- with urllib.request.urlopen(API_URL) as response:
21
- data = json.loads(response.read().decode())
22
-
23
- print(json.dumps({
24
- "summary": "Avatar capabilities retrieved.",
25
- "raw_data": data
26
- }, indent=2))
27
- except urllib.error.URLError as e:
28
- print(json.dumps({
29
- "error": f"Failed to connect to Oomi app: {str(e)}",
30
- "hint": "Is the Next.js app running on localhost:3000?"
31
- }))
32
- sys.exit(1)
33
- except Exception as e:
34
- print(json.dumps({
35
- "error": f"An error occurred: {str(e)}"
36
- }))
37
- sys.exit(1)
38
-
39
- if __name__ == "__main__":
40
- get_avatar_capabilities()
1
+ import urllib.request
2
+ import urllib.error
3
+ import json
4
+ import os
5
+ import sys
6
+
7
+ # Load config
8
+ try:
9
+ config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.json')
10
+ with open(config_path, 'r') as f:
11
+ config = json.load(f)
12
+ BASE_URL = config.get('api_url', 'http://localhost:3000/api/skill')
13
+ except:
14
+ BASE_URL = 'http://localhost:3000/api/skill'
15
+
16
+ API_URL = f"{BASE_URL}/avatar-capabilities"
17
+
18
+ def get_avatar_capabilities():
19
+ try:
20
+ with urllib.request.urlopen(API_URL) as response:
21
+ data = json.loads(response.read().decode())
22
+
23
+ print(json.dumps({
24
+ "summary": "Avatar capabilities retrieved.",
25
+ "raw_data": data
26
+ }, indent=2))
27
+ except urllib.error.URLError as e:
28
+ print(json.dumps({
29
+ "error": f"Failed to connect to Oomi app: {str(e)}",
30
+ "hint": "Is the Next.js app running on localhost:3000?"
31
+ }))
32
+ sys.exit(1)
33
+ except Exception as e:
34
+ print(json.dumps({
35
+ "error": f"An error occurred: {str(e)}"
36
+ }))
37
+ sys.exit(1)
38
+
39
+ if __name__ == "__main__":
40
+ get_avatar_capabilities()
@@ -1,49 +1,49 @@
1
- import urllib.request
2
- import urllib.error
3
- import json
4
- import os
5
- import sys
6
-
7
- # Load config
8
- try:
9
- config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.json')
10
- with open(config_path, 'r') as f:
11
- config = json.load(f)
12
- BASE_URL = config.get('api_url', 'http://localhost:3000/api/skill')
13
- except:
14
- BASE_URL = 'http://localhost:3000/api/skill'
15
-
16
- API_URL = f"{BASE_URL}/data"
17
-
18
- def get_data():
19
- try:
20
- with urllib.request.urlopen(API_URL) as response:
21
- data = json.loads(response.read().decode())
22
-
23
- # Format output for the agent
24
- summary = (
25
- f"Steps: {data['steps']} / {data['goals']['steps']}\n"
26
- f"Sleep: {data['sleep']}h / {data['goals']['sleep']}h\n"
27
- f"Energy: {data['energy']}/100\n"
28
- f"Mood: {data['mood']}"
29
- )
30
-
31
- print(json.dumps({
32
- "summary": summary,
33
- "raw_data": data
34
- }, indent=2))
35
-
36
- except urllib.error.URLError as e:
37
- print(json.dumps({
38
- "error": f"Failed to connect to Oomi app: {str(e)}",
39
- "hint": "Is the Next.js app running on localhost:3000?"
40
- }))
41
- sys.exit(1)
42
- except Exception as e:
43
- print(json.dumps({
44
- "error": f"An error occurred: {str(e)}"
45
- }))
46
- sys.exit(1)
47
-
48
- if __name__ == "__main__":
49
- get_data()
1
+ import urllib.request
2
+ import urllib.error
3
+ import json
4
+ import os
5
+ import sys
6
+
7
+ # Load config
8
+ try:
9
+ config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.json')
10
+ with open(config_path, 'r') as f:
11
+ config = json.load(f)
12
+ BASE_URL = config.get('api_url', 'http://localhost:3000/api/skill')
13
+ except:
14
+ BASE_URL = 'http://localhost:3000/api/skill'
15
+
16
+ API_URL = f"{BASE_URL}/data"
17
+
18
+ def get_data():
19
+ try:
20
+ with urllib.request.urlopen(API_URL) as response:
21
+ data = json.loads(response.read().decode())
22
+
23
+ # Format output for the agent
24
+ summary = (
25
+ f"Steps: {data['steps']} / {data['goals']['steps']}\n"
26
+ f"Sleep: {data['sleep']}h / {data['goals']['sleep']}h\n"
27
+ f"Energy: {data['energy']}/100\n"
28
+ f"Mood: {data['mood']}"
29
+ )
30
+
31
+ print(json.dumps({
32
+ "summary": summary,
33
+ "raw_data": data
34
+ }, indent=2))
35
+
36
+ except urllib.error.URLError as e:
37
+ print(json.dumps({
38
+ "error": f"Failed to connect to Oomi app: {str(e)}",
39
+ "hint": "Is the Next.js app running on localhost:3000?"
40
+ }))
41
+ sys.exit(1)
42
+ except Exception as e:
43
+ print(json.dumps({
44
+ "error": f"An error occurred: {str(e)}"
45
+ }))
46
+ sys.exit(1)
47
+
48
+ if __name__ == "__main__":
49
+ get_data()
@@ -1,78 +1,78 @@
1
- import argparse
2
- import os
3
- import sys
4
-
5
- DEFAULT_MARKER_START = "<oomi-agent-instructions>"
6
- DEFAULT_MARKER_END = "</oomi-agent-instructions>"
7
-
8
-
9
- def read_file(path: str) -> str:
10
- with open(path, "r") as f:
11
- return f.read()
12
-
13
-
14
- def write_file(path: str, content: str) -> None:
15
- with open(path, "w") as f:
16
- f.write(content)
17
-
18
-
19
- def get_default_agents_file() -> str:
20
- # Prefer explicit workspace env, otherwise use repo root if detected
21
- workspace = os.environ.get("OPENCLAW_WORKSPACE") or os.environ.get("OPENCLAW_HOME")
22
- if workspace:
23
- return os.path.join(workspace, "AGENTS.md")
24
- return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "AGENTS.md"))
25
-
26
-
27
- def build_block(instructions: str, marker_start: str, marker_end: str) -> str:
28
- return f"{marker_start}\n{instructions.strip()}\n{marker_end}"
29
-
30
-
31
- def install_block(agents_path: str, block: str, marker_start: str, marker_end: str) -> None:
32
- if os.path.exists(agents_path):
33
- existing = read_file(agents_path)
34
- else:
35
- existing = ""
36
-
37
- if marker_start in existing and marker_end in existing:
38
- # Replace existing block
39
- pre = existing.split(marker_start)[0]
40
- post = existing.split(marker_end)[1]
41
- content = f"{pre}{block}{post}"
42
- else:
43
- # Append block
44
- spacer = "\n\n" if existing and not existing.endswith("\n\n") else ""
45
- content = f"{existing}{spacer}{block}\n"
46
-
47
- write_file(agents_path, content)
48
-
49
-
50
- def main() -> None:
51
- parser = argparse.ArgumentParser(description="Install Oomi agent instructions into AGENTS.md.")
52
- parser.add_argument(
53
- "--agents-file",
54
- default=get_default_agents_file(),
55
- help="Path to AGENTS.md (defaults to OPENCLAW_WORKSPACE/AGENTS.md or repo AGENTS.md).",
56
- )
57
- parser.add_argument(
58
- "--instructions-file",
59
- default=os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "agent_instructions.md")),
60
- help="Path to instructions markdown file.",
61
- )
62
- parser.add_argument("--marker-start", default=DEFAULT_MARKER_START, help="Start marker.")
63
- parser.add_argument("--marker-end", default=DEFAULT_MARKER_END, help="End marker.")
64
- args = parser.parse_args()
65
-
66
- if not os.path.exists(args.instructions_file):
67
- print(f"Instructions file not found: {args.instructions_file}", file=sys.stderr)
68
- sys.exit(1)
69
-
70
- instructions = read_file(args.instructions_file)
71
- block = build_block(instructions, args.marker_start, args.marker_end)
72
- install_block(args.agents_file, block, args.marker_start, args.marker_end)
73
-
74
- print(f"Installed Oomi instructions into {args.agents_file}")
75
-
76
-
77
- if __name__ == "__main__":
78
- main()
1
+ import argparse
2
+ import os
3
+ import sys
4
+
5
+ DEFAULT_MARKER_START = "<oomi-agent-instructions>"
6
+ DEFAULT_MARKER_END = "</oomi-agent-instructions>"
7
+
8
+
9
+ def read_file(path: str) -> str:
10
+ with open(path, "r") as f:
11
+ return f.read()
12
+
13
+
14
+ def write_file(path: str, content: str) -> None:
15
+ with open(path, "w") as f:
16
+ f.write(content)
17
+
18
+
19
+ def get_default_agents_file() -> str:
20
+ # Prefer explicit workspace env, otherwise use repo root if detected
21
+ workspace = os.environ.get("OPENCLAW_WORKSPACE") or os.environ.get("OPENCLAW_HOME")
22
+ if workspace:
23
+ return os.path.join(workspace, "AGENTS.md")
24
+ return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "AGENTS.md"))
25
+
26
+
27
+ def build_block(instructions: str, marker_start: str, marker_end: str) -> str:
28
+ return f"{marker_start}\n{instructions.strip()}\n{marker_end}"
29
+
30
+
31
+ def install_block(agents_path: str, block: str, marker_start: str, marker_end: str) -> None:
32
+ if os.path.exists(agents_path):
33
+ existing = read_file(agents_path)
34
+ else:
35
+ existing = ""
36
+
37
+ if marker_start in existing and marker_end in existing:
38
+ # Replace existing block
39
+ pre = existing.split(marker_start)[0]
40
+ post = existing.split(marker_end)[1]
41
+ content = f"{pre}{block}{post}"
42
+ else:
43
+ # Append block
44
+ spacer = "\n\n" if existing and not existing.endswith("\n\n") else ""
45
+ content = f"{existing}{spacer}{block}\n"
46
+
47
+ write_file(agents_path, content)
48
+
49
+
50
+ def main() -> None:
51
+ parser = argparse.ArgumentParser(description="Install Oomi agent instructions into AGENTS.md.")
52
+ parser.add_argument(
53
+ "--agents-file",
54
+ default=get_default_agents_file(),
55
+ help="Path to AGENTS.md (defaults to OPENCLAW_WORKSPACE/AGENTS.md or repo AGENTS.md).",
56
+ )
57
+ parser.add_argument(
58
+ "--instructions-file",
59
+ default=os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "agent_instructions.md")),
60
+ help="Path to instructions markdown file.",
61
+ )
62
+ parser.add_argument("--marker-start", default=DEFAULT_MARKER_START, help="Start marker.")
63
+ parser.add_argument("--marker-end", default=DEFAULT_MARKER_END, help="End marker.")
64
+ args = parser.parse_args()
65
+
66
+ if not os.path.exists(args.instructions_file):
67
+ print(f"Instructions file not found: {args.instructions_file}", file=sys.stderr)
68
+ sys.exit(1)
69
+
70
+ instructions = read_file(args.instructions_file)
71
+ block = build_block(instructions, args.marker_start, args.marker_end)
72
+ install_block(args.agents_file, block, args.marker_start, args.marker_end)
73
+
74
+ print(f"Installed Oomi instructions into {args.agents_file}")
75
+
76
+
77
+ if __name__ == "__main__":
78
+ main()
@@ -1,53 +1,53 @@
1
- import urllib.request
2
- import urllib.error
3
- import json
4
- import argparse
5
- import os
6
- import sys
7
-
8
- # Load config
9
- try:
10
- config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.json')
11
- with open(config_path, 'r') as f:
12
- config = json.load(f)
13
- BASE_URL = config.get('api_url', 'http://localhost:3000/api/skill')
14
- except:
15
- BASE_URL = 'http://localhost:3000/api/skill'
16
-
17
- API_URL = f"{BASE_URL}/goal"
18
-
19
- def send_goal(goal_type, value, message=None):
20
- payload = {
21
- "type": goal_type,
22
- "value": value,
23
- "message": message
24
- }
25
-
26
- try:
27
- req = urllib.request.Request(
28
- API_URL,
29
- data=json.dumps(payload).encode('utf-8'),
30
- headers={'Content-Type': 'application/json'}
31
- )
32
-
33
- with urllib.request.urlopen(req) as response:
34
- result = json.loads(response.read().decode())
35
-
36
- print(json.dumps(result, indent=2))
37
-
38
- except urllib.error.URLError as e:
39
- print(json.dumps({
40
- "error": f"Failed to send goal: {str(e)}",
41
- "hint": "Is the Next.js app running on localhost:3000?"
42
- }))
43
- sys.exit(1)
44
-
45
- if __name__ == "__main__":
46
- parser = argparse.ArgumentParser(description="Set a goal in Oomi")
47
- parser.add_argument("--type", required=True, help="Type of goal (e.g. steps, sleep)")
48
- parser.add_argument("--value", required=True, type=float, help="Goal target value")
49
- parser.add_argument("--message", help="Optional motivational message")
50
-
51
- args = parser.parse_args()
52
-
53
- send_goal(args.type, args.value, args.message)
1
+ import urllib.request
2
+ import urllib.error
3
+ import json
4
+ import argparse
5
+ import os
6
+ import sys
7
+
8
+ # Load config
9
+ try:
10
+ config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.json')
11
+ with open(config_path, 'r') as f:
12
+ config = json.load(f)
13
+ BASE_URL = config.get('api_url', 'http://localhost:3000/api/skill')
14
+ except:
15
+ BASE_URL = 'http://localhost:3000/api/skill'
16
+
17
+ API_URL = f"{BASE_URL}/goal"
18
+
19
+ def send_goal(goal_type, value, message=None):
20
+ payload = {
21
+ "type": goal_type,
22
+ "value": value,
23
+ "message": message
24
+ }
25
+
26
+ try:
27
+ req = urllib.request.Request(
28
+ API_URL,
29
+ data=json.dumps(payload).encode('utf-8'),
30
+ headers={'Content-Type': 'application/json'}
31
+ )
32
+
33
+ with urllib.request.urlopen(req) as response:
34
+ result = json.loads(response.read().decode())
35
+
36
+ print(json.dumps(result, indent=2))
37
+
38
+ except urllib.error.URLError as e:
39
+ print(json.dumps({
40
+ "error": f"Failed to send goal: {str(e)}",
41
+ "hint": "Is the Next.js app running on localhost:3000?"
42
+ }))
43
+ sys.exit(1)
44
+
45
+ if __name__ == "__main__":
46
+ parser = argparse.ArgumentParser(description="Set a goal in Oomi")
47
+ parser.add_argument("--type", required=True, help="Type of goal (e.g. steps, sleep)")
48
+ parser.add_argument("--value", required=True, type=float, help="Goal target value")
49
+ parser.add_argument("--message", help="Optional motivational message")
50
+
51
+ args = parser.parse_args()
52
+
53
+ send_goal(args.type, args.value, args.message)