nextclaw-core 0.4.1 → 0.4.3

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,112 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'USAGE'
6
+ Usage: find-sessions.sh [-L socket-name|-S socket-path|-A] [-q pattern]
7
+
8
+ List tmux sessions on a socket (default tmux socket if none provided).
9
+
10
+ Options:
11
+ -L, --socket tmux socket name (passed to tmux -L)
12
+ -S, --socket-path tmux socket path (passed to tmux -S)
13
+ -A, --all scan all sockets under NEXTCLAW_TMUX_SOCKET_DIR
14
+ -q, --query case-insensitive substring to filter session names
15
+ -h, --help show this help
16
+ USAGE
17
+ }
18
+
19
+ socket_name=""
20
+ socket_path=""
21
+ query=""
22
+ scan_all=false
23
+ socket_dir="${NEXTCLAW_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/nextclaw-tmux-sockets}"
24
+
25
+ while [[ $# -gt 0 ]]; do
26
+ case "$1" in
27
+ -L|--socket) socket_name="${2-}"; shift 2 ;;
28
+ -S|--socket-path) socket_path="${2-}"; shift 2 ;;
29
+ -A|--all) scan_all=true; shift ;;
30
+ -q|--query) query="${2-}"; shift 2 ;;
31
+ -h|--help) usage; exit 0 ;;
32
+ *) echo "Unknown option: $1" >&2; usage; exit 1 ;;
33
+ esac
34
+ done
35
+
36
+ if [[ "$scan_all" == true && ( -n "$socket_name" || -n "$socket_path" ) ]]; then
37
+ echo "Cannot combine --all with -L or -S" >&2
38
+ exit 1
39
+ fi
40
+
41
+ if [[ -n "$socket_name" && -n "$socket_path" ]]; then
42
+ echo "Use either -L or -S, not both" >&2
43
+ exit 1
44
+ fi
45
+
46
+ if ! command -v tmux >/dev/null 2>&1; then
47
+ echo "tmux not found in PATH" >&2
48
+ exit 1
49
+ fi
50
+
51
+ list_sessions() {
52
+ local label="$1"; shift
53
+ local tmux_cmd=(tmux "$@")
54
+
55
+ if ! sessions="$("${tmux_cmd[@]}" list-sessions -F '#{session_name}\t#{session_attached}\t#{session_created_string}' 2>/dev/null)"; then
56
+ echo "No tmux server found on $label" >&2
57
+ return 1
58
+ fi
59
+
60
+ if [[ -n "$query" ]]; then
61
+ sessions="$(printf '%s\n' "$sessions" | grep -i -- "$query" || true)"
62
+ fi
63
+
64
+ if [[ -z "$sessions" ]]; then
65
+ echo "No sessions found on $label"
66
+ return 0
67
+ fi
68
+
69
+ echo "Sessions on $label:"
70
+ printf '%s\n' "$sessions" | while IFS=$'\t' read -r name attached created; do
71
+ attached_label=$([[ "$attached" == "1" ]] && echo "attached" || echo "detached")
72
+ printf ' - %s (%s, started %s)\n' "$name" "$attached_label" "$created"
73
+ done
74
+ }
75
+
76
+ if [[ "$scan_all" == true ]]; then
77
+ if [[ ! -d "$socket_dir" ]]; then
78
+ echo "Socket directory not found: $socket_dir" >&2
79
+ exit 1
80
+ fi
81
+
82
+ shopt -s nullglob
83
+ sockets=("$socket_dir"/*)
84
+ shopt -u nullglob
85
+
86
+ if [[ "${#sockets[@]}" -eq 0 ]]; then
87
+ echo "No sockets found under $socket_dir" >&2
88
+ exit 1
89
+ fi
90
+
91
+ exit_code=0
92
+ for sock in "${sockets[@]}"; do
93
+ if [[ ! -S "$sock" ]]; then
94
+ continue
95
+ fi
96
+ list_sessions "socket path '$sock'" -S "$sock" || exit_code=$?
97
+ done
98
+ exit "$exit_code"
99
+ fi
100
+
101
+ tmux_cmd=(tmux)
102
+ socket_label="default socket"
103
+
104
+ if [[ -n "$socket_name" ]]; then
105
+ tmux_cmd+=(-L "$socket_name")
106
+ socket_label="socket name '$socket_name'"
107
+ elif [[ -n "$socket_path" ]]; then
108
+ tmux_cmd+=(-S "$socket_path")
109
+ socket_label="socket path '$socket_path'"
110
+ fi
111
+
112
+ list_sessions "$socket_label" "${tmux_cmd[@]:1}"
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'USAGE'
6
+ Usage: wait-for-text.sh -t target -p pattern [options]
7
+
8
+ Poll a tmux pane for text and exit when found.
9
+
10
+ Options:
11
+ -t, --target tmux target (session:window.pane), required
12
+ -p, --pattern regex pattern to look for, required
13
+ -F, --fixed treat pattern as a fixed string (grep -F)
14
+ -T, --timeout seconds to wait (integer, default: 15)
15
+ -i, --interval poll interval in seconds (default: 0.5)
16
+ -l, --lines number of history lines to inspect (integer, default: 1000)
17
+ -h, --help show this help
18
+ USAGE
19
+ }
20
+
21
+ target=""
22
+ pattern=""
23
+ grep_flag="-E"
24
+ timeout=15
25
+ interval=0.5
26
+ lines=1000
27
+
28
+ while [[ $# -gt 0 ]]; do
29
+ case "$1" in
30
+ -t|--target) target="${2-}"; shift 2 ;;
31
+ -p|--pattern) pattern="${2-}"; shift 2 ;;
32
+ -F|--fixed) grep_flag="-F"; shift ;;
33
+ -T|--timeout) timeout="${2-}"; shift 2 ;;
34
+ -i|--interval) interval="${2-}"; shift 2 ;;
35
+ -l|--lines) lines="${2-}"; shift 2 ;;
36
+ -h|--help) usage; exit 0 ;;
37
+ *) echo "Unknown option: $1" >&2; usage; exit 1 ;;
38
+ esac
39
+ done
40
+
41
+ if [[ -z "$target" || -z "$pattern" ]]; then
42
+ echo "target and pattern are required" >&2
43
+ usage
44
+ exit 1
45
+ fi
46
+
47
+ if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
48
+ echo "timeout must be an integer number of seconds" >&2
49
+ exit 1
50
+ fi
51
+
52
+ if ! [[ "$lines" =~ ^[0-9]+$ ]]; then
53
+ echo "lines must be an integer" >&2
54
+ exit 1
55
+ fi
56
+
57
+ if ! command -v tmux >/dev/null 2>&1; then
58
+ echo "tmux not found in PATH" >&2
59
+ exit 1
60
+ fi
61
+
62
+ # End time in epoch seconds (integer, good enough for polling)
63
+ start_epoch=$(date +%s)
64
+ deadline=$((start_epoch + timeout))
65
+
66
+ while true; do
67
+ # -J joins wrapped lines, -S uses negative index to read last N lines
68
+ pane_text="$(tmux capture-pane -p -J -t "$target" -S "-${lines}" 2>/dev/null || true)"
69
+
70
+ if printf '%s\n' "$pane_text" | grep $grep_flag -- "$pattern" >/dev/null 2>&1; then
71
+ exit 0
72
+ fi
73
+
74
+ now=$(date +%s)
75
+ if (( now >= deadline )); then
76
+ echo "Timed out after ${timeout}s waiting for pattern: $pattern" >&2
77
+ echo "Last ${lines} lines from $target:" >&2
78
+ printf '%s\n' "$pane_text" >&2
79
+ exit 1
80
+ fi
81
+
82
+ sleep "$interval"
83
+ done
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: weather
3
+ description: Get current weather and forecasts (no API key required).
4
+ homepage: https://wttr.in/:help
5
+ metadata: {"nextclaw":{"emoji":"🌤️","requires":{"bins":["curl"]}}}
6
+ ---
7
+
8
+ # Weather
9
+
10
+ Two free services, no API keys needed.
11
+
12
+ ## wttr.in (primary)
13
+
14
+ Quick one-liner:
15
+ ```bash
16
+ curl -s "wttr.in/London?format=3"
17
+ # Output: London: ⛅️ +8°C
18
+ ```
19
+
20
+ Compact format:
21
+ ```bash
22
+ curl -s "wttr.in/London?format=%l:+%c+%t+%h+%w"
23
+ # Output: London: ⛅️ +8°C 71% ↙5km/h
24
+ ```
25
+
26
+ Full forecast:
27
+ ```bash
28
+ curl -s "wttr.in/London?T"
29
+ ```
30
+
31
+ Format codes: `%c` condition · `%t` temp · `%h` humidity · `%w` wind · `%l` location · `%m` moon
32
+
33
+ Tips:
34
+ - URL-encode spaces: `wttr.in/New+York`
35
+ - Airport codes: `wttr.in/JFK`
36
+ - Units: `?m` (metric) `?u` (USCS)
37
+ - Today only: `?1` · Current only: `?0`
38
+ - PNG: `curl -s "wttr.in/Berlin.png" -o /tmp/weather.png`
39
+
40
+ ## Open-Meteo (fallback, JSON)
41
+
42
+ Free, no key, good for programmatic use:
43
+ ```bash
44
+ curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12&current_weather=true"
45
+ ```
46
+
47
+ Find coordinates for a city, then query. Returns JSON with temp, windspeed, weathercode.
48
+
49
+ Docs: https://open-meteo.com/en/docs
package/dist/index.d.ts CHANGED
@@ -148,7 +148,7 @@ declare class SessionManager {
148
148
  getOrCreate(key: string): Session;
149
149
  getIfExists(key: string): Session | null;
150
150
  addMessage(session: Session, role: string, content: string, extra?: Record<string, unknown>): void;
151
- getHistory(session: Session, maxMessages?: number): Array<Record<string, string>>;
151
+ getHistory(session: Session, maxMessages?: number): Array<Record<string, unknown>>;
152
152
  clear(session: Session): void;
153
153
  private load;
154
154
  save(session: Session): void;
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  // src/agent/context.ts
2
2
  import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
3
3
  import { join as join3, extname } from "path";
4
- import { fileURLToPath as fileURLToPath2 } from "url";
5
4
 
6
5
  // src/agent/memory.ts
7
6
  import { readFileSync, writeFileSync, existsSync as existsSync2, readdirSync } from "fs";
@@ -402,7 +401,7 @@ var ContextBuilder = class {
402
401
  constructor(workspace, contextConfig) {
403
402
  this.workspace = workspace;
404
403
  this.memory = new MemoryStore(workspace);
405
- this.skills = new SkillsLoader(workspace, join3(fileURLToPath2(new URL("..", import.meta.url)), "skills"));
404
+ this.skills = new SkillsLoader(workspace);
406
405
  this.contextConfig = {
407
406
  bootstrap: {
408
407
  ...DEFAULT_CONTEXT_CONFIG.bootstrap,
@@ -2297,7 +2296,25 @@ var SessionManager = class {
2297
2296
  }
2298
2297
  getHistory(session, maxMessages = 50) {
2299
2298
  const recent = session.messages.length > maxMessages ? session.messages.slice(-maxMessages) : session.messages;
2300
- return recent.map((msg) => ({ role: msg.role, content: msg.content }));
2299
+ return recent.map((msg) => {
2300
+ const entry = {
2301
+ role: msg.role,
2302
+ content: msg.content
2303
+ };
2304
+ if (typeof msg.name === "string") {
2305
+ entry.name = msg.name;
2306
+ }
2307
+ if (typeof msg.tool_call_id === "string") {
2308
+ entry.tool_call_id = msg.tool_call_id;
2309
+ }
2310
+ if (Array.isArray(msg.tool_calls)) {
2311
+ entry.tool_calls = msg.tool_calls;
2312
+ }
2313
+ if (typeof msg.reasoning_content === "string" && msg.reasoning_content) {
2314
+ entry.reasoning_content = msg.reasoning_content;
2315
+ }
2316
+ return entry;
2317
+ });
2301
2318
  }
2302
2319
  clear(session) {
2303
2320
  session.messages = [];
@@ -2537,6 +2554,7 @@ var AgentLoop = class {
2537
2554
  chatId: msg.chatId,
2538
2555
  sessionKey
2539
2556
  });
2557
+ this.sessions.addMessage(session, "user", msg.content);
2540
2558
  let iteration = 0;
2541
2559
  let finalContent = null;
2542
2560
  const maxIterations = this.options.maxIterations ?? 20;
@@ -2557,9 +2575,17 @@ var AgentLoop = class {
2557
2575
  }
2558
2576
  }));
2559
2577
  this.context.addAssistantMessage(messages, response.content, toolCallDicts, response.reasoningContent ?? null);
2578
+ this.sessions.addMessage(session, "assistant", response.content ?? "", {
2579
+ tool_calls: toolCallDicts,
2580
+ reasoning_content: response.reasoningContent ?? null
2581
+ });
2560
2582
  for (const call of response.toolCalls) {
2561
2583
  const result = await this.tools.execute(call.name, call.arguments);
2562
2584
  this.context.addToolResult(messages, call.id, call.name, result);
2585
+ this.sessions.addMessage(session, "tool", result, {
2586
+ tool_call_id: call.id,
2587
+ name: call.name
2588
+ });
2563
2589
  }
2564
2590
  } else {
2565
2591
  finalContent = response.content;
@@ -2572,11 +2598,9 @@ var AgentLoop = class {
2572
2598
  const { content: cleanedContent, replyTo } = parseReplyTags(finalContent, messageId);
2573
2599
  finalContent = cleanedContent;
2574
2600
  if (isSilentReplyText(finalContent, SILENT_REPLY_TOKEN)) {
2575
- this.sessions.addMessage(session, "user", msg.content);
2576
2601
  this.sessions.save(session);
2577
2602
  return null;
2578
2603
  }
2579
- this.sessions.addMessage(session, "user", msg.content);
2580
2604
  this.sessions.addMessage(session, "assistant", finalContent);
2581
2605
  this.sessions.save(session);
2582
2606
  return {
@@ -2611,6 +2635,7 @@ var AgentLoop = class {
2611
2635
  chatId: originChatId,
2612
2636
  sessionKey
2613
2637
  });
2638
+ this.sessions.addMessage(session, "user", `[System: ${msg.senderId}] ${msg.content}`);
2614
2639
  let iteration = 0;
2615
2640
  let finalContent = null;
2616
2641
  const maxIterations = this.options.maxIterations ?? 20;
@@ -2631,9 +2656,17 @@ var AgentLoop = class {
2631
2656
  }
2632
2657
  }));
2633
2658
  this.context.addAssistantMessage(messages, response.content, toolCallDicts, response.reasoningContent ?? null);
2659
+ this.sessions.addMessage(session, "assistant", response.content ?? "", {
2660
+ tool_calls: toolCallDicts,
2661
+ reasoning_content: response.reasoningContent ?? null
2662
+ });
2634
2663
  for (const call of response.toolCalls) {
2635
2664
  const result = await this.tools.execute(call.name, call.arguments);
2636
2665
  this.context.addToolResult(messages, call.id, call.name, result);
2666
+ this.sessions.addMessage(session, "tool", result, {
2667
+ tool_call_id: call.id,
2668
+ name: call.name
2669
+ });
2637
2670
  }
2638
2671
  } else {
2639
2672
  finalContent = response.content;
@@ -2646,9 +2679,9 @@ var AgentLoop = class {
2646
2679
  const { content: cleanedContent, replyTo } = parseReplyTags(finalContent, void 0);
2647
2680
  finalContent = cleanedContent;
2648
2681
  if (isSilentReplyText(finalContent, SILENT_REPLY_TOKEN)) {
2682
+ this.sessions.save(session);
2649
2683
  return null;
2650
2684
  }
2651
- this.sessions.addMessage(session, "user", `[System: ${msg.senderId}] ${msg.content}`);
2652
2685
  this.sessions.addMessage(session, "assistant", finalContent);
2653
2686
  this.sessions.save(session);
2654
2687
  return {
@@ -0,0 +1,24 @@
1
+ # nextclaw Skills
2
+
3
+ This directory contains built-in skills that extend nextclaw's capabilities.
4
+
5
+ ## Skill Format
6
+
7
+ Each skill is a directory containing a `SKILL.md` file with:
8
+ - YAML frontmatter (name, description, metadata)
9
+ - Markdown instructions for the agent
10
+
11
+ ## Attribution
12
+
13
+ These skills are adapted from [OpenClaw](https://github.com/openclaw/openclaw)'s skill system.
14
+ The skill format and metadata structure follow OpenClaw's conventions to maintain compatibility.
15
+
16
+ ## Available Skills
17
+
18
+ | Skill | Description |
19
+ |-------|-------------|
20
+ | `github` | Interact with GitHub using the `gh` CLI |
21
+ | `weather` | Get weather info using wttr.in and Open-Meteo |
22
+ | `summarize` | Summarize URLs, files, and YouTube videos |
23
+ | `tmux` | Remote-control tmux sessions |
24
+ | `skill-creator` | Create new skills |
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: cron
3
+ description: Schedule reminders and recurring tasks.
4
+ ---
5
+
6
+ # Cron
7
+
8
+ Use the `cron` tool to schedule reminders or recurring tasks.
9
+
10
+ ## Two Modes
11
+
12
+ 1. **Reminder** - message is sent directly to user
13
+ 2. **Task** - message is a task description, agent executes and sends result
14
+
15
+ ## Examples
16
+
17
+ Fixed reminder:
18
+ ```
19
+ cron(action="add", message="Time to take a break!", every_seconds=1200)
20
+ ```
21
+
22
+ Dynamic task (agent executes each time):
23
+ ```
24
+ cron(action="add", message="Check Peiiii/nextclaw GitHub stars and report", every_seconds=600)
25
+ ```
26
+
27
+ List/remove:
28
+ ```
29
+ cron(action="list")
30
+ cron(action="remove", job_id="abc123")
31
+ ```
32
+
33
+ ## Time Expressions
34
+
35
+ | User says | Parameters |
36
+ |-----------|------------|
37
+ | every 20 minutes | every_seconds: 1200 |
38
+ | every hour | every_seconds: 3600 |
39
+ | every day at 8am | cron_expr: "0 8 * * *" |
40
+ | weekdays at 5pm | cron_expr: "0 17 * * 1-5" |
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: github
3
+ description: "Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries."
4
+ metadata: {"nextclaw":{"emoji":"🐙","requires":{"bins":["gh"]},"install":[{"id":"brew","kind":"brew","formula":"gh","bins":["gh"],"label":"Install GitHub CLI (brew)"},{"id":"apt","kind":"apt","package":"gh","bins":["gh"],"label":"Install GitHub CLI (apt)"}]}}
5
+ ---
6
+
7
+ # GitHub Skill
8
+
9
+ Use the `gh` CLI to interact with GitHub. Always specify `--repo owner/repo` when not in a git directory, or use URLs directly.
10
+
11
+ ## Pull Requests
12
+
13
+ Check CI status on a PR:
14
+ ```bash
15
+ gh pr checks 55 --repo owner/repo
16
+ ```
17
+
18
+ List recent workflow runs:
19
+ ```bash
20
+ gh run list --repo owner/repo --limit 10
21
+ ```
22
+
23
+ View a run and see which steps failed:
24
+ ```bash
25
+ gh run view <run-id> --repo owner/repo
26
+ ```
27
+
28
+ View logs for failed steps only:
29
+ ```bash
30
+ gh run view <run-id> --repo owner/repo --log-failed
31
+ ```
32
+
33
+ ## API for Advanced Queries
34
+
35
+ The `gh api` command is useful for accessing data not available through other subcommands.
36
+
37
+ Get PR with specific fields:
38
+ ```bash
39
+ gh api repos/owner/repo/pulls/55 --jq '.title, .state, .user.login'
40
+ ```
41
+
42
+ ## JSON Output
43
+
44
+ Most commands support `--json` for structured output. You can use `--jq` to filter:
45
+
46
+ ```bash
47
+ gh issue list --repo owner/repo --json number,title --jq '.[] | "\(.number): \(.title)"'
48
+ ```