@tekyzinc/gsd-t 2.23.0 → 2.24.6
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/CHANGELOG.md +94 -0
- package/README.md +14 -3
- package/bin/gsd-t.js +1381 -1300
- package/commands/gsd-t-complete-milestone.md +12 -12
- package/commands/gsd-t-debug.md +4 -4
- package/commands/gsd-t-discuss.md +7 -9
- package/commands/gsd-t-execute.md +5 -5
- package/commands/gsd-t-feature.md +2 -2
- package/commands/gsd-t-impact.md +9 -3
- package/commands/gsd-t-init.md +12 -12
- package/commands/gsd-t-integrate.md +5 -5
- package/commands/gsd-t-milestone.md +3 -3
- package/commands/gsd-t-partition.md +4 -4
- package/commands/gsd-t-plan.md +6 -6
- package/commands/gsd-t-project.md +3 -3
- package/commands/gsd-t-promote-debt.md +3 -3
- package/commands/gsd-t-qa.md +63 -0
- package/commands/gsd-t-quick.md +4 -4
- package/commands/gsd-t-scan.md +3 -3
- package/commands/gsd-t-test-sync.md +9 -9
- package/commands/gsd-t-verify.md +6 -6
- package/commands/gsd-t-wave.md +45 -3
- package/docs/GSD-T-README.md +12 -0
- package/docs/architecture.md +134 -14
- package/docs/infrastructure.md +33 -11
- package/docs/requirements.md +41 -11
- package/docs/workflows.md +86 -33
- package/package.json +4 -3
- package/scripts/gsd-t-fetch-version.js +25 -0
- package/scripts/gsd-t-heartbeat.js +180 -201
- package/scripts/gsd-t-update-check.js +79 -0
- package/scripts/npm-update-check.js +42 -27
- package/templates/CLAUDE-global.md +10 -3
|
@@ -1,201 +1,180 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* GSD-T Heartbeat — Claude Code Hook Event Writer
|
|
5
|
-
*
|
|
6
|
-
* Writes structured events to .gsd-t/heartbeat-{session_id}.jsonl
|
|
7
|
-
* Installed as an async hook for multiple Claude Code events.
|
|
8
|
-
*
|
|
9
|
-
* Events captured:
|
|
10
|
-
* SessionStart, PostToolUse, SubagentStart, SubagentStop,
|
|
11
|
-
* TaskCompleted, TeammateIdle, Notification, Stop, SessionEnd
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const fs = require("fs");
|
|
15
|
-
const path = require("path");
|
|
16
|
-
|
|
17
|
-
const MAX_STDIN = 1024 * 1024; // 1MB — prevent OOM from unbounded input
|
|
18
|
-
const SAFE_SID = /^[a-zA-Z0-9_-]+$/; // Allowlist for session_id — blocks path traversal
|
|
19
|
-
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days — auto-cleanup threshold
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
case "
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
case "
|
|
146
|
-
return {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
case "
|
|
161
|
-
return { file: shortPath(input.
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
case "NotebookEdit":
|
|
182
|
-
return { file: shortPath(input.notebook_path) };
|
|
183
|
-
default:
|
|
184
|
-
return {};
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function shortPath(p) {
|
|
189
|
-
if (!p) return null;
|
|
190
|
-
// Convert absolute paths to relative for readability
|
|
191
|
-
const cwd = process.cwd();
|
|
192
|
-
if (p.startsWith(cwd)) {
|
|
193
|
-
return p.slice(cwd.length + 1).replace(/\\/g, "/");
|
|
194
|
-
}
|
|
195
|
-
// For home-dir paths, abbreviate
|
|
196
|
-
const home = require("os").homedir();
|
|
197
|
-
if (p.startsWith(home)) {
|
|
198
|
-
return "~" + p.slice(home.length).replace(/\\/g, "/");
|
|
199
|
-
}
|
|
200
|
-
return p.replace(/\\/g, "/");
|
|
201
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GSD-T Heartbeat — Claude Code Hook Event Writer
|
|
5
|
+
*
|
|
6
|
+
* Writes structured events to .gsd-t/heartbeat-{session_id}.jsonl
|
|
7
|
+
* Installed as an async hook for multiple Claude Code events.
|
|
8
|
+
*
|
|
9
|
+
* Events captured:
|
|
10
|
+
* SessionStart, PostToolUse, SubagentStart, SubagentStop,
|
|
11
|
+
* TaskCompleted, TeammateIdle, Notification, Stop, SessionEnd
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
|
|
17
|
+
const MAX_STDIN = 1024 * 1024; // 1MB — prevent OOM from unbounded input
|
|
18
|
+
const SAFE_SID = /^[a-zA-Z0-9_-]+$/; // Allowlist for session_id — blocks path traversal
|
|
19
|
+
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days — auto-cleanup threshold
|
|
20
|
+
|
|
21
|
+
// ─── Exports (for testing) ───────────────────────────────────────────────────
|
|
22
|
+
module.exports = { scrubSecrets, scrubUrl, buildEvent, summarize, shortPath };
|
|
23
|
+
|
|
24
|
+
// ─── Main (stdin processing) ─────────────────────────────────────────────────
|
|
25
|
+
if (require.main === module) {
|
|
26
|
+
|
|
27
|
+
let input = "";
|
|
28
|
+
let aborted = false;
|
|
29
|
+
process.stdin.setEncoding("utf8");
|
|
30
|
+
process.stdin.on("data", (d) => {
|
|
31
|
+
input += d;
|
|
32
|
+
if (input.length > MAX_STDIN) {
|
|
33
|
+
aborted = true;
|
|
34
|
+
process.stdin.destroy();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
process.stdin.on("end", () => {
|
|
38
|
+
if (aborted) return; // Silently discard oversized input
|
|
39
|
+
try {
|
|
40
|
+
const hook = JSON.parse(input);
|
|
41
|
+
const dir = hook.cwd || process.cwd();
|
|
42
|
+
|
|
43
|
+
// Validate cwd is absolute path
|
|
44
|
+
if (!path.isAbsolute(dir)) return;
|
|
45
|
+
|
|
46
|
+
const gsdtDir = path.join(dir, ".gsd-t");
|
|
47
|
+
if (!fs.existsSync(gsdtDir)) return;
|
|
48
|
+
|
|
49
|
+
const sid = hook.session_id || "unknown";
|
|
50
|
+
|
|
51
|
+
// Validate session_id — block path traversal (e.g., "../../etc/evil")
|
|
52
|
+
if (!SAFE_SID.test(sid)) return;
|
|
53
|
+
|
|
54
|
+
const file = path.join(gsdtDir, `heartbeat-${sid}.jsonl`);
|
|
55
|
+
|
|
56
|
+
// Verify resolved path is still within .gsd-t/ directory
|
|
57
|
+
const resolvedFile = path.resolve(file);
|
|
58
|
+
const resolvedDir = path.resolve(gsdtDir);
|
|
59
|
+
if (!resolvedFile.startsWith(resolvedDir + path.sep)) return;
|
|
60
|
+
|
|
61
|
+
const event = buildEvent(hook);
|
|
62
|
+
if (event) {
|
|
63
|
+
if (hook.hook_event_name === "SessionStart") cleanupOldHeartbeats(gsdtDir);
|
|
64
|
+
// Symlink check — prevent redirection of event data to arbitrary files
|
|
65
|
+
try { if (fs.lstatSync(file).isSymbolicLink()) return; } catch { /* file doesn't exist yet — safe */ }
|
|
66
|
+
fs.appendFileSync(file, JSON.stringify(event) + "\n");
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Silent failure — never interfere with Claude Code
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
} // end require.main
|
|
74
|
+
|
|
75
|
+
function cleanupOldHeartbeats(gsdtDir) {
|
|
76
|
+
try {
|
|
77
|
+
const files = fs.readdirSync(gsdtDir);
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
for (const f of files) {
|
|
80
|
+
if (!f.startsWith("heartbeat-") || !f.endsWith(".jsonl")) continue;
|
|
81
|
+
const fp = path.join(gsdtDir, f);
|
|
82
|
+
const stat = fs.lstatSync(fp);
|
|
83
|
+
if (stat.isSymbolicLink()) continue; // Don't follow symlinks
|
|
84
|
+
if (now - stat.mtimeMs > MAX_AGE_MS) {
|
|
85
|
+
fs.unlinkSync(fp);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Silent failure — never interfere with Claude Code
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const EVENT_HANDLERS = {
|
|
94
|
+
SessionStart: (h) => ({ evt: "session_start", data: { source: h.source, model: h.model } }),
|
|
95
|
+
PostToolUse: (h) => ({ evt: "tool", tool: h.tool_name, data: summarize(h.tool_name, h.tool_input) }),
|
|
96
|
+
SubagentStart: (h) => ({ evt: "agent_spawn", data: { agent_id: h.agent_id, agent_type: h.agent_type } }),
|
|
97
|
+
SubagentStop: (h) => ({ evt: "agent_stop", data: { agent_id: h.agent_id, agent_type: h.agent_type } }),
|
|
98
|
+
TaskCompleted: (h) => ({ evt: "task_done", data: { task: h.task_subject, agent: h.teammate_name } }),
|
|
99
|
+
TeammateIdle: (h) => ({ evt: "agent_idle", data: { agent: h.teammate_name, team: h.team_name } }),
|
|
100
|
+
Notification: (h) => ({ evt: "notification", data: { message: scrubSecrets(h.message), title: scrubSecrets(h.title) } }),
|
|
101
|
+
Stop: () => ({ evt: "session_stop" }),
|
|
102
|
+
SessionEnd: (h) => ({ evt: "session_end", data: { reason: h.reason } }),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function buildEvent(hook) {
|
|
106
|
+
const handler = EVENT_HANDLERS[hook.hook_event_name];
|
|
107
|
+
if (!handler) return null;
|
|
108
|
+
return { ts: new Date().toISOString(), sid: hook.session_id, ...handler(hook) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Patterns that indicate sensitive values in CLI commands
|
|
112
|
+
const SECRET_FLAGS = /(--(password|token|secret|api[-_]?key|auth|credential|private[-_]?key)[\s=])\S+/gi;
|
|
113
|
+
const SECRET_SHORT = /(\s-p\s)\S+/gi;
|
|
114
|
+
const SECRET_ENV = /((API_KEY|SECRET|TOKEN|PASSWORD|BEARER|AUTH_TOKEN|PRIVATE_KEY|ACCESS_KEY|SECRET_KEY)=)\S+/gi;
|
|
115
|
+
const BEARER_HEADER = /(bearer\s+)\S+/gi;
|
|
116
|
+
|
|
117
|
+
function scrubSecrets(cmd) {
|
|
118
|
+
if (!cmd) return cmd;
|
|
119
|
+
return cmd
|
|
120
|
+
.replace(SECRET_FLAGS, "$1***")
|
|
121
|
+
.replace(SECRET_SHORT, "$1***")
|
|
122
|
+
.replace(SECRET_ENV, "$1***")
|
|
123
|
+
.replace(BEARER_HEADER, "$1***");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function scrubUrl(url) {
|
|
127
|
+
if (!url) return url;
|
|
128
|
+
try {
|
|
129
|
+
const u = new URL(url);
|
|
130
|
+
if (!u.search) return url;
|
|
131
|
+
for (const key of u.searchParams.keys()) {
|
|
132
|
+
u.searchParams.set(key, "***");
|
|
133
|
+
}
|
|
134
|
+
return u.toString();
|
|
135
|
+
} catch { return url; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function summarize(tool, input) {
|
|
139
|
+
if (!tool || !input) return {};
|
|
140
|
+
switch (tool) {
|
|
141
|
+
case "Read":
|
|
142
|
+
case "Edit":
|
|
143
|
+
case "Write":
|
|
144
|
+
return { file: shortPath(input.file_path) };
|
|
145
|
+
case "Bash":
|
|
146
|
+
return {
|
|
147
|
+
cmd: scrubSecrets((input.command || "").slice(0, 150)),
|
|
148
|
+
desc: input.description,
|
|
149
|
+
};
|
|
150
|
+
case "Grep":
|
|
151
|
+
return { pattern: input.pattern, path: shortPath(input.path) };
|
|
152
|
+
case "Glob":
|
|
153
|
+
return { pattern: input.pattern };
|
|
154
|
+
case "Task":
|
|
155
|
+
return { desc: input.description, type: input.subagent_type };
|
|
156
|
+
case "WebSearch":
|
|
157
|
+
return { query: input.query };
|
|
158
|
+
case "WebFetch":
|
|
159
|
+
return { url: scrubUrl(input.url) };
|
|
160
|
+
case "NotebookEdit":
|
|
161
|
+
return { file: shortPath(input.notebook_path) };
|
|
162
|
+
default:
|
|
163
|
+
return {};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function shortPath(p) {
|
|
168
|
+
if (!p) return null;
|
|
169
|
+
// Convert absolute paths to relative for readability
|
|
170
|
+
const cwd = process.cwd();
|
|
171
|
+
if (p.startsWith(cwd)) {
|
|
172
|
+
return p.slice(cwd.length + 1).replace(/\\/g, "/");
|
|
173
|
+
}
|
|
174
|
+
// For home-dir paths, abbreviate
|
|
175
|
+
const home = require("os").homedir();
|
|
176
|
+
if (p.startsWith(home)) {
|
|
177
|
+
return "~" + p.slice(home.length).replace(/\\/g, "/");
|
|
178
|
+
}
|
|
179
|
+
return p.replace(/\\/g, "/");
|
|
180
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GSD-T SessionStart hook — shows version banner, auto-updates if needed.
|
|
4
|
+
* Always outputs a version line. Auto-installs new versions when available.
|
|
5
|
+
*/
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
|
|
10
|
+
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
11
|
+
const VERSION_FILE = path.join(CLAUDE_DIR, ".gsd-t-version");
|
|
12
|
+
const CACHE_FILE = path.join(CLAUDE_DIR, ".gsd-t-update-check");
|
|
13
|
+
const CHANGELOG = "https://github.com/Tekyz-Inc/get-stuff-done-teams/blob/main/CHANGELOG.md";
|
|
14
|
+
|
|
15
|
+
function isNewer(a, b) {
|
|
16
|
+
const ap = a.split(".").map(Number);
|
|
17
|
+
const bp = b.split(".").map(Number);
|
|
18
|
+
for (let i = 0; i < 3; i++) {
|
|
19
|
+
if ((ap[i] || 0) > (bp[i] || 0)) return true;
|
|
20
|
+
if ((ap[i] || 0) < (bp[i] || 0)) return false;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Read installed version
|
|
27
|
+
if (!fs.existsSync(VERSION_FILE)) process.exit(0);
|
|
28
|
+
const installed = fs.readFileSync(VERSION_FILE, "utf8").trim();
|
|
29
|
+
if (!installed) process.exit(0);
|
|
30
|
+
|
|
31
|
+
// Read or create cache
|
|
32
|
+
let cached = null;
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
35
|
+
cached = JSON.parse(fs.readFileSync(CACHE_FILE, "utf8"));
|
|
36
|
+
}
|
|
37
|
+
} catch { /* ignore */ }
|
|
38
|
+
|
|
39
|
+
// Refresh cache if stale (>1h) or missing
|
|
40
|
+
const isStale = !cached || (Date.now() - cached.timestamp) > 3600000;
|
|
41
|
+
if (isStale) {
|
|
42
|
+
const { execSync } = require("child_process");
|
|
43
|
+
try {
|
|
44
|
+
const result = execSync(
|
|
45
|
+
`"${process.execPath}" -e "const h=require('https');h.get('https://registry.npmjs.org/@tekyzinc/gsd-t/latest',{timeout:5000},(r)=>{let d='';r.on('data',(c)=>d+=c);r.on('end',()=>{try{process.stdout.write(JSON.parse(d).version)}catch{}})}).on('error',()=>{})"`,
|
|
46
|
+
{ timeout: 8000, encoding: "utf8" }
|
|
47
|
+
).trim();
|
|
48
|
+
if (result) {
|
|
49
|
+
cached = { latest: result, timestamp: Date.now() };
|
|
50
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(cached));
|
|
51
|
+
}
|
|
52
|
+
} catch { /* network error — skip */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Auto-update if newer version available
|
|
56
|
+
if (cached && cached.latest && isNewer(cached.latest, installed)) {
|
|
57
|
+
const latest = cached.latest;
|
|
58
|
+
const { execSync } = require("child_process");
|
|
59
|
+
try {
|
|
60
|
+
// Install new version globally, then run update-all
|
|
61
|
+
execSync(`npm install -g @tekyzinc/gsd-t@${latest}`, {
|
|
62
|
+
timeout: 60000, encoding: "utf8", stdio: "pipe"
|
|
63
|
+
});
|
|
64
|
+
execSync("gsd-t update-all", {
|
|
65
|
+
timeout: 60000, encoding: "utf8", stdio: "pipe"
|
|
66
|
+
});
|
|
67
|
+
// Re-read version after update
|
|
68
|
+
const updated = fs.existsSync(VERSION_FILE)
|
|
69
|
+
? fs.readFileSync(VERSION_FILE, "utf8").trim()
|
|
70
|
+
: latest;
|
|
71
|
+
console.log(`[GSD-T AUTO-UPDATE] v${installed} → v${updated}. Changelog: ${CHANGELOG}`);
|
|
72
|
+
} catch {
|
|
73
|
+
// Auto-update failed — fall back to manual notice
|
|
74
|
+
console.log(`[GSD-T UPDATE] v${installed} — update available (v${installed} → v${latest}). Auto-update failed — run manually: /user:gsd-t-version-update-all. Changelog: ${CHANGELOG}`);
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
console.log(`[GSD-T] v${installed} — up to date. Changelog: ${CHANGELOG}`);
|
|
78
|
+
}
|
|
79
|
+
} catch { /* graceful failure — don't block session start */ }
|
|
@@ -1,27 +1,42 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Background update check — spawned detached by the CLI to refresh the version cache.
|
|
5
|
-
* Usage: node npm-update-check.js <cache-file-path>
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const https = require("https");
|
|
9
|
-
const fs = require("fs");
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Background update check — spawned detached by the CLI to refresh the version cache.
|
|
5
|
+
* Usage: node npm-update-check.js <cache-file-path>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const https = require("https");
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const os = require("os");
|
|
12
|
+
|
|
13
|
+
const cacheFile = process.argv[2];
|
|
14
|
+
if (!cacheFile) process.exit(1);
|
|
15
|
+
|
|
16
|
+
// Validate cache path is within ~/.claude/ to prevent arbitrary file writes
|
|
17
|
+
const resolved = path.resolve(cacheFile);
|
|
18
|
+
const claudeDir = path.join(os.homedir(), ".claude");
|
|
19
|
+
if (!resolved.startsWith(claudeDir + path.sep) && resolved !== claudeDir) {
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
https.get("https://registry.npmjs.org/@tekyzinc/gsd-t/latest",
|
|
24
|
+
{ timeout: 5000 }, (res) => {
|
|
25
|
+
const MAX_RESPONSE = 1024 * 1024; // 1MB limit
|
|
26
|
+
let d = "";
|
|
27
|
+
res.on("data", (c) => {
|
|
28
|
+
d += c;
|
|
29
|
+
if (d.length > MAX_RESPONSE) { res.destroy(); return; }
|
|
30
|
+
});
|
|
31
|
+
res.on("end", () => {
|
|
32
|
+
try {
|
|
33
|
+
const v = JSON.parse(d).version;
|
|
34
|
+
if (v && /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/.test(v)) {
|
|
35
|
+
// Symlink check — prevent redirection to arbitrary files
|
|
36
|
+
try { if (fs.lstatSync(cacheFile).isSymbolicLink()) return; } catch { /* doesn't exist yet — safe */ }
|
|
37
|
+
fs.writeFileSync(cacheFile,
|
|
38
|
+
JSON.stringify({ latest: v, timestamp: Date.now() }));
|
|
39
|
+
}
|
|
40
|
+
} catch { /* malformed response — skip */ }
|
|
41
|
+
});
|
|
42
|
+
}).on("error", () => {});
|
|
@@ -159,11 +159,17 @@ Even in development, the user may have:
|
|
|
159
159
|
|
|
160
160
|
## Update Notices
|
|
161
161
|
|
|
162
|
-
On session start, a version check hook outputs
|
|
162
|
+
On session start, a version check hook auto-updates GSD-T and outputs a status message. Show the result to the user at the **beginning** of your first response:
|
|
163
163
|
|
|
164
|
-
- If `[GSD-T UPDATE]` appears →
|
|
164
|
+
- If `[GSD-T AUTO-UPDATE]` appears → GSD-T was just auto-updated. Show:
|
|
165
165
|
```
|
|
166
|
-
|
|
166
|
+
✅ GSD-T auto-updated: v{old} → v{new}
|
|
167
|
+
Changelog: https://github.com/Tekyz-Inc/get-stuff-done-teams/blob/main/CHANGELOG.md
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- If `[GSD-T UPDATE]` appears → update available but auto-update failed. Show:
|
|
171
|
+
```
|
|
172
|
+
⬆️ GSD-T update available: v{installed} → v{latest} (auto-update failed)
|
|
167
173
|
Run: /user:gsd-t-version-update-all
|
|
168
174
|
Changelog: https://github.com/Tekyz-Inc/get-stuff-done-teams/blob/main/CHANGELOG.md
|
|
169
175
|
```
|
|
@@ -172,6 +178,7 @@ On session start, a version check hook outputs one of two messages. Show the res
|
|
|
172
178
|
- If `[GSD-T]` appears → up to date. Show:
|
|
173
179
|
```
|
|
174
180
|
GSD-T v{version} — up to date
|
|
181
|
+
Changelog: https://github.com/Tekyz-Inc/get-stuff-done-teams/blob/main/CHANGELOG.md
|
|
175
182
|
```
|
|
176
183
|
|
|
177
184
|
## Conversation vs. Work
|