@mgeri1993/claude-task-manager 1.0.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/.env.example +2 -0
- package/Dockerfile +11 -0
- package/LICENSE +21 -0
- package/README.hu.md +255 -0
- package/README.md +253 -0
- package/api/index.php +173 -0
- package/bin/add-agent.sh +77 -0
- package/bin/ctm +160 -0
- package/docker-compose.yml +14 -0
- package/engine/check-update.sh +48 -0
- package/engine/projects.sh +164 -0
- package/engine/task.sh +926 -0
- package/favicon.ico +6 -0
- package/favicon.svg +6 -0
- package/index.html +141 -0
- package/install.sh +231 -0
- package/js/ApiClient.js +63 -0
- package/js/App.js +547 -0
- package/js/BoardView.js +205 -0
- package/js/ContextPanel.js +47 -0
- package/js/ContextStore.js +39 -0
- package/js/ProjectStore.js +27 -0
- package/js/TaskModal.js +112 -0
- package/js/TaskStore.js +73 -0
- package/js/UrlState.js +53 -0
- package/js/Utils.js +202 -0
- package/js/i18n.js +338 -0
- package/js/main.js +3 -0
- package/package.json +85 -0
- package/style.css +396 -0
- package/templates/SKILL.md.tmpl +175 -0
- package/templates/agents/ctm-backend-developer.md.tmpl +53 -0
- package/templates/agents/ctm-code-investigator.md.tmpl +49 -0
- package/templates/agents/ctm-frontend-developer.md.tmpl +53 -0
- package/templates/hooks/allow-task-sh.sh.tmpl +59 -0
- package/templates/hooks/notify-inbox.sh.tmpl +53 -0
- package/templates/tm-custom.md.tmpl +54 -0
package/favicon.ico
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
2
|
+
<rect x="4" y="4" width="56" height="56" rx="16" fill="#5b8def" fill-opacity="0.16" stroke="#5b8def" stroke-opacity="0.4" stroke-width="2"/>
|
|
3
|
+
<rect x="16" y="34" width="7" height="18" rx="2" fill="#5b8def"/>
|
|
4
|
+
<rect x="28" y="22" width="7" height="30" rx="2" fill="#5b8def"/>
|
|
5
|
+
<rect x="40" y="38" width="7" height="14" rx="2" fill="#5b8def"/>
|
|
6
|
+
</svg>
|
package/favicon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
2
|
+
<rect x="4" y="4" width="56" height="56" rx="16" fill="#5b8def" fill-opacity="0.16" stroke="#5b8def" stroke-opacity="0.4" stroke-width="2"/>
|
|
3
|
+
<rect x="16" y="34" width="7" height="18" rx="2" fill="#5b8def"/>
|
|
4
|
+
<rect x="28" y="22" width="7" height="30" rx="2" fill="#5b8def"/>
|
|
5
|
+
<rect x="40" y="38" width="7" height="14" rx="2" fill="#5b8def"/>
|
|
6
|
+
</svg>
|
package/index.html
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Claude Task Manager – Kanban board</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
11
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
12
|
+
<script>
|
|
13
|
+
// Tailwind Play CDN – only processes the static markup's utility classes. The actual
|
|
14
|
+
// component styles (card, column, modal, badges, etc.) live in the separate style.css as
|
|
15
|
+
// plain CSS (the tokens below are defined there as CSS custom properties) — see the
|
|
16
|
+
// comment at the top of style.css.
|
|
17
|
+
tailwind.config = {
|
|
18
|
+
darkMode: "class",
|
|
19
|
+
// Preflight (the base reset) would add `button,input,select{padding:0}`, which the CDN
|
|
20
|
+
// injects AFTER style.css at runtime → it would override our own button/field padding.
|
|
21
|
+
// Disabled; style.css supplies the base styles, the utility classes still work.
|
|
22
|
+
corePlugins: { preflight: false },
|
|
23
|
+
theme: {
|
|
24
|
+
extend: {
|
|
25
|
+
colors: {
|
|
26
|
+
bg: "var(--bg)",
|
|
27
|
+
panel: "var(--panel)",
|
|
28
|
+
"panel-2": "var(--panel-2)",
|
|
29
|
+
"panel-3": "var(--panel-3)",
|
|
30
|
+
ink: "var(--text)",
|
|
31
|
+
muted: "var(--muted)",
|
|
32
|
+
edge: "var(--border)",
|
|
33
|
+
accent: "var(--accent)",
|
|
34
|
+
"accent-solid": "var(--accent-solid)",
|
|
35
|
+
todo: "var(--todo)",
|
|
36
|
+
inprogress: "var(--in_progress)",
|
|
37
|
+
blocked: "var(--blocked)",
|
|
38
|
+
review: "var(--review)",
|
|
39
|
+
done: "var(--done)",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
</script>
|
|
45
|
+
<link rel="stylesheet" href="style.css">
|
|
46
|
+
</head>
|
|
47
|
+
<body class="antialiased">
|
|
48
|
+
<header class="shadow-sm dark:shadow-black/40 transition-shadow">
|
|
49
|
+
<div class="hd-left">
|
|
50
|
+
<h1 class="tracking-tight"><span class="logo" aria-hidden="true"><i></i><i></i><i></i></span> Task Manager</h1>
|
|
51
|
+
<div class="status"><span class="dot" id="dot"></span><span id="statusText" data-i18n="app.starting">starting…</span><span class="clock" id="clock"></span></div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="hd-center">
|
|
54
|
+
<label class="search-box"><svg class="ico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.5-3.5"></path></svg> <input id="q" type="search" data-i18n-placeholder="hd.searchPlaceholder" placeholder="search…" spellcheck="false"></label>
|
|
55
|
+
<div class="src-box">
|
|
56
|
+
<span class="lbl" data-i18n="hd.source">Source</span>
|
|
57
|
+
<span class="src-dot"></span>
|
|
58
|
+
<select id="srcProject" class="src-select"></select>
|
|
59
|
+
<input id="src" type="hidden">
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="hd-right">
|
|
63
|
+
<label class="ctl" data-i18n-title="hd.poll" title="Poll">Poll
|
|
64
|
+
<select id="interval">
|
|
65
|
+
<option value="1000">1s</option><option value="2000" selected>2s</option>
|
|
66
|
+
<option value="5000">5s</option><option value="10000">10s</option>
|
|
67
|
+
</select>
|
|
68
|
+
</label>
|
|
69
|
+
<label class="ctl" data-i18n-title="hd.asTitle" title="Who the board writes as (task.sh --as). Pick an agent/reviewer, not a fixed "human"."><svg class="ico ico-accent" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="3.4"></circle><path d="M5.5 20a6.5 6.5 0 0 1 13 0"></path></svg> <span data-i18n="hd.as">As</span> <input id="actor" type="text" list="actorList" data-i18n-placeholder="hd.asPlaceholder" placeholder="e.g. reviewer" spellcheck="false" autocomplete="off"><datalist id="actorList"></datalist></label>
|
|
70
|
+
<button id="notifyBtn" data-i18n-title="hd.notifyTitle" title="Browser notification on status change (→ review / done)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.7 21a2 2 0 0 1-3.4 0"></path></svg></button>
|
|
71
|
+
<button id="projectsBtn" data-i18n-title="hd.projectsTitle" title="Registered projects + copy wrapper task.sh"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="14" rx="2"></rect><path d="M3 9h18"></path></svg> <span data-i18n="hd.projects">Projects</span></button>
|
|
72
|
+
<button id="ctxBtn"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M9 3.5h6l-1 6 3 3v2h-4.2v5.2l-.8 1.8-.8-1.8V14.5H7v-2l3-3-1-6Z"></path></svg> <span data-i18n="hd.context">Context</span></button>
|
|
73
|
+
<button id="langBtn" title="Switch language / Nyelv váltása">HU</button>
|
|
74
|
+
<button id="toggle"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14" rx="1"></rect><rect x="14" y="5" width="4" height="14" rx="1"></rect></svg> <span class="btxt">Pause</span></button>
|
|
75
|
+
<button id="refresh"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 11a8 8 0 1 0-.7 3.2"></path><path d="M20 4.5V11h-6.5"></path></svg></button>
|
|
76
|
+
</div>
|
|
77
|
+
</header>
|
|
78
|
+
|
|
79
|
+
<div class="stats" id="stats"></div>
|
|
80
|
+
<div class="controls" id="controls">
|
|
81
|
+
<span class="lbl" data-i18n="ctrl.sort">Sort</span>
|
|
82
|
+
<select id="sort">
|
|
83
|
+
<option value="activity" data-i18n="ctrl.sort.activity">last activity</option>
|
|
84
|
+
<option value="created" data-i18n="ctrl.sort.created">created</option>
|
|
85
|
+
<option value="title" data-i18n="ctrl.sort.title">title (A→Z)</option>
|
|
86
|
+
<option value="team" data-i18n="ctrl.sort.team">Team #</option>
|
|
87
|
+
</select>
|
|
88
|
+
<span class="lbl" data-i18n="ctrl.view">View</span>
|
|
89
|
+
<div class="segmented">
|
|
90
|
+
<button id="viewBoard" class="on" data-i18n="ctrl.view.board">Kanban</button>
|
|
91
|
+
<button id="viewSwim" data-i18n="ctrl.view.swim">Swimlane</button>
|
|
92
|
+
<button id="viewFeed" data-i18n="ctrl.view.feed">Feed</button>
|
|
93
|
+
<button id="compact" data-i18n="ctrl.view.compact">Compact</button>
|
|
94
|
+
</div>
|
|
95
|
+
<span class="sep"></span>
|
|
96
|
+
<span class="lbl" data-i18n="ctrl.module">Module</span>
|
|
97
|
+
<select id="moduleFilter">
|
|
98
|
+
<option value="" data-i18n="ctrl.module.all">— all modules —</option>
|
|
99
|
+
</select>
|
|
100
|
+
<span class="sep"></span>
|
|
101
|
+
<span class="lbl" data-i18n="ctrl.quickFilter">Quick filter</span>
|
|
102
|
+
<button id="qfReview" class="qf"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4h10M7 20h10M8 4v3l4 5 4-5V4M8 20v-3l4-5 4 5v3"></path></svg> <span data-i18n="ctrl.quick.review">Awaiting you</span></button>
|
|
103
|
+
<button id="qfActive" class="qf" data-i18n="ctrl.quick.active">Active</button>
|
|
104
|
+
<button id="qfBlocked" class="qf">Blocked</button>
|
|
105
|
+
<span class="ro"><svg class="ico" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="11" width="14" height="9" rx="2"></rect><path d="M8 11V8a4 4 0 0 1 8 0v3"></path></svg> <span data-i18n="ctrl.readonly">read-only view – the file is written by the agent</span></span>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="agents-wrap" id="agentsWrap">
|
|
108
|
+
<button type="button" class="agents-head" id="agentsHead" aria-expanded="true">
|
|
109
|
+
<span class="lbl" data-i18n="agents.title">Agent load & filter</span>
|
|
110
|
+
<span class="count-pill" id="agentsCount">0</span>
|
|
111
|
+
<svg class="chev" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg>
|
|
112
|
+
</button>
|
|
113
|
+
<div class="agents" id="agents"></div>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="banner" id="banner"></div>
|
|
116
|
+
<div class="board" id="board"></div>
|
|
117
|
+
|
|
118
|
+
<div class="overlay backdrop-blur" id="overlay">
|
|
119
|
+
<div class="modal" role="dialog" aria-modal="true">
|
|
120
|
+
<header><h2 id="mTitle">–</h2><button class="close" id="mClose" data-i18n="modal.close">✕ Close</button></header>
|
|
121
|
+
<div class="body" id="mBody"></div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div class="overlay backdrop-blur" id="ctxOverlay">
|
|
126
|
+
<div class="modal" role="dialog" aria-modal="true">
|
|
127
|
+
<header><h2><span data-i18n="ctx.title">📌 Context</span> <span class="mut" id="ctxUpdated"></span></h2><button class="close" id="ctxClose" data-i18n="modal.close">✕ Close</button></header>
|
|
128
|
+
<div class="body" id="ctxBody"></div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div class="overlay backdrop-blur" id="projectsOverlay">
|
|
133
|
+
<div class="modal" role="dialog" aria-modal="true">
|
|
134
|
+
<header><h2 data-i18n="project.modalTitle">📁 Projects</h2><button class="close" id="projectsClose" data-i18n="modal.close">✕ Close</button></header>
|
|
135
|
+
<div class="body" id="projectsBody"></div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<script type="module" src="js/main.js"></script>
|
|
140
|
+
</body>
|
|
141
|
+
</html>
|
package/install.sh
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# install.sh — installs the claude-task-manager "client" into an arbitrary project.
|
|
4
|
+
#
|
|
5
|
+
# Run from inside any project, it registers that project with claude-task-manager (data
|
|
6
|
+
# directory + wrapper task.sh generation via engine/projects.sh), then creates in the target
|
|
7
|
+
# project:
|
|
8
|
+
# - .claude/skills/task-manager/task.sh — a COPY of the generated wrapper (carries its
|
|
9
|
+
# own TM_DIR baked in; no docker needed, plain host-bash script)
|
|
10
|
+
# - .claude/skills/task-manager/SKILL.md — the Claude Code skill documentation (from this
|
|
11
|
+
# project's own "SKILL.md" template)
|
|
12
|
+
# - .claude/agents/ctm-*.md — generic teammate agent definitions
|
|
13
|
+
# - .claude/hooks/allow-task-sh.sh + notify-inbox.sh — PreToolUse/PostToolUse hooks
|
|
14
|
+
# (auto-allow + inbox notification), registered in .claude/settings.json
|
|
15
|
+
# and extends the target project's .claude/settings.local.json Bash allowlist with the
|
|
16
|
+
# task.sh call, so it runs without a permission prompt.
|
|
17
|
+
#
|
|
18
|
+
# The board itself (index.html, port 3333) does NOT get installed into the target project —
|
|
19
|
+
# it runs from claude-task-manager's own docker compose, shared by every installed project
|
|
20
|
+
# (pick your project in the Source selector).
|
|
21
|
+
#
|
|
22
|
+
# Usage:
|
|
23
|
+
# /Users/mgeri1993/code/projects/claude-task-manager/install.sh [target-dir] [project-id] [label] [--force|-y]
|
|
24
|
+
#
|
|
25
|
+
# target-dir – defaults to the current directory's git root (or the cwd itself, if no git)
|
|
26
|
+
# project-id – defaults to the target-dir folder name (A-Za-z0-9_- characters only)
|
|
27
|
+
# label – defaults to project-id
|
|
28
|
+
# --force/-y – overwrite existing generated files without prompting (also the default
|
|
29
|
+
# behavior in a non-interactive/non-tty session, where prompting isn't possible
|
|
30
|
+
# — pass this explicitly to opt into overwriting there)
|
|
31
|
+
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
|
|
34
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
|
|
36
|
+
die() { echo "error: $*" >&2; exit 1; }
|
|
37
|
+
|
|
38
|
+
# shellcheck source=engine/check-update.sh
|
|
39
|
+
source "$SCRIPT_DIR/engine/check-update.sh"
|
|
40
|
+
check_for_updates "$SCRIPT_DIR"
|
|
41
|
+
|
|
42
|
+
command -v jq >/dev/null 2>&1 || die "jq is not installed (required for this script)."
|
|
43
|
+
|
|
44
|
+
# --force/-y/--yes can appear anywhere in the arguments; the rest are positional.
|
|
45
|
+
FORCE=0
|
|
46
|
+
ARGS=()
|
|
47
|
+
for a in "$@"; do
|
|
48
|
+
case "$a" in
|
|
49
|
+
--force|-y|--yes) FORCE=1 ;;
|
|
50
|
+
*) ARGS+=("$a") ;;
|
|
51
|
+
esac
|
|
52
|
+
done
|
|
53
|
+
set -- ${ARGS[@]+"${ARGS[@]}"}
|
|
54
|
+
|
|
55
|
+
TARGET_ARG="${1:-}"
|
|
56
|
+
if [[ -n "$TARGET_ARG" ]]; then
|
|
57
|
+
TARGET_DIR="$(cd "$TARGET_ARG" && pwd)"
|
|
58
|
+
elif ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"; then
|
|
59
|
+
TARGET_DIR="$ROOT"
|
|
60
|
+
else
|
|
61
|
+
TARGET_DIR="$(pwd)"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Two steps (not `basename ... | tr ...` in one): basename's own trailing newline would
|
|
65
|
+
# otherwise be fed into tr and get translated into a stray trailing "-" (tr -c matches it
|
|
66
|
+
# as a "non-allowed" byte too), leaving every directory-derived id with a spurious dash.
|
|
67
|
+
DEFAULT_ID="$(basename "$TARGET_DIR")"
|
|
68
|
+
DEFAULT_ID="$(printf '%s' "$DEFAULT_ID" | tr -c 'A-Za-z0-9_-' '-')"
|
|
69
|
+
PROJECT_ID="${2:-$DEFAULT_ID}"
|
|
70
|
+
LABEL="${3:-$PROJECT_ID}"
|
|
71
|
+
|
|
72
|
+
[[ "$PROJECT_ID" =~ ^[A-Za-z0-9_-]+$ ]] || die "invalid project-id (only A-Za-z0-9_- allowed): $PROJECT_ID"
|
|
73
|
+
|
|
74
|
+
echo "Install target : $TARGET_DIR"
|
|
75
|
+
echo "Project id : $PROJECT_ID"
|
|
76
|
+
echo "Label : $LABEL"
|
|
77
|
+
echo
|
|
78
|
+
|
|
79
|
+
# Ask before overwriting an already-generated file. Skips (returns 1) without asking in a
|
|
80
|
+
# non-interactive session — pass --force/-y to overwrite there instead.
|
|
81
|
+
confirm_overwrite() {
|
|
82
|
+
local path="$1"
|
|
83
|
+
[[ -e "$path" ]] || return 0
|
|
84
|
+
if [[ "$FORCE" == "1" ]]; then
|
|
85
|
+
echo "overwriting (--force): $path"
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
if [[ ! -t 0 ]]; then
|
|
89
|
+
echo "skipped (already exists, non-interactive session — pass --force to overwrite): $path" >&2
|
|
90
|
+
return 1
|
|
91
|
+
fi
|
|
92
|
+
local reply
|
|
93
|
+
read -r -p "File already exists: $path — overwrite? [y/N] " reply || reply="n"
|
|
94
|
+
if [[ "$reply" =~ ^[Yy]$ ]]; then
|
|
95
|
+
return 0
|
|
96
|
+
fi
|
|
97
|
+
echo "skipped: $path"
|
|
98
|
+
return 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# 1) Registration + wrapper generation in claude-task-manager (host, no docker).
|
|
102
|
+
"$SCRIPT_DIR/engine/projects.sh" add "$PROJECT_ID" "$LABEL"
|
|
103
|
+
|
|
104
|
+
# 2) Target project's .claude/skills/task-manager/ directory.
|
|
105
|
+
SKILL_DIR="$TARGET_DIR/.claude/skills/task-manager"
|
|
106
|
+
mkdir -p "$SKILL_DIR"
|
|
107
|
+
|
|
108
|
+
# task.sh — a copy of the generated wrapper in the target project, so the usual
|
|
109
|
+
# ".claude/skills/task-manager/task.sh" relative calling pattern keeps working unchanged.
|
|
110
|
+
if confirm_overwrite "$SKILL_DIR/task.sh"; then
|
|
111
|
+
cp "$SCRIPT_DIR/wrappers/$PROJECT_ID.sh" "$SKILL_DIR/task.sh"
|
|
112
|
+
chmod +x "$SKILL_DIR/task.sh"
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# SKILL.md — fill in the template.
|
|
116
|
+
if confirm_overwrite "$SKILL_DIR/SKILL.md"; then
|
|
117
|
+
sed \
|
|
118
|
+
-e "s#__PROJECT_LABEL__#$LABEL#g" \
|
|
119
|
+
-e "s#__PROJECT_ID__#$PROJECT_ID#g" \
|
|
120
|
+
-e "s#__TASK_SH_PATH__#$SKILL_DIR/task.sh#g" \
|
|
121
|
+
"$SCRIPT_DIR/templates/SKILL.md.tmpl" > "$SKILL_DIR/SKILL.md"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# 3) Generic ctm-* teammate definitions (.claude/agents/) — NOT project-specific; they read
|
|
125
|
+
# the concrete stack/conventions from the target project's own documentation.
|
|
126
|
+
AGENTS_DIR="$TARGET_DIR/.claude/agents"
|
|
127
|
+
mkdir -p "$AGENTS_DIR"
|
|
128
|
+
installed_agents=()
|
|
129
|
+
for tmpl in "$SCRIPT_DIR"/templates/agents/*.md.tmpl; do
|
|
130
|
+
[[ -e "$tmpl" ]] || continue
|
|
131
|
+
name="$(basename "$tmpl" .md.tmpl)"
|
|
132
|
+
out="$AGENTS_DIR/$name.md"
|
|
133
|
+
if confirm_overwrite "$out"; then
|
|
134
|
+
sed \
|
|
135
|
+
-e "s#__PROJECT_LABEL__#$LABEL#g" \
|
|
136
|
+
-e "s#__PROJECT_ID__#$PROJECT_ID#g" \
|
|
137
|
+
-e "s#__TASK_SH_PATH__#$SKILL_DIR/task.sh#g" \
|
|
138
|
+
"$tmpl" > "$out"
|
|
139
|
+
installed_agents+=("$name")
|
|
140
|
+
fi
|
|
141
|
+
done
|
|
142
|
+
|
|
143
|
+
# 4) Hooks (.claude/hooks/) — PreToolUse auto-allow + PostToolUse inbox notification for
|
|
144
|
+
# task.sh calls, registered additively in .claude/settings.json.
|
|
145
|
+
HOOKS_DIR="$TARGET_DIR/.claude/hooks"
|
|
146
|
+
mkdir -p "$HOOKS_DIR"
|
|
147
|
+
installed_hooks=()
|
|
148
|
+
for tmpl in "$SCRIPT_DIR"/templates/hooks/*.sh.tmpl; do
|
|
149
|
+
[[ -e "$tmpl" ]] || continue
|
|
150
|
+
name="$(basename "$tmpl" .tmpl)"
|
|
151
|
+
out="$HOOKS_DIR/$name"
|
|
152
|
+
if confirm_overwrite "$out"; then
|
|
153
|
+
sed -e "s#__TARGET_DIR__#$TARGET_DIR#g" "$tmpl" > "$out"
|
|
154
|
+
chmod +x "$out"
|
|
155
|
+
installed_hooks+=("$name")
|
|
156
|
+
fi
|
|
157
|
+
done
|
|
158
|
+
|
|
159
|
+
SETTINGS_JSON_FILE="$TARGET_DIR/.claude/settings.json"
|
|
160
|
+
[[ -f "$SETTINGS_JSON_FILE" ]] || echo '{"hooks":{}}' > "$SETTINGS_JSON_FILE"
|
|
161
|
+
PRE_CMD="bash \"\$CLAUDE_PROJECT_DIR/.claude/hooks/allow-task-sh.sh\""
|
|
162
|
+
POST_CMD="bash \"\$CLAUDE_PROJECT_DIR/.claude/hooks/notify-inbox.sh\""
|
|
163
|
+
tmp="$(mktemp)"
|
|
164
|
+
jq --arg pre "$PRE_CMD" --arg post "$POST_CMD" '
|
|
165
|
+
.hooks.PreToolUse //= [] |
|
|
166
|
+
.hooks.PreToolUse |= (
|
|
167
|
+
if any(.[]; (.hooks // [])[]?.command == $pre) then .
|
|
168
|
+
else . + [{matcher:"Bash", hooks:[{type:"command", command:$pre, timeout:10}]}] end
|
|
169
|
+
) |
|
|
170
|
+
.hooks.PostToolUse //= [] |
|
|
171
|
+
.hooks.PostToolUse |= (
|
|
172
|
+
if any(.[]; (.hooks // [])[]?.command == $post) then .
|
|
173
|
+
else . + [{matcher:"Bash", hooks:[{type:"command", command:$post, timeout:10}]}] end
|
|
174
|
+
)
|
|
175
|
+
' "$SETTINGS_JSON_FILE" > "$tmp" && mv "$tmp" "$SETTINGS_JSON_FILE"
|
|
176
|
+
|
|
177
|
+
# 5) Extend .claude/settings.local.json's Bash allowlist (keeps the existing file, only
|
|
178
|
+
# appends — never overwrites other permissions already present).
|
|
179
|
+
SETTINGS_FILE="$TARGET_DIR/.claude/settings.local.json"
|
|
180
|
+
mkdir -p "$TARGET_DIR/.claude"
|
|
181
|
+
ENTRIES_JSON=$(jq -n --arg abs "$SKILL_DIR/task.sh" '
|
|
182
|
+
[
|
|
183
|
+
("Bash(" + $abs + ":*)"),
|
|
184
|
+
"Bash(.claude/skills/task-manager/task.sh:*)",
|
|
185
|
+
"Bash(bash .claude/skills/task-manager/task.sh:*)",
|
|
186
|
+
"Bash(./task.sh:*)",
|
|
187
|
+
"Bash(bash ./task.sh:*)"
|
|
188
|
+
]')
|
|
189
|
+
if [[ -f "$SETTINGS_FILE" ]]; then
|
|
190
|
+
tmp="$(mktemp)"
|
|
191
|
+
jq --argjson entries "$ENTRIES_JSON" '
|
|
192
|
+
.permissions //= {} | .permissions.allow //= [] |
|
|
193
|
+
.permissions.allow = ((.permissions.allow + $entries) | unique)
|
|
194
|
+
' "$SETTINGS_FILE" > "$tmp" && mv "$tmp" "$SETTINGS_FILE"
|
|
195
|
+
else
|
|
196
|
+
jq -n --argjson entries "$ENTRIES_JSON" '{permissions:{allow:$entries}}' > "$SETTINGS_FILE"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
# 6) Register the global `ctm` command (if not already on PATH) — so "ctm init"/"ctm
|
|
200
|
+
# list"/"ctm up" are reachable from any project without remembering the absolute
|
|
201
|
+
# install.sh path.
|
|
202
|
+
ensure_ctm_command() {
|
|
203
|
+
if command -v ctm >/dev/null 2>&1; then
|
|
204
|
+
return 0
|
|
205
|
+
fi
|
|
206
|
+
local target="$SCRIPT_DIR/bin/ctm"
|
|
207
|
+
local dir candidates=("/usr/local/bin" "$HOME/.local/bin")
|
|
208
|
+
for dir in "${candidates[@]}"; do
|
|
209
|
+
if [[ -d "$dir" && -w "$dir" ]]; then
|
|
210
|
+
ln -sf "$target" "$dir/ctm"
|
|
211
|
+
echo "ctm command registered: $dir/ctm -> $target"
|
|
212
|
+
[[ ":$PATH:" == *":$dir:"* ]] || echo " NOTE: $dir is not on PATH — add it: export PATH=\"$dir:\$PATH\""
|
|
213
|
+
return 0
|
|
214
|
+
fi
|
|
215
|
+
done
|
|
216
|
+
mkdir -p "$HOME/.local/bin"
|
|
217
|
+
ln -sf "$target" "$HOME/.local/bin/ctm"
|
|
218
|
+
echo "ctm command registered: $HOME/.local/bin/ctm -> $target"
|
|
219
|
+
[[ ":$PATH:" == *":$HOME/.local/bin:"* ]] || echo " NOTE: $HOME/.local/bin is not on PATH — add it: export PATH=\"$HOME/.local/bin:\$PATH\""
|
|
220
|
+
}
|
|
221
|
+
ensure_ctm_command
|
|
222
|
+
|
|
223
|
+
echo
|
|
224
|
+
echo "Done."
|
|
225
|
+
echo " task.sh : $SKILL_DIR/task.sh (host-bash, docker NOT required)"
|
|
226
|
+
echo " SKILL.md : $SKILL_DIR/SKILL.md"
|
|
227
|
+
echo " agents : ${installed_agents[*]:-(none written)} ($AGENTS_DIR)"
|
|
228
|
+
echo " hooks : ${installed_hooks[*]:-(none written)} ($HOOKS_DIR) — registered in $SETTINGS_JSON_FILE"
|
|
229
|
+
echo " allowlist : $SETTINGS_FILE extended"
|
|
230
|
+
echo " Board : ctm up → http://localhost:3333/"
|
|
231
|
+
echo " (pick \"$LABEL\" in the Source selector)"
|
package/js/ApiClient.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { I18n } from "./i18n.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ApiClient — the board's WRITE client toward the api/index.php bridge.
|
|
5
|
+
*
|
|
6
|
+
* Writes go EXCLUSIVELY through `POST api/index.php`, which calls engine/task.sh on the
|
|
7
|
+
* server (`--as <actor>`, `TM_DIR=<project data dir>`). The browser never writes the JSON
|
|
8
|
+
* directly → task.sh's sole-writer invariant is preserved (atomic lock, history, events,
|
|
9
|
+
* inbox). Multi-project: every request states WHICH registered project it targets (the
|
|
10
|
+
* server allowlists this against data/projects.json).
|
|
11
|
+
*
|
|
12
|
+
* Every write also carries the board's current UI language (`lang`). The server persists
|
|
13
|
+
* it as the project's preferred language (NOT inside any task) — engine/task.sh then prints
|
|
14
|
+
* a language reminder whenever an agent next runs it, so the agent replies/works in the
|
|
15
|
+
* language the human was using on the board.
|
|
16
|
+
*/
|
|
17
|
+
export class ApiClient {
|
|
18
|
+
/**
|
|
19
|
+
* @param getActor - () => string : the current actor (agent name) for --as
|
|
20
|
+
* @param getProject - () => string : the currently selected project id
|
|
21
|
+
*/
|
|
22
|
+
constructor(getActor, getProject) {
|
|
23
|
+
this.getActor = getActor;
|
|
24
|
+
this.getProject = getProject;
|
|
25
|
+
this.enabled = location.protocol === "http:" || location.protocol === "https:";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One task.sh command. args: string[]. Throws on error (the caller shows it in the banner). */
|
|
29
|
+
cmd(cmd, args = []) {
|
|
30
|
+
return this.run([{ cmd, args }]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Multiple task.sh commands in ONE request, in order (e.g. "changes needed" = note + status).
|
|
35
|
+
* The server stops at the first failing step → no half-applied transition.
|
|
36
|
+
* @param ops - [{ cmd, args }]
|
|
37
|
+
*/
|
|
38
|
+
async run(ops) {
|
|
39
|
+
if (!this.enabled)
|
|
40
|
+
throw new Error(I18n.t("app.notEnabled"));
|
|
41
|
+
const as = (this.getActor() || "").trim();
|
|
42
|
+
if (!as)
|
|
43
|
+
throw new Error(I18n.t("app.setActorFirst"));
|
|
44
|
+
const project = (this.getProject() || "").trim();
|
|
45
|
+
if (!project)
|
|
46
|
+
throw new Error(I18n.t("app.noProjectSelected"));
|
|
47
|
+
|
|
48
|
+
// Relative path: index.html lives at the project root, so this resolves to
|
|
49
|
+
// api/index.php (port/host-independent).
|
|
50
|
+
const res = await fetch("api/index.php", {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "Content-Type": "application/json" },
|
|
53
|
+
body: JSON.stringify({ as, project, lang: I18n.lang, ops }),
|
|
54
|
+
});
|
|
55
|
+
let data = {};
|
|
56
|
+
try { data = await res.json(); } catch { /* non-JSON response */ }
|
|
57
|
+
if (!res.ok || !data.ok) {
|
|
58
|
+
const first = Array.isArray(data.results) ? data.results.find(r => !r.ok) : null;
|
|
59
|
+
throw new Error((first && (first.err || first.cmd)) || data.error || ("HTTP " + res.status));
|
|
60
|
+
}
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
}
|