Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions bin/ask
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ Examples:
from __future__ import annotations

import os
import shutil
import subprocess
import sys
import tempfile
import time
from datetime import datetime
from pathlib import Path

Expand Down Expand Up @@ -112,6 +114,12 @@ def _send_via_unified_daemon(
from askd_runtime import state_file_path
import askd_rpc

ready_timeout = min(timeout, 2.0) if timeout and timeout > 0 else 2.0
if not _ensure_unified_daemon_ready(timeout_s=ready_timeout):
print("[ERROR] Unified askd daemon not running", file=sys.stderr)
print("Start it with `askd` (or enable autostart via CCB_ASKD_AUTOSTART=1).", file=sys.stderr)
return EXIT_ERROR

# Use CCB_RUN_DIR (set by CCB startup) to locate the state file.
# This already contains the correct project-specific path.
state_file = state_file_path("askd.json")
Expand Down Expand Up @@ -190,6 +198,96 @@ def _env_bool(name: str, default: bool = False) -> bool:
return val not in ("0", "false", "no", "off")


def _is_pid_alive(pid: int) -> bool:
if pid <= 0:
return False
try:
os.kill(pid, 0)
return True
except OSError:
return False
except Exception:
return True


def _askd_start_argv() -> list[str] | None:
local = script_dir / "askd"
candidates: list[str] = []
if local.exists():
candidates.append(str(local))
found = shutil.which("askd")
if found:
candidates.append(found)
if not candidates:
return None

entry = candidates[0]
lower = entry.lower()
if lower.endswith((".cmd", ".bat", ".exe")):
return [entry]
return [sys.executable, entry]


def _ensure_unified_daemon_ready(timeout_s: float = 2.0) -> bool:
if not _use_unified_daemon():
return True

from askd_runtime import state_file_path
import askd_rpc

state_file = state_file_path("askd.json")
try:
if askd_rpc.ping_daemon("ask", 0.2, state_file):
return True
except Exception:
pass

if not _env_bool("CCB_ASKD_AUTOSTART", True):
return False

argv = _askd_start_argv()
if not argv:
return False

env = os.environ.copy()
parent_raw = (env.get("CCB_PARENT_PID") or "").strip()
if parent_raw:
try:
parent_pid = int(parent_raw)
except Exception:
parent_pid = 0
if parent_pid <= 0 or not _is_pid_alive(parent_pid):
env.pop("CCB_PARENT_PID", None)
env.pop("CCB_MANAGED", None)

kwargs = {
"stdin": subprocess.DEVNULL,
"stdout": subprocess.DEVNULL,
"stderr": subprocess.DEVNULL,
"close_fds": True,
"env": env,
}
if os.name == "nt":
kwargs["creationflags"] = getattr(subprocess, "DETACHED_PROCESS", 0) | getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
else:
kwargs["start_new_session"] = True

try:
subprocess.Popen(argv, **kwargs)
except Exception:
return False

deadline = time.time() + max(0.2, float(timeout_s))
while time.time() < deadline:
try:
if askd_rpc.ping_daemon("ask", 0.2, state_file):
return True
except Exception:
pass
time.sleep(0.1)
return False


def _default_foreground() -> bool:
# Allow explicit override
if _env_bool("CCB_ASK_BACKGROUND", False):
Expand Down Expand Up @@ -337,6 +435,13 @@ def main(argv: list[str]) -> int:
return EXIT_ERROR

# Default async mode: background task via nohup, using unified askd daemon
if _use_unified_daemon():
ready_timeout = min(timeout, 2.0) if timeout and timeout > 0 else 2.0
if not _ensure_unified_daemon_ready(timeout_s=ready_timeout):
print("[ERROR] Unified askd daemon not running", file=sys.stderr)
print("Start it with `askd` (or enable autostart via CCB_ASKD_AUTOSTART=1).", file=sys.stderr)
return EXIT_ERROR

task_id = make_task_id()
log_dir = Path(tempfile.gettempdir()) / "ccb-tasks"
log_dir.mkdir(parents=True, exist_ok=True)
Expand Down
51 changes: 47 additions & 4 deletions bin/ccb-mounted
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
#!/usr/bin/env bash
# ccb-mounted - Check which CCB providers are mounted
# Usage: ccb-mounted [--json|--simple]
# Usage: ccb-mounted [--json|--simple] [--autostart] [path]

set -euo pipefail

PROVIDERS="codex:cask gemini:gask opencode:oask claude:lask droid:dask"
CWD="${1:-$(pwd)}"
FORMAT="${2:---json}"
CWD=$(pwd)
FORMAT="--json"
AUTOSTART=false

# Get script directory
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# Argument parsing
while [[ $# -gt 0 ]]; do
case "$1" in
--simple)
FORMAT="--simple"
shift
;;
--json)
FORMAT="--json"
shift
;;
--autostart)
AUTOSTART=true
shift
;;
-*)
echo "Unknown option: $1" >&2
exit 1
;;
*)
CWD="$1"
shift
;;
esac
done

# Normalize CWD for Windows (convert /e/... to E:/...)
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
# Convert msys path /e/foo to E:/foo
Expand All @@ -28,7 +55,11 @@ if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ -n "${WINDIR:-
# Check session file exists first
if [[ -f "$CWD/.ccb/.${provider}-session" ]] || [[ -f "$CWD/.ccb_config/.${provider}-session" ]] || [[ -f "$CWD/.${provider}-session" ]]; then
# Try the ping command to check if daemon is responsive
if "$SCRIPT_DIR/ccb-ping" "$provider" >/dev/null 2>&1; then
PING_ARGS=("$provider")
if [[ "$AUTOSTART" == true ]]; then
PING_ARGS+=("--autostart")
fi
if (cd "$CWD" && "$SCRIPT_DIR/ccb-ping" "${PING_ARGS[@]}") >/dev/null 2>&1; then
MOUNTED="$MOUNTED $provider"
fi
fi
Expand All @@ -45,7 +76,19 @@ else
# Check session file exists
if [[ -f "$CWD/.ccb/.${provider}-session" ]] || [[ -f "$CWD/.ccb_config/.${provider}-session" ]] || [[ -f "$CWD/.${provider}-session" ]]; then
# Check daemon is online
IS_ONLINE=false
if echo "$ONLINE" | grep -q "${daemon}d"; then
IS_ONLINE=true
fi

if [[ "$IS_ONLINE" == false ]] && [[ "$AUTOSTART" == true ]]; then
# Try to autostart if requested and not online
if (cd "$CWD" && "$SCRIPT_DIR/ccb-ping" "$provider" --autostart) >/dev/null 2>&1; then
IS_ONLINE=true
fi
fi

if [[ "$IS_ONLINE" == true ]]; then
MOUNTED="$MOUNTED $provider"
fi
fi
Expand Down
53 changes: 50 additions & 3 deletions bin/ccb-ping
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
ccb-ping - Test connectivity with AI providers.

Usage:
ccb-ping <provider> [--session-file FILE]
ccb-ping <provider> [--session-file FILE] [--autostart]

Providers:
gemini, codex, opencode, droid, claude
Expand Down Expand Up @@ -31,14 +31,30 @@ PROVIDER_COMMS = {
"claude": ("claude_comm", "ClaudeCommunicator"),
}


def _usage():
print("Usage: ccb-ping <provider> [--session-file FILE]", file=sys.stderr)
print("Usage: ccb-ping <provider> [--session-file FILE] [--autostart]", file=sys.stderr)
print("", file=sys.stderr)
print("Providers:", file=sys.stderr)
print(" gemini, codex, opencode, droid, claude", file=sys.stderr)


def _resolve_work_dir(session_file: str | None) -> Path:
if not session_file:
return Path.cwd()
raw = str(session_file).strip()
if not raw:
return Path.cwd()
try:
p = Path(os.path.expanduser(raw)).resolve()
except Exception:
p = Path(os.path.expanduser(raw)).absolute()
# Session files normally live in <project>/.ccb/.<provider>-session
parent_name = p.parent.name
if parent_name in (".ccb", ".ccb_config"):
return p.parent.parent
return p.parent


def main():
if len(sys.argv) < 2:
_usage()
Expand All @@ -58,16 +74,47 @@ def main():
# Parse remaining arguments
parser = argparse.ArgumentParser(prog=f"ccb-ping {provider}", add_help=False)
parser.add_argument("--session-file", dest="session_file", default=None)
parser.add_argument("--autostart", action="store_true", help="Start daemon if needed")
args, _ = parser.parse_known_args(sys.argv[2:])

if args.session_file:
os.environ["CCB_SESSION_FILE"] = str(args.session_file)
work_dir = _resolve_work_dir(args.session_file)

# Import and instantiate the communicator
module_name, class_name = PROVIDER_COMMS[provider]
try:
module = __import__(module_name)
comm_class = getattr(module, class_name)

# Pre-emptive autostart check if requested
if args.autostart:
try:
from askd_client import maybe_start_daemon, wait_for_daemon_ready
from providers import (
CASK_CLIENT_SPEC,
DASK_CLIENT_SPEC,
GASK_CLIENT_SPEC,
LASK_CLIENT_SPEC,
OASK_CLIENT_SPEC,
)

specs = {
"codex": CASK_CLIENT_SPEC,
"gemini": GASK_CLIENT_SPEC,
"opencode": OASK_CLIENT_SPEC,
"claude": LASK_CLIENT_SPEC,
"droid": DASK_CLIENT_SPEC,
}

spec = specs.get(provider)
if spec:
maybe_start_daemon(spec, work_dir)
# Small wait for it to bind
wait_for_daemon_ready(spec, 0.5)
except Exception as e:
print(f"[WARN] Autostart pre-check failed: {e}", file=sys.stderr)

comm = comm_class()
healthy, message = comm.ping(display=False)
print(message)
Expand Down
2 changes: 1 addition & 1 deletion lib/daskd_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
strip_done_text,
)

ANY_DONE_LINE_RE = re.compile(r"^\s*CCB_DONE:\s*\d{8}-\d{6}-\d{3}-\d+\s*$", re.IGNORECASE)
ANY_DONE_LINE_RE = re.compile(r"^\s*CCB_DONE:\s*(?:[0-9a-f]{32}|\d{8}-\d{6}-\d{3}-\d+-\d+)\s*$", re.IGNORECASE)
_SKILL_CACHE: str | None = None


Expand Down
4 changes: 2 additions & 2 deletions lib/gaskd_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
strip_done_text,
)

# Match both old (32-char hex) and new (YYYYMMDD-HHMMSS-mmm-PID) req_id formats
ANY_DONE_LINE_RE = re.compile(r"^\s*CCB_DONE:\s*(?:[0-9a-f]{32}|\d{8}-\d{6}-\d{3}-\d+)\s*$", re.IGNORECASE)
# Match both old (32-char hex) and new (YYYYMMDD-HHMMSS-mmm-PID-counter) req_id formats
ANY_DONE_LINE_RE = re.compile(r"^\s*CCB_DONE:\s*(?:[0-9a-f]{32}|\d{8}-\d{6}-\d{3}-\d+-\d+)\s*$", re.IGNORECASE)


def wrap_gemini_prompt(message: str, req_id: str) -> str:
Expand Down
Loading
Loading