@useshifu/coding-harness 0.3.37 → 0.3.38

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.
@@ -52,7 +52,7 @@ function renderWelcome({ harness, upgraded = false } = {}, terminal) {
52
52
  "Automatic work sync: every hour, after Start sync",
53
53
  "Saves redacted work summaries to your Shifu account, keeping decisions, lessons and their reasoning available for future conversations.",
54
54
  "",
55
- "Files stay off until you enable uploads and approve a folder. Files in that folder are uploaded separately for processing.",
55
+ "File parsing is selected by default for new setups. Review the folder before Start sync, or turn it off. Files are sent to your connected Shifu workspace for parsing into context.",
56
56
  "Preparing selected history uses your configured model account and allowance.",
57
57
  "",
58
58
  "Review your projects, history and optional file folder in local setup, then select Start sync. You can pause local preparation and uploads there anytime. Just ask your assistant to open setup.",
package/bin/onboarding.js CHANGED
@@ -25,6 +25,11 @@ function write(fileName, value) {
25
25
  fs.renameSync(temporary, fileName);
26
26
  }
27
27
  function fingerprint(config) { return config?.token ? createHash('sha256').update(config.token).digest('hex') : null; }
28
+ function wizardCodeRevision(runtime) {
29
+ const hash = createHash('sha256').update(fs.readFileSync(__filename));
30
+ for (const name of ['index.html', 'wizard.js']) hash.update(fs.readFileSync(path.join(runtime.assets, name)));
31
+ return hash.digest('hex');
32
+ }
28
33
  function load(runtime, runId) {
29
34
  const state = read(file(runtime));
30
35
  if (runId && (!ID.test(runId) || state?.runId !== runId)) throw new Error('This setup run is not available on this device.');
@@ -159,6 +164,7 @@ async function bodyJSON(request) {
159
164
  }
160
165
  async function startServer(runtime, state, { port=0 }={}) {
161
166
  const token = randomBytes(32).toString('hex');
167
+ const codeRevision = wizardCodeRevision(runtime);
162
168
  let origin, busy=false;
163
169
  const server = http.createServer(async (request,response) => {
164
170
  response.setHeader('Cache-Control','no-store'); response.setHeader('Referrer-Policy','no-referrer'); response.setHeader('X-Content-Type-Options','nosniff');
@@ -209,7 +215,7 @@ async function startServer(runtime, state, { port=0 }={}) {
209
215
  if(runtime.readConfig()) runtime.pause();
210
216
  const result=await runtime.setupLock(()=>runtime.lock(async()=>{const current=load(runtime,state.runId);if(current.authorized && !TERMINAL.has(current.backfillStatus) && choice.abandon!==true)throw new Error('Active selection requires explicit abandonment.');state=initial(runtime);}));
211
217
  if(result?.skipped) return send(409,{error:'Another Shifu sync is active.'});
212
- write(file(runtime,'wizard'),{pid:process.pid,url:origin+'/#'+token,runId:state.runId});
218
+ write(file(runtime,'wizard'),{pid:process.pid,url:origin+'/#'+token,runId:state.runId,codeRevision});
213
219
  return send(200,safeStatus(runtime,state));
214
220
  }
215
221
  if (request.method === 'POST' && url.pathname === '/api/resume') {
@@ -220,13 +226,13 @@ async function startServer(runtime, state, { port=0 }={}) {
220
226
  return send(404,{error:'This setup action is unavailable.'});
221
227
  } catch (error) {
222
228
  // Never relay provider or connector errors: those can contain source text or credentials.
223
- return send(400,{error: ['Choose a valid history window and both feedback options.','The connection key format is invalid.','Unsupported setup option.','This run has a fixed history selection. Finish it before changing the history window.','Choose a project or explicitly choose all projects.','Choose valid file and schedule settings.','Review the selected scope before starting sync.','The selected scope changed. Review it again before starting sync.','The project list changed. Review the selection again.','Choose an existing readable folder.'].includes(error.message) ? error.message : 'Setup could not finish. Your accepted work is preserved; check the connection and retry.'});
229
+ return send(400,{error: ['Your connection key is saved, but the coding tool connection could not be completed. Open or update your coding tool, choose Edit selection, review it again and retry. You do not need a new key.','Could not reach Shifu to verify the connection key. Nothing was changed.','Shifu rejected this connection key for the selected coding tool. Nothing was changed.','The connection key is invalid.','Choose a valid history window and both feedback options.','The connection key format is invalid.','Unsupported setup option.','This run has a fixed history selection. Finish it before changing the history window.','Choose a project or explicitly choose all projects.','Choose valid file and schedule settings.','Review the selected scope before starting sync.','The selected scope changed. Review it again before starting sync.','The project list changed. Review the selection again.','Choose an existing readable folder.'].includes(error.message) ? error.message : 'Setup could not finish. Your accepted work is preserved; check the connection and retry.'});
224
230
  }
225
231
  });
226
232
  server.requestTimeout=15000;
227
233
  await new Promise((resolve,reject) => { server.once('error',reject); server.listen(port,'127.0.0.1',resolve); });
228
234
  origin = `http://127.0.0.1:${server.address().port}`;
229
- return {server,token,origin,url:origin+'/#'+token};
235
+ return {server,token,origin,url:origin+'/#'+token,codeRevision};
230
236
  }
231
237
  async function main(runtime, options={}) {
232
238
  if(options.abandon && !options.fresh)throw new Error('Use --abandon only with --new to explicitly leave an unfinished selection.');
@@ -248,7 +254,7 @@ async function main(runtime, options={}) {
248
254
  }
249
255
  if (options.serve) {
250
256
  const instance=await startServer(runtime,state);
251
- write(file(runtime,'wizard'),{pid:process.pid,url:instance.url,runId:state.runId});
257
+ write(file(runtime,'wizard'),{pid:process.pid,url:instance.url,runId:state.runId,codeRevision:instance.codeRevision});
252
258
  const close=()=>instance.server.close(); process.once('SIGTERM',close); process.once('SIGINT',close);
253
259
  // The wizard is temporary; scheduled runner state owns unfinished backfill.
254
260
  const idle=setTimeout(()=>instance.server.close(),60*60*1000); idle.unref();
@@ -257,7 +263,7 @@ async function main(runtime, options={}) {
257
263
  runtime.copyRunner();
258
264
  const endpoint=read(file(runtime,'wizard'));
259
265
  let url;
260
- if (endpoint?.runId === state.runId) {
266
+ if (endpoint?.runId === state.runId && endpoint.codeRevision === wizardCodeRevision(runtime)) {
261
267
  try { const u=new URL(endpoint.url); const r=await fetch(u.origin+'/api/status',{headers:{'X-Shifu-Wizard':u.hash.slice(1)},signal:AbortSignal.timeout(1000)}); if(r.ok) url=endpoint.url; } catch {}
262
268
  }
263
269
  if (!url) {
@@ -70,13 +70,13 @@ const ACTIVE_WORK_FILE_STATUSES = new Set(["queued", "processing", "retrying"]);
70
70
  const INPUT_FOLDER_README_NAME = "README.md";
71
71
  const INPUT_FOLDER_README = `# Shifu sync folder
72
72
 
73
- File uploads stay off until you enable them and approve this folder in Shifu setup. Once approved, put work logs, meeting transcripts, and other work documents directly here; Shifu checks them on your selected schedule and keeps claims only when the file supports a contribution you made. Other people's work and meeting action items alone are not your claims.
73
+ Files in this folder are processed only after you review the folder and select Start sync in Shifu setup. File parsing is selected by default for new setups and can be turned off. Once approved, put work logs, meeting transcripts, and other work documents directly here; Shifu checks them on your selected schedule and keeps claims only when the file supports a contribution you made. Other people's work and meeting action items alone are not your claims.
74
74
 
75
75
  Supported files: .txt, .md, .rtf, and .docx, up to 20 MB each. Subfolders, links, images, and screenshots are not processed. This README is not synced. Unsupported files are counted but never uploaded.
76
76
 
77
77
  Uploading starts a background job. Shifu checks unfinished jobs before the next scheduled or manual sync; a file is complete only when the server reports its final result. A partial result stays pending for retry. A completed file may have published claims, needs-evidence suggestions, or neither; suggestions are not verified claims.
78
78
 
79
- Files you add are sent to Shifu for parsing and sanitization; they are not redacted on this device. Only place files here if you want them processed. For named meeting transcripts, set your display name in Shifu Profile so your speaker turns can be matched.
79
+ Files you add are sent as provided to your connected Shifu workspace for parsing and sanitization. In Shifu Desktop, parsing runs on your Mac; AI processing may send extracted text through Shifu’s gateway. In the web app, files are uploaded to your hosted workspace. Only place files here if you want them processed. For named meeting transcripts, set your display name in Shifu Profile so your speaker turns can be matched.
80
80
  `;
81
81
 
82
82
  function configRoot() {
@@ -3930,13 +3930,18 @@ function onboardingRuntime(harness, { prepareNative } = {}) {
3930
3930
  const scopeStatus = () => {
3931
3931
  const config = readJSON(configPath(harness), null);
3932
3932
  const authorization = controls.validAuthorization(config);
3933
+ const desktop = readDesktopProfile(configRoot());
3934
+ const apiUrl = requireSafeApiUrl(config?.apiUrl || option("--api-url") || process.env.SHIFU_API_URL || DEFAULT_API_URL);
3935
+ let connectionKeyUrl;
3936
+ if (desktop?.connectionKeyLinks === true) connectionKeyUrl = `shifu://sources/coding-sessions?${new URLSearchParams({ harness, profile: desktop.namespace, connection: 'key' })}`;
3937
+ else if (!desktop && apiUrl === DEFAULT_API_URL) connectionKeyUrl = `https://app.useshifu.com/#/sources/coding-sessions?${new URLSearchParams({ harness, connection: 'key' })}`;
3933
3938
  let projects = [], projectDiscoveryError;
3934
3939
  try { projects = controls.projectCatalog(localSessions(harness)); }
3935
3940
  catch { projectDiscoveryError = "Local project history is unavailable. Check the coding tool installation before selecting history."; }
3936
3941
  const current = controls.projectIdentity({ workspace: process.cwd() }).ref;
3937
3942
  const projectRefs = authorization?.projectRefs || (projects.some((item) => item.ref === current) ? [current] : projects.length === 1 ? [projects[0].ref] : []);
3938
- return { runnerVersion: RUNNER_VERSION, projects, projectDiscoveryError, projectScope: authorization?.projectScope || "selected", projectRefs,
3939
- filesEnabled: authorization?.filesEnabled === true, inputFolder: config?.inputFolder || defaultInputFolder(),
3943
+ return { runnerVersion: RUNNER_VERSION, connectionKeyUrl, projects, projectDiscoveryError, projectScope: authorization?.projectScope || "selected", projectRefs,
3944
+ filesEnabled: authorization ? authorization.filesEnabled : true, inputFolder: config?.inputFolder || defaultInputFolder(),
3940
3945
  paused: config?.syncPaused === true, scopeReviewRequired: !authorization,
3941
3946
  syncPolicy: authorization ? normalizedPolicy(config) : { intervalHours: 1, approvalMode: "automatic", scheduleEnabled: true } };
3942
3947
  };
@@ -3995,7 +4000,8 @@ function onboardingRuntime(harness, { prepareNative } = {}) {
3995
4000
  await connect(harness, async () => key, choices.preferences, true);
3996
4001
  generation = readConfig(harness).syncGeneration || 0;
3997
4002
  }
3998
- await setupMcp(harness);
4003
+ try { await setupMcp(harness); }
4004
+ catch { throw new Error("Your connection key is saved, but the coding tool connection could not be completed. Open or update your coding tool, choose Edit selection, review it again and retry. You do not need a new key."); }
3999
4005
  const config = readConfig(harness);
4000
4006
  if ((config.syncGeneration || 0) !== generation) throw new Error("Setup was paused or changed. Review and start again.");
4001
4007
  if (choices.scope.filesEnabled) prepareInputFolder(choices.scope.inputFolder, choices.scope.inputFolder === defaultInputFolder());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useshifu/coding-harness",
3
- "version": "0.3.37",
3
+ "version": "0.3.38",
4
4
  "description": "Scheduled, redacted coding-harness activity sync for Shifu",
5
5
  "bin": {
6
6
  "coding-harness": "bin/shifu-harness.js",
package/wizard/index.html CHANGED
@@ -25,14 +25,15 @@
25
25
  <p class="example">More history uses more of your coding model account’s allowance. You can start with a small window and add more later.</p>
26
26
  <fieldset class="option"><legend>Ongoing sync</legend><label><input id="schedule-enabled" type="checkbox" checked> Keep work context current with scheduled sync</label><p class="example">The schedule starts only after Start sync. You can leave it off and import just the selected history.</p></fieldset>
27
27
  <fieldset class="option"><legend>Files</legend>
28
- <label><input id="files-enabled" type="checkbox"> Enable file uploads from a folder</label>
29
- <div id="folder-choice" hidden><label class="label" for="input-folder">Folder to upload</label><input id="input-folder" type="text" autocomplete="off" spellcheck="false"><p class="example">Supported files in this folder upload as provided, separately from session-summary approval. The review shows the resolved folder and file count.</p></div>
30
- <p class="example">Files stay off until you enable them and approve the folder. A folder change requires another review.</p>
28
+ <label><input id="files-enabled" type="checkbox" checked> Parse work files into context</label>
29
+ <div id="folder-choice"><label class="label" for="input-folder">Folder to read</label><input id="input-folder" type="text" autocomplete="off" spellcheck="false"><p class="example">Supported files are sent as provided to your connected Shifu workspace for parsing into context. The review shows the resolved folder and file count.</p></div>
30
+ <p class="example">In Shifu Desktop, parsing runs on your Mac; AI processing may use Shifu’s gateway. In the web app, files are uploaded to your hosted workspace.</p>
31
+ <p class="example">Review the folder before Start sync. Uncheck this option to leave files out. A folder change requires another review.</p>
31
32
  </fieldset>
32
33
  <fieldset class="option"><legend>Helpful growth feedback</legend><label><input type="radio" name="growth" value="on" checked> On · recommended</label><label><input type="radio" name="growth" value="off"> Off</label><p class="example">“Your trade-off is clear. Adding a success measure could strengthen the recommendation.”</p></fieldset>
33
34
  <fieldset class="option"><legend>Decision alignment</legend><label><input type="radio" name="alignment" value="on" checked> On · recommended</label><label><input type="radio" name="alignment" value="off"> Off</label><p class="example">“Earlier notes say 30 minutes; this proposal uses an hour. Has the requirement changed?”</p></fieldset>
34
35
  <p class="note">Historical retrieval stays on. Feedback choices are independent, and your current instructions take priority.</p>
35
- <div id="key-entry"><label class="label" for="key">Connection key for <span id="harness"></span></label><input id="key" type="password" autocomplete="off" spellcheck="false" placeholder="Paste your tool-specific key"><p class="note">Create a key on your Shifu dashboard. It goes directly to Shifu through local setup for verification; keep it out of chat.</p></div>
36
+ <div id="key-entry"><label class="label" for="key">Connection key for <span id="harness"></span></label><input id="key" type="password" autocomplete="off" spellcheck="false" placeholder="Paste your Shifu connection key"><p><a id="get-key" hidden>Get connection key</a></p><ol class="note"><li>Select Get connection key to open this tool in Shifu, then select Create &amp; copy key (or Copy connection key).</li><li>Come back to this window and paste the key above. Then review your selection and select Start sync.</li><li>If the link does not open, return to the Shifu app or dashboard where you started setup, open Sources, and choose this coding tool.</li></ol><p class="note">Shifu creates this key to connect your coding tool to your workspace. It is not a Google password or a model provider API key. Keep it out of chat.</p></div>
36
37
  <div id="saved-key" class="note" hidden>Your saved connection is ready to use.
37
38
  <details><summary>Need to replace your key?</summary><p>Ask your assistant to reconnect Shifu. Enter the new key in the secure prompt, never in chat. Replacing a connection can reset local sync progress; work already saved in Shifu remains.</p></details>
38
39
  </div>
@@ -52,5 +53,5 @@
52
53
  <p id="handoff" class="note" hidden>Reload your coding assistant or start a new chat to load Shifu tools, then paste this short prompt. Your settings and saved import progress are kept.</p><textarea id="prompt" readonly hidden aria-label="Assistant resume prompt"></textarea>
53
54
  </div>
54
55
  <p id="message" role="status" aria-live="polite"></p>
55
- <details class="privacy"><summary>What happens to my data?</summary><p>Selection and review use local metadata. Preparing selected coding history uses your configured model account; actual allowance usage depends on that history. Shifu receives selected redacted work records, which can still contain confidential business context.</p><p>Files are optional and upload as provided from the folder you approve. Symlinks and subfolders are skipped and reported.</p><p>Revoking access stops future Shifu requests. Retained-data deletion is a separate request. <a href="https://www.useshifu.com/data" target="_blank" rel="noreferrer">Data handling</a> · <a href="https://www.useshifu.com/privacy" target="_blank" rel="noreferrer">Privacy policy</a></p></details>
56
+ <details class="privacy"><summary>What happens to my data?</summary><p>Selection and review use local metadata. Preparing selected coding history uses your configured model account; actual allowance usage depends on that history. Shifu receives selected redacted work records, which can still contain confidential business context.</p><p>Files are included by default in a new setup and can be turned off. After you review the folder and select Start sync, supported files are sent as provided to the connected Shifu workspace for parsing into context. Symlinks and subfolders are skipped and reported.</p><p>Revoking access stops future Shifu requests. Retained-data deletion is a separate request. <a href="https://www.useshifu.com/data" target="_blank" rel="noreferrer">Data handling</a> · <a href="https://www.useshifu.com/privacy" target="_blank" rel="noreferrer">Privacy policy</a></p></details>
56
57
  </section></main></body></html>
package/wizard/wizard.js CHANGED
@@ -32,6 +32,29 @@
32
32
  el('project-note').textContent = all ? 'All projects in this coding tool are included, including new projects.' : 'Select the projects whose history and new work you want Shifu to use.';
33
33
  }
34
34
 
35
+ function connectionKeyLink(value) {
36
+ const link = el('get-key');
37
+ link.hidden = true;
38
+ link.removeAttribute('href');
39
+ if (typeof value.connectionKeyUrl !== 'string' || !Object.hasOwn(names, value.harness)) return;
40
+ try {
41
+ const url = new URL(value.connectionKeyUrl);
42
+ if (url.username || url.password || url.port) return;
43
+ const desktop = url.protocol === 'shifu:' && url.hostname === 'sources' && url.pathname === '/coding-sessions' && !url.hash;
44
+ const hosted = url.origin === 'https://app.useshifu.com' && url.pathname === '/' && !url.search && url.hash.startsWith('#/sources/coding-sessions?');
45
+ if (!desktop && !hosted) return;
46
+ const params = desktop ? url.searchParams : new URLSearchParams(url.hash.slice('#/sources/coding-sessions?'.length));
47
+ const expected = desktop ? ['harness', 'profile', 'connection'] : ['harness', 'connection'];
48
+ if (params.size !== expected.length || expected.some((key) => params.getAll(key).length !== 1) ||
49
+ params.get('harness') !== value.harness || params.get('connection') !== 'key' ||
50
+ (desktop && !/^desktop-[a-f0-9]{12}$/.test(params.get('profile') || ''))) return;
51
+ link.href = url.href;
52
+ link.target = desktop ? '_self' : '_blank';
53
+ link.rel = 'noopener noreferrer';
54
+ link.hidden = false;
55
+ } catch { /* Keep the manual key instructions when a shortcut is unavailable. */ }
56
+ }
57
+
35
58
  function settings(value) {
36
59
  el('history').value = ['0', '15', '30'].includes(String(value.historyDays)) ? String(value.historyDays) : 'custom';
37
60
  el('custom').value = value.historyDays || 7;
@@ -59,12 +82,13 @@
59
82
  el('project-list').replaceChildren(...projects);
60
83
  projectScope();
61
84
  el('schedule-enabled').checked = value.scheduleEnabled ?? value.syncPolicy?.scheduleEnabled ?? true;
62
- el('files-enabled').checked = value.filesEnabled === true;
85
+ el('files-enabled').checked = value.filesEnabled ?? true;
63
86
  el('input-folder').value = value.inputFolder || '';
64
87
  el('folder-choice').hidden = !el('files-enabled').checked;
65
88
  el('key-entry').hidden = value.configured;
66
89
  el('saved-key').hidden = !value.configured;
67
90
  el('harness').textContent = names[value.harness] || value.harness;
91
+ connectionKeyLink(value);
68
92
  }
69
93
 
70
94
  function syncLabel(value) {
@@ -114,6 +138,11 @@
114
138
  el('settings').addEventListener('submit', async (event) => {
115
139
  event.preventDefault();
116
140
  if (busy) return;
141
+ if (!state.configured && !/^cs_sk_[a-f0-9]{16}_[a-f0-9]{64}$/.test(el('key').value.trim())) {
142
+ el('message').textContent = `Return to Shifu → Sources → ${names[state.harness] || state.harness}, select Create & copy key (or Copy connection key), then paste it in Connection key below. This is a Shifu connection key, not a model provider key.`;
143
+ el('key').focus();
144
+ return;
145
+ }
117
146
  const historyDays = el('history').value === 'custom' ? Number(el('custom').value) : Number(el('history').value);
118
147
  if (!Number.isSafeInteger(historyDays) || historyDays < 0 || historyDays > 36500 || (el('history').value === 'custom' && historyDays === 0)) {
119
148
  el('message').textContent = 'Choose a positive whole number of custom days, or Start from now.';
@@ -128,7 +157,7 @@
128
157
  const filesEnabled = el('files-enabled').checked;
129
158
  const inputFolder = el('input-folder').value.trim();
130
159
  if (filesEnabled && !inputFolder) {
131
- el('message').textContent = 'Choose the folder whose files you want to upload.';
160
+ el('message').textContent = 'Choose the folder whose files you want parsed into context.';
132
161
  return;
133
162
  }
134
163
  choices = { historyDays, projectScope, projectRefs, scheduleEnabled: el('schedule-enabled').checked, filesEnabled, ...(filesEnabled ? { inputFolder } : {}), growthFeedback: document.querySelector('input[name=growth]:checked').value === 'on', decisionAlignment: document.querySelector('input[name=alignment]:checked').value === 'on' };
@@ -157,23 +186,21 @@
157
186
  } catch (error) { el('message').textContent = error.message; }
158
187
  finally { busy = false; el('review-button').disabled = false; }
159
188
  });
160
- el('edit').addEventListener('click', () => { choices = undefined; el('review').hidden = true; el('welcome').hidden = false; el('settings').hidden = false; });
189
+ el('edit').addEventListener('click', () => { choices = undefined; el('key-entry').hidden = state.configured; el('saved-key').hidden = !state.configured; el('review').hidden = true; el('welcome').hidden = false; el('settings').hidden = false; });
161
190
  el('apply').addEventListener('click', async () => {
162
191
  if (busy || !choices?.scopeRevision) return;
163
192
  busy = true;
164
193
  el('apply').disabled = true;
165
194
  el('message').textContent = 'Verifying your connection and starting the reviewed selection…';
166
195
  const key = el('key').value.trim();
167
- el('key').value = '';
168
196
  try {
169
197
  state = await api('apply', { ...choices, ...(!state.configured ? { key } : {}) });
198
+ el('key').value = '';
170
199
  renderProgress();
171
200
  el('message').textContent = '';
172
201
  } catch (error) {
173
- choices = undefined;
202
+ try { state = await api('status'); } catch { /* Keep the reviewed selection available during a connection failure. */ }
174
203
  el('message').textContent = error.message;
175
- el('review').hidden = true;
176
- el('welcome').hidden = false; el('settings').hidden = false;
177
204
  } finally { busy = false; el('apply').disabled = false; }
178
205
  });
179
206
  el('change').addEventListener('click', async () => {