dsh-tacit 0.2.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/LICENSE +21 -0
- package/README.md +83 -0
- package/README.zh.md +77 -0
- package/client/client.js +1691 -0
- package/cordis.patch.yml +15 -0
- package/lib/analyze.js +893 -0
- package/lib/fold.js +300 -0
- package/lib/index.js +86 -0
- package/lib/routes.js +152 -0
- package/lib/schema.js +294 -0
- package/lib/service.js +1044 -0
- package/lib/store.js +219 -0
- package/package.json +104 -0
package/lib/store.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 hackernotfound — https://github.com/hackernotfound/dsh-tacit
|
|
3
|
+
/**
|
|
4
|
+
* dsh-tacit — local storage (plugin-owned only).
|
|
5
|
+
*
|
|
6
|
+
* Everything lives under $DSH_HOME/storages/tacit/:
|
|
7
|
+
* config.patch.json UI-written config fields (loader/YAML config is the base)
|
|
8
|
+
* profile.json persistent user mistake profile
|
|
9
|
+
* reports/<sessionId>/<turn>.json analysis reports
|
|
10
|
+
*
|
|
11
|
+
* Safety rules (hard constraints):
|
|
12
|
+
* - writes are atomic (temp file + rename) and never truncate an existing
|
|
13
|
+
* file in place;
|
|
14
|
+
* - the ONLY deletion this plugin ever performs is `clearReports()`, which
|
|
15
|
+
* unlinks files matching /^\d+\.json$/ inside its own reports directory
|
|
16
|
+
* (then removes that directory only if empty) — nothing else on disk;
|
|
17
|
+
* - session ids are sanitized before touching the filesystem (no traversal).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs'
|
|
21
|
+
import path from 'node:path'
|
|
22
|
+
|
|
23
|
+
export function emptyProfile() {
|
|
24
|
+
return {
|
|
25
|
+
analyzedCount: 0,
|
|
26
|
+
patterns: [],
|
|
27
|
+
updatedAt: 0,
|
|
28
|
+
styleRules: [],
|
|
29
|
+
feedbackLog: [],
|
|
30
|
+
pendingDistill: 0,
|
|
31
|
+
directives: [],
|
|
32
|
+
analysesSinceDirectives: 0,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class CoachStore {
|
|
37
|
+
constructor(root) {
|
|
38
|
+
this.root = root
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ensureDir(dir) {
|
|
42
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
readJson(file, fallback) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
48
|
+
} catch {
|
|
49
|
+
return fallback
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
writeJsonAtomic(file, value) {
|
|
54
|
+
this.ensureDir(path.dirname(file))
|
|
55
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`
|
|
56
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2), 'utf8')
|
|
57
|
+
fs.renameSync(tmp, file)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Filesystem-safe session id (the browser id never reaches a path verbatim). */
|
|
61
|
+
safeSessionId(sessionId) {
|
|
62
|
+
const value = String(sessionId ?? '').replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 128)
|
|
63
|
+
return value.length > 0 ? value : 'unknown'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
configPatch() {
|
|
67
|
+
const value = this.readJson(path.join(this.root, 'config.patch.json'), {})
|
|
68
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
saveConfigPatch(patch) {
|
|
72
|
+
this.writeJsonAtomic(path.join(this.root, 'config.patch.json'), patch)
|
|
73
|
+
return patch
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
profile() {
|
|
77
|
+
const value = this.readJson(path.join(this.root, 'profile.json'), emptyProfile())
|
|
78
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
79
|
+
? { ...emptyProfile(), ...value }
|
|
80
|
+
: emptyProfile()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
saveProfile(profile) {
|
|
84
|
+
this.writeJsonAtomic(path.join(this.root, 'profile.json'), profile)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** {date: 'YYYY-MM-DD', count} of automatic analyses spent today. */
|
|
88
|
+
autoLedger(date) {
|
|
89
|
+
const value = this.readJson(path.join(this.root, 'auto.json'), null)
|
|
90
|
+
if (value !== null && typeof value === 'object' && value.date === date && typeof value.count === 'number') {
|
|
91
|
+
return { date, count: Math.max(0, Math.round(value.count)) }
|
|
92
|
+
}
|
|
93
|
+
return { date, count: 0 }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
bumpAuto(date) {
|
|
97
|
+
const ledger = this.autoLedger(date)
|
|
98
|
+
const next = { date, count: ledger.count + 1 }
|
|
99
|
+
this.writeJsonAtomic(path.join(this.root, 'auto.json'), next)
|
|
100
|
+
return next
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
reportFile(sessionId, turn) {
|
|
104
|
+
return path.join(this.root, 'reports', this.safeSessionId(sessionId), `${turn}.json`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
report(sessionId, turn) {
|
|
108
|
+
const value = this.readJson(this.reportFile(sessionId, turn), null)
|
|
109
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
saveReport(sessionId, turn, report) {
|
|
113
|
+
this.writeJsonAtomic(this.reportFile(sessionId, turn), report)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Latest analysis reports across EVERY session (for the settings/sidebar
|
|
118
|
+
* panel), newest first, capped at `limit`. Only the plugin's own
|
|
119
|
+
* /^\d+\.json$/ files are read.
|
|
120
|
+
*/
|
|
121
|
+
listAllReports(limit = 50) {
|
|
122
|
+
const root = path.join(this.root, 'reports')
|
|
123
|
+
let sessionDirs = []
|
|
124
|
+
try {
|
|
125
|
+
sessionDirs = fs.readdirSync(root)
|
|
126
|
+
} catch {
|
|
127
|
+
return []
|
|
128
|
+
}
|
|
129
|
+
const entries = []
|
|
130
|
+
for (const name of sessionDirs) {
|
|
131
|
+
if (!/^[A-Za-z0-9._-]{1,128}$/.test(name)) continue
|
|
132
|
+
const sessionDir = path.join(root, name)
|
|
133
|
+
let files = []
|
|
134
|
+
try {
|
|
135
|
+
files = fs.readdirSync(sessionDir)
|
|
136
|
+
} catch {
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
const match = /^(\d+)\.json$/.exec(file)
|
|
141
|
+
if (match === null) continue
|
|
142
|
+
const report = this.readJson(path.join(sessionDir, file), null)
|
|
143
|
+
if (report === null || typeof report !== 'object' || Array.isArray(report)) continue
|
|
144
|
+
entries.push({
|
|
145
|
+
sessionId: name,
|
|
146
|
+
turn: Number(match[1]),
|
|
147
|
+
time: typeof report.time === 'number' ? report.time : 0,
|
|
148
|
+
model: typeof report.model === 'string' ? report.model : '',
|
|
149
|
+
promptExcerpt: typeof report.promptExcerpt === 'string' ? report.promptExcerpt : '',
|
|
150
|
+
improvedPrompt: typeof report.improvedPrompt === 'string' ? report.improvedPrompt : '',
|
|
151
|
+
trigger: typeof report.trigger === 'string' ? report.trigger : 'manual',
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
entries.sort((a, b) => b.time - a.time)
|
|
156
|
+
return entries.slice(0, Math.max(0, Math.min(500, Math.floor(Number(limit) || 50))))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
listReports(sessionId) {
|
|
160
|
+
const dir = path.join(this.root, 'reports', this.safeSessionId(sessionId))
|
|
161
|
+
let names = []
|
|
162
|
+
try {
|
|
163
|
+
names = fs.readdirSync(dir)
|
|
164
|
+
} catch {
|
|
165
|
+
return []
|
|
166
|
+
}
|
|
167
|
+
const out = []
|
|
168
|
+
for (const name of names) {
|
|
169
|
+
const match = /^(\d+)\.json$/.exec(name)
|
|
170
|
+
if (match === null) continue
|
|
171
|
+
const report = this.readJson(path.join(dir, name), null)
|
|
172
|
+
if (report !== null && typeof report === 'object' && !Array.isArray(report)) {
|
|
173
|
+
out.push({ turn: Number(match[1]), report })
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return out
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Remove only plugin-created report files (strict /^\d+\.json$/ naming)
|
|
181
|
+
* inside the plugin's own reports directory; the per-session directory is
|
|
182
|
+
* removed only when empty. Returns the number of files removed.
|
|
183
|
+
*/
|
|
184
|
+
clearReports() {
|
|
185
|
+
const root = path.join(this.root, 'reports')
|
|
186
|
+
let removed = 0
|
|
187
|
+
let sessionDirs = []
|
|
188
|
+
try {
|
|
189
|
+
sessionDirs = fs.readdirSync(root)
|
|
190
|
+
} catch {
|
|
191
|
+
return 0
|
|
192
|
+
}
|
|
193
|
+
for (const name of sessionDirs) {
|
|
194
|
+
if (!/^[A-Za-z0-9._-]{1,128}$/.test(name)) continue
|
|
195
|
+
const sessionDir = path.join(root, name)
|
|
196
|
+
let files = []
|
|
197
|
+
try {
|
|
198
|
+
files = fs.readdirSync(sessionDir)
|
|
199
|
+
} catch {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
if (!/^\d+\.json$/.test(file)) continue
|
|
204
|
+
try {
|
|
205
|
+
fs.unlinkSync(path.join(sessionDir, file))
|
|
206
|
+
removed += 1
|
|
207
|
+
} catch {
|
|
208
|
+
// Keep going: one unreadable file must not block the rest.
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
fs.rmdirSync(sessionDir)
|
|
213
|
+
} catch {
|
|
214
|
+
// Only removed when empty; any leftover means the directory stays.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return removed
|
|
218
|
+
}
|
|
219
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-tacit",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Tacit learns what you leave unsaid in your prompts — from messy turns and your own corrections, with zero clicks — and tells the agent how to compensate, on every turn, via a system-prompt section you can read and edit.",
|
|
5
|
+
"author": "hackernotfound",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/hackernotfound/dsh-tacit.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/hackernotfound/dsh-tacit#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/hackernotfound/dsh-tacit/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22"
|
|
18
|
+
},
|
|
19
|
+
"main": "lib/index.js",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"default": "./lib/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./client": {
|
|
25
|
+
"default": "./client/client.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"lib",
|
|
31
|
+
"client",
|
|
32
|
+
"cordis.patch.yml",
|
|
33
|
+
"README.md",
|
|
34
|
+
"README.zh.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "node --test",
|
|
39
|
+
"smoke": "node scripts/smoke.mjs"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dsh": {
|
|
45
|
+
"bundle": {
|
|
46
|
+
"patch": "./cordis.patch.yml"
|
|
47
|
+
},
|
|
48
|
+
"client": {
|
|
49
|
+
"platform": "web"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"dshhub": {
|
|
53
|
+
"schemaVersion": 1,
|
|
54
|
+
"displayName": "Tacit",
|
|
55
|
+
"summary": "Learns what you leave unsaid (auto-analysis of messy turns and your corrections) and steers the agent for you through an editable system-prompt section. Optional ✨ Improve rewrite and opt-in pre-send context.",
|
|
56
|
+
"categories": [
|
|
57
|
+
"prompt",
|
|
58
|
+
"workflow",
|
|
59
|
+
"web-ui"
|
|
60
|
+
],
|
|
61
|
+
"surfaces": [
|
|
62
|
+
"host",
|
|
63
|
+
"web"
|
|
64
|
+
],
|
|
65
|
+
"capabilities": {
|
|
66
|
+
"provides": [
|
|
67
|
+
"service:tacit"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"compatibility": {
|
|
71
|
+
"dsh": ">=0.1.1-rc.1",
|
|
72
|
+
"node": ">=22"
|
|
73
|
+
},
|
|
74
|
+
"permissions": {
|
|
75
|
+
"network": [
|
|
76
|
+
"https://api.deepseek.com"
|
|
77
|
+
],
|
|
78
|
+
"filesystem": [
|
|
79
|
+
"filesystem:read",
|
|
80
|
+
"filesystem:write"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"dependencies": {
|
|
85
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.1-rc.2",
|
|
86
|
+
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
87
|
+
"zod": "^4.4.3"
|
|
88
|
+
},
|
|
89
|
+
"devDependencies": {
|
|
90
|
+
"react": "^18.3.1",
|
|
91
|
+
"react-dom": "^18.3.1"
|
|
92
|
+
},
|
|
93
|
+
"keywords": [
|
|
94
|
+
"deepseek",
|
|
95
|
+
"deepseek-harness",
|
|
96
|
+
"dsh",
|
|
97
|
+
"dsh-plugin",
|
|
98
|
+
"prompt",
|
|
99
|
+
"prompt-engineering",
|
|
100
|
+
"system-prompt",
|
|
101
|
+
"personalization",
|
|
102
|
+
"tacit"
|
|
103
|
+
]
|
|
104
|
+
}
|