@goodandready/dsh-goal 0.2.10 → 0.2.12
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/lib/client.js +142 -3
- package/lib/engine-issue-sync.js +128 -0
- package/lib/engine-store.js +4 -3
- package/lib/goal-engine.js +26 -8
- package/lib/index.js +50 -49
- package/lib/routes.js +76 -8
- package/lib/tools.js +10 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1609,7 +1609,7 @@ window.__ModuleLoader__.load({
|
|
|
1609
1609
|
if (!AudioCtx) return;
|
|
1610
1610
|
const actx = new AudioCtx();
|
|
1611
1611
|
if (actx.state === 'suspended') {
|
|
1612
|
-
actx.resume().catch(() => {});
|
|
1612
|
+
actx.resume().catch((err) => { console.warn('[dsh-goal] Fetch templates error:', err); });
|
|
1613
1613
|
}
|
|
1614
1614
|
const now = actx.currentTime;
|
|
1615
1615
|
if (scheme === 'retro') {
|
|
@@ -2590,9 +2590,51 @@ window.__ModuleLoader__.load({
|
|
|
2590
2590
|
function QuickLaunchModal({ onClose, onStart, t }) {
|
|
2591
2591
|
|
|
2592
2592
|
const [title, setTitle] = useState('');
|
|
2593
|
+
const [showTemplates, setShowTemplates] = useState(false);
|
|
2594
|
+
const [backendTemplates, setBackendTemplates] = useState([]);
|
|
2595
|
+
const [selectedTpl, setSelectedTpl] = useState(null);
|
|
2596
|
+
const [tplInputs, setTplInputs] = useState({});
|
|
2597
|
+
const [loadingTpls, setLoadingTpls] = useState(false);
|
|
2593
2598
|
|
|
2594
2599
|
const inputRef = useRef(null);
|
|
2595
2600
|
|
|
2601
|
+
useEffect(() => {
|
|
2602
|
+
if (showTemplates && backendTemplates.length === 0) {
|
|
2603
|
+
setLoadingTpls(true);
|
|
2604
|
+
fetch('/dsh-goal/templates')
|
|
2605
|
+
.then((r) => r.json())
|
|
2606
|
+
.then((d) => {
|
|
2607
|
+
if (d && Array.isArray(d.templates)) {
|
|
2608
|
+
setBackendTemplates(d.templates);
|
|
2609
|
+
}
|
|
2610
|
+
})
|
|
2611
|
+
.catch((err) => { console.warn('[dsh-goal] Fetch templates error:', err); })
|
|
2612
|
+
.finally(() => setLoadingTpls(false));
|
|
2613
|
+
}
|
|
2614
|
+
}, [showTemplates, backendTemplates.length]);
|
|
2615
|
+
|
|
2616
|
+
const handleApplyTemplate = async () => {
|
|
2617
|
+
if (!selectedTpl) return;
|
|
2618
|
+
try {
|
|
2619
|
+
const res = await fetch('/dsh-goal/action', {
|
|
2620
|
+
method: 'POST',
|
|
2621
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2622
|
+
body: JSON.stringify({
|
|
2623
|
+
action: 'instantiate_template',
|
|
2624
|
+
templateId: selectedTpl.id,
|
|
2625
|
+
inputs: tplInputs,
|
|
2626
|
+
}),
|
|
2627
|
+
});
|
|
2628
|
+
if (res.ok) {
|
|
2629
|
+
const data = await res.json();
|
|
2630
|
+
if (data.ok && data.template) {
|
|
2631
|
+
setTitle(data.template.title);
|
|
2632
|
+
setShowTemplates(false);
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
} catch (err) { console.warn('[dsh-goal] Apply template error:', err); }
|
|
2636
|
+
};
|
|
2637
|
+
|
|
2596
2638
|
useEffect(() => {
|
|
2597
2639
|
|
|
2598
2640
|
if (inputRef.current) inputRef.current.focus();
|
|
@@ -2782,8 +2824,94 @@ window.__ModuleLoader__.load({
|
|
|
2782
2824
|
},
|
|
2783
2825
|
tpl.label
|
|
2784
2826
|
)
|
|
2827
|
+
),
|
|
2828
|
+
React.createElement(
|
|
2829
|
+
'button',
|
|
2830
|
+
{
|
|
2831
|
+
type: 'button',
|
|
2832
|
+
className: 'dsh-goal-chip-btn',
|
|
2833
|
+
style: { fontWeight: 600, borderColor: 'var(--dsw-alias-border-brand, var(--dsw-alias-border-l2))' },
|
|
2834
|
+
onClick: () => setShowTemplates(!showTemplates),
|
|
2835
|
+
},
|
|
2836
|
+
(showTemplates ? '▲ ' : '▼ ') + '📋 ' + (t('templatesTitle') || 'Engineering Templates')
|
|
2785
2837
|
)
|
|
2786
2838
|
),
|
|
2839
|
+
showTemplates
|
|
2840
|
+
? React.createElement(
|
|
2841
|
+
'div',
|
|
2842
|
+
{
|
|
2843
|
+
style: {
|
|
2844
|
+
marginTop: 10,
|
|
2845
|
+
padding: '10px 12px',
|
|
2846
|
+
borderRadius: 6,
|
|
2847
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
2848
|
+
background: 'var(--dsw-alias-bg-hover)',
|
|
2849
|
+
},
|
|
2850
|
+
},
|
|
2851
|
+
loadingTpls
|
|
2852
|
+
? React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Loading templates…')
|
|
2853
|
+
: React.createElement(
|
|
2854
|
+
'div',
|
|
2855
|
+
null,
|
|
2856
|
+
React.createElement(
|
|
2857
|
+
'div',
|
|
2858
|
+
{ style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 8 } },
|
|
2859
|
+
backendTemplates.map((bt) =>
|
|
2860
|
+
React.createElement(
|
|
2861
|
+
'button',
|
|
2862
|
+
{
|
|
2863
|
+
key: bt.id,
|
|
2864
|
+
type: 'button',
|
|
2865
|
+
className: `dsh-goal-chip-btn ${selectedTpl?.id === bt.id ? 'active' : ''}`,
|
|
2866
|
+
style: selectedTpl?.id === bt.id ? { fontWeight: 600, border: '1px solid var(--dsw-alias-accent-primary)' } : {},
|
|
2867
|
+
onClick: () => {
|
|
2868
|
+
setSelectedTpl(bt);
|
|
2869
|
+
const initial = {};
|
|
2870
|
+
(bt.inputs || []).forEach((inp) => {
|
|
2871
|
+
initial[inp.key] = inp.defaultValue || '';
|
|
2872
|
+
});
|
|
2873
|
+
setTplInputs(initial);
|
|
2874
|
+
},
|
|
2875
|
+
},
|
|
2876
|
+
bt.title
|
|
2877
|
+
)
|
|
2878
|
+
)
|
|
2879
|
+
),
|
|
2880
|
+
selectedTpl
|
|
2881
|
+
? React.createElement(
|
|
2882
|
+
'div',
|
|
2883
|
+
{ style: { borderTop: '1px solid var(--dsw-alias-border-l1)', paddingTop: 8 } },
|
|
2884
|
+
React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginBottom: 6 } }, selectedTpl.description),
|
|
2885
|
+
(selectedTpl.inputs || []).map((inp) =>
|
|
2886
|
+
React.createElement(
|
|
2887
|
+
'div',
|
|
2888
|
+
{ key: inp.key, style: { marginBottom: 6 } },
|
|
2889
|
+
React.createElement('label', { style: { fontSize: 11, display: 'block', marginBottom: 2 } }, inp.label),
|
|
2890
|
+
React.createElement('input', {
|
|
2891
|
+
type: 'text',
|
|
2892
|
+
className: 'dsh-goal-card-input',
|
|
2893
|
+
style: { width: '100%', boxSizing: 'border-box', fontSize: 12, padding: '4px 8px' },
|
|
2894
|
+
placeholder: inp.placeholder || '',
|
|
2895
|
+
value: tplInputs[inp.key] || '',
|
|
2896
|
+
onChange: (e) => setTplInputs({ ...tplInputs, [inp.key]: e.target.value }),
|
|
2897
|
+
})
|
|
2898
|
+
)
|
|
2899
|
+
),
|
|
2900
|
+
React.createElement(
|
|
2901
|
+
'button',
|
|
2902
|
+
{
|
|
2903
|
+
type: 'button',
|
|
2904
|
+
className: 'dsh-goal-btn dsh-goal-btn-primary',
|
|
2905
|
+
style: { marginTop: 4, padding: '4px 10px', fontSize: 12 },
|
|
2906
|
+
onClick: handleApplyTemplate,
|
|
2907
|
+
},
|
|
2908
|
+
'Apply Template'
|
|
2909
|
+
)
|
|
2910
|
+
)
|
|
2911
|
+
: null
|
|
2912
|
+
)
|
|
2913
|
+
)
|
|
2914
|
+
: null
|
|
2787
2915
|
),
|
|
2788
2916
|
|
|
2789
2917
|
React.createElement(
|
|
@@ -3398,6 +3526,17 @@ window.__ModuleLoader__.load({
|
|
|
3398
3526
|
|
|
3399
3527
|
const isCompleted = state.state === 'COMPLETED';
|
|
3400
3528
|
|
|
3529
|
+
const formatElapsed = (totalSeconds) => {
|
|
3530
|
+
const s = Math.max(0, Math.floor(Number(totalSeconds) || 0));
|
|
3531
|
+
if (s < 60) return `${s}s`;
|
|
3532
|
+
const m = Math.floor(s / 60);
|
|
3533
|
+
const remS = s % 60;
|
|
3534
|
+
if (m < 60) return remS > 0 ? `${m}m ${remS}s` : `${m}m`;
|
|
3535
|
+
const h = Math.floor(m / 60);
|
|
3536
|
+
const remM = m % 60;
|
|
3537
|
+
return remM > 0 ? `${h}h ${remM}m` : `${h}h`;
|
|
3538
|
+
};
|
|
3539
|
+
|
|
3401
3540
|
const formatLiveElapsed = () => {
|
|
3402
3541
|
|
|
3403
3542
|
if (!state) return '0s';
|
|
@@ -3821,7 +3960,7 @@ window.__ModuleLoader__.load({
|
|
|
3821
3960
|
const [updateState, setUpdateState] = useState({
|
|
3822
3961
|
checking: false,
|
|
3823
3962
|
updating: false,
|
|
3824
|
-
currentVersion: '
|
|
3963
|
+
currentVersion: '',
|
|
3825
3964
|
latestVersion: '',
|
|
3826
3965
|
updateAvailable: false,
|
|
3827
3966
|
notice: '',
|
|
@@ -4159,7 +4298,7 @@ window.__ModuleLoader__.load({
|
|
|
4159
4298
|
'div',
|
|
4160
4299
|
{ style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
4161
4300
|
React.createElement('span', { style: { fontSize: 13, fontWeight: 500, color: 'var(--dsw-alias-label-primary)' } }, t('version') || 'Version'),
|
|
4162
|
-
React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'v' +
|
|
4301
|
+
React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, updateState.currentVersion ? 'v' + updateState.currentVersion : 'v0.2.12'),
|
|
4163
4302
|
updateState.updateAvailable
|
|
4164
4303
|
? React.createElement('span', { className: 'dsh-goal-badge dsh-goal-badge-warn', style: { fontSize: 11 } }, 'v' + updateState.latestVersion + ' ' + (t('updateAvailable') || 'available'))
|
|
4165
4304
|
: React.createElement('span', { className: 'dsh-goal-badge dsh-goal-badge-ok', style: { fontSize: 11 } }, '✓ ' + (t('upToDate') || 'Up to date')),
|
package/lib/engine-issue-sync.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Parse issue identifier from goal title or string
|
|
3
6
|
* @param {string} input
|
|
@@ -64,3 +67,128 @@ export function updateChecklistInMarkdown(markdown, itemText, done) {
|
|
|
64
67
|
return line;
|
|
65
68
|
}).join('\n');
|
|
66
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve Gitea API credentials from options, environment, or agent config
|
|
73
|
+
* @param {object} [options]
|
|
74
|
+
* @returns {{ token?: string, baseUrl: string }}
|
|
75
|
+
*/
|
|
76
|
+
export function resolveGiteaAuth(options = {}) {
|
|
77
|
+
const defaultUrl = options.baseUrl || process.env.GITEA_BASE_URL || 'http://127.0.0.1:3005';
|
|
78
|
+
if (options.token) {
|
|
79
|
+
return { token: options.token, baseUrl: defaultUrl };
|
|
80
|
+
}
|
|
81
|
+
if (process.env.GITEA_TOKEN) {
|
|
82
|
+
return { token: process.env.GITEA_TOKEN, baseUrl: defaultUrl };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const credPath = process.env.DSH_GITEA_CREDENTIALS || '/mnt/external/Project/DEV/.gitea-agent-credentials.json';
|
|
86
|
+
try {
|
|
87
|
+
if (fs.existsSync(credPath)) {
|
|
88
|
+
const raw = fs.readFileSync(credPath, 'utf8');
|
|
89
|
+
const creds = JSON.parse(raw);
|
|
90
|
+
const agent = creds?.agents?.['deepseek-harness'] || creds?.agents?.antigravity;
|
|
91
|
+
if (agent?.token) {
|
|
92
|
+
return {
|
|
93
|
+
token: agent.token,
|
|
94
|
+
baseUrl: options.baseUrl || agent.http_remote_base || defaultUrl,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// Non-fatal credential read error
|
|
100
|
+
}
|
|
101
|
+
return { baseUrl: defaultUrl };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve repository owner and repo name from git remote or options
|
|
106
|
+
* @param {string} [cwd]
|
|
107
|
+
* @param {object} [options]
|
|
108
|
+
* @returns {{ owner: string, repo: string }}
|
|
109
|
+
*/
|
|
110
|
+
export function resolveRepoInfo(cwd = process.cwd(), options = {}) {
|
|
111
|
+
if (options.owner && options.repo) {
|
|
112
|
+
return { owner: options.owner, repo: options.repo };
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const remote = execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
116
|
+
cwd,
|
|
117
|
+
encoding: 'utf8',
|
|
118
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
119
|
+
}).trim();
|
|
120
|
+
const match = remote.match(/(?:[:/])([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
121
|
+
if (match) {
|
|
122
|
+
return { owner: match[1], repo: match[2] };
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
// Fallback to default repository
|
|
126
|
+
}
|
|
127
|
+
return { owner: 'goodandready', repo: 'dsh-goal' };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Actively sync completed milestone checklist item with Gitea issue
|
|
132
|
+
* @param {object} goal
|
|
133
|
+
* @param {object} milestone
|
|
134
|
+
* @param {object} [options]
|
|
135
|
+
* @returns {Promise<{ synced: boolean, updated?: boolean, issueNumber?: number, error?: string }>}
|
|
136
|
+
*/
|
|
137
|
+
export async function syncMilestoneWithIssue(goal, milestone, options = {}) {
|
|
138
|
+
if (!goal?.issueRef?.issueNumber || !milestone?.title) {
|
|
139
|
+
return { synced: false, error: 'No issue reference or milestone title' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const auth = resolveGiteaAuth(options);
|
|
143
|
+
if (!auth.token) {
|
|
144
|
+
return { synced: false, error: 'No Gitea API token available' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const repoInfo = resolveRepoInfo(goal.cwd || process.cwd(), options);
|
|
148
|
+
const issueNum = goal.issueRef.issueNumber;
|
|
149
|
+
const issueApiUrl = `${auth.baseUrl}/api/v1/repos/${repoInfo.owner}/${repoInfo.repo}/issues/${issueNum}`;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const fetchFn = options.fetch || globalThis.fetch;
|
|
153
|
+
if (typeof fetchFn !== 'function') {
|
|
154
|
+
return { synced: false, error: 'Fetch API unavailable' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const getRes = await fetchFn(issueApiUrl, {
|
|
158
|
+
headers: {
|
|
159
|
+
'Authorization': `token ${auth.token}`,
|
|
160
|
+
'Accept': 'application/json',
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (!getRes.ok) {
|
|
165
|
+
return { synced: false, error: `HTTP ${getRes.status} fetching issue #${issueNum}` };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const issueData = await getRes.json();
|
|
169
|
+
const currentBody = issueData.body || '';
|
|
170
|
+
const updatedBody = updateChecklistInMarkdown(currentBody, milestone.title, true);
|
|
171
|
+
|
|
172
|
+
if (updatedBody === currentBody) {
|
|
173
|
+
return { synced: true, updated: false, issueNumber: issueNum };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const patchRes = await fetchFn(issueApiUrl, {
|
|
177
|
+
method: 'PATCH',
|
|
178
|
+
headers: {
|
|
179
|
+
'Authorization': `token ${auth.token}`,
|
|
180
|
+
'Content-Type': 'application/json',
|
|
181
|
+
'Accept': 'application/json',
|
|
182
|
+
},
|
|
183
|
+
body: JSON.stringify({ body: updatedBody }),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (!patchRes.ok) {
|
|
187
|
+
return { synced: false, error: `HTTP ${patchRes.status} updating issue #${issueNum}` };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { synced: true, updated: true, issueNumber: issueNum };
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return { synced: false, error: err.message };
|
|
193
|
+
}
|
|
194
|
+
}
|
package/lib/engine-store.js
CHANGED
|
@@ -6,9 +6,10 @@ import { GoalState } from './goal-engine-constants.js';
|
|
|
6
6
|
* Storage manager for goal engine disk state persistence
|
|
7
7
|
*/
|
|
8
8
|
export class EngineStore {
|
|
9
|
-
constructor(engine, storagePath) {
|
|
9
|
+
constructor(engine, storagePath, logger) {
|
|
10
10
|
this.engine = engine;
|
|
11
11
|
this.storagePath = storagePath || null;
|
|
12
|
+
this.logger = logger || engine?.logger || console;
|
|
12
13
|
this.saveTimer = null;
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -77,7 +78,7 @@ export class EngineStore {
|
|
|
77
78
|
}
|
|
78
79
|
} catch (err) {
|
|
79
80
|
// best-effort load failure logged defensively
|
|
80
|
-
|
|
81
|
+
this.logger?.warn?.('[GoalEngine] Failed to load state from disk:', err?.message || err);
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -128,7 +129,7 @@ export class EngineStore {
|
|
|
128
129
|
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
129
130
|
fs.renameSync(tmp, this.storagePath);
|
|
130
131
|
} catch (err) {
|
|
131
|
-
|
|
132
|
+
this.logger?.warn?.('[GoalEngine] Failed to write state to disk:', err?.message || err);
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
|
package/lib/goal-engine.js
CHANGED
|
@@ -5,12 +5,12 @@ import { buildStatePromptInjection } from './engine-prompt.js';
|
|
|
5
5
|
import { matchMilestone, parseMilestoneItems, applyMilestoneUpdate, toggleMilestoneChecklistItem } from './engine-milestones.js';
|
|
6
6
|
import { applyIntervention, recordActivity, processTokenBudget } from './engine-intervention.js';
|
|
7
7
|
import { createGoalBranch, sanitizeBranchSlug, getCurrentGitBranch, mergeGoalBranch, discardGoalBranch } from './engine-git-branch.js';
|
|
8
|
-
import { parseIssueRef } from './engine-issue-sync.js';
|
|
8
|
+
import { parseIssueRef, parseIssueChecklist, syncMilestoneWithIssue } from './engine-issue-sync.js';
|
|
9
9
|
import { generateRetrospectiveData } from './engine-retrospective.js';
|
|
10
10
|
import { generatePreplanDraft } from './engine-templates.js';
|
|
11
11
|
import { pruneInactiveSessions, addNudgeToGoal, consumeGoalNudge, extendGoalBudget } from './engine-sessions.js';
|
|
12
12
|
|
|
13
|
-
export { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone };
|
|
13
|
+
export { GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone, parseIssueChecklist };
|
|
14
14
|
export { getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact };
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -73,7 +73,7 @@ export class GoalEngine {
|
|
|
73
73
|
try {
|
|
74
74
|
listener(snapshot, sid);
|
|
75
75
|
} catch (err) {
|
|
76
|
-
|
|
76
|
+
this.logger?.error?.('[dsh-goal] Error in GoalEngine listener:', err);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -234,6 +234,13 @@ export class GoalEngine {
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
if (options.issueBody && (!options.milestones || options.milestones.length === 0)) {
|
|
238
|
+
const items = parseIssueChecklist(options.issueBody);
|
|
239
|
+
if (items.length > 0) {
|
|
240
|
+
options.milestones = items.map((it) => ({ title: it.text, status: it.done ? MilestoneStatus.COMPLETED : MilestoneStatus.PENDING }));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
237
244
|
if (this.enablePreplanning && options.preplan !== false && (!options.milestones || options.milestones.length === 0)) {
|
|
238
245
|
goal.preplan = { active: true, draftMilestones: generatePreplanDraft(cleanTitle) };
|
|
239
246
|
}
|
|
@@ -378,11 +385,22 @@ export class GoalEngine {
|
|
|
378
385
|
const prevStatus = res.prevStatus;
|
|
379
386
|
recordActivity(goal, { type: 'milestone', tool: 'updateMilestone', status: 'success', details: `${id} -> ${status}` });
|
|
380
387
|
|
|
381
|
-
if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
388
|
+
if (target.status === MilestoneStatus.COMPLETED && prevStatus !== MilestoneStatus.COMPLETED) {
|
|
389
|
+
if (this.autoCheckpointOnMilestone) {
|
|
390
|
+
const commitHash = createMilestoneCheckpoint(target, sid);
|
|
391
|
+
if (commitHash) {
|
|
392
|
+
target.checkpointCommit = commitHash;
|
|
393
|
+
goal.logs.push({ timestamp: Date.now(), type: 'info', message: `Git checkpoint saved for [${target.title}]: ${commitHash}` });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (goal.issueRef) {
|
|
397
|
+
syncMilestoneWithIssue(goal, target).then((syncRes) => {
|
|
398
|
+
if (syncRes?.updated) {
|
|
399
|
+
goal.logs.push({ timestamp: Date.now(), type: 'info', message: `Synced checklist for issue #${syncRes.issueNumber}` });
|
|
400
|
+
recordActivity(goal, { type: 'issue', tool: 'syncIssueChecklist', status: 'success', details: `Issue #${syncRes.issueNumber} updated` });
|
|
401
|
+
this.emit(sid);
|
|
402
|
+
}
|
|
403
|
+
}).catch(() => {});
|
|
386
404
|
}
|
|
387
405
|
}
|
|
388
406
|
|
package/lib/index.js
CHANGED
|
@@ -65,9 +65,12 @@ export function apply(ctx, config = {}) {
|
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
const defaultStorageDir = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
68
|
-
const
|
|
69
|
-
? currentSettings.storagePath
|
|
70
|
-
:
|
|
68
|
+
const customStorage = typeof currentSettings.storagePath === 'string' && currentSettings.storagePath.trim()
|
|
69
|
+
? currentSettings.storagePath.trim()
|
|
70
|
+
: null;
|
|
71
|
+
const isTest = process.env.NODE_ENV === 'test' || Boolean(process.env.NODE_TEST_CONTEXT);
|
|
72
|
+
const storagePath = customStorage || (isTest ? null : path.join(defaultStorageDir, 'dsh-goal-state.json'));
|
|
73
|
+
const logger = ctx.logger ? ctx.logger('goal') : console;
|
|
71
74
|
|
|
72
75
|
const engine = new GoalEngine({
|
|
73
76
|
defaultMaxIterations: currentSettings.maxIterations,
|
|
@@ -88,6 +91,7 @@ export function apply(ctx, config = {}) {
|
|
|
88
91
|
soundScheme: currentSettings.soundScheme,
|
|
89
92
|
enableVoiceAnnouncements: currentSettings.enableVoiceAnnouncements,
|
|
90
93
|
storagePath,
|
|
94
|
+
logger,
|
|
91
95
|
});
|
|
92
96
|
|
|
93
97
|
// Track running agents through global DSH core event
|
|
@@ -122,7 +126,7 @@ export function apply(ctx, config = {}) {
|
|
|
122
126
|
try {
|
|
123
127
|
specificAgent.cancel({ kind: 'user' });
|
|
124
128
|
} catch (err) {
|
|
125
|
-
|
|
129
|
+
logger.warn('Failed to cancel specific agent for session:', err);
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
|
|
@@ -135,7 +139,7 @@ export function apply(ctx, config = {}) {
|
|
|
135
139
|
}
|
|
136
140
|
}
|
|
137
141
|
} catch (err) {
|
|
138
|
-
|
|
142
|
+
logger.warn('Failed to cancel running agent:', err);
|
|
139
143
|
}
|
|
140
144
|
}
|
|
141
145
|
|
|
@@ -145,7 +149,7 @@ export function apply(ctx, config = {}) {
|
|
|
145
149
|
try {
|
|
146
150
|
lastActiveAgent.cancel({ kind: 'user' });
|
|
147
151
|
} catch (err) {
|
|
148
|
-
|
|
152
|
+
logger.warn('Failed to cancel lastActiveAgent:', err);
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
}
|
|
@@ -193,7 +197,7 @@ export function apply(ctx, config = {}) {
|
|
|
193
197
|
sessionAgents.set(sid, target);
|
|
194
198
|
return true;
|
|
195
199
|
} catch (err) {
|
|
196
|
-
|
|
200
|
+
logger.warn('Failed to resume agent:', err);
|
|
197
201
|
}
|
|
198
202
|
}
|
|
199
203
|
return false;
|
|
@@ -202,35 +206,19 @@ export function apply(ctx, config = {}) {
|
|
|
202
206
|
const getConfig = () => {
|
|
203
207
|
if (settingsScope) {
|
|
204
208
|
const snap = typeof settingsScope.getSnapshot === 'function' ? settingsScope.getSnapshot() : null;
|
|
209
|
+
let live = null;
|
|
205
210
|
if (snap) {
|
|
206
|
-
if (snap.status === 'ready' || snap.status === undefined) {
|
|
207
|
-
|
|
208
|
-
if (live && typeof live === 'object') {
|
|
209
|
-
return {
|
|
210
|
-
maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
|
|
211
|
-
autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
|
|
212
|
-
enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
|
|
213
|
-
showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
|
|
214
|
-
consecutiveToolFailureLimit: typeof live.consecutiveToolFailureLimit === 'number' ? live.consecutiveToolFailureLimit : currentSettings.consecutiveToolFailureLimit,
|
|
215
|
-
enableBrowserNotifications: typeof live.enableBrowserNotifications === 'boolean' ? live.enableBrowserNotifications : currentSettings.enableBrowserNotifications,
|
|
216
|
-
maxTokenBudget: typeof live.maxTokenBudget === 'number' ? live.maxTokenBudget : currentSettings.maxTokenBudget,
|
|
217
|
-
budgetWarningThreshold: typeof live.budgetWarningThreshold === 'number' ? live.budgetWarningThreshold : currentSettings.budgetWarningThreshold,
|
|
218
|
-
autoCheckpointOnMilestone: typeof live.autoCheckpointOnMilestone === 'boolean' ? live.autoCheckpointOnMilestone : currentSettings.autoCheckpointOnMilestone,
|
|
219
|
-
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
211
|
+
if (snap.status === 'ready' || snap.status === 'writable' || snap.status === undefined) {
|
|
212
|
+
live = snap.value || (typeof settingsScope.get === 'function' ? settingsScope.get() : null);
|
|
222
213
|
}
|
|
223
214
|
} else if (typeof settingsScope.get === 'function') {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
215
|
+
live = settingsScope.get();
|
|
216
|
+
}
|
|
217
|
+
if (live && typeof live === 'object') {
|
|
218
|
+
return {
|
|
219
|
+
...currentSettings,
|
|
220
|
+
...live,
|
|
221
|
+
};
|
|
234
222
|
}
|
|
235
223
|
}
|
|
236
224
|
return currentSettings;
|
|
@@ -279,7 +267,7 @@ export function apply(ctx, config = {}) {
|
|
|
279
267
|
}
|
|
280
268
|
}
|
|
281
269
|
} catch (err) {
|
|
282
|
-
|
|
270
|
+
logger.warn('Settings register skipped:', err.message);
|
|
283
271
|
}
|
|
284
272
|
});
|
|
285
273
|
|
|
@@ -307,7 +295,7 @@ export function apply(ctx, config = {}) {
|
|
|
307
295
|
cctx.effect(() => () => unregister(), 'dsh-goal: /goal command unregister');
|
|
308
296
|
}
|
|
309
297
|
} catch (err) {
|
|
310
|
-
|
|
298
|
+
logger.warn('Commands register skipped:', err.message);
|
|
311
299
|
}
|
|
312
300
|
});
|
|
313
301
|
|
|
@@ -316,7 +304,9 @@ export function apply(ctx, config = {}) {
|
|
|
316
304
|
try {
|
|
317
305
|
if (typeof pctx.systemPrompt?.section === 'function') {
|
|
318
306
|
const globalSections = pctx.systemPrompt.layers?.global?.sections;
|
|
307
|
+
let displacedPromptSection = null;
|
|
319
308
|
if (globalSections?.data instanceof Map && globalSections.data.has('tool:goal')) {
|
|
309
|
+
displacedPromptSection = globalSections.data.get('tool:goal');
|
|
320
310
|
globalSections.data.delete('tool:goal');
|
|
321
311
|
}
|
|
322
312
|
|
|
@@ -334,28 +324,39 @@ export function apply(ctx, config = {}) {
|
|
|
334
324
|
});
|
|
335
325
|
|
|
336
326
|
if (typeof unregisterSection === 'function') {
|
|
337
|
-
pctx.effect(() => () =>
|
|
327
|
+
pctx.effect(() => () => {
|
|
328
|
+
unregisterSection();
|
|
329
|
+
if (displacedPromptSection && globalSections?.data instanceof Map) {
|
|
330
|
+
globalSections.data.set('tool:goal', displacedPromptSection);
|
|
331
|
+
}
|
|
332
|
+
}, 'dsh-goal: system prompt section');
|
|
338
333
|
}
|
|
339
334
|
}
|
|
340
335
|
} catch (err) {
|
|
341
|
-
|
|
336
|
+
logger.warn('SystemPrompt section register skipped:', err.message);
|
|
342
337
|
}
|
|
343
338
|
});
|
|
344
339
|
|
|
345
340
|
// 4. Tools registration
|
|
346
|
-
registerTools(ctx, { engine, resumeActiveAgent });
|
|
341
|
+
registerTools(ctx, { engine, resumeActiveAgent, logger });
|
|
347
342
|
|
|
348
|
-
// 5. One-click auto-updater registration
|
|
343
|
+
// 5. One-click auto-updater registration with Cordis effect cleanup (Issue #99)
|
|
349
344
|
if (ctx.webServer?.register) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
345
|
+
ctx.effect(() => {
|
|
346
|
+
let unreg = null;
|
|
347
|
+
try {
|
|
348
|
+
unreg = registerPluginUpdater(ctx, {
|
|
349
|
+
packageName: '@goodandready/dsh-goal',
|
|
350
|
+
endpoint: '/api/dsh-goal/update',
|
|
351
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
352
|
+
});
|
|
353
|
+
} catch (err) {
|
|
354
|
+
logger.warn('Failed to register plugin updater:', err.message);
|
|
355
|
+
}
|
|
356
|
+
return () => {
|
|
357
|
+
if (typeof unreg === 'function') unreg();
|
|
358
|
+
};
|
|
359
|
+
}, 'dsh-goal: plugin auto-updater');
|
|
359
360
|
}
|
|
360
361
|
|
|
361
362
|
// 6. WebServer HTTP & SSE routes
|
|
@@ -466,7 +467,7 @@ export function apply(ctx, config = {}) {
|
|
|
466
467
|
target.followup(promptMsg);
|
|
467
468
|
sessionAgents.set(sid, target);
|
|
468
469
|
} catch (err) {
|
|
469
|
-
|
|
470
|
+
logger.warn('Failed auto-drive followup:', err);
|
|
470
471
|
}
|
|
471
472
|
}
|
|
472
473
|
}
|
package/lib/routes.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { MilestoneStatus, detectLanguage, sessionIdOf } from './goal-engine-constants.js';
|
|
4
4
|
import { saveGoalArtifact, rollbackToCheckpoint } from './engine-reports.js';
|
|
5
|
-
import { getTemplatesCatalogue } from './engine-templates.js';
|
|
5
|
+
import { getTemplatesCatalogue, instantiateTemplate } from './engine-templates.js';
|
|
6
6
|
|
|
7
7
|
export function isLoopback(value) {
|
|
8
8
|
if (!value) return false;
|
|
@@ -21,10 +21,6 @@ export function isTrustedCaller(req) {
|
|
|
21
21
|
const remote = req.socket?.remoteAddress || req.connection?.remoteAddress || '';
|
|
22
22
|
if (isLoopback(remote)) return true;
|
|
23
23
|
|
|
24
|
-
const secSite = String(req.headers?.['sec-fetch-site'] || '').toLowerCase();
|
|
25
|
-
if (secSite === 'same-origin' || secSite === 'same-site' || secSite === 'none') return true;
|
|
26
|
-
if (secSite === 'cross-site') return false;
|
|
27
|
-
|
|
28
24
|
const host = String(req.headers?.host || '').toLowerCase();
|
|
29
25
|
const origin = String(req.headers?.origin || '').trim().toLowerCase();
|
|
30
26
|
if (origin) {
|
|
@@ -167,6 +163,11 @@ export function registerRoutes(ctx, {
|
|
|
167
163
|
|
|
168
164
|
// GET /dsh-goal/events: Server-Sent Events realtime snapshot stream
|
|
169
165
|
if (req.method === 'GET' && (pathname === '/dsh-goal/events' || pathname === '/dsh-goal/events/')) {
|
|
166
|
+
if (!isTrustedCaller(req)) {
|
|
167
|
+
res.statusCode = 403;
|
|
168
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
169
|
+
return res.end(JSON.stringify({ error: 'Forbidden: untrusted caller origin' }));
|
|
170
|
+
}
|
|
170
171
|
const sid = sessionIdOf(req, 'default');
|
|
171
172
|
res.writeHead(200, {
|
|
172
173
|
'Content-Type': 'text/event-stream',
|
|
@@ -197,12 +198,20 @@ export function registerRoutes(ctx, {
|
|
|
197
198
|
|
|
198
199
|
// GET /dsh-goal/templates
|
|
199
200
|
if (req.method === 'GET' && (pathname === '/dsh-goal/templates' || pathname === '/dsh-goal/templates/')) {
|
|
201
|
+
if (!isTrustedCaller(req)) {
|
|
202
|
+
res.statusCode = 403;
|
|
203
|
+
return res.end(JSON.stringify({ error: 'Forbidden: untrusted caller origin' }));
|
|
204
|
+
}
|
|
200
205
|
res.statusCode = 200;
|
|
201
206
|
return res.end(JSON.stringify({ ok: true, templates: getTemplatesCatalogue() }));
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
// GET /dsh-goal/state
|
|
205
210
|
if (req.method === 'GET' && (pathname === '/dsh-goal/state' || pathname === '/dsh-goal/state/')) {
|
|
211
|
+
if (!isTrustedCaller(req)) {
|
|
212
|
+
res.statusCode = 403;
|
|
213
|
+
return res.end(JSON.stringify({ error: 'Forbidden: untrusted caller origin' }));
|
|
214
|
+
}
|
|
206
215
|
const sid = sessionIdOf(req, 'default');
|
|
207
216
|
res.statusCode = 200;
|
|
208
217
|
return res.end(JSON.stringify(engine.getSnapshot(sid)));
|
|
@@ -268,13 +277,27 @@ export function registerRoutes(ctx, {
|
|
|
268
277
|
res.statusCode = 400;
|
|
269
278
|
return res.end(JSON.stringify({ error: 'Goal title cannot be empty' }));
|
|
270
279
|
}
|
|
280
|
+
const trustedRoots = [process.cwd()];
|
|
281
|
+
if (process.env.DSH_WORKSPACE_ROOT) {
|
|
282
|
+
trustedRoots.push(process.env.DSH_WORKSPACE_ROOT);
|
|
283
|
+
}
|
|
284
|
+
let safeStartCwd = undefined;
|
|
285
|
+
if (data.cwd) {
|
|
286
|
+
const validated = getSafeCwd(data.cwd, trustedRoots);
|
|
287
|
+
if (!validated) {
|
|
288
|
+
res.statusCode = 400;
|
|
289
|
+
return res.end(JSON.stringify({ error: 'Invalid or unauthorized working directory' }));
|
|
290
|
+
}
|
|
291
|
+
safeStartCwd = validated;
|
|
292
|
+
}
|
|
271
293
|
const detectedLang = data.lang || detectLanguage(cleanTitle);
|
|
272
294
|
const conf = typeof getConfig === 'function' ? getConfig() : { maxIterations: 25 };
|
|
273
295
|
result = engine.startGoal(cleanTitle, {
|
|
274
296
|
description: typeof description === 'string' ? description.trim() : '',
|
|
275
297
|
maxIterations: conf.maxIterations || 25,
|
|
276
298
|
lang: detectedLang,
|
|
277
|
-
cwd:
|
|
299
|
+
cwd: safeStartCwd,
|
|
300
|
+
issueBody: data.issueBody,
|
|
278
301
|
}, sid);
|
|
279
302
|
|
|
280
303
|
const startPrompt = detectedLang === 'zh'
|
|
@@ -390,7 +413,15 @@ export function registerRoutes(ctx, {
|
|
|
390
413
|
}
|
|
391
414
|
const sessionGoal = engine.getGoal(sid);
|
|
392
415
|
const allowedRoots = [process.cwd()];
|
|
393
|
-
if (
|
|
416
|
+
if (process.env.DSH_WORKSPACE_ROOT) {
|
|
417
|
+
allowedRoots.push(process.env.DSH_WORKSPACE_ROOT);
|
|
418
|
+
}
|
|
419
|
+
if (sessionGoal?.cwd) {
|
|
420
|
+
const isCwdAllowed = allowedRoots.some((root) => isPathInsideOrEqual(root, sessionGoal.cwd));
|
|
421
|
+
if (isCwdAllowed) {
|
|
422
|
+
allowedRoots.push(sessionGoal.cwd);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
394
425
|
const safeCwd = getSafeCwd(data.cwd, allowedRoots);
|
|
395
426
|
if (!safeCwd) {
|
|
396
427
|
res.statusCode = 400;
|
|
@@ -413,7 +444,15 @@ export function registerRoutes(ctx, {
|
|
|
413
444
|
}
|
|
414
445
|
const sessionGoal = engine.getGoal(sid);
|
|
415
446
|
const allowedRoots = [process.cwd()];
|
|
416
|
-
if (
|
|
447
|
+
if (process.env.DSH_WORKSPACE_ROOT) {
|
|
448
|
+
allowedRoots.push(process.env.DSH_WORKSPACE_ROOT);
|
|
449
|
+
}
|
|
450
|
+
if (sessionGoal?.cwd) {
|
|
451
|
+
const isCwdAllowed = allowedRoots.some((root) => isPathInsideOrEqual(root, sessionGoal.cwd));
|
|
452
|
+
if (isCwdAllowed) {
|
|
453
|
+
allowedRoots.push(sessionGoal.cwd);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
417
456
|
const safeCwd = getSafeCwd(data.cwd, allowedRoots);
|
|
418
457
|
if (!safeCwd) {
|
|
419
458
|
res.statusCode = 400;
|
|
@@ -473,6 +512,21 @@ export function registerRoutes(ctx, {
|
|
|
473
512
|
res.statusCode = 200;
|
|
474
513
|
return res.end(JSON.stringify({ ok: true, templates: getTemplatesCatalogue() }));
|
|
475
514
|
|
|
515
|
+
case 'instantiate_template': {
|
|
516
|
+
const tplId = data.templateId || data.id;
|
|
517
|
+
if (!tplId) {
|
|
518
|
+
res.statusCode = 400;
|
|
519
|
+
return res.end(JSON.stringify({ error: 'templateId is required' }));
|
|
520
|
+
}
|
|
521
|
+
const instance = instantiateTemplate(tplId, data.inputs || {});
|
|
522
|
+
if (!instance) {
|
|
523
|
+
res.statusCode = 404;
|
|
524
|
+
return res.end(JSON.stringify({ error: `Template "${tplId}" not found` }));
|
|
525
|
+
}
|
|
526
|
+
res.statusCode = 200;
|
|
527
|
+
return res.end(JSON.stringify({ ok: true, template: instance }));
|
|
528
|
+
}
|
|
529
|
+
|
|
476
530
|
default:
|
|
477
531
|
res.statusCode = 400;
|
|
478
532
|
return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
|
|
@@ -488,6 +542,20 @@ export function registerRoutes(ctx, {
|
|
|
488
542
|
return;
|
|
489
543
|
}
|
|
490
544
|
|
|
545
|
+
const KNOWN_ROUTES = {
|
|
546
|
+
'/dsh-goal/events': ['GET'],
|
|
547
|
+
'/dsh-goal/templates': ['GET'],
|
|
548
|
+
'/dsh-goal/state': ['GET'],
|
|
549
|
+
'/dsh-goal/action': ['POST'],
|
|
550
|
+
};
|
|
551
|
+
const cleanPath = pathname.replace(/\/+$/, '') || '/';
|
|
552
|
+
const allowedMethods = KNOWN_ROUTES[cleanPath];
|
|
553
|
+
if (allowedMethods && !allowedMethods.includes(req.method)) {
|
|
554
|
+
res.statusCode = 405;
|
|
555
|
+
res.setHeader('Allow', allowedMethods.join(', '));
|
|
556
|
+
return res.end(JSON.stringify({ error: `Method Not Allowed: use ${allowedMethods.join(', ')}` }));
|
|
557
|
+
}
|
|
558
|
+
|
|
491
559
|
res.statusCode = 404;
|
|
492
560
|
res.end(JSON.stringify({ error: 'Endpoint not found' }));
|
|
493
561
|
},
|
package/lib/tools.js
CHANGED
|
@@ -42,23 +42,30 @@ export function formatGoalValue(snap) {
|
|
|
42
42
|
* @param {any} options.engine
|
|
43
43
|
* @param {Function} [options.resumeActiveAgent]
|
|
44
44
|
*/
|
|
45
|
-
export function registerTools(ctx, { engine, resumeActiveAgent }) {
|
|
45
|
+
export function registerTools(ctx, { engine, resumeActiveAgent, logger = console }) {
|
|
46
46
|
ctx.inject(['tools'], (tctx) => {
|
|
47
47
|
if (!tctx.tools?.register) return;
|
|
48
48
|
|
|
49
|
+
const displacedTools = new Map();
|
|
49
50
|
const safeRegister = (definition) => {
|
|
50
51
|
try {
|
|
51
52
|
const name = definition.name;
|
|
52
53
|
const globalTools = tctx.tools.layers?.global?.tools;
|
|
53
54
|
if (globalTools?.data instanceof Map && globalTools.data.has(name)) {
|
|
55
|
+
displacedTools.set(name, globalTools.data.get(name));
|
|
54
56
|
globalTools.data.delete(name);
|
|
55
57
|
}
|
|
56
58
|
const unregister = tctx.tools.register(definition);
|
|
57
59
|
if (typeof unregister === 'function') {
|
|
58
|
-
tctx.effect(() => () =>
|
|
60
|
+
tctx.effect(() => () => {
|
|
61
|
+
unregister();
|
|
62
|
+
if (displacedTools.has(name) && globalTools?.data instanceof Map) {
|
|
63
|
+
globalTools.data.set(name, displacedTools.get(name));
|
|
64
|
+
}
|
|
65
|
+
}, `dsh-goal: tool ${name}`);
|
|
59
66
|
}
|
|
60
67
|
} catch (err) {
|
|
61
|
-
|
|
68
|
+
logger.warn(`[dsh-goal] Tool ${definition.name} registration skipped:`, err.message);
|
|
62
69
|
}
|
|
63
70
|
};
|
|
64
71
|
|
package/package.json
CHANGED