@telemai/opencode-plugin 0.2.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.
Files changed (4) hide show
  1. package/LICENSE +121 -0
  2. package/README.md +38 -0
  3. package/dist/index.js +23 -0
  4. package/package.json +18 -0
package/LICENSE ADDED
@@ -0,0 +1,121 @@
1
+ Telem AI Proprietary Software License
2
+ Version 1.0, 2026
3
+
4
+ Copyright (c) 2026 Telem AI. All rights reserved.
5
+
6
+ This is a proprietary license. It describes how the Telem AI software is
7
+ licensed today. It is not an open source license.
8
+
9
+ The software covered by this license, including its source code, compiled
10
+ output, documentation, and any files distributed with it, is referred to below
11
+ as the "Software". Telem AI is referred to as the "Licensor". "You" means the
12
+ individual or legal entity exercising the rights granted below.
13
+
14
+ By downloading, installing, or using the Software, you agree to these terms. If
15
+ you do not agree to them, do not download, install, or use the Software.
16
+
17
+
18
+ 1. GRANT OF RIGHTS
19
+
20
+ Subject to Section 2, the Licensor grants you a non-exclusive,
21
+ non-transferable, worldwide, royalty-free license to:
22
+
23
+ (a) download and install the Software;
24
+
25
+ (b) run and use the Software, in unmodified form, for any lawful purpose,
26
+ including personal use, internal business use, and commercial use; and
27
+
28
+ (c) make copies of the Software only as reasonably necessary to install, run,
29
+ and back up the Software as permitted by (a) and (b).
30
+
31
+ This grant is available to anyone who lawfully obtains a copy of the Software.
32
+ No fee and no separate agreement is required to exercise these rights.
33
+
34
+
35
+ 2. RESTRICTIONS
36
+
37
+ You may not:
38
+
39
+ (a) modify, adapt, translate, or otherwise alter the Software, or create any
40
+ derivative work based on the Software;
41
+
42
+ (b) redistribute, publish, sublicense, sell, resell, rent, lease, lend, host
43
+ for download, or otherwise make the Software available to any third
44
+ party, whether alone or as part of something else, and whether or not for
45
+ a fee;
46
+
47
+ (c) incorporate the Software, or any part of it, into another package,
48
+ library, product, or distribution that you provide to any third party; or
49
+
50
+ (d) remove, obscure, or alter any copyright, trademark, license, or other
51
+ proprietary notice contained in or displayed by the Software, including
52
+ this license file.
53
+
54
+ Nothing in this Section limits any right you have under applicable law that
55
+ cannot be waived by contract.
56
+
57
+
58
+ 3. OWNERSHIP
59
+
60
+ The Software is licensed, not sold. The Licensor and its licensors retain all
61
+ right, title, and interest in and to the Software, including all intellectual
62
+ property rights in it. All rights not expressly granted in Section 1 are
63
+ reserved by the Licensor.
64
+
65
+
66
+ 4. FUTURE VERSIONS AND RELICENSING
67
+
68
+ These terms reflect the Software's current licensing state and are not intended
69
+ to be permanent. The Licensor may release the Software, or any past, present, or
70
+ future version of it, under different terms, including an open source license
71
+ such as the Apache License, Version 2.0. Any such release applies only to the
72
+ version or versions the Licensor expressly designates, and does not by itself
73
+ change the terms of the copy you received under this license. Nothing in this
74
+ license obligates the Licensor to make any such release.
75
+
76
+
77
+ 5. TERM AND TERMINATION
78
+
79
+ This license takes effect when you first download, install, or use the Software
80
+ and continues until terminated. It terminates automatically if you breach
81
+ Section 2. On termination you must stop using the Software and destroy all
82
+ copies of it in your possession or control. Sections 3, 6, and 7 survive
83
+ termination.
84
+
85
+
86
+ 6. DISCLAIMER OF WARRANTY
87
+
88
+ THE SOFTWARE IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT WARRANTY OF ANY
89
+ KIND, EXPRESS OR IMPLIED. THE LICENSOR DISCLAIMS ALL WARRANTIES, INCLUDING BUT
90
+ NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
91
+ PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT. THE LICENSOR DOES NOT WARRANT
92
+ THAT THE SOFTWARE WILL BE UNINTERRUPTED, ERROR FREE, OR SECURE, OR THAT IT WILL
93
+ MEET YOUR REQUIREMENTS. YOU BEAR THE ENTIRE RISK OF USING THE SOFTWARE.
94
+
95
+
96
+ 7. LIMITATION OF LIABILITY
97
+
98
+ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL THE LICENSOR
99
+ BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, CONSEQUENTIAL, OR
100
+ PUNITIVE DAMAGES, OR FOR ANY LOSS OF PROFITS, REVENUE, DATA, GOODWILL, OR
101
+ BUSINESS INTERRUPTION, ARISING OUT OF OR RELATING TO THE SOFTWARE OR THIS
102
+ LICENSE, REGARDLESS OF THE THEORY OF LIABILITY AND EVEN IF THE LICENSOR HAS BEEN
103
+ ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. THE LICENSOR'S TOTAL AGGREGATE
104
+ LIABILITY ARISING OUT OF OR RELATING TO THE SOFTWARE OR THIS LICENSE WILL NOT
105
+ EXCEED ONE HUNDRED US DOLLARS (USD 100).
106
+
107
+ SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN
108
+ WARRANTIES OR LIABILITIES. IN THOSE JURISDICTIONS THE EXCLUSIONS AND LIMITATIONS
109
+ ABOVE APPLY ONLY TO THE EXTENT PERMITTED BY LAW.
110
+
111
+
112
+ 8. SEPARATE AGREEMENTS
113
+
114
+ The Licensor may grant rights beyond this license, including rights otherwise
115
+ restricted by Section 2, in a separate written agreement signed by the Licensor.
116
+ Where such an agreement exists, it controls to the extent of any conflict with
117
+ this license.
118
+
119
+
120
+ Questions about this license, and requests for rights it does not grant, should
121
+ be directed to Telem AI.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @telemai/opencode-plugin
2
+
3
+ Telem web search (telem_search) and page fetch (telem_fetch) tools for opencode, with trajectory v5 session recording.
4
+
5
+ ## Installation
6
+
7
+ Install with the opencode CLI:
8
+
9
+ ```bash
10
+ opencode plugin @telemai/opencode-plugin --global
11
+ export TELEM_API_KEY=... # your key
12
+ ```
13
+
14
+ This writes the plugin entry to opencode's config file (`opencode.jsonc`,
15
+ despite the `.json`-sounding name) and materializes the package — one command
16
+ for both.
17
+
18
+ ### Manual alternative
19
+
20
+ Add the package to the `plugin` array in `opencode.jsonc` yourself:
21
+
22
+ ```json
23
+ {
24
+ "plugin": ["@telemai/opencode-plugin"]
25
+ }
26
+ ```
27
+
28
+ ## Configuration
29
+
30
+ Set your Telem API key via the `TELEM_API_KEY` environment variable:
31
+
32
+ ```bash
33
+ export TELEM_API_KEY="your-api-key-here"
34
+ ```
35
+
36
+ The plugin will use this key to authenticate with the Telem service for all search and fetch operations.
37
+
38
+ See [docs.telem.ai](https://docs.telem.ai) for more information.
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ import{tool}from"@opencode-ai/plugin";import{createHash,randomUUID}from"node:crypto";import{readFileSync,statSync}from"node:fs";import{homedir}from"node:os";import{join}from"node:path";var HISTORY_TEXT_CAP=128e3,TOOL_INPUT_CAP=128e3,FETCH_MAX_URLS=5,FETCH_CONTENT_CAP=2e4,PROJECT_CONFIG_PATH=[".opencode","telem.json"],HOME_CONFIG_PATH=[".config","opencode","telem.json"],configFileCache=new Map;function readConfigFile(path){let signature;try{let stat=statSync(path);signature=`${stat.mtimeMs}:${stat.size}`}catch{return configFileCache.delete(path),null}let cached=configFileCache.get(path);if(cached&&cached.signature===signature)return cached.data;let data=null;try{let parsed=JSON.parse(readFileSync(path,"utf8"));parsed&&typeof parsed=="object"&&!Array.isArray(parsed)?data=parsed:console.warn(`[telem] ignoring ${path}: expected a JSON object`)}catch(error){console.warn(`[telem] ignoring ${path}: ${error?.message??"unreadable"}`)}return configFileCache.set(path,{signature,data}),data}function readConfigLayer(root,parts){let path;try{let base=root();if(!base)return null;path=join(base,...parts)}catch{return null}return readConfigFile(path)}function asName(value){if(typeof value=="string")return value.trim()||void 0}function asNameList(value){if(!Array.isArray(value))return;let names=value.map(item=>typeof item=="string"?item.trim():"").filter(Boolean);return names.length?names:void 0}function asFlag(value){return typeof value=="boolean"?value:void 0}function csvEnv(name){return asNameList(process.env[name]?.split(","))}function resolveKey(layers,key,coerce,fromEnv){for(let level=0;level<layers.length;level++){let layer=layers[level];if(!layer)continue;let value2=coerce(layer[key]);if(value2!==void 0)return{value:value2,level}}let value=fromEnv();return value===void 0?void 0:{value,level:layers.length}}var lastCompositionWarning;function resolveTelemConfig(projectRoot){let layers=[readConfigLayer(()=>projectRoot??process.cwd(),PROJECT_CONFIG_PATH),readConfigLayer(()=>homedir(),HOME_CONFIG_PATH)],config={baseUrl:(process.env.TELEM_BASE_URL??"https://router.telem.ai").replace(/\/+$/,"")};process.env.TELEM_API_KEY&&(config.apiKey=process.env.TELEM_API_KEY);let tier=resolveKey(layers,"tier",asName,()=>asName(process.env.TELEM_TIER)),fields=resolveKey(layers,"fields",asNameList,()=>csvEnv("TELEM_FIELDS"));if(tier&&fields){let keepTier=tier.level<fields.level,signature=`${tier.value}@${tier.level}|${fields.value.join(",")}@${fields.level}`;lastCompositionWarning!==signature&&(lastCompositionWarning=signature,console.warn(`[telem] both tier (${tier.value}) and fields (${fields.value.join(", ")}) are configured, but a request may carry only one \u2014 `+(keepTier?"keeping tier, ignoring fields":"keeping fields, ignoring tier")+" (the more specific source wins; on a tie, fields).")),keepTier?fields=void 0:tier=void 0}tier&&(config.tier=tier.value),fields&&(config.fields=fields.value);let providersInclude=resolveKey(layers,"providersInclude",asNameList,()=>csvEnv("TELEM_PROVIDERS_INCLUDE")??csvEnv("TELEM_PROVIDERS"));providersInclude&&(config.providersInclude=providersInclude.value);let providersExclude=resolveKey(layers,"providersExclude",asNameList,()=>csvEnv("TELEM_PROVIDERS_EXCLUDE"));providersExclude&&(config.providersExclude=providersExclude.value);let fullContent=resolveKey(layers,"fullContent",asFlag,()=>process.env.TELEM_FULL_CONTENT==="1"?!0:void 0);return fullContent&&(config.fullContent=fullContent.value),config}function buildSearchBlock(options){let block={};options.tier!==void 0&&(block.tier=options.tier),options.fields!==void 0&&(block.fields=options.fields);let providers={};return options.providersInclude!==void 0&&(providers.include=options.providersInclude),options.providersExclude!==void 0&&(providers.exclude=options.providersExclude),Object.keys(providers).length&&(block.providers=providers),options.fullContent!==void 0&&(block.include_full_content=options.fullContent),Object.keys(block).length?block:null}var HARNESS_ID="opencode",NS_TRAJECTORY="443866ab-1b45-5ed8-979e-52fdad07b810",NONE="none";function sha256hex(x){return createHash("sha256").update(x).digest("hex")}function lp(s){return String(Buffer.byteLength(s,"utf8"))+":"+s}function uuid5(namespace,name){let nsb=Buffer.from(namespace.replace(/-/g,""),"hex"),hash=createHash("sha1").update(nsb).update(Buffer.from(name,"utf8")).digest(),b=Buffer.from(hash.subarray(0,16));b[6]=b[6]&15|80,b[8]=b[8]&63|128;let x=b.toString("hex");return`${x.slice(0,8)}-${x.slice(8,12)}-${x.slice(12,16)}-${x.slice(16,20)}-${x.slice(20,32)}`}function contextWindowHash(sid){return sha256hex(sid)}function sessionKey(sid,comp,rev){return uuid5(NS_TRAJECTORY,lp(HARNESS_ID)+lp(contextWindowHash(sid))+lp(comp)+lp(rev))}function fingerprint(sid){return sha256hex(lp(HARNESS_ID)+lp(sid))}function snapshotNodeKey(sid,msg){return uuid5(NS_TRAJECTORY,lp(HARNESS_ID)+lp(contextWindowHash(sid))+lp(msg)+lp("snap"))}function lastCompactionId(msgs){let last=NONE;for(let message of msgs??[])for(let part of message?.parts??[])part?.type==="compaction"&&typeof part.id=="string"&&(last=part.id);return last}function lastAssistantMessage(msgs){let found;for(let message of msgs??[])if(message?.info?.role==="assistant"&&typeof message.info.id=="string"){let t=message.info.time?.created;found={id:message.info.id,timeMs:typeof t=="number"?t:void 0}}return found}function toolMarker(part,override){let status=override?.status??part?.state?.status??"unknown",input="";try{let raw=override?override.input:part?.state?.input;raw!==void 0&&(input=" "+JSON.stringify(raw).slice(0,TOOL_INPUT_CAP))}catch{}return`[tool ${part?.tool??"?"}: ${status}${input}]`}function isEmptyInput(input){return input===void 0||typeof input=="object"&&input!==null&&Object.keys(input).length===0}function flattenMessages(data,opts){let history=[];for(let message of data??[]){let role=message?.info?.role;if(role!=="user"&&role!=="assistant")continue;let claimed;if(opts?.currentCall&&message?.info?.id===opts.currentCall.messageID){let candidates=(message?.parts??[]).filter(p=>p?.type==="tool"&&p?.tool===opts.currentCall.tool&&p?.state?.status==="pending"&&isEmptyInput(p?.state?.input));candidates.length===1&&(claimed=candidates[0])}let contentPieces=[],reasoningPieces=[];for(let part of message?.parts??[])if(part?.type==="text"&&typeof part.text=="string")contentPieces.push(part.text);else if(part?.type==="reasoning"){let stored=typeof part.text=="string"?part.text:"",streamed=opts?.streamedReasoning?.get(part.id)??"",text=streamed.length>stored.length?streamed:stored;text&&reasoningPieces.push(text)}else part?.type==="tool"&&contentPieces.push(part===claimed?toolMarker(part,{status:"running",input:opts.currentCall.args}):toolMarker(part));let content=contentPieces.join(`
2
+ `).slice(0,HISTORY_TEXT_CAP),reasoning=reasoningPieces.join(`
3
+ `).slice(0,HISTORY_TEXT_CAP);if(!content&&!reasoning)continue;let entry={role,content};reasoning&&(entry.reasoning=reasoning),history.push(entry)}return history}function assertV2Envelope(interaction){let version=interaction?.normalized_schema_version;if(!Number.isInteger(version)||version<2)throw new Error(`Telem server answered without the V2 normalized contract (normalized_schema_version=${version}); upgrade the backend or point TELEM_BASE_URL at a V2 deployment`)}var RENDER_EXCERPT_MAX_ENTRIES=4,RENDER_EXCERPT_MAX_CHARS=1e3,RENDER_RELATED_MAX_ITEMS=6,RENDER_TOTAL_CAP=128e3;function line(value){return typeof value=="string"?value.trim().replace(/\s*\n\s*/g," "):""}function renderRow(provider,row){let lines=[`[${provider}] URL: ${line(row?.url)}`],title=line(row?.title);title&&lines.push(`Title: ${title}`);let summary=line(row?.summary);summary&&lines.push(`Summary: ${summary}`);let entries=(Array.isArray(row?.excerpt)?row.excerpt:[]).map(entry=>line(entry)).filter(Boolean);if(entries.length){lines.push("Excerpt:");for(let entry of entries.slice(0,RENDER_EXCERPT_MAX_ENTRIES)){let cut=entry.length>RENDER_EXCERPT_MAX_CHARS;lines.push(`- ${cut?entry.slice(0,RENDER_EXCERPT_MAX_CHARS)+"\u2026":entry}`)}let dropped=entries.length-RENDER_EXCERPT_MAX_ENTRIES;dropped>0&&lines.push(`\u2026(${dropped} more excerpt entries)`)}let published=line(row?.publish_date);published&&lines.push(`Published: ${published}`);let source=row?.source;if(source&&typeof source=="object"){let name=line(source.name)||line(source.domain),author=line(source.author);author?lines.push(`Source: ${name?`${name} by ${author}`:`by ${author}`}`):line(source.name)&&lines.push(`Source: ${name}`)}return lines.join(`
4
+ `)}function renderQueryBlock(runs){let lines=[],answer="",questions=[],searches=[];for(let run of runs){let payload=run?.output_payload;if(!payload||typeof payload!="object")continue;answer||(answer=line(payload.answer));let related2=payload.related;if(!(!related2||typeof related2!="object")){for(let item of Array.isArray(related2.questions)?related2.questions:[]){let text=line(item);text&&questions.push(text)}for(let item of Array.isArray(related2.searches)?related2.searches:[]){let text=line(item);text&&searches.push(text)}}}answer&&lines.push(`Answer: ${answer}`);let related=[...new Set([...questions,...searches])].slice(0,RENDER_RELATED_MAX_ITEMS);return related.length&&lines.push(`Related: ${related.join(", ")}`),lines}function renderQuerySection(runs){let blocks=[],rowBlocks=[];for(let run of runs){let provider=line(run?.preprocessor_name)||"unknown",payload=run?.output_payload,rows=Array.isArray(payload?.results)?payload.results:null,error=run?.error;if(run?.status==="failed"||!rows&&error){let message=line(error?.message)||line(error?.type)||"unknown error";rowBlocks.push(`[${provider}] failed: ${message}`);continue}if(!rows?.length){let degraded=(Array.isArray(payload?.warnings)?payload.warnings:[]).find(warning=>warning?.code==="normalize_failed");if(degraded){let message=line(degraded.message)||"normalize_failed";rowBlocks.push(`[${provider}] no rows (${message})`)}continue}for(let row of rows)rowBlocks.push(renderRow(provider,row))}let head=renderQueryBlock(runs);return head.length&&blocks.push(head.join(`
5
+ `)),blocks.push(...rowBlocks.length?rowBlocks:["No results found."]),blocks.join(`
6
+
7
+ `)}function formatSearchResults(interaction){let groups=new Map;for(let run of interaction?.preprocessor_runs??[]){let index=typeof run?.batch_index=="number"?run.batch_index:0,group=groups.get(index);group||groups.set(index,group={query:"",runs:[]}),group.query||(group.query=line(run?.query)),group.runs.push(run)}let ordered=[...groups.entries()].sort((a,b)=>a[0]-b[0]).map(([,group])=>group);if(ordered.length<=1)return renderQuerySection(ordered[0]?.runs??[]);let text=ordered.map((group,i)=>`### ${group.query?`Query ${i+1}: ${group.query}`:`Query ${i+1}`}
8
+ ${renderQuerySection(group.runs)}`).join(`
9
+
10
+ `);return text.length<=RENDER_TOTAL_CAP?text:text.slice(0,RENDER_TOTAL_CAP)+`
11
+
12
+ \u2026(batch output truncated at ${RENDER_TOTAL_CAP} characters \u2014 re-run the remaining queries in a smaller batch)`}function validateFetchUrls(raw){if(!Array.isArray(raw)||raw.length===0)throw new Error("telem_fetch requires at least one URL in `urls`.");for(let item of raw)if(typeof item!="string")throw new Error("telem_fetch: every item in `urls` must be a URL string.");let trimmed=raw.map(url=>url.trim());for(let url of trimmed)if(!/^https?:\/\//i.test(url))throw new Error(`telem_fetch only reads http(s) URLs, got ${JSON.stringify(url)}. Pass absolute URLs starting with http:// or https://.`);let urls=[...new Set(trimmed)];if(urls.length>FETCH_MAX_URLS)throw new Error(`telem_fetch reads at most ${FETCH_MAX_URLS} URLs per call (got ${urls.length}). Split the read into multiple calls.`);return urls}function formatFetchedRow(row){let url=typeof row?.url=="string"?row.url:"",title=typeof row?.title=="string"?row.title:"",status=typeof row?.status=="string"?row.status:"unknown",header=`### ${url}`+(title?`
13
+ Title: ${title}`:"")+`
14
+ Status: ${status}`;if(status!=="succeeded"){let error=row?.error,brief=error&&(error.type||error.message)?`
15
+ Error: ${[error.type,error.message].filter(Boolean).join(": ")}`:"";return header+brief}let content=typeof row?.content=="string"?row.content:"",note=content.length>FETCH_CONTENT_CAP||row?.content_truncated===!0?`
16
+
17
+ [Content truncated at ${FETCH_CONTENT_CAP} characters]`:"";return`${header}
18
+
19
+ ${content.slice(0,FETCH_CONTENT_CAP)}${note}`}function formatFetchResults(interaction){let preRuns=(interaction?.preprocessor_runs??[]).filter(r=>r?.preprocessor_name==="web_fetch"),rows=[];if(preRuns.length>0){preRuns.sort((a,b)=>(a?.batch_index??0)-(b?.batch_index??0));for(let run of preRuns){let fetched=run?.output_payload?.fetched_results;Array.isArray(fetched)&&rows.push(...fetched)}}else{let fetched=(interaction?.postprocessor_runs??[]).find(r=>r?.postprocessor_name==="web_fetch_cache")?.output_payload?.fetched_results;Array.isArray(fetched)&&rows.push(...fetched)}return rows.length===0?"The fetch produced no results (the backend returned no web fetch output).":rows.map(formatFetchedRow).join(`
20
+
21
+ `)}var TelemPlugin=async({client,directory})=>{let reasoningBySession=new Map,allowBuiltinWebfetch=process.env.TELEM_ALLOW_BUILTIN_WEBFETCH==="1";async function buildTrajectory(selfId,selfMsgs,nodeKey,kind="search"){let selfComp=lastCompactionId(selfMsgs),selfSession;try{selfSession=(await client.session.get({path:{id:selfId}}))?.data}catch{}let selfRev=selfSession?.revert?.messageID??NONE,traj={session_key:sessionKey(selfId,selfComp,selfRev),fingerprint:fingerprint(selfId),node_key:nodeKey,kind,parent_node_key:null,ancestors:[]},lineage=[],visited=new Set([selfId]),cur=selfSession?.parentID;for(;cur&&!visited.has(cur);){visited.add(cur);let s;try{s=(await client.session.get({path:{id:cur}}))?.data}catch{lineage.push({materialized:!1});break}if(!s){lineage.push({materialized:!1});break}let msgs;try{msgs=(await client.session.messages({path:{id:cur}})).data}catch{lineage.push({materialized:!1}),cur=s.parentID;continue}let comp=lastCompactionId(msgs??[]),rev=s.revert?.messageID??NONE,lastAsst=lastAssistantMessage(msgs??[]),snapKey=snapshotNodeKey(cur,lastAsst?.id??NONE);lineage.push({materialized:!0,key:snapKey,entry:{session_key:sessionKey(cur,comp,rev),fingerprint:fingerprint(cur),node_key:snapKey,parent_node_key:null,context:flattenMessages(msgs??[]),spawned_at:lastAsst?.timeMs!=null?new Date(lastAsst.timeMs).toISOString():null}}),cur=s.parentID}lineage.reverse();let ancestors=[];for(let i=0;i<lineage.length;i++){let m=lineage[i];if(!m.materialized||!m.entry)continue;let prev=lineage[i-1];m.entry.parent_node_key=prev&&prev.materialized?prev.key:null,ancestors.push(m.entry)}let direct=lineage[lineage.length-1];return traj.parent_node_key=direct&&direct.materialized?direct.key:null,traj.ancestors=ancestors,traj}return{config:async config=>{config.permission??={},config.permission.telem_search??="allow",config.permission.telem_fetch??="allow",allowBuiltinWebfetch||(config.permission.webfetch??="deny"),config.agent??={};let injected={telem_search:"allow",telem_fetch:"allow"};allowBuiltinWebfetch||(injected.webfetch="deny");for(let name of["explore","general"])config.agent[name]??={},config.agent[name].permission={...injected,...config.agent[name].permission??{}}},event:async({event})=>{let properties=event?.properties;if(event?.type==="message.part.updated"){let part=properties?.part;if(part?.type!=="reasoning"||typeof part.sessionID!="string"||typeof part.id!="string")return;let parts=reasoningBySession.get(part.sessionID);parts||reasoningBySession.set(part.sessionID,parts=new Map);let text=typeof part.text=="string"?part.text.slice(0,HISTORY_TEXT_CAP):"",prev=parts.get(part.id);(prev===void 0||text.length>=prev.length)&&parts.set(part.id,text)}else if(event?.type==="message.part.delta"){if(properties?.field!=="text"||typeof properties?.delta!="string")return;let parts=reasoningBySession.get(properties.sessionID),prev=parts?.get(properties.partID);if(parts===void 0||prev===void 0)return;prev.length<HISTORY_TEXT_CAP&&parts.set(properties.partID,(prev+properties.delta).slice(0,HISTORY_TEXT_CAP))}else if(event?.type==="session.idle"||event?.type==="session.deleted"){let endedSession=typeof properties?.sessionID=="string"?properties.sessionID:typeof properties?.info?.id=="string"?properties.info.id:void 0;endedSession&&reasoningBySession.delete(endedSession)}},tool:{telem_search:tool({description:"Search the web for relevant information. Pass one or more queries in the `queries` array: a multi-query call runs them concurrently and records them as ONE interaction, and each result block is labelled with the query that produced it \u2014 prefer batching several queries for the same step into a single call over making several separate calls. Just call it whenever you need to search; you do not manage or thread any session id \u2014 searches are automatically grouped by the current conversation. Pass `goal`: a short phrase naming what this search is for (a label on this search), useful when a conversation pursues several distinct objectives.",args:{queries:tool.schema.array(tool.schema.string().describe("A single search query.")).min(1).describe("One or more queries to search for. Pass several to run them concurrently as a single interaction when the current step needs several searches; each result block is labelled with its query."),goal:tool.schema.string().optional().describe("A short phrase naming what this search is for (the sub-task or objective it serves) \u2014 it labels this step in monitoring, so include it whenever you have one in mind.")},async execute(args,ctx){let queries=(args.queries??[]).map(q=>typeof q=="string"?q.trim():"").filter(Boolean);if(!queries.length)throw new Error("telem_search requires at least one non-empty query in `queries`.");let messages=await client.session.messages({path:{id:ctx.sessionID}}),history=flattenMessages(messages.data??[],{streamedReasoning:reasoningBySession.get(ctx.sessionID),currentCall:{messageID:ctx.messageID,args,tool:"telem_search"}}),nodeKey=randomUUID(),metadata={message_history:history};try{Object.assign(metadata,await buildTrajectory(ctx.sessionID,messages.data??[],nodeKey))}catch{metadata.session_key=sessionKey(ctx.sessionID,lastCompactionId(messages.data??[]),NONE),metadata.fingerprint=fingerprint(ctx.sessionID),metadata.node_key=nodeKey,metadata.kind="search",metadata.parent_node_key=null,metadata.ancestors=[]}args.goal&&(metadata.goal=args.goal);let userInput=queries.length===1?{query:queries[0]}:queries.map(query=>({query})),config=resolveTelemConfig(directory),body={user_input:userInput,postprocessor_names:[],metadata},search=buildSearchBlock(config);search&&(body.search=search);let headers={"Content-Type":"application/json"};config.apiKey&&(headers.Authorization=`Bearer ${config.apiKey}`);let response=await fetch(`${config.baseUrl}/v1/interactions`,{method:"POST",headers,body:JSON.stringify(body),signal:ctx.abort});if(!response.ok){let detail=await response.text().catch(()=>"");throw new Error(`Telem search failed: HTTP ${response.status} ${detail.slice(0,200)}`)}let interaction=await response.json();assertV2Envelope(interaction);let sessionId=interaction.session_id?String(interaction.session_id):void 0;return{output:(sessionId?`Telem search session: ${sessionId}
22
+
23
+ `:"")+formatSearchResults(interaction),metadata:sessionId?{telem_session_id:sessionId}:void 0}}}),telem_fetch:tool({description:"Read the full text of web pages by URL. telem_search returns snippets and never reads pages; this tool is how pages are read here. Up to 5 http(s) URLs per call, fetched together as one batch; for more pages make several calls.",args:{urls:tool.schema.array(tool.schema.string().describe("An absolute http(s) URL to read.")).min(1).describe("The http(s) URLs of the pages to read, at most 5 per call. Duplicates are removed.")},async execute(args,ctx){let urls=validateFetchUrls(args.urls),messages=await client.session.messages({path:{id:ctx.sessionID}}),history=flattenMessages(messages.data??[],{streamedReasoning:reasoningBySession.get(ctx.sessionID),currentCall:{messageID:ctx.messageID,args,tool:"telem_fetch"}}),nodeKey=randomUUID(),metadata={message_history:history};try{Object.assign(metadata,await buildTrajectory(ctx.sessionID,messages.data??[],nodeKey,"fetch"))}catch{metadata.session_key=sessionKey(ctx.sessionID,lastCompactionId(messages.data??[]),NONE),metadata.fingerprint=fingerprint(ctx.sessionID),metadata.node_key=nodeKey,metadata.kind="fetch",metadata.parent_node_key=null,metadata.ancestors=[]}let body={user_input:{urls},metadata},config=resolveTelemConfig(directory),headers={"Content-Type":"application/json"};config.apiKey&&(headers.Authorization=`Bearer ${config.apiKey}`);let response=await fetch(`${config.baseUrl}/v1/interactions`,{method:"POST",headers,body:JSON.stringify(body),signal:ctx.abort});if(!response.ok){let detail=await response.text().catch(()=>"");throw new Error(`Telem fetch failed: HTTP ${response.status} ${detail.slice(0,200)}`)}let interaction=await response.json(),sessionId=interaction.session_id?String(interaction.session_id):void 0;return{output:formatFetchResults(interaction),metadata:sessionId?{telem_session_id:sessionId}:void 0}}})}}};export{TelemPlugin};
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@telemai/opencode-plugin",
3
+ "version": "0.2.0",
4
+ "description": "Telem web search (telem_search) and page fetch (telem_fetch) tools for opencode, with trajectory v5 session recording.",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "publishConfig": { "access": "restricted" },
8
+ "main": "./dist/index.js",
9
+ "exports": { ".": "./dist/index.js" },
10
+ "files": ["dist", "LICENSE", "README.md"],
11
+ "scripts": {
12
+ "build": "esbuild ../.opencode/plugins/telem.ts --bundle --platform=node --format=esm --external:@opencode-ai/plugin --outfile=dist/index.js --minify-whitespace --minify-syntax --legal-comments=none",
13
+ "prepack": "npm run build",
14
+ "test": "node --test 'test/**/*.test.mjs'"
15
+ },
16
+ "peerDependencies": { "@opencode-ai/plugin": ">=1.17.0" },
17
+ "devDependencies": { "esbuild": "^0.24.0" }
18
+ }