oh-my-opencode-slim 0.9.11 → 0.9.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/README.md +2 -1
- package/dist/cli/index.js +94 -23
- package/dist/hooks/apply-patch/execution-context.d.ts +1 -4
- package/dist/hooks/auto-update-checker/cache.d.ts +9 -4
- package/dist/hooks/auto-update-checker/checker.d.ts +4 -0
- package/dist/hooks/todo-continuation/index.d.ts +9 -0
- package/dist/hooks/todo-continuation/todo-hygiene.d.ts +37 -0
- package/dist/index.js +338 -161
- package/dist/tools/lsp/client.d.ts +1 -0
- package/package.json +12 -12
- package/src/skills/cartography/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -388,7 +388,7 @@ Slim only intercepts `apply_patch` before native execution. It rewrites recovera
|
|
|
388
388
|
<p><sub>Every merged contribution leaves a mark on the realm.</sub></p>
|
|
389
389
|
|
|
390
390
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
|
391
|
-
[](#contributors-)
|
|
392
392
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
|
393
393
|
</div>
|
|
394
394
|
|
|
@@ -445,6 +445,7 @@ Slim only intercepts `apply_patch` before native execution. It rewrites recovera
|
|
|
445
445
|
<td align="center" valign="top" width="16.66%"><a href="https://nettee.io/"><img src="https://avatars.githubusercontent.com/u/3953668?v=4?s=100" width="100px;" alt="nettee"/><br /><sub><b>nettee</b></sub></a><br /><a href="https://github.com/alvinunreal/oh-my-opencode-slim/commits?author=nettee" title="Code">💻</a></td>
|
|
446
446
|
<td align="center" valign="top" width="16.66%"><a href="https://github.com/atomlink-ye"><img src="https://avatars.githubusercontent.com/u/48194045?v=4?s=100" width="100px;" alt="Link"/><br /><sub><b>Link</b></sub></a><br /><a href="https://github.com/alvinunreal/oh-my-opencode-slim/commits?author=atomlink-ye" title="Code">💻</a></td>
|
|
447
447
|
<td align="center" valign="top" width="16.66%"><a href="https://github.com/blaszewski"><img src="https://avatars.githubusercontent.com/u/14119531?v=4?s=100" width="100px;" alt="Bartosz Łaszewski"/><br /><sub><b>Bartosz Łaszewski</b></sub></a><br /><a href="https://github.com/alvinunreal/oh-my-opencode-slim/commits?author=blaszewski" title="Code">💻</a></td>
|
|
448
|
+
<td align="center" valign="top" width="16.66%"><a href="https://github.com/huilang021x"><img src="https://avatars.githubusercontent.com/u/77293911?v=4?s=100" width="100px;" alt="huilang021x"/><br /><sub><b>huilang021x</b></sub></a><br /><a href="https://github.com/alvinunreal/oh-my-opencode-slim/commits?author=huilang021x" title="Code">💻</a></td>
|
|
448
449
|
</tr>
|
|
449
450
|
</tbody>
|
|
450
451
|
</table>
|
package/dist/cli/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
statSync as statSync2,
|
|
14
14
|
writeFileSync
|
|
15
15
|
} from "fs";
|
|
16
|
-
import {
|
|
16
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
17
17
|
|
|
18
18
|
// src/cli/paths.ts
|
|
19
19
|
import { existsSync, mkdirSync } from "fs";
|
|
@@ -481,8 +481,49 @@ function generateLiteConfig(installConfig) {
|
|
|
481
481
|
|
|
482
482
|
// src/cli/config-io.ts
|
|
483
483
|
var PACKAGE_NAME = "oh-my-opencode-slim";
|
|
484
|
+
function normalizePathForMatch(path) {
|
|
485
|
+
return path.replaceAll("\\", "/");
|
|
486
|
+
}
|
|
487
|
+
function findPackageRoot(startPath) {
|
|
488
|
+
let currentPath = dirname3(startPath);
|
|
489
|
+
while (true) {
|
|
490
|
+
const packageJsonPath = join3(currentPath, "package.json");
|
|
491
|
+
if (existsSync3(packageJsonPath)) {
|
|
492
|
+
try {
|
|
493
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
494
|
+
if (packageJson.name === PACKAGE_NAME) {
|
|
495
|
+
return currentPath;
|
|
496
|
+
}
|
|
497
|
+
} catch {}
|
|
498
|
+
}
|
|
499
|
+
const parentPath = dirname3(currentPath);
|
|
500
|
+
if (parentPath === currentPath) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
currentPath = parentPath;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function isPackageManagerInstall(path) {
|
|
507
|
+
const normalizedPath = normalizePathForMatch(path);
|
|
508
|
+
return normalizedPath.includes(`/node_modules/${PACKAGE_NAME}`);
|
|
509
|
+
}
|
|
510
|
+
function isLocalPackageRootEntry(entry) {
|
|
511
|
+
if (!entry || entry.startsWith("file://")) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
const packageJsonPath = join3(entry, "package.json");
|
|
515
|
+
if (!existsSync3(packageJsonPath)) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
520
|
+
return packageJson.name === PACKAGE_NAME;
|
|
521
|
+
} catch {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
484
525
|
function isPluginEntry(entry) {
|
|
485
|
-
return entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`) || entry.startsWith("file://") && entry.includes(PACKAGE_NAME);
|
|
526
|
+
return entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`) || entry.startsWith("file://") && entry.includes(PACKAGE_NAME) || isLocalPackageRootEntry(entry);
|
|
486
527
|
}
|
|
487
528
|
function getPluginEntry() {
|
|
488
529
|
const cliEntryPath = process.argv[1];
|
|
@@ -490,11 +531,11 @@ function getPluginEntry() {
|
|
|
490
531
|
return PACKAGE_NAME;
|
|
491
532
|
}
|
|
492
533
|
try {
|
|
493
|
-
const
|
|
494
|
-
if (!
|
|
534
|
+
const packageRoot = findPackageRoot(cliEntryPath);
|
|
535
|
+
if (!packageRoot || isPackageManagerInstall(packageRoot)) {
|
|
495
536
|
return PACKAGE_NAME;
|
|
496
537
|
}
|
|
497
|
-
return
|
|
538
|
+
return packageRoot;
|
|
498
539
|
} catch {
|
|
499
540
|
return PACKAGE_NAME;
|
|
500
541
|
}
|
|
@@ -684,8 +725,35 @@ function detectCurrentConfig() {
|
|
|
684
725
|
return result;
|
|
685
726
|
}
|
|
686
727
|
// src/cli/system.ts
|
|
728
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
687
729
|
import { statSync as statSync3 } from "fs";
|
|
688
730
|
var cachedOpenCodePath = null;
|
|
731
|
+
function resolvePathCommand(command) {
|
|
732
|
+
try {
|
|
733
|
+
const resolver = process.platform === "win32" ? "where" : "which";
|
|
734
|
+
const result = spawnSync2(resolver, [command], {
|
|
735
|
+
encoding: "utf-8",
|
|
736
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
737
|
+
});
|
|
738
|
+
if (result.status !== 0) {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
const resolved = result.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
742
|
+
return resolved ?? null;
|
|
743
|
+
} catch {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function canExecute(command, args) {
|
|
748
|
+
try {
|
|
749
|
+
const result = spawnSync2(command, args, {
|
|
750
|
+
stdio: "ignore"
|
|
751
|
+
});
|
|
752
|
+
return result.status === 0;
|
|
753
|
+
} catch {
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
689
757
|
function getOpenCodePaths() {
|
|
690
758
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
691
759
|
return [
|
|
@@ -721,6 +789,11 @@ function resolveOpenCodePath() {
|
|
|
721
789
|
if (cachedOpenCodePath) {
|
|
722
790
|
return cachedOpenCodePath;
|
|
723
791
|
}
|
|
792
|
+
const pathOpenCodePath = resolvePathCommand("opencode");
|
|
793
|
+
if (pathOpenCodePath) {
|
|
794
|
+
cachedOpenCodePath = pathOpenCodePath;
|
|
795
|
+
return pathOpenCodePath;
|
|
796
|
+
}
|
|
724
797
|
const paths = getOpenCodePaths();
|
|
725
798
|
for (const opencodePath of paths) {
|
|
726
799
|
if (opencodePath === "opencode")
|
|
@@ -736,33 +809,31 @@ function resolveOpenCodePath() {
|
|
|
736
809
|
return "opencode";
|
|
737
810
|
}
|
|
738
811
|
async function isOpenCodeInstalled() {
|
|
812
|
+
const pathOpenCodePath = resolvePathCommand("opencode");
|
|
813
|
+
if (pathOpenCodePath && canExecute(pathOpenCodePath, ["--version"])) {
|
|
814
|
+
cachedOpenCodePath = pathOpenCodePath;
|
|
815
|
+
return true;
|
|
816
|
+
}
|
|
739
817
|
const paths = getOpenCodePaths();
|
|
740
818
|
for (const opencodePath of paths) {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (proc.exitCode === 0) {
|
|
748
|
-
cachedOpenCodePath = opencodePath;
|
|
749
|
-
return true;
|
|
750
|
-
}
|
|
751
|
-
} catch {}
|
|
819
|
+
if (opencodePath === "opencode")
|
|
820
|
+
continue;
|
|
821
|
+
if (canExecute(opencodePath, ["--version"])) {
|
|
822
|
+
cachedOpenCodePath = opencodePath;
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
752
825
|
}
|
|
753
826
|
return false;
|
|
754
827
|
}
|
|
755
828
|
async function getOpenCodeVersion() {
|
|
756
829
|
const opencodePath = resolveOpenCodePath();
|
|
757
830
|
try {
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
831
|
+
const result = spawnSync2(opencodePath, ["--version"], {
|
|
832
|
+
encoding: "utf-8",
|
|
833
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
761
834
|
});
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (proc.exitCode === 0) {
|
|
765
|
-
return output.trim();
|
|
835
|
+
if (result.status === 0) {
|
|
836
|
+
return result.stdout.trim();
|
|
766
837
|
}
|
|
767
838
|
} catch {}
|
|
768
839
|
return null;
|
|
@@ -21,10 +21,7 @@ export type ResolvedPreparedUpdate = {
|
|
|
21
21
|
nextText: string;
|
|
22
22
|
};
|
|
23
23
|
export declare function isMissingPathError(error: unknown): boolean;
|
|
24
|
-
export declare function parseValidatedPatch(
|
|
25
|
-
hunks: PatchHunk[];
|
|
26
|
-
pathsNormalized: boolean;
|
|
27
|
-
}>;
|
|
24
|
+
export declare function parseValidatedPatch(patchText: string): PatchHunk[];
|
|
28
25
|
export declare function createPatchExecutionContext(root: string, patchText: string, worktree?: string): Promise<PatchExecutionContext>;
|
|
29
26
|
export declare function resolvePreparedUpdate(filePath: string, currentText: string, hunk: UpdatePatchHunk, cfg: ApplyPatchRuntimeOptions): ResolvedPreparedUpdate;
|
|
30
27
|
export declare function stageAddedText(contents: string): string;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
interface AutoUpdateInstallContext {
|
|
2
|
+
installDir: string;
|
|
3
|
+
packageJsonPath: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function resolveInstallContext(runtimePackageJsonPath?: string | null): AutoUpdateInstallContext | null;
|
|
1
6
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* @param packageName The name of the package to invalidate.
|
|
7
|
+
* Prepares the current install root for a clean re-install of the target version.
|
|
8
|
+
* Returns the install directory to run `bun install` in.
|
|
5
9
|
*/
|
|
6
|
-
export declare function
|
|
10
|
+
export declare function preparePackageUpdate(version: string, packageName?: string, runtimePackageJsonPath?: string | null): string | null;
|
|
11
|
+
export {};
|
|
@@ -9,6 +9,10 @@ export declare function extractChannel(version: string | null): string;
|
|
|
9
9
|
* Resolves the version of the plugin when running in local development mode.
|
|
10
10
|
*/
|
|
11
11
|
export declare function getLocalDevVersion(directory: string): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Resolves the package.json for the currently running plugin bundle.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getCurrentRuntimePackageJsonPath(currentModuleUrl?: string): string | null;
|
|
12
16
|
/**
|
|
13
17
|
* Searches across all config locations to find the current installation entry for this plugin.
|
|
14
18
|
*/
|
|
@@ -6,6 +6,15 @@ export declare function createTodoContinuationHook(ctx: PluginInput, config?: {
|
|
|
6
6
|
autoEnableThreshold?: number;
|
|
7
7
|
}): {
|
|
8
8
|
tool: Record<string, unknown>;
|
|
9
|
+
handleToolExecuteAfter: (input: {
|
|
10
|
+
tool: string;
|
|
11
|
+
sessionID?: string;
|
|
12
|
+
}) => Promise<void>;
|
|
13
|
+
handleChatSystemTransform: (input: {
|
|
14
|
+
sessionID?: string;
|
|
15
|
+
}, output: {
|
|
16
|
+
system: string[];
|
|
17
|
+
}) => Promise<void>;
|
|
9
18
|
handleEvent: (input: {
|
|
10
19
|
event: {
|
|
11
20
|
type: string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const TODO_HYGIENE_REMINDER = "If the active task changed or finished, update the todo list to match the current work state.";
|
|
2
|
+
export declare const TODO_FINAL_ACTIVE_REMINDER = "If you are finishing now, do not leave the active todo in_progress. Mark it completed, or move unfinished work back to pending.";
|
|
3
|
+
interface ToolInput {
|
|
4
|
+
tool: string;
|
|
5
|
+
sessionID?: string;
|
|
6
|
+
}
|
|
7
|
+
interface SystemInput {
|
|
8
|
+
sessionID?: string;
|
|
9
|
+
}
|
|
10
|
+
interface SystemOutput {
|
|
11
|
+
system: string[];
|
|
12
|
+
}
|
|
13
|
+
interface EventInput {
|
|
14
|
+
type: string;
|
|
15
|
+
properties?: {
|
|
16
|
+
info?: {
|
|
17
|
+
id?: string;
|
|
18
|
+
};
|
|
19
|
+
sessionID?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
interface Options {
|
|
23
|
+
getTodoState: (sessionID: string) => Promise<{
|
|
24
|
+
hasOpenTodos: boolean;
|
|
25
|
+
openCount: number;
|
|
26
|
+
inProgressCount: number;
|
|
27
|
+
pendingCount: number;
|
|
28
|
+
}>;
|
|
29
|
+
shouldInject?: (sessionID: string) => boolean;
|
|
30
|
+
log?: (message: string, meta?: Record<string, unknown>) => void;
|
|
31
|
+
}
|
|
32
|
+
export declare function createTodoHygiene(options: Options): {
|
|
33
|
+
handleToolExecuteAfter(input: ToolInput): Promise<void>;
|
|
34
|
+
handleChatSystemTransform(input: SystemInput, output: SystemOutput): Promise<void>;
|
|
35
|
+
handleEvent(event: EventInput): void;
|
|
36
|
+
};
|
|
37
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -3906,44 +3906,19 @@ function collectPatchTargets(root, hunks) {
|
|
|
3906
3906
|
}
|
|
3907
3907
|
return [...targets];
|
|
3908
3908
|
}
|
|
3909
|
-
function validatePatchPaths(hunks) {
|
|
3910
|
-
for (const hunk of hunks) {
|
|
3911
|
-
if (path3.isAbsolute(hunk.path)) {
|
|
3912
|
-
throw createApplyPatchValidationError(`absolute patch paths are not allowed: ${hunk.path}`);
|
|
3913
|
-
}
|
|
3914
|
-
if (hunk.type === "update" && hunk.move_path && path3.isAbsolute(hunk.move_path)) {
|
|
3915
|
-
throw createApplyPatchValidationError(`absolute patch paths are not allowed: ${hunk.move_path}`);
|
|
3916
|
-
}
|
|
3917
|
-
}
|
|
3918
|
-
}
|
|
3919
|
-
function toPortablePatchPath(filePath) {
|
|
3920
|
-
return filePath.split(path3.sep).join("/");
|
|
3921
|
-
}
|
|
3922
3909
|
function toRelativePatchPath(root, target) {
|
|
3923
3910
|
const relative = path3.relative(root, target);
|
|
3924
|
-
return
|
|
3911
|
+
return (relative.length === 0 ? "." : relative).replaceAll("\\", "/");
|
|
3925
3912
|
}
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
return value;
|
|
3929
|
-
}
|
|
3930
|
-
const guardContext = createPathGuardContext(root, worktree);
|
|
3931
|
-
const target = path3.resolve(value);
|
|
3932
|
-
await guard(guardContext, target);
|
|
3933
|
-
const [rootReal, targetReal] = await Promise.all([
|
|
3934
|
-
guardContext.rootReal,
|
|
3935
|
-
realCached(guardContext, target)
|
|
3936
|
-
]);
|
|
3937
|
-
if (!inside(rootReal, targetReal)) {
|
|
3938
|
-
throw createApplyPatchBlockedError(`patch contains path outside workspace root: ${target}`);
|
|
3939
|
-
}
|
|
3940
|
-
return toRelativePatchPath(root, target);
|
|
3913
|
+
function normalizePatchPath(root, value) {
|
|
3914
|
+
return path3.isAbsolute(value) ? toRelativePatchPath(root, path3.resolve(value)) : value;
|
|
3941
3915
|
}
|
|
3942
|
-
|
|
3916
|
+
function normalizePatchPaths(root, hunks) {
|
|
3917
|
+
const resolvedRoot = path3.resolve(root);
|
|
3943
3918
|
const normalized = [];
|
|
3944
3919
|
let changed = false;
|
|
3945
3920
|
for (const hunk of hunks) {
|
|
3946
|
-
const normalizedPath =
|
|
3921
|
+
const normalizedPath = normalizePatchPath(resolvedRoot, hunk.path);
|
|
3947
3922
|
if (hunk.type !== "update") {
|
|
3948
3923
|
changed ||= normalizedPath !== hunk.path;
|
|
3949
3924
|
normalized.push(normalizedPath === hunk.path ? hunk : {
|
|
@@ -3952,7 +3927,7 @@ async function normalizeAbsolutePatchPaths(root, worktree, hunks) {
|
|
|
3952
3927
|
});
|
|
3953
3928
|
continue;
|
|
3954
3929
|
}
|
|
3955
|
-
const normalizedMovePath = hunk.move_path ?
|
|
3930
|
+
const normalizedMovePath = hunk.move_path ? normalizePatchPath(resolvedRoot, hunk.move_path) : undefined;
|
|
3956
3931
|
changed ||= normalizedPath !== hunk.path || normalizedMovePath !== hunk.move_path;
|
|
3957
3932
|
normalized.push(normalizedPath === hunk.path && normalizedMovePath === hunk.move_path ? hunk : {
|
|
3958
3933
|
...hunk,
|
|
@@ -3969,7 +3944,7 @@ async function guardPatchTargets(root, worktree, targets) {
|
|
|
3969
3944
|
}
|
|
3970
3945
|
return targets.length;
|
|
3971
3946
|
}
|
|
3972
|
-
|
|
3947
|
+
function parseValidatedPatch(patchText) {
|
|
3973
3948
|
let hunks;
|
|
3974
3949
|
try {
|
|
3975
3950
|
hunks = parsePatchStrict(patchText).hunks;
|
|
@@ -3986,12 +3961,7 @@ async function parseValidatedPatch(root, patchText, worktree) {
|
|
|
3986
3961
|
}
|
|
3987
3962
|
throw createApplyPatchValidationError("no hunks found");
|
|
3988
3963
|
}
|
|
3989
|
-
|
|
3990
|
-
validatePatchPaths(normalizedPatch.hunks);
|
|
3991
|
-
return {
|
|
3992
|
-
hunks: normalizedPatch.hunks,
|
|
3993
|
-
pathsNormalized: normalizedPatch.changed
|
|
3994
|
-
};
|
|
3964
|
+
return hunks;
|
|
3995
3965
|
}
|
|
3996
3966
|
async function readPreparedFileText(filePath, verb) {
|
|
3997
3967
|
try {
|
|
@@ -4004,8 +3974,9 @@ async function readPreparedFileText(filePath, verb) {
|
|
|
4004
3974
|
}
|
|
4005
3975
|
}
|
|
4006
3976
|
async function createPatchExecutionContext(root, patchText, worktree) {
|
|
4007
|
-
const
|
|
4008
|
-
await guardPatchTargets(root, worktree, collectPatchTargets(root,
|
|
3977
|
+
const parsedHunks = parseValidatedPatch(patchText);
|
|
3978
|
+
await guardPatchTargets(root, worktree, collectPatchTargets(root, parsedHunks));
|
|
3979
|
+
const normalized = normalizePatchPaths(root, parsedHunks);
|
|
4009
3980
|
const files = createFileCacheContext();
|
|
4010
3981
|
const staged = new Map;
|
|
4011
3982
|
async function assertPreparedPathMissing(filePath, verb) {
|
|
@@ -4043,8 +4014,8 @@ async function createPatchExecutionContext(root, patchText, worktree) {
|
|
|
4043
4014
|
return state;
|
|
4044
4015
|
}
|
|
4045
4016
|
return {
|
|
4046
|
-
hunks,
|
|
4047
|
-
pathsNormalized,
|
|
4017
|
+
hunks: normalized.hunks,
|
|
4018
|
+
pathsNormalized: normalized.changed,
|
|
4048
4019
|
staged,
|
|
4049
4020
|
getPreparedFileState,
|
|
4050
4021
|
assertPreparedPathMissing
|
|
@@ -4233,6 +4204,9 @@ async function rewritePatch(root, patchText, cfg, worktree) {
|
|
|
4233
4204
|
let rewrittenChunks = 0;
|
|
4234
4205
|
const rewriteModes = new Set;
|
|
4235
4206
|
const totalChunks = hunks.reduce((count, hunk) => count + (hunk.type === "update" ? hunk.chunks.length : 0), 0);
|
|
4207
|
+
if (pathsNormalized) {
|
|
4208
|
+
rewriteModes.add("normalize:patch-paths");
|
|
4209
|
+
}
|
|
4236
4210
|
const dependencyGroups = new Map;
|
|
4237
4211
|
for (const hunk of hunks) {
|
|
4238
4212
|
if (hunk.type === "add") {
|
|
@@ -4352,7 +4326,7 @@ async function rewritePatch(root, patchText, cfg, worktree) {
|
|
|
4352
4326
|
changed: true,
|
|
4353
4327
|
rewrittenChunks: 0,
|
|
4354
4328
|
totalChunks,
|
|
4355
|
-
rewriteModes: [
|
|
4329
|
+
rewriteModes: [...rewriteModes].sort()
|
|
4356
4330
|
};
|
|
4357
4331
|
}
|
|
4358
4332
|
if (normalizedPatchText !== patchText) {
|
|
@@ -4435,8 +4409,13 @@ function createApplyPatchHook(ctx) {
|
|
|
4435
4409
|
};
|
|
4436
4410
|
}
|
|
4437
4411
|
// src/hooks/auto-update-checker/cache.ts
|
|
4412
|
+
import * as fs5 from "fs";
|
|
4413
|
+
import * as path7 from "path";
|
|
4414
|
+
// src/hooks/auto-update-checker/checker.ts
|
|
4438
4415
|
import * as fs4 from "fs";
|
|
4439
4416
|
import * as path6 from "path";
|
|
4417
|
+
import { fileURLToPath } from "url";
|
|
4418
|
+
|
|
4440
4419
|
// src/hooks/auto-update-checker/constants.ts
|
|
4441
4420
|
import * as os2 from "os";
|
|
4442
4421
|
import * as path5 from "path";
|
|
@@ -4455,80 +4434,7 @@ var configPaths = getOpenCodeConfigPaths();
|
|
|
4455
4434
|
var USER_OPENCODE_CONFIG = configPaths[0];
|
|
4456
4435
|
var USER_OPENCODE_CONFIG_JSONC = configPaths[1];
|
|
4457
4436
|
|
|
4458
|
-
// src/hooks/auto-update-checker/cache.ts
|
|
4459
|
-
function removeFromBunLock(packageName) {
|
|
4460
|
-
const lockPath = path6.join(CACHE_DIR, "bun.lock");
|
|
4461
|
-
if (!fs4.existsSync(lockPath))
|
|
4462
|
-
return false;
|
|
4463
|
-
try {
|
|
4464
|
-
const content = fs4.readFileSync(lockPath, "utf-8");
|
|
4465
|
-
let lock;
|
|
4466
|
-
try {
|
|
4467
|
-
lock = JSON.parse(stripJsonComments(content));
|
|
4468
|
-
} catch {
|
|
4469
|
-
return false;
|
|
4470
|
-
}
|
|
4471
|
-
let modified = false;
|
|
4472
|
-
if (lock.workspaces?.[""]?.dependencies?.[packageName]) {
|
|
4473
|
-
delete lock.workspaces[""].dependencies[packageName];
|
|
4474
|
-
modified = true;
|
|
4475
|
-
}
|
|
4476
|
-
if (lock.packages?.[packageName]) {
|
|
4477
|
-
delete lock.packages[packageName];
|
|
4478
|
-
modified = true;
|
|
4479
|
-
}
|
|
4480
|
-
if (modified) {
|
|
4481
|
-
fs4.writeFileSync(lockPath, JSON.stringify(lock, null, 2));
|
|
4482
|
-
log(`[auto-update-checker] Removed from bun.lock: ${packageName}`);
|
|
4483
|
-
}
|
|
4484
|
-
return modified;
|
|
4485
|
-
} catch (err) {
|
|
4486
|
-
log(`[auto-update-checker] Failed to process bun.lock:`, err);
|
|
4487
|
-
return false;
|
|
4488
|
-
}
|
|
4489
|
-
}
|
|
4490
|
-
function invalidatePackage(packageName = PACKAGE_NAME) {
|
|
4491
|
-
try {
|
|
4492
|
-
const pkgDir = path6.join(CACHE_DIR, "node_modules", packageName);
|
|
4493
|
-
const pkgJsonPath = path6.join(CACHE_DIR, "package.json");
|
|
4494
|
-
let packageRemoved = false;
|
|
4495
|
-
let dependencyRemoved = false;
|
|
4496
|
-
let lockRemoved = false;
|
|
4497
|
-
if (fs4.existsSync(pkgDir)) {
|
|
4498
|
-
fs4.rmSync(pkgDir, { recursive: true, force: true });
|
|
4499
|
-
log(`[auto-update-checker] Package removed: ${pkgDir}`);
|
|
4500
|
-
packageRemoved = true;
|
|
4501
|
-
}
|
|
4502
|
-
if (fs4.existsSync(pkgJsonPath)) {
|
|
4503
|
-
try {
|
|
4504
|
-
const content = fs4.readFileSync(pkgJsonPath, "utf-8");
|
|
4505
|
-
const pkgJson = JSON.parse(stripJsonComments(content));
|
|
4506
|
-
if (pkgJson.dependencies?.[packageName]) {
|
|
4507
|
-
delete pkgJson.dependencies[packageName];
|
|
4508
|
-
fs4.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
4509
|
-
log(`[auto-update-checker] Dependency removed from package.json: ${packageName}`);
|
|
4510
|
-
dependencyRemoved = true;
|
|
4511
|
-
}
|
|
4512
|
-
} catch (err) {
|
|
4513
|
-
log(`[auto-update-checker] Failed to update package.json for invalidation:`, err);
|
|
4514
|
-
}
|
|
4515
|
-
}
|
|
4516
|
-
lockRemoved = removeFromBunLock(packageName);
|
|
4517
|
-
if (!packageRemoved && !dependencyRemoved && !lockRemoved) {
|
|
4518
|
-
log(`[auto-update-checker] Package not found, nothing to invalidate: ${packageName}`);
|
|
4519
|
-
return false;
|
|
4520
|
-
}
|
|
4521
|
-
return true;
|
|
4522
|
-
} catch (err) {
|
|
4523
|
-
log("[auto-update-checker] Failed to invalidate package:", err);
|
|
4524
|
-
return false;
|
|
4525
|
-
}
|
|
4526
|
-
}
|
|
4527
|
-
|
|
4528
4437
|
// src/hooks/auto-update-checker/checker.ts
|
|
4529
|
-
import * as fs5 from "fs";
|
|
4530
|
-
import * as path7 from "path";
|
|
4531
|
-
import { fileURLToPath } from "url";
|
|
4532
4438
|
function isPrereleaseVersion(version) {
|
|
4533
4439
|
return version.includes("-");
|
|
4534
4440
|
}
|
|
@@ -4552,8 +4458,8 @@ function extractChannel(version) {
|
|
|
4552
4458
|
}
|
|
4553
4459
|
function getConfigPaths(directory) {
|
|
4554
4460
|
return [
|
|
4555
|
-
|
|
4556
|
-
|
|
4461
|
+
path6.join(directory, ".opencode", "opencode.json"),
|
|
4462
|
+
path6.join(directory, ".opencode", "opencode.jsonc"),
|
|
4557
4463
|
USER_OPENCODE_CONFIG,
|
|
4558
4464
|
USER_OPENCODE_CONFIG_JSONC
|
|
4559
4465
|
];
|
|
@@ -4561,9 +4467,9 @@ function getConfigPaths(directory) {
|
|
|
4561
4467
|
function getLocalDevPath(directory) {
|
|
4562
4468
|
for (const configPath of getConfigPaths(directory)) {
|
|
4563
4469
|
try {
|
|
4564
|
-
if (!
|
|
4470
|
+
if (!fs4.existsSync(configPath))
|
|
4565
4471
|
continue;
|
|
4566
|
-
const content =
|
|
4472
|
+
const content = fs4.readFileSync(configPath, "utf-8");
|
|
4567
4473
|
const config = JSON.parse(stripJsonComments(content));
|
|
4568
4474
|
const plugins = config.plugin ?? [];
|
|
4569
4475
|
for (const entry of plugins) {
|
|
@@ -4581,19 +4487,19 @@ function getLocalDevPath(directory) {
|
|
|
4581
4487
|
}
|
|
4582
4488
|
function findPackageJsonUp(startPath) {
|
|
4583
4489
|
try {
|
|
4584
|
-
const stat2 =
|
|
4585
|
-
let dir = stat2.isDirectory() ? startPath :
|
|
4490
|
+
const stat2 = fs4.statSync(startPath);
|
|
4491
|
+
let dir = stat2.isDirectory() ? startPath : path6.dirname(startPath);
|
|
4586
4492
|
for (let i = 0;i < 10; i++) {
|
|
4587
|
-
const pkgPath =
|
|
4588
|
-
if (
|
|
4493
|
+
const pkgPath = path6.join(dir, "package.json");
|
|
4494
|
+
if (fs4.existsSync(pkgPath)) {
|
|
4589
4495
|
try {
|
|
4590
|
-
const content =
|
|
4496
|
+
const content = fs4.readFileSync(pkgPath, "utf-8");
|
|
4591
4497
|
const pkg = JSON.parse(content);
|
|
4592
4498
|
if (pkg.name === PACKAGE_NAME)
|
|
4593
4499
|
return pkgPath;
|
|
4594
4500
|
} catch {}
|
|
4595
4501
|
}
|
|
4596
|
-
const parent =
|
|
4502
|
+
const parent = path6.dirname(dir);
|
|
4597
4503
|
if (parent === dir)
|
|
4598
4504
|
break;
|
|
4599
4505
|
dir = parent;
|
|
@@ -4609,19 +4515,28 @@ function getLocalDevVersion(directory) {
|
|
|
4609
4515
|
const pkgPath = findPackageJsonUp(localPath);
|
|
4610
4516
|
if (!pkgPath)
|
|
4611
4517
|
return null;
|
|
4612
|
-
const content =
|
|
4518
|
+
const content = fs4.readFileSync(pkgPath, "utf-8");
|
|
4613
4519
|
const pkg = JSON.parse(content);
|
|
4614
4520
|
return pkg.version ?? null;
|
|
4615
4521
|
} catch {
|
|
4616
4522
|
return null;
|
|
4617
4523
|
}
|
|
4618
4524
|
}
|
|
4525
|
+
function getCurrentRuntimePackageJsonPath(currentModuleUrl = import.meta.url) {
|
|
4526
|
+
try {
|
|
4527
|
+
const currentDir = path6.dirname(fileURLToPath(currentModuleUrl));
|
|
4528
|
+
return findPackageJsonUp(currentDir);
|
|
4529
|
+
} catch (err) {
|
|
4530
|
+
log("[auto-update-checker] Failed to resolve runtime package path:", err);
|
|
4531
|
+
return null;
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4619
4534
|
function findPluginEntry(directory) {
|
|
4620
4535
|
for (const configPath of getConfigPaths(directory)) {
|
|
4621
4536
|
try {
|
|
4622
|
-
if (!
|
|
4537
|
+
if (!fs4.existsSync(configPath))
|
|
4623
4538
|
continue;
|
|
4624
|
-
const content =
|
|
4539
|
+
const content = fs4.readFileSync(configPath, "utf-8");
|
|
4625
4540
|
const config = JSON.parse(stripJsonComments(content));
|
|
4626
4541
|
const plugins = config.plugin ?? [];
|
|
4627
4542
|
for (const entry of plugins) {
|
|
@@ -4648,8 +4563,9 @@ function getCachedVersion() {
|
|
|
4648
4563
|
if (cachedPackageVersion)
|
|
4649
4564
|
return cachedPackageVersion;
|
|
4650
4565
|
try {
|
|
4651
|
-
|
|
4652
|
-
|
|
4566
|
+
const runtimePackageJsonPath = getCurrentRuntimePackageJsonPath();
|
|
4567
|
+
if (runtimePackageJsonPath && fs4.existsSync(runtimePackageJsonPath)) {
|
|
4568
|
+
const content = fs4.readFileSync(runtimePackageJsonPath, "utf-8");
|
|
4653
4569
|
const pkg = JSON.parse(content);
|
|
4654
4570
|
if (pkg.version) {
|
|
4655
4571
|
cachedPackageVersion = pkg.version;
|
|
@@ -4658,10 +4574,8 @@ function getCachedVersion() {
|
|
|
4658
4574
|
}
|
|
4659
4575
|
} catch {}
|
|
4660
4576
|
try {
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
if (pkgPath) {
|
|
4664
|
-
const content = fs5.readFileSync(pkgPath, "utf-8");
|
|
4577
|
+
if (fs4.existsSync(INSTALLED_PACKAGE_JSON)) {
|
|
4578
|
+
const content = fs4.readFileSync(INSTALLED_PACKAGE_JSON, "utf-8");
|
|
4665
4579
|
const pkg = JSON.parse(content);
|
|
4666
4580
|
if (pkg.version) {
|
|
4667
4581
|
cachedPackageVersion = pkg.version;
|
|
@@ -4692,6 +4606,108 @@ async function getLatestVersion(channel = "latest") {
|
|
|
4692
4606
|
}
|
|
4693
4607
|
}
|
|
4694
4608
|
|
|
4609
|
+
// src/hooks/auto-update-checker/cache.ts
|
|
4610
|
+
function removeFromBunLock(installDir, packageName) {
|
|
4611
|
+
const lockPath = path7.join(installDir, "bun.lock");
|
|
4612
|
+
if (!fs5.existsSync(lockPath))
|
|
4613
|
+
return false;
|
|
4614
|
+
try {
|
|
4615
|
+
const content = fs5.readFileSync(lockPath, "utf-8");
|
|
4616
|
+
let lock;
|
|
4617
|
+
try {
|
|
4618
|
+
lock = JSON.parse(stripJsonComments(content));
|
|
4619
|
+
} catch {
|
|
4620
|
+
return false;
|
|
4621
|
+
}
|
|
4622
|
+
let modified = false;
|
|
4623
|
+
if (lock.workspaces?.[""]?.dependencies?.[packageName]) {
|
|
4624
|
+
delete lock.workspaces[""].dependencies[packageName];
|
|
4625
|
+
modified = true;
|
|
4626
|
+
}
|
|
4627
|
+
if (lock.packages?.[packageName]) {
|
|
4628
|
+
delete lock.packages[packageName];
|
|
4629
|
+
modified = true;
|
|
4630
|
+
}
|
|
4631
|
+
if (modified) {
|
|
4632
|
+
fs5.writeFileSync(lockPath, JSON.stringify(lock, null, 2));
|
|
4633
|
+
log(`[auto-update-checker] Removed from bun.lock: ${packageName}`);
|
|
4634
|
+
}
|
|
4635
|
+
return modified;
|
|
4636
|
+
} catch (err) {
|
|
4637
|
+
log(`[auto-update-checker] Failed to process bun.lock:`, err);
|
|
4638
|
+
return false;
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
function ensureDependencyVersion(packageJsonPath, packageName, version) {
|
|
4642
|
+
if (!fs5.existsSync(packageJsonPath))
|
|
4643
|
+
return false;
|
|
4644
|
+
try {
|
|
4645
|
+
const content = fs5.readFileSync(packageJsonPath, "utf-8");
|
|
4646
|
+
const pkgJson = JSON.parse(stripJsonComments(content));
|
|
4647
|
+
const dependencies = { ...pkgJson.dependencies ?? {} };
|
|
4648
|
+
if (dependencies[packageName] === version) {
|
|
4649
|
+
return true;
|
|
4650
|
+
}
|
|
4651
|
+
dependencies[packageName] = version;
|
|
4652
|
+
pkgJson.dependencies = dependencies;
|
|
4653
|
+
fs5.writeFileSync(packageJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
4654
|
+
log(`[auto-update-checker] Updated dependency in package.json: ${packageName} \u2192 ${version}`);
|
|
4655
|
+
return true;
|
|
4656
|
+
} catch (err) {
|
|
4657
|
+
log(`[auto-update-checker] Failed to update package.json dependency for auto-update:`, err);
|
|
4658
|
+
return false;
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
function removeInstalledPackage(installDir, packageName) {
|
|
4662
|
+
const pkgDir = path7.join(installDir, "node_modules", packageName);
|
|
4663
|
+
if (!fs5.existsSync(pkgDir))
|
|
4664
|
+
return false;
|
|
4665
|
+
fs5.rmSync(pkgDir, { recursive: true, force: true });
|
|
4666
|
+
log(`[auto-update-checker] Package removed: ${pkgDir}`);
|
|
4667
|
+
return true;
|
|
4668
|
+
}
|
|
4669
|
+
function resolveInstallContext(runtimePackageJsonPath = getCurrentRuntimePackageJsonPath()) {
|
|
4670
|
+
if (runtimePackageJsonPath) {
|
|
4671
|
+
const packageDir = path7.dirname(runtimePackageJsonPath);
|
|
4672
|
+
const nodeModulesDir = path7.dirname(packageDir);
|
|
4673
|
+
if (path7.basename(packageDir) === PACKAGE_NAME && path7.basename(nodeModulesDir) === "node_modules") {
|
|
4674
|
+
const installDir = path7.dirname(nodeModulesDir);
|
|
4675
|
+
const packageJsonPath = path7.join(installDir, "package.json");
|
|
4676
|
+
if (fs5.existsSync(packageJsonPath)) {
|
|
4677
|
+
return { installDir, packageJsonPath };
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
return null;
|
|
4681
|
+
}
|
|
4682
|
+
const legacyPackageJsonPath = path7.join(CACHE_DIR, "package.json");
|
|
4683
|
+
if (fs5.existsSync(legacyPackageJsonPath)) {
|
|
4684
|
+
return { installDir: CACHE_DIR, packageJsonPath: legacyPackageJsonPath };
|
|
4685
|
+
}
|
|
4686
|
+
return null;
|
|
4687
|
+
}
|
|
4688
|
+
function preparePackageUpdate(version, packageName = PACKAGE_NAME, runtimePackageJsonPath = getCurrentRuntimePackageJsonPath()) {
|
|
4689
|
+
try {
|
|
4690
|
+
const installContext = resolveInstallContext(runtimePackageJsonPath);
|
|
4691
|
+
if (!installContext) {
|
|
4692
|
+
log("[auto-update-checker] No install context found for auto-update");
|
|
4693
|
+
return null;
|
|
4694
|
+
}
|
|
4695
|
+
const dependencyReady = ensureDependencyVersion(installContext.packageJsonPath, packageName, version);
|
|
4696
|
+
if (!dependencyReady) {
|
|
4697
|
+
return null;
|
|
4698
|
+
}
|
|
4699
|
+
const packageRemoved = removeInstalledPackage(installContext.installDir, packageName);
|
|
4700
|
+
const lockRemoved = removeFromBunLock(installContext.installDir, packageName);
|
|
4701
|
+
if (!packageRemoved && !lockRemoved) {
|
|
4702
|
+
log(`[auto-update-checker] No cached package artifacts removed for ${packageName}; continuing with updated dependency spec`);
|
|
4703
|
+
}
|
|
4704
|
+
return installContext.installDir;
|
|
4705
|
+
} catch (err) {
|
|
4706
|
+
log("[auto-update-checker] Failed to prepare package update:", err);
|
|
4707
|
+
return null;
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4695
4711
|
// src/hooks/auto-update-checker/index.ts
|
|
4696
4712
|
function createAutoUpdateCheckerHook(ctx, options = {}) {
|
|
4697
4713
|
const { showStartupToast = true, autoUpdate = true } = options;
|
|
@@ -4761,23 +4777,24 @@ Version is pinned. Update your plugin config to apply.`, "info", 8000);
|
|
|
4761
4777
|
log("[auto-update-checker] Auto-update disabled, notification only");
|
|
4762
4778
|
return;
|
|
4763
4779
|
}
|
|
4764
|
-
|
|
4765
|
-
|
|
4780
|
+
const installDir = preparePackageUpdate(latestVersion, PACKAGE_NAME);
|
|
4781
|
+
if (!installDir) {
|
|
4782
|
+
showToast(ctx, `OMO-Slim ${latestVersion}`, `v${latestVersion} available. Auto-update could not prepare the active install.`, "info", 8000);
|
|
4783
|
+
log("[auto-update-checker] Failed to prepare install root for auto-update");
|
|
4784
|
+
return;
|
|
4785
|
+
}
|
|
4786
|
+
const installSuccess = await runBunInstallSafe(installDir);
|
|
4766
4787
|
if (installSuccess) {
|
|
4767
4788
|
showToast(ctx, "OMO-Slim Updated!", `v${currentVersion} \u2192 v${latestVersion}
|
|
4768
4789
|
Restart OpenCode to apply.`, "success", 8000);
|
|
4769
4790
|
log(`[auto-update-checker] Update installed: ${currentVersion} \u2192 ${latestVersion}`);
|
|
4770
4791
|
} else {
|
|
4771
|
-
showToast(ctx, `OMO-Slim ${latestVersion}`, `v${latestVersion} available
|
|
4792
|
+
showToast(ctx, `OMO-Slim ${latestVersion}`, `v${latestVersion} available, but auto-update failed to install it. Check logs or retry manually.`, "error", 8000);
|
|
4772
4793
|
log("[auto-update-checker] bun install failed; update not installed");
|
|
4773
4794
|
}
|
|
4774
4795
|
}
|
|
4775
|
-
function
|
|
4776
|
-
return CACHE_DIR;
|
|
4777
|
-
}
|
|
4778
|
-
async function runBunInstallSafe() {
|
|
4796
|
+
async function runBunInstallSafe(installDir) {
|
|
4779
4797
|
try {
|
|
4780
|
-
const installDir = getAutoUpdateInstallDir();
|
|
4781
4798
|
const proc = Bun.spawn(["bun", "install"], {
|
|
4782
4799
|
cwd: installDir,
|
|
4783
4800
|
stdout: "pipe",
|
|
@@ -5370,6 +5387,126 @@ function createPostFileToolNudgeHook(options = {}) {
|
|
|
5370
5387
|
}
|
|
5371
5388
|
// src/hooks/todo-continuation/index.ts
|
|
5372
5389
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
5390
|
+
|
|
5391
|
+
// src/hooks/todo-continuation/todo-hygiene.ts
|
|
5392
|
+
var TODO_HYGIENE_REMINDER = "If the active task changed or finished, update the todo list to match the current work state.";
|
|
5393
|
+
var TODO_FINAL_ACTIVE_REMINDER = "If you are finishing now, do not leave the active todo in_progress. Mark it completed, or move unfinished work back to pending.";
|
|
5394
|
+
var RESET = new Set(["todowrite"]);
|
|
5395
|
+
var IGNORE = new Set(["auto_continue"]);
|
|
5396
|
+
function createTodoHygiene(options) {
|
|
5397
|
+
const pending = new Set;
|
|
5398
|
+
const done = new Set;
|
|
5399
|
+
function clear(sessionID) {
|
|
5400
|
+
pending.delete(sessionID);
|
|
5401
|
+
done.delete(sessionID);
|
|
5402
|
+
}
|
|
5403
|
+
function isFinalActive(state) {
|
|
5404
|
+
return state.inProgressCount === 1 && state.pendingCount === 0 && state.openCount === 1;
|
|
5405
|
+
}
|
|
5406
|
+
return {
|
|
5407
|
+
async handleToolExecuteAfter(input) {
|
|
5408
|
+
if (!input.sessionID) {
|
|
5409
|
+
return;
|
|
5410
|
+
}
|
|
5411
|
+
const tool = input.tool.toLowerCase();
|
|
5412
|
+
if (IGNORE.has(tool)) {
|
|
5413
|
+
return;
|
|
5414
|
+
}
|
|
5415
|
+
try {
|
|
5416
|
+
if (RESET.has(tool)) {
|
|
5417
|
+
const state = await options.getTodoState(input.sessionID);
|
|
5418
|
+
if (!state.hasOpenTodos) {
|
|
5419
|
+
clear(input.sessionID);
|
|
5420
|
+
options.log?.("Cleared todo hygiene cycle", {
|
|
5421
|
+
sessionID: input.sessionID,
|
|
5422
|
+
tool
|
|
5423
|
+
});
|
|
5424
|
+
return;
|
|
5425
|
+
}
|
|
5426
|
+
pending.delete(input.sessionID);
|
|
5427
|
+
done.delete(input.sessionID);
|
|
5428
|
+
if (isFinalActive(state)) {
|
|
5429
|
+
pending.add(input.sessionID);
|
|
5430
|
+
options.log?.("Armed final-active todo hygiene reminder", {
|
|
5431
|
+
sessionID: input.sessionID,
|
|
5432
|
+
tool
|
|
5433
|
+
});
|
|
5434
|
+
return;
|
|
5435
|
+
}
|
|
5436
|
+
options.log?.("Reset todo hygiene cycle", {
|
|
5437
|
+
sessionID: input.sessionID,
|
|
5438
|
+
tool
|
|
5439
|
+
});
|
|
5440
|
+
return;
|
|
5441
|
+
}
|
|
5442
|
+
if (pending.has(input.sessionID) || done.has(input.sessionID)) {
|
|
5443
|
+
return;
|
|
5444
|
+
}
|
|
5445
|
+
if (!(await options.getTodoState(input.sessionID)).hasOpenTodos) {
|
|
5446
|
+
return;
|
|
5447
|
+
}
|
|
5448
|
+
pending.add(input.sessionID);
|
|
5449
|
+
options.log?.("Armed todo hygiene reminder", {
|
|
5450
|
+
sessionID: input.sessionID,
|
|
5451
|
+
tool
|
|
5452
|
+
});
|
|
5453
|
+
} catch (error) {
|
|
5454
|
+
if (RESET.has(tool)) {
|
|
5455
|
+
clear(input.sessionID);
|
|
5456
|
+
}
|
|
5457
|
+
options.log?.("Skipped todo hygiene reminder: failed to inspect todos", {
|
|
5458
|
+
sessionID: input.sessionID,
|
|
5459
|
+
tool,
|
|
5460
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5461
|
+
});
|
|
5462
|
+
}
|
|
5463
|
+
},
|
|
5464
|
+
async handleChatSystemTransform(input, output) {
|
|
5465
|
+
if (!input.sessionID || !pending.has(input.sessionID)) {
|
|
5466
|
+
return;
|
|
5467
|
+
}
|
|
5468
|
+
if (options.shouldInject && !options.shouldInject(input.sessionID)) {
|
|
5469
|
+
pending.delete(input.sessionID);
|
|
5470
|
+
done.add(input.sessionID);
|
|
5471
|
+
return;
|
|
5472
|
+
}
|
|
5473
|
+
try {
|
|
5474
|
+
const state = await options.getTodoState(input.sessionID);
|
|
5475
|
+
if (!state.hasOpenTodos) {
|
|
5476
|
+
clear(input.sessionID);
|
|
5477
|
+
return;
|
|
5478
|
+
}
|
|
5479
|
+
const finalActive = isFinalActive(state);
|
|
5480
|
+
const reminder = finalActive ? TODO_FINAL_ACTIVE_REMINDER : TODO_HYGIENE_REMINDER;
|
|
5481
|
+
pending.delete(input.sessionID);
|
|
5482
|
+
done.add(input.sessionID);
|
|
5483
|
+
output.system.push(reminder);
|
|
5484
|
+
options.log?.("Injected todo hygiene reminder", {
|
|
5485
|
+
sessionID: input.sessionID,
|
|
5486
|
+
reminder: finalActive ? "final-active" : "general"
|
|
5487
|
+
});
|
|
5488
|
+
} catch (error) {
|
|
5489
|
+
clear(input.sessionID);
|
|
5490
|
+
options.log?.("Skipped todo hygiene reminder: failed to inspect todos", {
|
|
5491
|
+
sessionID: input.sessionID,
|
|
5492
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5493
|
+
});
|
|
5494
|
+
}
|
|
5495
|
+
},
|
|
5496
|
+
handleEvent(event) {
|
|
5497
|
+
if (event.type !== "session.deleted") {
|
|
5498
|
+
return;
|
|
5499
|
+
}
|
|
5500
|
+
const sessionID = event.properties?.sessionID ?? event.properties?.info?.id;
|
|
5501
|
+
if (!sessionID) {
|
|
5502
|
+
return;
|
|
5503
|
+
}
|
|
5504
|
+
clear(sessionID);
|
|
5505
|
+
}
|
|
5506
|
+
};
|
|
5507
|
+
}
|
|
5508
|
+
|
|
5509
|
+
// src/hooks/todo-continuation/index.ts
|
|
5373
5510
|
var HOOK_NAME = "todo-continuation";
|
|
5374
5511
|
var COMMAND_NAME = "auto-continue";
|
|
5375
5512
|
var CONTINUATION_PROMPT = "[Auto-continue: enabled - there are incomplete todos remaining. Continue with the next uncompleted item. Press Esc to cancel. If you need user input or review for the next item, ask instead of proceeding.]";
|
|
@@ -5427,6 +5564,23 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
5427
5564
|
notifyingSessionIds: new Set,
|
|
5428
5565
|
notificationBusyUntilBySession: new Map
|
|
5429
5566
|
};
|
|
5567
|
+
const hygiene = createTodoHygiene({
|
|
5568
|
+
getTodoState: async (sessionID) => {
|
|
5569
|
+
const result = await ctx.client.session.todo({
|
|
5570
|
+
path: { id: sessionID }
|
|
5571
|
+
});
|
|
5572
|
+
const todos = result.data;
|
|
5573
|
+
const openTodos = todos.filter((todo) => !TERMINAL_TODO_STATUSES.includes(todo.status));
|
|
5574
|
+
return {
|
|
5575
|
+
hasOpenTodos: openTodos.length > 0,
|
|
5576
|
+
openCount: openTodos.length,
|
|
5577
|
+
inProgressCount: openTodos.filter((todo) => todo.status === "in_progress").length,
|
|
5578
|
+
pendingCount: openTodos.filter((todo) => todo.status === "pending").length
|
|
5579
|
+
};
|
|
5580
|
+
},
|
|
5581
|
+
shouldInject: (sessionID) => isOrchestratorSession(sessionID),
|
|
5582
|
+
log: (message, meta) => log(`[${HOOK_NAME}] ${message}`, meta)
|
|
5583
|
+
});
|
|
5430
5584
|
function markNotificationStarted(sessionID) {
|
|
5431
5585
|
state.notifyingSessionIds.add(sessionID);
|
|
5432
5586
|
}
|
|
@@ -5484,6 +5638,13 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
5484
5638
|
async function handleEvent(input) {
|
|
5485
5639
|
const { event } = input;
|
|
5486
5640
|
const properties = event.properties ?? {};
|
|
5641
|
+
hygiene.handleEvent({
|
|
5642
|
+
type: event.type,
|
|
5643
|
+
properties: {
|
|
5644
|
+
info: properties.info,
|
|
5645
|
+
sessionID: properties.sessionID
|
|
5646
|
+
}
|
|
5647
|
+
});
|
|
5487
5648
|
if (event.type === "session.idle" || event.type === "session.status" && properties.status?.type === "idle") {
|
|
5488
5649
|
const sessionID = properties.sessionID;
|
|
5489
5650
|
if (!sessionID) {
|
|
@@ -5765,6 +5926,8 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
5765
5926
|
}
|
|
5766
5927
|
return {
|
|
5767
5928
|
tool: { auto_continue: autoContinue },
|
|
5929
|
+
handleToolExecuteAfter: hygiene.handleToolExecuteAfter,
|
|
5930
|
+
handleChatSystemTransform: hygiene.handleChatSystemTransform,
|
|
5768
5931
|
handleEvent,
|
|
5769
5932
|
handleChatMessage,
|
|
5770
5933
|
handleCommandExecuteBefore
|
|
@@ -7344,7 +7507,7 @@ var {spawn: spawn5 } = globalThis.Bun;
|
|
|
7344
7507
|
// src/tools/ast-grep/constants.ts
|
|
7345
7508
|
import { existsSync as existsSync6, statSync as statSync2 } from "fs";
|
|
7346
7509
|
import { createRequire as createRequire2 } from "module";
|
|
7347
|
-
import { dirname as
|
|
7510
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
7348
7511
|
|
|
7349
7512
|
// src/tools/ast-grep/downloader.ts
|
|
7350
7513
|
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync2, unlinkSync } from "fs";
|
|
@@ -7501,7 +7664,7 @@ function findSgCliPathSync() {
|
|
|
7501
7664
|
try {
|
|
7502
7665
|
const require2 = createRequire2(import.meta.url);
|
|
7503
7666
|
const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
|
|
7504
|
-
const cliDir =
|
|
7667
|
+
const cliDir = dirname6(cliPkgPath);
|
|
7505
7668
|
const sgPath = join9(cliDir, binaryName);
|
|
7506
7669
|
if (existsSync6(sgPath) && isValidBinary(sgPath)) {
|
|
7507
7670
|
return sgPath;
|
|
@@ -7512,7 +7675,7 @@ function findSgCliPathSync() {
|
|
|
7512
7675
|
try {
|
|
7513
7676
|
const require2 = createRequire2(import.meta.url);
|
|
7514
7677
|
const pkgPath = require2.resolve(`${platformPkg}/package.json`);
|
|
7515
|
-
const pkgDir =
|
|
7678
|
+
const pkgDir = dirname6(pkgPath);
|
|
7516
7679
|
const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
7517
7680
|
const binaryPath = join9(pkgDir, astGrepName);
|
|
7518
7681
|
if (existsSync6(binaryPath) && isValidBinary(binaryPath)) {
|
|
@@ -8074,7 +8237,7 @@ import {
|
|
|
8074
8237
|
// src/tools/lsp/config.ts
|
|
8075
8238
|
import { existsSync as existsSync9 } from "fs";
|
|
8076
8239
|
import { homedir as homedir4 } from "os";
|
|
8077
|
-
import { dirname as
|
|
8240
|
+
import { dirname as dirname8, join as join10, resolve as resolve3 } from "path";
|
|
8078
8241
|
import whichSync from "which";
|
|
8079
8242
|
|
|
8080
8243
|
// src/tools/lsp/config-store.ts
|
|
@@ -8106,7 +8269,7 @@ function hasUserLspConfig() {
|
|
|
8106
8269
|
|
|
8107
8270
|
// src/tools/lsp/constants.ts
|
|
8108
8271
|
import { existsSync as existsSync8, readdirSync, statSync as statSync3 } from "fs";
|
|
8109
|
-
import { dirname as
|
|
8272
|
+
import { dirname as dirname7, resolve as resolve2 } from "path";
|
|
8110
8273
|
var SEVERITY_MAP = {
|
|
8111
8274
|
1: "error",
|
|
8112
8275
|
2: "warning",
|
|
@@ -8126,10 +8289,10 @@ function* walkUpDirectories(start, stop) {
|
|
|
8126
8289
|
let dir = resolve2(start);
|
|
8127
8290
|
try {
|
|
8128
8291
|
if (!statSync3(dir).isDirectory()) {
|
|
8129
|
-
dir =
|
|
8292
|
+
dir = dirname7(dir);
|
|
8130
8293
|
}
|
|
8131
8294
|
} catch {
|
|
8132
|
-
dir =
|
|
8295
|
+
dir = dirname7(dir);
|
|
8133
8296
|
}
|
|
8134
8297
|
let prevDir = "";
|
|
8135
8298
|
while (dir !== prevDir && dir !== "/") {
|
|
@@ -8137,7 +8300,7 @@ function* walkUpDirectories(start, stop) {
|
|
|
8137
8300
|
prevDir = dir;
|
|
8138
8301
|
if (dir === stop)
|
|
8139
8302
|
break;
|
|
8140
|
-
dir =
|
|
8303
|
+
dir = dirname7(dir);
|
|
8141
8304
|
}
|
|
8142
8305
|
}
|
|
8143
8306
|
function NearestRoot(includePatterns, excludePatterns) {
|
|
@@ -8683,7 +8846,7 @@ function getServerWorkspace(config, filePath) {
|
|
|
8683
8846
|
return;
|
|
8684
8847
|
}
|
|
8685
8848
|
if (!config.root) {
|
|
8686
|
-
return
|
|
8849
|
+
return dirname8(resolve3(filePath));
|
|
8687
8850
|
}
|
|
8688
8851
|
return config.root(filePath);
|
|
8689
8852
|
}
|
|
@@ -8707,7 +8870,7 @@ function findInstalledServer(configs, filePath) {
|
|
|
8707
8870
|
let firstNotInstalled = null;
|
|
8708
8871
|
for (const config of configs) {
|
|
8709
8872
|
const workspace = getServerWorkspace(config, filePath);
|
|
8710
|
-
const resolvedCommand = resolveServerCommand(config.command, workspace ?? (filePath ?
|
|
8873
|
+
const resolvedCommand = resolveServerCommand(config.command, workspace ?? (filePath ? dirname8(resolve3(filePath)) : undefined));
|
|
8711
8874
|
const server = toResolvedServer(config, resolvedCommand ?? undefined);
|
|
8712
8875
|
log(`[LSP] Considering server for ${config.extensions.join(", ")}: ${config.id} with command ${config.command.join(" ")}`);
|
|
8713
8876
|
if (resolvedCommand) {
|
|
@@ -8778,12 +8941,14 @@ function resolveServerCommand(command, cwd) {
|
|
|
8778
8941
|
// src/tools/lsp/client.ts
|
|
8779
8942
|
var START_TIMEOUT_MS = 5000;
|
|
8780
8943
|
var REQUEST_TIMEOUT_MS = 5000;
|
|
8944
|
+
var DIAGNOSTICS_TIMEOUT_MS = 15000;
|
|
8781
8945
|
var OPEN_FILE_DELAY_MS = 250;
|
|
8782
8946
|
var INITIALIZE_DELAY_MS = 100;
|
|
8783
8947
|
var DIAGNOSTIC_SETTLE_DELAY_MS = 250;
|
|
8784
8948
|
var LSP_TIMEOUTS = {
|
|
8785
8949
|
start: START_TIMEOUT_MS,
|
|
8786
8950
|
request: REQUEST_TIMEOUT_MS,
|
|
8951
|
+
diagnostics: DIAGNOSTICS_TIMEOUT_MS,
|
|
8787
8952
|
openFileDelay: OPEN_FILE_DELAY_MS,
|
|
8788
8953
|
initializeDelay: INITIALIZE_DELAY_MS,
|
|
8789
8954
|
diagnosticSettleDelay: DIAGNOSTIC_SETTLE_DELAY_MS
|
|
@@ -9208,7 +9373,7 @@ stderr: ${stderr}` : ""));
|
|
|
9208
9373
|
await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.initializeDelay));
|
|
9209
9374
|
log("[lsp] LSPClient.initialize: complete", { server: this.server.id });
|
|
9210
9375
|
}
|
|
9211
|
-
async waitForPublishedDiagnostics(uri, timeoutMs = LSP_TIMEOUTS.
|
|
9376
|
+
async waitForPublishedDiagnostics(uri, timeoutMs = LSP_TIMEOUTS.diagnostics) {
|
|
9212
9377
|
const cachedDiagnostics = this.diagnosticsStore.get(uri);
|
|
9213
9378
|
if (cachedDiagnostics) {
|
|
9214
9379
|
return cachedDiagnostics;
|
|
@@ -9288,6 +9453,7 @@ stderr: ${stderr}` : ""));
|
|
|
9288
9453
|
async diagnostics(filePath) {
|
|
9289
9454
|
const absPath = resolve4(filePath);
|
|
9290
9455
|
const uri = pathToFileURL(absPath).href;
|
|
9456
|
+
const startedAt = Date.now();
|
|
9291
9457
|
await this.openFile(absPath);
|
|
9292
9458
|
await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.diagnosticSettleDelay));
|
|
9293
9459
|
log("[lsp] diagnostics mode selected", {
|
|
@@ -9305,7 +9471,7 @@ stderr: ${stderr}` : ""));
|
|
|
9305
9471
|
const result = this.connection ? await withTimeout(this.connection.sendRequest("textDocument/diagnostic", {
|
|
9306
9472
|
textDocument: { uri },
|
|
9307
9473
|
previousResultId: this.diagnosticResultIds.get(uri)
|
|
9308
|
-
}), LSP_TIMEOUTS.
|
|
9474
|
+
}), LSP_TIMEOUTS.diagnostics, `LSP diagnostics (${this.server.id})`) : undefined;
|
|
9309
9475
|
const report = result;
|
|
9310
9476
|
if (report?.kind === "full") {
|
|
9311
9477
|
if (report.resultId) {
|
|
@@ -9334,7 +9500,9 @@ stderr: ${stderr}` : ""));
|
|
|
9334
9500
|
});
|
|
9335
9501
|
}
|
|
9336
9502
|
}
|
|
9337
|
-
const
|
|
9503
|
+
const elapsed = Date.now() - startedAt;
|
|
9504
|
+
const remainingTimeout = Math.max(LSP_TIMEOUTS.diagnostics - elapsed, 0);
|
|
9505
|
+
const cachedDiagnostics = await this.waitForPublishedDiagnostics(uri, remainingTimeout);
|
|
9338
9506
|
if (cachedDiagnostics) {
|
|
9339
9507
|
return { items: cachedDiagnostics };
|
|
9340
9508
|
}
|
|
@@ -9386,13 +9554,13 @@ import {
|
|
|
9386
9554
|
unlinkSync as unlinkSync2,
|
|
9387
9555
|
writeFileSync as writeFileSync3
|
|
9388
9556
|
} from "fs";
|
|
9389
|
-
import { dirname as
|
|
9557
|
+
import { dirname as dirname9, extname as extname3, join as join11, resolve as resolve5 } from "path";
|
|
9390
9558
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9391
9559
|
function findServerProjectRoot(filePath, server) {
|
|
9392
9560
|
if (server.root) {
|
|
9393
|
-
return server.root(filePath) ??
|
|
9561
|
+
return server.root(filePath) ?? dirname9(resolve5(filePath));
|
|
9394
9562
|
}
|
|
9395
|
-
return
|
|
9563
|
+
return dirname9(resolve5(filePath));
|
|
9396
9564
|
}
|
|
9397
9565
|
function uriToPath(uri) {
|
|
9398
9566
|
return fileURLToPath2(uri);
|
|
@@ -9422,7 +9590,7 @@ async function withLspClient(filePath, fn) {
|
|
|
9422
9590
|
throw new Error(formatServerLookupError(result));
|
|
9423
9591
|
}
|
|
9424
9592
|
const server = result.server;
|
|
9425
|
-
const root = findServerProjectRoot(absPath, server) ??
|
|
9593
|
+
const root = findServerProjectRoot(absPath, server) ?? dirname9(absPath);
|
|
9426
9594
|
log("[lsp] withLspClient: selected server", {
|
|
9427
9595
|
filePath: absPath,
|
|
9428
9596
|
extension: ext,
|
|
@@ -11618,6 +11786,13 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
11618
11786
|
await multiplexerSessionManager.onSessionDeleted(input.event);
|
|
11619
11787
|
await interviewManager.handleEvent(input);
|
|
11620
11788
|
await postFileToolNudgeHook.event(input);
|
|
11789
|
+
if (input.event.type === "session.deleted") {
|
|
11790
|
+
const props = input.event.properties;
|
|
11791
|
+
const sessionID = props?.info?.id ?? props?.sessionID;
|
|
11792
|
+
if (sessionID) {
|
|
11793
|
+
sessionAgentMap.delete(sessionID);
|
|
11794
|
+
}
|
|
11795
|
+
}
|
|
11621
11796
|
},
|
|
11622
11797
|
"tool.execute.before": async (input, output) => {
|
|
11623
11798
|
await applyPatchHook["tool.execute.before"](input, output);
|
|
@@ -11648,6 +11823,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
11648
11823
|
${output.system[0]}` : "");
|
|
11649
11824
|
}
|
|
11650
11825
|
}
|
|
11826
|
+
await todoContinuationHook.handleChatSystemTransform(input, output);
|
|
11651
11827
|
await postFileToolNudgeHook["experimental.chat.system.transform"](input, output);
|
|
11652
11828
|
},
|
|
11653
11829
|
"experimental.chat.messages.transform": async (input, output) => {
|
|
@@ -11658,6 +11834,7 @@ ${output.system[0]}` : "");
|
|
|
11658
11834
|
"tool.execute.after": async (input, output) => {
|
|
11659
11835
|
await delegateTaskRetryHook["tool.execute.after"](input, output);
|
|
11660
11836
|
await jsonErrorRecoveryHook["tool.execute.after"](input, output);
|
|
11837
|
+
await todoContinuationHook.handleToolExecuteAfter(input);
|
|
11661
11838
|
await postFileToolNudgeHook["tool.execute.after"](input, output);
|
|
11662
11839
|
}
|
|
11663
11840
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-opencode-slim",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.12",
|
|
4
4
|
"description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -55,27 +55,27 @@
|
|
|
55
55
|
"release:major": "npm version major && git push --follow-tags && npm publish"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
+
"@ast-grep/cli": "^0.42.1",
|
|
59
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
58
60
|
"@mozilla/readability": "^0.6.0",
|
|
59
|
-
"@
|
|
60
|
-
"@
|
|
61
|
-
"@opencode-ai/plugin": "^1.2.6",
|
|
62
|
-
"@opencode-ai/sdk": "^1.2.6",
|
|
61
|
+
"@opencode-ai/plugin": "^1.3.17",
|
|
62
|
+
"@opencode-ai/sdk": "^1.3.17",
|
|
63
63
|
"jsdom": "^26.1.0",
|
|
64
|
-
"lru-cache": "^11.
|
|
65
|
-
"turndown": "^7.2.
|
|
66
|
-
"vscode-jsonrpc": "^8.2.
|
|
64
|
+
"lru-cache": "^11.3.3",
|
|
65
|
+
"turndown": "^7.2.4",
|
|
66
|
+
"vscode-jsonrpc": "^8.2.1",
|
|
67
67
|
"vscode-languageserver-protocol": "^3.17.5",
|
|
68
|
-
"which": "^6.0.
|
|
68
|
+
"which": "^6.0.1",
|
|
69
69
|
"zod": "^4.3.6"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@biomejs/biome": "2.4.
|
|
72
|
+
"@biomejs/biome": "2.4.11",
|
|
73
73
|
"@types/jsdom": "^21.1.7",
|
|
74
74
|
"@types/node": "^24.6.1",
|
|
75
|
-
"@types/turndown": "^5.0.
|
|
75
|
+
"@types/turndown": "^5.0.6",
|
|
76
76
|
"@types/which": "^3.0.4",
|
|
77
77
|
"all-contributors-cli": "^6.26.1",
|
|
78
|
-
"bun-types": "1.3.
|
|
78
|
+
"bun-types": "1.3.12",
|
|
79
79
|
"typescript": "^5.9.3"
|
|
80
80
|
},
|
|
81
81
|
"trustedDependencies": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cartography
|
|
3
|
-
description:
|
|
3
|
+
description: Generate comprehensive hierarchical codemaps for UNFAMILIAR repositories. Expensive operation - only use when explicitly asked for codebase documentation or initial repository mapping
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Cartography Skill
|