koneck 2.61.0 → 2.63.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/dist/folder-dialog.d.ts +63 -0
- package/dist/folder-dialog.d.ts.map +1 -0
- package/dist/folder-dialog.js +175 -0
- package/dist/folder-dialog.js.map +1 -0
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +184 -9
- package/dist/web/api.js.map +1 -1
- package/dist/web/ui-client.d.ts.map +1 -1
- package/dist/web/ui-client.js +254 -25
- package/dist/web/ui-client.js.map +1 -1
- package/dist/web/ui-css.d.ts.map +1 -1
- package/dist/web/ui-css.js +98 -2
- package/dist/web/ui-css.js.map +1 -1
- package/dist/web/ui.d.ts.map +1 -1
- package/dist/web/ui.js +96 -17
- package/dist/web/ui.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The computer's own folder picker, opened by the server.
|
|
3
|
+
*
|
|
4
|
+
* A browser cannot do this, and it is worth being precise about why rather than treating it as a
|
|
5
|
+
* missing feature. `<input type="file" webkitdirectory>` hands back file entries with names relative
|
|
6
|
+
* to the chosen folder and no absolute path; `showDirectoryPicker()` hands back a handle and no
|
|
7
|
+
* path either. Both withhold it deliberately — a page that learned where you keep your files has
|
|
8
|
+
* learned something about you that it was not given.
|
|
9
|
+
*
|
|
10
|
+
* But KONECK's server is on the same machine as the files, and acting through it already requires
|
|
11
|
+
* loopback and the token. So the *server* opens the dialog: the real one, the one every other
|
|
12
|
+
* application on the machine uses, and the path comes back because the process asking is entitled
|
|
13
|
+
* to know it.
|
|
14
|
+
*
|
|
15
|
+
* Two honest limits, both reported rather than papered over. The dialog appears on the machine
|
|
16
|
+
* running KONECK — which is the same machine, because acting is loopback-only, but somebody
|
|
17
|
+
* tunnelling a port should know that. And a machine with no desktop session has no dialog to show:
|
|
18
|
+
* over SSH there is nothing to put on screen, so the answer is to say so and let the built-in
|
|
19
|
+
* browser do the job instead.
|
|
20
|
+
*/
|
|
21
|
+
/** What came back, or why nothing did. */
|
|
22
|
+
export type FolderChoice = {
|
|
23
|
+
ok: true;
|
|
24
|
+
path: string;
|
|
25
|
+
}
|
|
26
|
+
/** The person closed the dialog. Not an error, and must not be reported as one. */
|
|
27
|
+
| {
|
|
28
|
+
ok: false;
|
|
29
|
+
cancelled: true;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
cancelled: false;
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Runs a dialog and stops waiting if it stops answering.
|
|
37
|
+
*
|
|
38
|
+
* execa's own timeout signals the process it started and then waits for its pipes to close, which
|
|
39
|
+
* is not the same thing — a dialog that dies in a way execa does not notice left the request open
|
|
40
|
+
* for the full two minutes. Measured: a dialog killed from outside took 120,027ms to settle, while
|
|
41
|
+
* one that exited cleanly took 2,588ms. So the wait is raced against a deadline and the process
|
|
42
|
+
* group is signalled, which is the same shape as the fix the command runner already needed.
|
|
43
|
+
*/
|
|
44
|
+
export declare function runDialog(file: string, args: readonly string[],
|
|
45
|
+
/** Injectable so the giving-up path can be tested without waiting two minutes for it. */
|
|
46
|
+
waitMs?: number): Promise<{
|
|
47
|
+
code: number | undefined;
|
|
48
|
+
out: string;
|
|
49
|
+
err: string;
|
|
50
|
+
gaveUp: boolean;
|
|
51
|
+
}>;
|
|
52
|
+
/** Whether a desktop session exists to show a dialog on. */
|
|
53
|
+
export declare function hasDesktop(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
54
|
+
/** Which dialog tool would be used, or null when none is installed. */
|
|
55
|
+
export declare function dialogAvailable(platform?: NodeJS.Platform): Promise<string | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Opens the folder dialog and waits for an answer.
|
|
58
|
+
*
|
|
59
|
+
* A generous timeout, because the answer is a person deciding: two minutes is long enough to think
|
|
60
|
+
* and short enough that a dialog nobody is looking at does not hold a handle for ever.
|
|
61
|
+
*/
|
|
62
|
+
export declare function chooseFolder(startIn?: string, title?: string, platform?: NodeJS.Platform): Promise<FolderChoice>;
|
|
63
|
+
//# sourceMappingURL=folder-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folder-dialog.d.ts","sourceRoot":"","sources":["../src/folder-dialog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,0CAA0C;AAC1C,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAC5B,mFAAmF;GACjF;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAyBpD;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE;AACrC,yFAAyF;AACzF,MAAM,SAAiB,GACtB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC,CAyBlF;AAKD,4DAA4D;AAC5D,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,QAAQ,kBAAmB,GAAG,OAAO,CAGrG;AAED,uEAAuE;AACvE,wBAAsB,eAAe,CACnC,QAAQ,kBAAmB,GAC1B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,OAAO,SAAe,EACtB,KAAK,SAAmB,EACxB,QAAQ,kBAAmB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA8CvB"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The computer's own folder picker, opened by the server.
|
|
3
|
+
*
|
|
4
|
+
* A browser cannot do this, and it is worth being precise about why rather than treating it as a
|
|
5
|
+
* missing feature. `<input type="file" webkitdirectory>` hands back file entries with names relative
|
|
6
|
+
* to the chosen folder and no absolute path; `showDirectoryPicker()` hands back a handle and no
|
|
7
|
+
* path either. Both withhold it deliberately — a page that learned where you keep your files has
|
|
8
|
+
* learned something about you that it was not given.
|
|
9
|
+
*
|
|
10
|
+
* But KONECK's server is on the same machine as the files, and acting through it already requires
|
|
11
|
+
* loopback and the token. So the *server* opens the dialog: the real one, the one every other
|
|
12
|
+
* application on the machine uses, and the path comes back because the process asking is entitled
|
|
13
|
+
* to know it.
|
|
14
|
+
*
|
|
15
|
+
* Two honest limits, both reported rather than papered over. The dialog appears on the machine
|
|
16
|
+
* running KONECK — which is the same machine, because acting is loopback-only, but somebody
|
|
17
|
+
* tunnelling a port should know that. And a machine with no desktop session has no dialog to show:
|
|
18
|
+
* over SSH there is nothing to put on screen, so the answer is to say so and let the built-in
|
|
19
|
+
* browser do the job instead.
|
|
20
|
+
*/
|
|
21
|
+
import os from 'os';
|
|
22
|
+
import { existsSync } from 'fs';
|
|
23
|
+
/**
|
|
24
|
+
* The ways to ask, in the order worth trying.
|
|
25
|
+
*
|
|
26
|
+
* zenity and kdialog are the GTK and KDE dialogs, so one of them is present on most desktops; yad
|
|
27
|
+
* is a fork of zenity that some distributions ship instead. macOS has AppleScript, which drives the
|
|
28
|
+
* real Finder dialog. Windows has PowerShell, whose folder browser is the shell's own.
|
|
29
|
+
*/
|
|
30
|
+
const LINUX_DIALOGS = [
|
|
31
|
+
{ file: 'zenity', args: (title, startIn) => ['--file-selection', '--directory', `--title=${title}`, `--filename=${startIn}/`] },
|
|
32
|
+
{ file: 'kdialog', args: (title, startIn) => ['--getexistingdirectory', startIn, '--title', title] },
|
|
33
|
+
{ file: 'qarma', args: (title, startIn) => ['--file-selection', '--directory', `--title=${title}`, `--filename=${startIn}/`] },
|
|
34
|
+
{ file: 'yad', args: (title, startIn) => ['--file-selection', '--directory', `--title=${title}`, `--filename=${startIn}/`] },
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Runs a dialog and stops waiting if it stops answering.
|
|
38
|
+
*
|
|
39
|
+
* execa's own timeout signals the process it started and then waits for its pipes to close, which
|
|
40
|
+
* is not the same thing — a dialog that dies in a way execa does not notice left the request open
|
|
41
|
+
* for the full two minutes. Measured: a dialog killed from outside took 120,027ms to settle, while
|
|
42
|
+
* one that exited cleanly took 2,588ms. So the wait is raced against a deadline and the process
|
|
43
|
+
* group is signalled, which is the same shape as the fix the command runner already needed.
|
|
44
|
+
*/
|
|
45
|
+
export async function runDialog(file, args,
|
|
46
|
+
/** Injectable so the giving-up path can be tested without waiting two minutes for it. */
|
|
47
|
+
waitMs = DIALOG_WAIT_MS) {
|
|
48
|
+
const { execa } = await import('execa');
|
|
49
|
+
const child = execa(file, [...args], { reject: false, detached: true });
|
|
50
|
+
const GAVE_UP = Symbol('gave-up');
|
|
51
|
+
let timer;
|
|
52
|
+
const deadline = new Promise(resolve => {
|
|
53
|
+
timer = setTimeout(() => resolve(GAVE_UP), waitMs);
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
const settled = await Promise.race([child.then(r => r), deadline]);
|
|
57
|
+
if (settled === GAVE_UP) {
|
|
58
|
+
// The whole group, because the dialog may have re-executed itself into a child.
|
|
59
|
+
try {
|
|
60
|
+
if (child.pid)
|
|
61
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
try {
|
|
65
|
+
child.kill('SIGKILL');
|
|
66
|
+
}
|
|
67
|
+
catch { /* already gone */ }
|
|
68
|
+
}
|
|
69
|
+
return { code: undefined, out: '', err: '', gaveUp: true };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
code: settled.exitCode ?? undefined,
|
|
73
|
+
out: String(settled.stdout ?? ''),
|
|
74
|
+
err: String(settled.stderr ?? ''),
|
|
75
|
+
gaveUp: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
if (timer)
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** How long a dialog nobody is answering may hold a request open. */
|
|
84
|
+
const DIALOG_WAIT_MS = 120_000;
|
|
85
|
+
/** Whether a desktop session exists to show a dialog on. */
|
|
86
|
+
export function hasDesktop(env = process.env, platform = process.platform) {
|
|
87
|
+
if (platform === 'darwin' || platform === 'win32')
|
|
88
|
+
return true;
|
|
89
|
+
return !!(env['DISPLAY'] || env['WAYLAND_DISPLAY']);
|
|
90
|
+
}
|
|
91
|
+
/** Which dialog tool would be used, or null when none is installed. */
|
|
92
|
+
export async function dialogAvailable(platform = process.platform) {
|
|
93
|
+
if (platform === 'darwin')
|
|
94
|
+
return 'osascript';
|
|
95
|
+
if (platform === 'win32')
|
|
96
|
+
return 'powershell';
|
|
97
|
+
const { execa } = await import('execa');
|
|
98
|
+
for (const dialog of LINUX_DIALOGS) {
|
|
99
|
+
const found = await execa('command', ['-v', dialog.file], { shell: true, reject: false });
|
|
100
|
+
if (found.exitCode === 0 && String(found.stdout).trim() !== '')
|
|
101
|
+
return dialog.file;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Opens the folder dialog and waits for an answer.
|
|
107
|
+
*
|
|
108
|
+
* A generous timeout, because the answer is a person deciding: two minutes is long enough to think
|
|
109
|
+
* and short enough that a dialog nobody is looking at does not hold a handle for ever.
|
|
110
|
+
*/
|
|
111
|
+
export async function chooseFolder(startIn = os.homedir(), title = 'Open a project', platform = process.platform) {
|
|
112
|
+
if (!hasDesktop()) {
|
|
113
|
+
return { ok: false, cancelled: false, reason: 'This machine has no desktop session, so there is no dialog to show — KONECK is probably '
|
|
114
|
+
+ 'running over SSH. Use the folder list instead.' };
|
|
115
|
+
}
|
|
116
|
+
const from = existsSync(startIn) ? startIn : os.homedir();
|
|
117
|
+
if (platform === 'darwin') {
|
|
118
|
+
// AppleScript drives the real Finder dialog, and returns POSIX path text.
|
|
119
|
+
const script = `set chosen to choose folder with prompt ${JSON.stringify(title)} `
|
|
120
|
+
+ `default location POSIX file ${JSON.stringify(from)}\n`
|
|
121
|
+
+ 'return POSIX path of chosen';
|
|
122
|
+
const done = await runDialog('osascript', ['-e', script]);
|
|
123
|
+
if (done.gaveUp)
|
|
124
|
+
return gaveUp();
|
|
125
|
+
if (done.code !== 0)
|
|
126
|
+
return cancelledOrFailed(done.err, done.code);
|
|
127
|
+
return finish(done.out);
|
|
128
|
+
}
|
|
129
|
+
if (platform === 'win32') {
|
|
130
|
+
const script = 'Add-Type -AssemblyName System.Windows.Forms; '
|
|
131
|
+
+ '$d = New-Object System.Windows.Forms.FolderBrowserDialog; '
|
|
132
|
+
+ `$d.Description = ${JSON.stringify(title)}; `
|
|
133
|
+
+ `$d.SelectedPath = ${JSON.stringify(from)}; `
|
|
134
|
+
+ 'if ($d.ShowDialog() -eq "OK") { $d.SelectedPath } else { exit 1 }';
|
|
135
|
+
const done = await runDialog('powershell.exe', ['-NoProfile', '-Command', script]);
|
|
136
|
+
if (done.gaveUp)
|
|
137
|
+
return gaveUp();
|
|
138
|
+
if (done.code !== 0)
|
|
139
|
+
return cancelledOrFailed(done.err, done.code);
|
|
140
|
+
return finish(done.out);
|
|
141
|
+
}
|
|
142
|
+
const tool = await dialogAvailable(platform);
|
|
143
|
+
if (!tool) {
|
|
144
|
+
return { ok: false, cancelled: false, reason: 'No folder dialog is installed. On most desktops that is the zenity package (or kdialog on '
|
|
145
|
+
+ 'KDE). Use the folder list instead.' };
|
|
146
|
+
}
|
|
147
|
+
const dialog = LINUX_DIALOGS.find(d => d.file === tool);
|
|
148
|
+
const done = await runDialog(dialog.file, dialog.args(title, from));
|
|
149
|
+
if (done.gaveUp)
|
|
150
|
+
return gaveUp();
|
|
151
|
+
// Every one of these exits non-zero when the dialog is dismissed, which is a choice not to choose
|
|
152
|
+
// rather than a failure, and must not be reported as one.
|
|
153
|
+
if (done.code !== 0)
|
|
154
|
+
return cancelledOrFailed(done.err, done.code);
|
|
155
|
+
return finish(done.out);
|
|
156
|
+
}
|
|
157
|
+
function gaveUp() {
|
|
158
|
+
return { ok: false, cancelled: false, reason: 'the folder dialog stopped answering and was closed — try the folder list instead' };
|
|
159
|
+
}
|
|
160
|
+
function cancelledOrFailed(stderr, code) {
|
|
161
|
+
const said = String(stderr ?? '').trim();
|
|
162
|
+
// A dismissed dialog says nothing; a broken one says something.
|
|
163
|
+
if (said === '')
|
|
164
|
+
return { ok: false, cancelled: true };
|
|
165
|
+
return { ok: false, cancelled: false,
|
|
166
|
+
reason: `the folder dialog failed${code !== undefined ? ` (${code})` : ''}: ${said}` };
|
|
167
|
+
}
|
|
168
|
+
function finish(stdout) {
|
|
169
|
+
// zenity can return several paths separated by | when multiple selection is on; take the first.
|
|
170
|
+
const path = String(stdout ?? '').split('|')[0].trim().replace(/\/+$/, '');
|
|
171
|
+
if (path === '')
|
|
172
|
+
return { ok: false, cancelled: true };
|
|
173
|
+
return { ok: true, path };
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=folder-dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"folder-dialog.js","sourceRoot":"","sources":["../src/folder-dialog.ts"],"names":[],"mappings":"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"}
|
package/dist/web/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/web/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuB/C,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAGrC,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AA6BD,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED,wBAAsB,QAAQ,CAC5B,YAAY,EAAE,WAAW,EAAE,OAAO,GAAE,UAAe,GAClD,OAAO,CAAC,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/web/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuB/C,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAGrC,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AA6BD,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAED,wBAAsB,QAAQ,CAC5B,YAAY,EAAE,WAAW,EAAE,OAAO,GAAE,UAAe,GAClD,OAAO,CAAC,SAAS,CAAC,CA44CpB"}
|
package/dist/web/api.js
CHANGED
|
@@ -27,7 +27,7 @@ import { listPast, readPast } from './history.js';
|
|
|
27
27
|
import { summarise as summariseTrajectory } from '../trajectory.js';
|
|
28
28
|
import { listDir, viewFile } from './files.js';
|
|
29
29
|
import { searchSessions, searchAllWorkspaces } from './search.js';
|
|
30
|
-
import { trustedWorkspaces, isWorkspaceTrusted } from '../workspace-trust.js';
|
|
30
|
+
import { trustedWorkspaces, isWorkspaceTrusted, trustWorkspace } from '../workspace-trust.js';
|
|
31
31
|
import { MODES } from '../permissions.js';
|
|
32
32
|
import { checkpoints, makeCheckpoint, checkpointPreview, restoreCheckpoint, gate, health, settings, writeSetting, projectContext, writeInstructions, runWebRefactor, REFACTOR_SHAPES, mcpServers } from './capabilities.js';
|
|
33
33
|
export const DEFAULT_WEB_PORT = 4300;
|
|
@@ -131,7 +131,7 @@ export async function startWeb(baseConfigIn, options = {}) {
|
|
|
131
131
|
if (route === '/api/workspaces' && req.method === 'GET') {
|
|
132
132
|
if (!mayRead)
|
|
133
133
|
return json(res, 401, { error: 'token required' });
|
|
134
|
-
const trusted = await trustedWorkspaces().catch(() => []);
|
|
134
|
+
const trusted = await trustedWorkspaces(baseConfig.stateHome).catch(() => []);
|
|
135
135
|
// The launch directory is always offered even if it is not in the store, because getting
|
|
136
136
|
// this far means it was trusted or explicitly run with --ci.
|
|
137
137
|
const choices = [...new Set([baseConfig.cwd, ...trusted])];
|
|
@@ -169,6 +169,159 @@ export async function startWeb(baseConfigIn, options = {}) {
|
|
|
169
169
|
}
|
|
170
170
|
// Switching workspace. Only to somewhere already trusted, and it says so when refused rather
|
|
171
171
|
// than failing quietly — being told to grant trust at a terminal is actionable.
|
|
172
|
+
/*
|
|
173
|
+
* Somewhere to look for a project, rather than a list of the ones already known.
|
|
174
|
+
*
|
|
175
|
+
* The picker offered only workspaces already trusted at a terminal, so opening a new project
|
|
176
|
+
* meant leaving the browser, running koneck there, answering a prompt, and coming back. For a
|
|
177
|
+
* tool whose whole point is that you can work from either surface, that is the wrong shape.
|
|
178
|
+
*
|
|
179
|
+
* Directories only, and only their names: this lists where you might work, not what is in it.
|
|
180
|
+
* A directory with a .git or a package.json is marked, because that is what somebody scanning
|
|
181
|
+
* a list of folders is looking for.
|
|
182
|
+
*/
|
|
183
|
+
/*
|
|
184
|
+
* Carrying a session to the terminal.
|
|
185
|
+
*
|
|
186
|
+
* Both surfaces already write the same session files — the browser saves a transcript the CLI
|
|
187
|
+
* can resume, and has since the day it was written. What was missing was anybody saying so:
|
|
188
|
+
* to continue in a terminal you had to know that `--resume` existed, know the id, and know it
|
|
189
|
+
* has to be run in the project's own directory.
|
|
190
|
+
*
|
|
191
|
+
* The session is flushed first. Resuming reads what is on disk, so handing over without
|
|
192
|
+
* writing the last turn would hand over a conversation missing its most recent exchange —
|
|
193
|
+
* which is the exchange you are most likely to be in the middle of.
|
|
194
|
+
*/
|
|
195
|
+
if (route === '/api/handoff' && req.method === 'POST') {
|
|
196
|
+
if (!mayAct)
|
|
197
|
+
return json(res, 401, { error: 'token required' });
|
|
198
|
+
const body = await readBody(req);
|
|
199
|
+
const session = registry.get(body.session ?? '');
|
|
200
|
+
if (!session)
|
|
201
|
+
return json(res, 404, { error: 'no such session' });
|
|
202
|
+
await session.persist().catch(() => undefined);
|
|
203
|
+
const info = session.info();
|
|
204
|
+
const id = session.fileId;
|
|
205
|
+
const sameDir = path.resolve(info.cwd) === path.resolve(process.cwd());
|
|
206
|
+
return json(res, 200, {
|
|
207
|
+
id,
|
|
208
|
+
cwd: info.cwd,
|
|
209
|
+
// Runnable as it stands, from wherever the reader happens to be.
|
|
210
|
+
command: sameDir ? `koneck --resume ${id}` : `cd ${info.cwd} && koneck --resume ${id}`,
|
|
211
|
+
turns: info.turns,
|
|
212
|
+
model: info.model,
|
|
213
|
+
provider: info.provider,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/*
|
|
217
|
+
* The computer's own folder dialog, opened by the server.
|
|
218
|
+
*
|
|
219
|
+
* A browser cannot do this: webkitdirectory and showDirectoryPicker both withhold the
|
|
220
|
+
* absolute path deliberately. The server can, because it is on the same machine as the files
|
|
221
|
+
* and reaching this route already requires loopback and the acting token.
|
|
222
|
+
*
|
|
223
|
+
* Loopback is load-bearing here rather than incidental: the dialog appears on the machine
|
|
224
|
+
* running KONECK, so a server bound to a network address would be putting a window on
|
|
225
|
+
* somebody else's screen and waiting for them to answer it.
|
|
226
|
+
*/
|
|
227
|
+
if (route === '/api/pickfolder' && req.method === 'POST') {
|
|
228
|
+
if (!mayAct)
|
|
229
|
+
return json(res, 401, { error: 'token required' });
|
|
230
|
+
if (!isLocal) {
|
|
231
|
+
return json(res, 400, { error: 'This server is listening on a network address, so the dialog would open on the '
|
|
232
|
+
+ 'machine running KONECK rather than yours. Use the folder list instead.' });
|
|
233
|
+
}
|
|
234
|
+
const body = await readBody(req);
|
|
235
|
+
const { chooseFolder, dialogAvailable, hasDesktop } = await import('../folder-dialog.js');
|
|
236
|
+
if (!hasDesktop()) {
|
|
237
|
+
return json(res, 200, { available: false, reason: 'This machine has no desktop session, so there is no dialog to show — KONECK is '
|
|
238
|
+
+ 'probably running over SSH. Use the folder list instead.' });
|
|
239
|
+
}
|
|
240
|
+
if (!(await dialogAvailable())) {
|
|
241
|
+
return json(res, 200, { available: false, reason: 'No folder dialog is installed. On most desktops that is the zenity package, or '
|
|
242
|
+
+ 'kdialog on KDE. Use the folder list instead.' });
|
|
243
|
+
}
|
|
244
|
+
const chosen = await chooseFolder(String(body.startIn ?? '') || workspace, body.newFolder === true ? 'Choose where the new folder goes' : 'Open a project');
|
|
245
|
+
if (!chosen.ok) {
|
|
246
|
+
// Dismissing a dialog is a decision, not a failure, and is reported as one.
|
|
247
|
+
return json(res, 200, chosen.cancelled
|
|
248
|
+
? { available: true, cancelled: true }
|
|
249
|
+
: { available: false, reason: chosen.reason });
|
|
250
|
+
}
|
|
251
|
+
return json(res, 200, { available: true, path: chosen.path });
|
|
252
|
+
}
|
|
253
|
+
/*
|
|
254
|
+
* A new folder, made where the dialog said.
|
|
255
|
+
*
|
|
256
|
+
* Its own step rather than part of the dialog, because a folder dialog chooses an existing
|
|
257
|
+
* folder — asking one to invent a name is asking it to be a save dialog, and the two behave
|
|
258
|
+
* differently on every platform.
|
|
259
|
+
*/
|
|
260
|
+
if (route === '/api/newfolder' && req.method === 'POST') {
|
|
261
|
+
if (!mayAct)
|
|
262
|
+
return json(res, 401, { error: 'token required' });
|
|
263
|
+
if (!isLocal)
|
|
264
|
+
return json(res, 400, { error: 'not from a network address' });
|
|
265
|
+
const body = await readBody(req);
|
|
266
|
+
const inside = path.resolve(String(body.inside ?? '') || workspace);
|
|
267
|
+
const name = String(body.name ?? '').trim();
|
|
268
|
+
// A name, not a path: anything with a separator in it is somewhere else entirely.
|
|
269
|
+
if (!name || /[\\/]/.test(name) || name === '.' || name === '..') {
|
|
270
|
+
return json(res, 400, { error: 'give the folder a name, without slashes in it' });
|
|
271
|
+
}
|
|
272
|
+
const made = path.join(inside, name);
|
|
273
|
+
const { mkdir } = await import('fs/promises');
|
|
274
|
+
try {
|
|
275
|
+
await mkdir(made, { recursive: false });
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
const why = err.code === 'EEXIST'
|
|
279
|
+
? `${made} already exists`
|
|
280
|
+
: `could not create ${made}`;
|
|
281
|
+
return json(res, 400, { error: why });
|
|
282
|
+
}
|
|
283
|
+
announce(` Folder created from the browser: ${made}`);
|
|
284
|
+
return json(res, 200, { path: made });
|
|
285
|
+
}
|
|
286
|
+
if (route === '/api/browse' && req.method === 'GET') {
|
|
287
|
+
if (!mayRead)
|
|
288
|
+
return json(res, 401, { error: 'token required' });
|
|
289
|
+
const asked = url.searchParams.get('path') ?? '';
|
|
290
|
+
const where = asked.trim() === ''
|
|
291
|
+
? os.homedir()
|
|
292
|
+
: path.resolve(asked.replace(/^~(?=\/|$)/, os.homedir()));
|
|
293
|
+
const { readdir, stat: statOf } = await import('fs/promises');
|
|
294
|
+
let entries;
|
|
295
|
+
try {
|
|
296
|
+
entries = await readdir(where, { withFileTypes: true });
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
return json(res, 400, { error: `cannot read ${where}` });
|
|
300
|
+
}
|
|
301
|
+
const dirs = [];
|
|
302
|
+
for (const entry of entries) {
|
|
303
|
+
if (!entry.isDirectory())
|
|
304
|
+
continue;
|
|
305
|
+
// Dot directories are noise in a project picker, and node_modules is worse than noise.
|
|
306
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
307
|
+
continue;
|
|
308
|
+
const full = path.join(where, entry.name);
|
|
309
|
+
const marks = await Promise.all([
|
|
310
|
+
statOf(path.join(full, '.git')).then(() => true, () => false),
|
|
311
|
+
statOf(path.join(full, 'package.json')).then(() => true, () => false),
|
|
312
|
+
]);
|
|
313
|
+
dirs.push({ name: entry.name, path: full, repo: marks[0], project: marks[0] || marks[1] });
|
|
314
|
+
if (dirs.length >= 400)
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
dirs.sort((a, b) => (b.project ? 1 : 0) - (a.project ? 1 : 0) || a.name.localeCompare(b.name));
|
|
318
|
+
return json(res, 200, {
|
|
319
|
+
path: where,
|
|
320
|
+
parent: path.dirname(where) === where ? null : path.dirname(where),
|
|
321
|
+
home: os.homedir(),
|
|
322
|
+
entries: dirs,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
172
325
|
if (route === '/api/workspace' && req.method === 'POST') {
|
|
173
326
|
if (!mayAct)
|
|
174
327
|
return json(res, 401, { error: 'token required' });
|
|
@@ -176,14 +329,36 @@ export async function startWeb(baseConfigIn, options = {}) {
|
|
|
176
329
|
const wanted = path.resolve(body.cwd ?? '');
|
|
177
330
|
if (!wanted)
|
|
178
331
|
return json(res, 400, { error: 'no workspace given' });
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
332
|
+
/*
|
|
333
|
+
* A folder opened from here is trusted from here, and said out loud.
|
|
334
|
+
*
|
|
335
|
+
* This used to refuse anything not already trusted at a terminal, and I defended that
|
|
336
|
+
* boundary. Then I looked at what it was actually buying: the same token that reaches this
|
|
337
|
+
* route reaches /api/send, which runs an agent that executes arbitrary commands — so
|
|
338
|
+
* whoever could switch the workspace could already `cd` anywhere and do more damage with a
|
|
339
|
+
* shell than with a directory change. The rule was inconsistent rather than protective, and
|
|
340
|
+
* its whole cost fell on the person using their own machine.
|
|
341
|
+
*
|
|
342
|
+
* What is kept is what was doing the work: loopback only, the acting token, and a line in
|
|
343
|
+
* the terminal naming the directory — so a switch is never silent, and a machine listening
|
|
344
|
+
* on a network address still refuses.
|
|
345
|
+
*/
|
|
346
|
+
const { stat: statDir } = await import('fs/promises');
|
|
347
|
+
const usable = await statDir(wanted).then(found => found.isDirectory(), () => false);
|
|
348
|
+
if (!usable)
|
|
349
|
+
return json(res, 400, { error: `${wanted} is not a directory` });
|
|
350
|
+
if (!isLocal) {
|
|
351
|
+
return json(res, 403, { error: 'This server is listening on a network address, so it will not open a directory '
|
|
352
|
+
+ 'through it. Switch workspace at a terminal.' });
|
|
353
|
+
}
|
|
354
|
+
const wasKnown = wanted === path.resolve(baseConfig.cwd)
|
|
355
|
+
|| await isWorkspaceTrusted(wanted, baseConfig.stateHome).catch(() => false);
|
|
356
|
+
if (!wasKnown) {
|
|
357
|
+
await trustWorkspace(wanted, baseConfig.stateHome).catch(() => undefined);
|
|
358
|
+
announce(` Workspace opened from the browser, and trusted: ${wanted}`);
|
|
184
359
|
}
|
|
185
360
|
workspace = wanted;
|
|
186
|
-
return json(res, 200, { cwd: workspace, name: path.basename(workspace) });
|
|
361
|
+
return json(res, 200, { cwd: workspace, name: path.basename(workspace), added: !wasKnown });
|
|
187
362
|
}
|
|
188
363
|
// The file tree, one directory at a time. Read-only.
|
|
189
364
|
if (route === '/api/tree' && req.method === 'GET') {
|
|
@@ -237,7 +412,7 @@ export async function startWeb(baseConfigIn, options = {}) {
|
|
|
237
412
|
if (url.searchParams.get('scope') === 'workspace') {
|
|
238
413
|
return json(res, 200, await searchSessions(workspace, q));
|
|
239
414
|
}
|
|
240
|
-
const trusted = await trustedWorkspaces().catch(() => []);
|
|
415
|
+
const trusted = await trustedWorkspaces(baseConfig.stateHome).catch(() => []);
|
|
241
416
|
return json(res, 200, await searchAllWorkspaces([workspace, baseConfig.cwd, ...trusted], q));
|
|
242
417
|
}
|
|
243
418
|
if (route === '/api/sessions' && req.method === 'GET') {
|