think-inc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 whpthomas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # Think-Inc OpenCode Plugin
2
+
3
+ [![npm version](https://badge.fury.io/js/@whpthomas%2Fthink-inc.svg)](https://www.npmjs.com/package/@whpthomas/think-inc)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ ## What Is This?
7
+
8
+ Think-Inc is an simple OpenCode plugin that injects thinking traces back into the message context. It captures the reasoning parts as they stream in, stores them temporarily, then injects them back into message context so the model can see its thinking process.
9
+
10
+ ## Why Use This?
11
+
12
+ To improve model self efficacy when workflows spawn new contexts as a first class construct:
13
+
14
+ - Ralph Loops
15
+ - Research -> Plan -> Implement (RPI) Workflows
16
+ - Questions -> Research -> Design -> Structure -> Plan -> Implement (QRSPI) Workflows
17
+ - Acceptance Criteria -> Implement <-> Validate (GAN) Loops
18
+
19
+ ## How It Works
20
+
21
+ The plugin works in three stages:
22
+
23
+ 1. **Capture**: Listens for `message.part.updated` events and accumulates reasoning chunks
24
+ 2. **Store**: Maintains session-based storage for reasoning data across streaming events
25
+ 3. **Inject**: Uses the `experimental.chat.messages.transform` hook to prepend reasoning blocks to messages
26
+
27
+ ## Quick Start
28
+
29
+ ### Installation
30
+
31
+ ```bash
32
+ npm install @whpthomas/think-inc
33
+ ```
34
+
35
+ ### Usage
36
+
37
+ Create a plugin configuration file in your `.opencode/plugins/` directory:
38
+
39
+ ```javascript
40
+ // .opencode/plugins/think-inc.js
41
+ import thinkIncPlugin from '@whpthomas/think-inc';
42
+ export default thinkIncPlugin;
43
+ ```
44
+
45
+ ### AGENTS.md
46
+
47
+ Include the following section in AGENTS.md
48
+
49
+ ```markdown
50
+ ## Reasoning
51
+
52
+ The Think-Inc OpenCode plugin is providing thinking traces inside `<reasoning>` blocks in your context. It provides transparency to understand your thinking process and reduce repetition. Follow these guidelines to reduce context duplication:
53
+
54
+ - Assume the reader has seen reasoning
55
+ - Keep final response concise and action-oriented
56
+ - Focus on deliverables, decisions, and next steps
57
+ - Don't repeat reasoning content in response
58
+ - Don't provide verbose explanations when reasoning covers thinking
59
+ - Don't add unnecessary preamble or restatement of the approach
60
+ ```
61
+
62
+ ## License
63
+
64
+ MIT License - see LICENSE file for details
65
+
66
+ ## Authors
67
+
68
+ - [@whpthomas](https://github.com/whpthomas) - Concept, logic and troubleshooting
69
+ - Qwen3.5 122B hybrid int4fp8 - Research and coding
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Think-Inc Plugin - Reasoning Block Interceptor
3
+ *
4
+ * Intercepts reasoning parts from vllm responses and transforms them into
5
+ * visible, formatted conversation content.
6
+ */
7
+ import type { Plugin } from '@opencode-ai/plugin';
8
+ /**
9
+ * Main plugin implementation
10
+ */
11
+ export declare const thoughtsIncPlugin: Plugin;
12
+ export default thoughtsIncPlugin;
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAsB,MAAM,qBAAqB,CAAC;AA0BtE;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAqE/B,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import*as i from"fs";import*as d from"path";var k=d.join(process.cwd(),".logs"),L=d.join(k,"think-inc.log"),n=null,W=!1;function F(){if(W)return!0;try{if(!i.existsSync(k))i.mkdirSync(k,{recursive:!0});return n=i.createWriteStream(L,{flags:"a"}),W=!0,!0}catch{return!1}}function $(){return new Date().toISOString()}function u(...S){if(!F()||!n)return;let j=$(),x=S.map((l)=>typeof l==="object"?JSON.stringify(l,null,2):String(l)).join(" ");n.write(`[${j}] ${x}
2
+ `)}function G(){if(!n)return Promise.resolve();return new Promise((S)=>{n.end(()=>{n=null,W=!1,S()})})}function _(){if(!F())return;n?.close(),i.writeFileSync(L,""),n=i.createWriteStream(L,{flags:"a"})}var w=new Map,E=async(S)=>{return _(),u("=== Think-Inc Plugin Initialized ==="),{event:async({event:x})=>{if(x.type!=="message.part.updated")return;let r=x.properties.part;if(r.type!=="reasoning")return;if(!r.text?.trim())return;let{messageID:t,sessionID:f}=r,O=r.text.slice(0,35).replace(/\n/g,"\\n");if(u(`[CAPTURE] "${O}..." ${r.text.length} chars`),!w.has(f))w.set(f,{reasoning:new Map,complete:new Set});let c=w.get(f);if(!c.reasoning.has(t))c.reasoning.set(t,[]);c.reasoning.get(t).push(r.text)},"experimental.chat.messages.transform":async(x,l)=>{u(`[TRANSFORM] Processing ${l.messages.length} messages`);for(let r=l.messages.length-1;r>=0;r--){let t=l.messages[r],f=t.info.id,O=t.info.sessionID,c=w.get(O);if(!c)continue;if(!c.reasoning.has(f))continue;if(c.complete.has(f)){u(`[TRANSFORM] Message ${f} already completed`);break}let h=c.reasoning.get(f).filter((y)=>y.trim()).join(`
3
+ `);if(h.trim()){let y={type:"text",text:`<reasoning>
4
+ ${h}
5
+ </reasoning>`,synthetic:!0,id:`reasoning-${f}`,sessionID:O,messageID:f};t.parts.unshift(y),u(`[INJECT] ${h.length} chars ${f} ${O}`)}c.complete.add(f),c.reasoning.delete(f);for(let[y,z]of w)if(z.reasoning.size===0&&z.complete.size>0)w.delete(y),u(`[CLEANUP] Session ${y} cleaned up`),await G();break}return l}}},T=E;export{E as thoughtsIncPlugin,T as default};
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export declare function log(...args: unknown[]): void;
2
+ export declare function clearLog(): void;
3
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAoDA,wBAAgB,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,QASrC;AAED,wBAAgB,QAAQ,SAGvB"}
package/dist/logger.js ADDED
@@ -0,0 +1,61 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ const LOG_DIR = path.join(process.cwd(), ".logs");
4
+ const LOG_FILE = path.join(LOG_DIR, "think-inc.log");
5
+ const WRITE_INTERVAL_MS = 100;
6
+ let logBuffer = [];
7
+ let writeScheduled = false;
8
+ let initialized = false;
9
+ function ensureInitialized() {
10
+ if (initialized)
11
+ return true;
12
+ try {
13
+ if (!fs.existsSync(LOG_DIR)) {
14
+ fs.mkdirSync(LOG_DIR, { recursive: true });
15
+ }
16
+ fs.writeFileSync(LOG_FILE, "");
17
+ initialized = true;
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ function formatTimestamp() {
25
+ return new Date().toISOString();
26
+ }
27
+ async function flushLogs() {
28
+ if (logBuffer.length === 0) {
29
+ writeScheduled = false;
30
+ return;
31
+ }
32
+ const toWrite = logBuffer.join("");
33
+ logBuffer = [];
34
+ writeScheduled = false;
35
+ try {
36
+ await fs.promises.appendFile(LOG_FILE, toWrite);
37
+ }
38
+ catch { }
39
+ }
40
+ function scheduleFlush() {
41
+ if (!writeScheduled) {
42
+ writeScheduled = true;
43
+ setTimeout(flushLogs, WRITE_INTERVAL_MS);
44
+ }
45
+ }
46
+ export function log(...args) {
47
+ if (!ensureInitialized())
48
+ return;
49
+ const timestamp = formatTimestamp();
50
+ const message = args
51
+ .map(a => (typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)))
52
+ .join(" ");
53
+ logBuffer.push(`[${timestamp}] ${message}\n`);
54
+ scheduleFlush();
55
+ }
56
+ export function clearLog() {
57
+ if (!ensureInitialized())
58
+ return;
59
+ fs.writeFileSync(LOG_FILE, "");
60
+ }
61
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,CAAC;AAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;AACrD,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAE9B,IAAI,SAAS,GAAa,EAAE,CAAC;AAC7B,IAAI,cAAc,GAAG,KAAK,CAAC;AAC3B,IAAI,WAAW,GAAG,KAAK,CAAC;AAExB,SAAS,iBAAiB;IACxB,IAAI,WAAW;QAAE,OAAO,IAAI,CAAC;IAE7B,IAAI,CAAC;QACH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5B,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7C,CAAC;QACD,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC/B,WAAW,GAAG,IAAI,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,eAAe;IACtB,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC;AAED,KAAK,UAAU,SAAS;IACtB,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,cAAc,GAAG,KAAK,CAAC;QACvB,OAAO;IACT,CAAC;IAED,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACnC,SAAS,GAAG,EAAE,CAAC;IACf,cAAc,GAAG,KAAK,CAAC;IAEvB,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC,CAAC,CAAC;AACb,CAAC;AAED,SAAS,aAAa;IACpB,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,cAAc,GAAG,IAAI,CAAC;QACtB,UAAU,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC;IAC3C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,GAAG,IAAe;IACpC,IAAI,CAAC,iBAAiB,EAAE;QAAE,OAAO;IAEjC,MAAM,SAAS,GAAG,eAAe,EAAE,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI;SACjB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;SAC1E,IAAI,CAAC,GAAG,CAAC,CAAC;IACb,SAAS,CAAC,IAAI,CAAC,IAAI,SAAS,KAAK,OAAO,IAAI,CAAC,CAAC;IAC9C,aAAa,EAAE,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,QAAQ;IACtB,IAAI,CAAC,iBAAiB,EAAE;QAAE,OAAO;IACjC,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AACjC,CAAC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Parser for Reasoning Blocks
3
+ *
4
+ * Detects and extracts reasoning blocks from LLM responses.
5
+ * Handles multiple tag formats, edge cases, and malformed input gracefully.
6
+ */
7
+ import type { ReasoningBlock } from './types.js';
8
+ /**
9
+ * Parse all reasoning blocks from content
10
+ * @param content - Raw content from LLM response
11
+ * @returns Array of ReasoningBlock objects sorted by position
12
+ */
13
+ export declare function parseReasoning(content: string): ReasoningBlock[];
14
+ /**
15
+ * Parse a single reasoning block at given position
16
+ * @param content - Raw content
17
+ * @param startTag - Opening tag to look for (default: '<thoughts>')
18
+ * @returns ReasoningBlock or null if not found
19
+ */
20
+ export declare function parseSingleReasoning(content: string, startTag?: string): ReasoningBlock | null;
21
+ /**
22
+ * Quick check if content contains any reasoning blocks
23
+ * @param content - Raw content
24
+ * @returns boolean indicating if any reasoning blocks are present
25
+ */
26
+ export declare function hasReasoning(content: string): boolean;
27
+ /**
28
+ * Extract reasoning content only (without tags)
29
+ * @param content - Raw content
30
+ * @returns Concatenated reasoning content from all blocks
31
+ */
32
+ export declare function extractReasoningContent(content: string): string;
33
+ /**
34
+ * Remove all reasoning blocks from content
35
+ * @param content - Raw content
36
+ * @returns Content with reasoning blocks removed
37
+ */
38
+ export declare function removeReasoningBlocks(content: string): string;
39
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAgB,MAAM,YAAY,CAAC;AAG/D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CA6ChE;AAwBD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,MAAqB,GAC9B,cAAc,GAAG,IAAI,CAsCvB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAcrD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAQ/D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAkB7D"}
package/dist/parser.js ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Parser for Reasoning Blocks
3
+ *
4
+ * Detects and extracts reasoning blocks from LLM responses.
5
+ * Handles multiple tag formats, edge cases, and malformed input gracefully.
6
+ */
7
+ import { DEFAULT_PATTERNS } from './types.js';
8
+ /**
9
+ * Parse all reasoning blocks from content
10
+ * @param content - Raw content from LLM response
11
+ * @returns Array of ReasoningBlock objects sorted by position
12
+ */
13
+ export function parseReasoning(content) {
14
+ if (!content || typeof content !== 'string') {
15
+ return [];
16
+ }
17
+ const blocks = [];
18
+ try {
19
+ // Iterate through each pattern
20
+ for (const patternConfig of DEFAULT_PATTERNS) {
21
+ const pattern = patternConfig.pattern;
22
+ // Reset regex lastIndex to ensure fresh matching
23
+ pattern.lastIndex = 0;
24
+ const matches = [...content.matchAll(pattern)];
25
+ for (const match of matches) {
26
+ const fullMatch = match[0];
27
+ const content = match[1] ?? '';
28
+ // Extract actual tags from the match
29
+ const firstBracket = fullMatch.indexOf('>');
30
+ const lastBracket = fullMatch.lastIndexOf('<');
31
+ const startTag = fullMatch.substring(0, firstBracket + 1);
32
+ const endTag = fullMatch.substring(lastBracket);
33
+ blocks.push({
34
+ content: content.trim(),
35
+ startTag,
36
+ endTag,
37
+ startPosition: match.index,
38
+ endPosition: match.index + fullMatch.length
39
+ });
40
+ }
41
+ }
42
+ }
43
+ catch (error) {
44
+ console.error('[Parser] Error parsing:', error);
45
+ return [];
46
+ }
47
+ // Remove duplicates and sort by position
48
+ const uniqueBlocks = removeDuplicateBlocks(blocks);
49
+ return uniqueBlocks.sort((a, b) => a.startPosition - b.startPosition);
50
+ }
51
+ /**
52
+ * Remove duplicate blocks that may be detected by multiple patterns
53
+ */
54
+ function removeDuplicateBlocks(blocks) {
55
+ if (blocks.length <= 1) {
56
+ return blocks;
57
+ }
58
+ const seen = new Set();
59
+ const result = [];
60
+ for (const block of blocks) {
61
+ const key = `${block.startPosition}-${block.endPosition}`;
62
+ if (!seen.has(key)) {
63
+ seen.add(key);
64
+ result.push(block);
65
+ }
66
+ }
67
+ return result;
68
+ }
69
+ /**
70
+ * Parse a single reasoning block at given position
71
+ * @param content - Raw content
72
+ * @param startTag - Opening tag to look for (default: '<thoughts>')
73
+ * @returns ReasoningBlock or null if not found
74
+ */
75
+ export function parseSingleReasoning(content, startTag = '<thoughts>') {
76
+ if (!content || typeof content !== 'string') {
77
+ return null;
78
+ }
79
+ try {
80
+ // Extract tag name from startTag (e.g., 'thoughts' from '<thoughts>')
81
+ const tagName = startTag.replace(/^<\s*/, '').replace(/\s*>$/, '');
82
+ // Create flexible pattern that handles whitespace variations
83
+ const pattern = new RegExp(`<\\s*${tagName}\\s*>([\\s\\S]*?)<\\s*/${tagName}\\s*>`, 'gi');
84
+ const match = pattern.exec(content);
85
+ if (!match) {
86
+ return null;
87
+ }
88
+ const fullMatch = match[0];
89
+ const contentText = match[1] ?? '';
90
+ const firstBracket = fullMatch.indexOf('>');
91
+ const lastBracket = fullMatch.lastIndexOf('<');
92
+ return {
93
+ content: contentText.trim(),
94
+ startTag: fullMatch.substring(0, firstBracket + 1),
95
+ endTag: fullMatch.substring(lastBracket),
96
+ startPosition: match.index,
97
+ endPosition: match.index + fullMatch.length
98
+ };
99
+ }
100
+ catch (error) {
101
+ console.error('[Parser] Error parsing single block:', error);
102
+ return null;
103
+ }
104
+ }
105
+ /**
106
+ * Quick check if content contains any reasoning blocks
107
+ * @param content - Raw content
108
+ * @returns boolean indicating if any reasoning blocks are present
109
+ */
110
+ export function hasReasoning(content) {
111
+ if (!content || typeof content !== 'string') {
112
+ return false;
113
+ }
114
+ try {
115
+ return DEFAULT_PATTERNS.some(pattern => {
116
+ pattern.pattern.lastIndex = 0;
117
+ return pattern.pattern.test(content);
118
+ });
119
+ }
120
+ catch (error) {
121
+ console.error('[Parser] Error checking for reasoning:', error);
122
+ return false;
123
+ }
124
+ }
125
+ /**
126
+ * Extract reasoning content only (without tags)
127
+ * @param content - Raw content
128
+ * @returns Concatenated reasoning content from all blocks
129
+ */
130
+ export function extractReasoningContent(content) {
131
+ const blocks = parseReasoning(content);
132
+ if (blocks.length === 0) {
133
+ return '';
134
+ }
135
+ return blocks.map(block => block.content).join('\n\n');
136
+ }
137
+ /**
138
+ * Remove all reasoning blocks from content
139
+ * @param content - Raw content
140
+ * @returns Content with reasoning blocks removed
141
+ */
142
+ export function removeReasoningBlocks(content) {
143
+ if (!content || typeof content !== 'string') {
144
+ return content;
145
+ }
146
+ let result = content;
147
+ try {
148
+ for (const patternConfig of DEFAULT_PATTERNS) {
149
+ const pattern = patternConfig.pattern;
150
+ pattern.lastIndex = 0;
151
+ result = result.replace(pattern, '');
152
+ }
153
+ }
154
+ catch (error) {
155
+ console.error('[Parser] Error removing reasoning blocks:', error);
156
+ }
157
+ return result;
158
+ }
159
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Transformer for Reasoning Blocks
3
+ *
4
+ * Converts raw reasoning blocks into formatted, readable output.
5
+ * Cleans up whitespace, adds markdown formatting, and reconstructs
6
+ * the full content with reasoning visible.
7
+ */
8
+ import type { ReasoningBlock, TransformResult } from './types.js';
9
+ /**
10
+ * Format a single reasoning block for display
11
+ * @param reasoning - Raw reasoning content
12
+ * @returns Formatted reasoning string with header
13
+ */
14
+ export declare function formatReasoning(reasoning: string): string;
15
+ /**
16
+ * Transform content with reasoning blocks into formatted output
17
+ * @param content - Original content with reasoning tags
18
+ * @param blocks - Array of parsed ReasoningBlock objects
19
+ * @returns TransformResult with transformed content
20
+ */
21
+ export declare function transformContent(content: string, blocks: ReasoningBlock[]): TransformResult;
22
+ /**
23
+ * Transform content using regex patterns (alternative approach)
24
+ * @param content - Original content
25
+ * @returns TransformResult
26
+ */
27
+ export declare function transformWithRegex(content: string): TransformResult;
28
+ /**
29
+ * Check if transformation is needed (has reasoning blocks)
30
+ * @param content - Content to check
31
+ * @returns boolean
32
+ */
33
+ export declare function needsTransformation(content: string): boolean;
34
+ //# sourceMappingURL=transformer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transformer.d.ts","sourceRoot":"","sources":["../src/transformer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGlE;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAazD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,cAAc,EAAE,GACvB,eAAe,CAmCjB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAoFnE;AAwBD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAa5D"}
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Transformer for Reasoning Blocks
3
+ *
4
+ * Converts raw reasoning blocks into formatted, readable output.
5
+ * Cleans up whitespace, adds markdown formatting, and reconstructs
6
+ * the full content with reasoning visible.
7
+ */
8
+ import { DEFAULT_PATTERNS } from './types.js';
9
+ /**
10
+ * Format a single reasoning block for display
11
+ * @param reasoning - Raw reasoning content
12
+ * @returns Formatted reasoning string with header
13
+ */
14
+ export function formatReasoning(reasoning) {
15
+ if (!reasoning) {
16
+ return '**Reasoning Process:**\n';
17
+ }
18
+ // Clean up internal formatting
19
+ const cleaned = reasoning
20
+ .replace(/^\s+|\s+$/g, '') // Trim leading/trailing whitespace
21
+ .replace(/\n{3,}/g, '\n\n') // Collapse multiple newlines to double
22
+ .replace(/\n\s*\n/g, '\n\n') // Normalize whitespace between lines
23
+ .trim();
24
+ return `**Reasoning Process:**\n${cleaned}`;
25
+ }
26
+ /**
27
+ * Transform content with reasoning blocks into formatted output
28
+ * @param content - Original content with reasoning tags
29
+ * @param blocks - Array of parsed ReasoningBlock objects
30
+ * @returns TransformResult with transformed content
31
+ */
32
+ export function transformContent(content, blocks) {
33
+ if (!blocks || blocks.length === 0) {
34
+ return {
35
+ original: content,
36
+ transformed: content,
37
+ blocksFound: 0,
38
+ blocks: []
39
+ };
40
+ }
41
+ // Extract and format all reasoning blocks
42
+ const reasoningBlocks = blocks.map(block => formatReasoning(block.content));
43
+ // Remove tags from original content using position-based splicing
44
+ let cleanContent = content;
45
+ const sortedBlocks = [...blocks].sort((a, b) => b.startPosition - a.startPosition);
46
+ for (const block of sortedBlocks) {
47
+ cleanContent = cleanContent.substring(0, block.startPosition) +
48
+ cleanContent.substring(block.endPosition);
49
+ }
50
+ // Reconstruct with reasoning visible
51
+ const transformed = [
52
+ ...reasoningBlocks,
53
+ '---',
54
+ cleanContent.trim()
55
+ ].join('\n\n');
56
+ return {
57
+ original: content,
58
+ transformed,
59
+ blocksFound: blocks.length,
60
+ blocks
61
+ };
62
+ }
63
+ /**
64
+ * Transform content using regex patterns (alternative approach)
65
+ * @param content - Original content
66
+ * @returns TransformResult
67
+ */
68
+ export function transformWithRegex(content) {
69
+ if (!content || typeof content !== 'string') {
70
+ return {
71
+ original: '',
72
+ transformed: '',
73
+ blocksFound: 0,
74
+ blocks: []
75
+ };
76
+ }
77
+ const allBlocks = [];
78
+ try {
79
+ // Collect all matches from all patterns
80
+ for (const patternConfig of DEFAULT_PATTERNS) {
81
+ const pattern = patternConfig.pattern;
82
+ pattern.lastIndex = 0;
83
+ const matches = [...content.matchAll(pattern)];
84
+ for (const match of matches) {
85
+ const fullMatch = match[0];
86
+ const blockContent = match[1] ?? '';
87
+ const firstBracket = fullMatch.indexOf('>');
88
+ const lastBracket = fullMatch.lastIndexOf('<');
89
+ allBlocks.push({
90
+ content: blockContent.trim(),
91
+ startTag: fullMatch.substring(0, firstBracket + 1),
92
+ endTag: fullMatch.substring(lastBracket),
93
+ startPosition: match.index,
94
+ endPosition: match.index + fullMatch.length
95
+ });
96
+ }
97
+ }
98
+ }
99
+ catch (error) {
100
+ console.error('[Transformer] Error in transformWithRegex:', error);
101
+ return {
102
+ original: content,
103
+ transformed: content,
104
+ blocksFound: 0,
105
+ blocks: []
106
+ };
107
+ }
108
+ // Remove duplicates
109
+ const uniqueBlocks = removeDuplicateBlocks(allBlocks);
110
+ // Sort by position
111
+ const sortedBlocks = uniqueBlocks.sort((a, b) => a.startPosition - b.startPosition);
112
+ if (sortedBlocks.length === 0) {
113
+ return {
114
+ original: content,
115
+ transformed: content,
116
+ blocksFound: 0,
117
+ blocks: []
118
+ };
119
+ }
120
+ // Format reasoning blocks
121
+ const reasoningBlocks = sortedBlocks.map(block => formatReasoning(block.content));
122
+ // Remove reasoning blocks from content (work from end to start)
123
+ let cleanContent = content;
124
+ for (const block of sortedBlocks.reverse()) {
125
+ cleanContent = cleanContent.substring(0, block.startPosition) +
126
+ cleanContent.substring(block.endPosition);
127
+ }
128
+ // Reconstruct with reasoning visible
129
+ const transformed = [
130
+ ...reasoningBlocks,
131
+ '---',
132
+ cleanContent.trim()
133
+ ].join('\n\n');
134
+ return {
135
+ original: content,
136
+ transformed,
137
+ blocksFound: sortedBlocks.length,
138
+ blocks: sortedBlocks
139
+ };
140
+ }
141
+ /**
142
+ * Remove duplicate blocks that may be detected by multiple patterns
143
+ */
144
+ function removeDuplicateBlocks(blocks) {
145
+ if (blocks.length <= 1) {
146
+ return blocks;
147
+ }
148
+ const seen = new Set();
149
+ const result = [];
150
+ for (const block of blocks) {
151
+ const key = `${block.startPosition}-${block.endPosition}`;
152
+ if (!seen.has(key)) {
153
+ seen.add(key);
154
+ result.push(block);
155
+ }
156
+ }
157
+ return result;
158
+ }
159
+ /**
160
+ * Check if transformation is needed (has reasoning blocks)
161
+ * @param content - Content to check
162
+ * @returns boolean
163
+ */
164
+ export function needsTransformation(content) {
165
+ if (!content || typeof content !== 'string') {
166
+ return false;
167
+ }
168
+ try {
169
+ return DEFAULT_PATTERNS.some(pattern => {
170
+ pattern.pattern.lastIndex = 0;
171
+ return pattern.pattern.test(content);
172
+ });
173
+ }
174
+ catch {
175
+ return false;
176
+ }
177
+ }
178
+ //# sourceMappingURL=transformer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transformer.js","sourceRoot":"","sources":["../src/transformer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Think-Inc Type Definitions
3
+ *
4
+ * Comprehensive TypeScript interfaces for the Think-Inc plugin.
5
+ * All types are strictly typed with no `any` types, supporting
6
+ * future extensibility for custom tag patterns.
7
+ */
8
+ /**
9
+ * Represents a single reasoning block extracted from content.
10
+ * Contains both the extracted content and position information
11
+ * for potential debugging or transformation purposes.
12
+ */
13
+ export interface ReasoningBlock {
14
+ content: string;
15
+ startTag: string;
16
+ endTag: string;
17
+ startPosition: number;
18
+ endPosition: number;
19
+ }
20
+ /**
21
+ * Result of transforming content with reasoning blocks.
22
+ * Tracks the original content, transformed output, and all
23
+ * extracted reasoning blocks for debugging and analysis.
24
+ */
25
+ export interface TransformResult {
26
+ original: string;
27
+ transformed: string;
28
+ blocksFound: number;
29
+ blocks: ReasoningBlock[];
30
+ }
31
+ /**
32
+ * Configuration options for the Think-Inc plugin.
33
+ * Allows enabling/disabling the plugin and customizing
34
+ * tag patterns and formatting behavior.
35
+ */
36
+ export interface ThinkIncConfig {
37
+ enabled: boolean;
38
+ tagPatterns: string[];
39
+ formatTemplate?: string;
40
+ maxReasoningLength?: number;
41
+ }
42
+ /**
43
+ * Represents a regex pattern for detecting reasoning blocks.
44
+ * Supports multiple tag formats and provides descriptive metadata
45
+ * for debugging and configuration purposes.
46
+ */
47
+ export interface RegexPattern {
48
+ name: string;
49
+ pattern: RegExp;
50
+ description: string;
51
+ }
52
+ /**
53
+ * Default regex patterns for detecting various reasoning tag formats.
54
+ * Includes support for standard thoughts tags, generic think tags,
55
+ * reasoning tags, and Qwen model-specific format.
56
+ */
57
+ export declare const DEFAULT_PATTERNS: RegexPattern[];
58
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,cAAc,EAAE,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,YAAY,EAsB1C,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Think-Inc Type Definitions
3
+ *
4
+ * Comprehensive TypeScript interfaces for the Think-Inc plugin.
5
+ * All types are strictly typed with no `any` types, supporting
6
+ * future extensibility for custom tag patterns.
7
+ */
8
+ /**
9
+ * Default regex patterns for detecting various reasoning tag formats.
10
+ * Includes support for standard thoughts tags, generic think tags,
11
+ * reasoning tags, and Qwen model-specific format.
12
+ */
13
+ export const DEFAULT_PATTERNS = [
14
+ {
15
+ name: 'thoughts',
16
+ pattern: /<\s*thoughts\s*>([\s\S]*?)<\s*\/\s*thoughts\s*>/gi,
17
+ description: 'Standard thoughts tags'
18
+ },
19
+ {
20
+ name: 'think',
21
+ pattern: /<\s*think\s*>([\s\S]*?)<\s*\/\s*think\s*>/gi,
22
+ description: 'Generic think tags'
23
+ },
24
+ {
25
+ name: 'reasoning',
26
+ pattern: /<\s*reasoning\s*>([\s\S]*?)<\s*\/\s*reasoning\s*>/gi,
27
+ description: 'Reasoning tags'
28
+ },
29
+ {
30
+ name: 'qwen',
31
+ // Qwen model format - uses special token sequences
32
+ pattern: /<\/?thoughts>[\s\S]*?<\/thoughts>/gi,
33
+ description: 'Qwen model format'
34
+ }
35
+ ];
36
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAkDH;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAmB;IAC9C;QACE,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,mDAAmD;QAC5D,WAAW,EAAE,wBAAwB;KACtC;IACD;QACE,IAAI,EAAE,OAAO;QACb,OAAO,EAAE,6CAA6C;QACtD,WAAW,EAAE,oBAAoB;KAClC;IACD;QACE,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,qDAAqD;QAC9D,WAAW,EAAE,gBAAgB;KAC9B;IACD;QACE,IAAI,EAAE,MAAM;QACZ,mDAAmD;QACnD,OAAO,EAAE,qCAAqC;QAC9C,WAAW,EAAE,mBAAmB;KACjC;CACF,CAAC"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "think-inc",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin that displays LLM reasoning traces in conversations",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js"
10
+ },
11
+ "keywords": [
12
+ "opencode",
13
+ "plugin",
14
+ "reasoning",
15
+ "llm",
16
+ "think-inc"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/whpthomas/think-inc.git"
21
+ },
22
+ "author": "whpthomas",
23
+ "license": "MIT",
24
+ "bugs": {
25
+ "url": "https://github.com/whpthomas/think-inc/issues"
26
+ },
27
+ "homepage": "https://github.com/whpthomas/think-inc#readme",
28
+ "scripts": {
29
+ "build": "bun build src/index.ts --outdir dist --target node --minify",
30
+ "dev": "bun build src/index.ts --outdir dist --target node --watch",
31
+ "clean": "rm -rf dist",
32
+ "test": "bun test",
33
+ "prepublishOnly": "bun run build"
34
+ },
35
+ "peerDependencies": {
36
+ "@opencode-ai/plugin": "^1.4.0",
37
+ "@opencode-ai/sdk": "^1.4.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^25.6.0",
41
+ "bun-types": "^1.1.0",
42
+ "typescript": "^5.4.5"
43
+ }
44
+ }
package/src/index.ts ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Think-Inc Plugin - Reasoning Block Interceptor
3
+ *
4
+ * Captures reasoning parts as they stream in, stores them temporarily,
5
+ * then injects them back into message context for the next turn.
6
+ */
7
+
8
+ import type { Plugin, Hooks, PluginInput } from '@opencode-ai/plugin';
9
+ import { log, clearLog, flush } from './logger.js';
10
+
11
+ /**
12
+ * Store reasoning per session with completion tracking
13
+ *
14
+ * Reasoning arrives via streaming events, but needs to persist until
15
+ * the transform hook runs to inject it into message context.
16
+ */
17
+ interface ReasoningStore {
18
+ reasoning: Map<string, string[]>; // messageID -> reasoning chunks (accumulating)
19
+ complete: Set<string>; // messageIDs that have finished streaming
20
+ }
21
+
22
+ // Global store: sessionID -> ReasoningStore
23
+ // Needed because reasoning chunks arrive asynchronously and must persist
24
+ // until the transform hook runs to inject them into context.
25
+ const sessionStores = new Map<string, ReasoningStore>();
26
+
27
+ /**
28
+ * Main plugin implementation
29
+ */
30
+ export const thoughtsIncPlugin: Plugin = async (_input: PluginInput): Promise<Hooks> => {
31
+ clearLog();
32
+ log('=== Think-Inc Plugin Initialized ===');
33
+
34
+ const hooks: Hooks = {
35
+ /**
36
+ * Event Hook: Capture reasoning chunks as they stream in
37
+ *
38
+ * Triggered on every event from the model. We listen for 'message.part.updated'
39
+ * events where the part type is 'reasoning'. Each chunk is accumulated in
40
+ * sessionStores so it can be injected later when the message context is built.
41
+ */
42
+ event: async ({ event }) => {
43
+ if (event.type !== 'message.part.updated') return;
44
+
45
+ const properties = event.properties as any;
46
+ const part = properties.part;
47
+
48
+ // Only capture reasoning parts
49
+ if (part.type !== 'reasoning') return;
50
+ if (!part.text?.trim()) return;
51
+
52
+ const messageID = part.messageID;
53
+ const sessionID = part.sessionID;
54
+ const preview = part.text.slice(0, 35).replace(/\n/g, '\\n');
55
+ log(`[CAPTURE] "${preview}..." ${part.text.length} chars`);
56
+
57
+ // Initialize or get session store
58
+ if (!sessionStores.has(sessionID)) {
59
+ sessionStores.set(sessionID, {
60
+ reasoning: new Map(),
61
+ complete: new Set()
62
+ });
63
+ }
64
+ const store = sessionStores.get(sessionID)!;
65
+
66
+ // Initialize message array if needed
67
+ if (!store.reasoning.has(messageID)) {
68
+ store.reasoning.set(messageID, []);
69
+ }
70
+
71
+ // Add reasoning chunk to accumulate all pieces
72
+ store.reasoning.get(messageID)!.push(part.text);
73
+ },
74
+
75
+ /**
76
+ * Transform Hook: Inject captured reasoning into message context
77
+ *
78
+ * This runs when building the message context for the next turn. We:
79
+ * 1. Look up any captured reasoning for each message from sessionStores
80
+ * 2. Combine all chunks into full reasoning text
81
+ * 3. Inject it as <reasoning> tags at the top of the first text part
82
+ * 4. Mark as complete and clean up to prevent re-injection
83
+ *
84
+ * Iterates backwards from the most recent message since that's where
85
+ * new reasoning would be. Stops after injection to avoid unnecessary processing.
86
+ */
87
+ 'experimental.chat.messages.transform': async (_input, output) => {
88
+ log(`[TRANSFORM] Processing ${output.messages.length} messages`);
89
+
90
+ // Iterate backwards from most recent message
91
+ for (let i = output.messages.length - 1; i >= 0; i--) {
92
+ const msg = output.messages[i];
93
+ const messageID = msg.info.id;
94
+ const sessionID = msg.info.sessionID;
95
+
96
+ const store = sessionStores.get(sessionID);
97
+ if (!store) {
98
+ continue;
99
+ }
100
+
101
+ if (!store.reasoning.has(messageID)) {
102
+ continue;
103
+ }
104
+
105
+ if (store.complete.has(messageID)) {
106
+ log(`[TRANSFORM] Message ${messageID} already completed`);
107
+ break;
108
+ }
109
+
110
+ // Combine all streaming chunks into complete reasoning text
111
+ const combinedReasoning = store.reasoning.get(messageID)!
112
+ .filter(r => r.trim())
113
+ .join('\n');
114
+
115
+ if (combinedReasoning.trim()) {
116
+ // Inject reasoning as a new text part at the beginning of the message
117
+ // This works even for tool-only messages that have no existing text parts
118
+ const reasoningPart: any = {
119
+ type: 'text',
120
+ text: `<reasoning>\n${combinedReasoning}\n</reasoning>`,
121
+ synthetic: true,
122
+ id: `reasoning-${messageID}`,
123
+ sessionID: sessionID,
124
+ messageID: messageID,
125
+ };
126
+ // Insert reasoning part at the beginning of the message
127
+ msg.parts.unshift(reasoningPart);
128
+
129
+ log(`[INJECT] ${combinedReasoning.length} chars ${messageID} ${sessionID}`);
130
+ }
131
+
132
+ store.complete.add(messageID);
133
+ store.reasoning.delete(messageID);
134
+
135
+ // Cleanup: remove sessions with no pending reasoning
136
+ for (const [sessionID, store] of sessionStores) {
137
+ if (store.reasoning.size === 0 && store.complete.size > 0) {
138
+ sessionStores.delete(sessionID);
139
+ log(`[CLEANUP] Session ${sessionID} cleaned up`);
140
+ await flush();
141
+ }
142
+ }
143
+
144
+ // Stop after first injection - newer messages processed first
145
+ break;
146
+ }
147
+
148
+ return output;
149
+ },
150
+ };
151
+
152
+ return hooks;
153
+ };
154
+
155
+ export default thoughtsIncPlugin;
package/src/logger.ts ADDED
@@ -0,0 +1,56 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+
4
+ const LOG_DIR = path.join(process.cwd(), ".logs");
5
+ const LOG_FILE = path.join(LOG_DIR, "think-inc.log");
6
+
7
+ let writeStream: fs.WriteStream | null = null;
8
+ let initialized = false;
9
+
10
+ function ensureInitialized(): boolean {
11
+ if (initialized) return true;
12
+
13
+ try {
14
+ if (!fs.existsSync(LOG_DIR)) {
15
+ fs.mkdirSync(LOG_DIR, { recursive: true });
16
+ }
17
+ writeStream = fs.createWriteStream(LOG_FILE, { flags: "a" });
18
+ initialized = true;
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ function formatTimestamp(): string {
26
+ return new Date().toISOString();
27
+ }
28
+
29
+ export function log(...args: unknown[]) {
30
+ if (!ensureInitialized() || !writeStream) return;
31
+
32
+ const timestamp = formatTimestamp();
33
+ const message = args
34
+ .map(a => (typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)))
35
+ .join(" ");
36
+ writeStream.write(`[${timestamp}] ${message}\n`);
37
+ }
38
+
39
+ export function flush(): Promise<void> {
40
+ if (!writeStream) return Promise.resolve();
41
+
42
+ return new Promise((resolve) => {
43
+ writeStream!.end(() => {
44
+ writeStream = null;
45
+ initialized = false;
46
+ resolve();
47
+ });
48
+ });
49
+ }
50
+
51
+ export function clearLog() {
52
+ if (!ensureInitialized()) return;
53
+ writeStream?.close();
54
+ fs.writeFileSync(LOG_FILE, "");
55
+ writeStream = fs.createWriteStream(LOG_FILE, { flags: "a" });
56
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "strict": true,
13
+ "noImplicitAny": true,
14
+ "strictNullChecks": true,
15
+ "strictFunctionTypes": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noImplicitReturns": true,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "esModuleInterop": true,
21
+ "skipLibCheck": true,
22
+ "forceConsistentCasingInFileNames": true
23
+ },
24
+ "include": ["src/**/*"],
25
+ "exclude": ["node_modules", "dist", ".opencode"]
26
+ }