@katipally/webplus 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 +21 -0
- package/README.md +72 -0
- package/bin/webplus.js +544 -0
- package/package.json +44 -0
- package/src/skill/webplus/SKILL.md +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 katipally
|
|
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,72 @@
|
|
|
1
|
+
# web+
|
|
2
|
+
|
|
3
|
+
Make any AI coding agent fetch the **latest, official, verified** information from the web instead of answering from stale training memory or random blog spam. It's one [Agent Skill](https://agentskills.io) (a `SKILL.md`) that rides on the web tools your agent already has, so it stays out of the way until a task actually needs current facts, then it spends searches smartly and cites sources with dates.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @katipally/webplus
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
When a task depends on current or external facts (software versions, releases, API/library docs, pricing, news, events, people, orgs, standards), the skill auto-triggers and makes the agent:
|
|
12
|
+
|
|
13
|
+
- **Get the real date first** — from the system clock or an authoritative timestamp, never its assumed "today," then judge recency against that.
|
|
14
|
+
- **Go to the primary source** — official docs, vendor sites, GitHub releases, standards bodies, the org's own announcement. Never content farms, SEO spam, or AI-generated filler.
|
|
15
|
+
- **Cross-verify** — confirm in two independent reputable sources before stating a fact; otherwise label it unconfirmed.
|
|
16
|
+
- **Be exact** — quote the precise version string or number, and distinguish stable from beta.
|
|
17
|
+
- **Never assume** — if access fails or sources conflict, say so and mark the gap unknown instead of guessing.
|
|
18
|
+
- **Stay unbiased and cite** — represent sources faithfully, surface disagreement, link every fresh claim with its date.
|
|
19
|
+
- **Spend searches wisely** — search only what the question needs, one precise query over many vague ones, fetch only the page that answers it, and stop once it's verified. No tangents, no off-topic detours.
|
|
20
|
+
|
|
21
|
+
No API key, no extra server. The skill is plain Markdown.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
`SKILL.md` is an open standard, so web+ installs the same skill folder into each tool's own skills directory.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx @katipally/webplus # wizard: pick project or global scope, then the agents
|
|
29
|
+
npx @katipally/webplus init --all # every supported tool, non-interactive
|
|
30
|
+
npx @katipally/webplus init --only claude,agents,cursor
|
|
31
|
+
npx @katipally/webplus init --global # machine-wide (~/.claude/skills, ~/.agents/skills, ...)
|
|
32
|
+
npx @katipally/webplus list # show the catalog and what's detected
|
|
33
|
+
npx @katipally/webplus remove # delete only web+'s skill folder, nothing else
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Scope is asked per run; `--global` / `--local` set it for scripts. Re-running is idempotent. `remove` deletes only our own `webplus/` folder (verified by its frontmatter) and never touches your other skills.
|
|
37
|
+
|
|
38
|
+
## Supported tools (verified paths, Jun 2026)
|
|
39
|
+
|
|
40
|
+
The `.agents/skills` entry is the cross-tool open standard, read by Codex, Goose, OpenHands and others in one shot.
|
|
41
|
+
|
|
42
|
+
| Tool | Project | Global |
|
|
43
|
+
|------|---------|--------|
|
|
44
|
+
| `.agents/skills` (Codex, Goose, OpenHands, standard) | `.agents/skills/` | `~/.agents/skills/` |
|
|
45
|
+
| Claude Code | `.claude/skills/` | `~/.claude/skills/` |
|
|
46
|
+
| Gemini CLI | `.gemini/skills/` | `~/.gemini/skills/` |
|
|
47
|
+
| GitHub Copilot / VS Code | `.github/skills/` | `~/.copilot/skills/` |
|
|
48
|
+
| Cursor | `.cursor/skills/` | `~/.cursor/skills/` |
|
|
49
|
+
| Windsurf | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` |
|
|
50
|
+
| OpenCode | `.opencode/skills/` | `~/.config/opencode/skills/` |
|
|
51
|
+
| Cline | `.cline/skills/` | `~/.cline/skills/` |
|
|
52
|
+
| Roo Code | `.roo/skills/` | `~/.roo/skills/` |
|
|
53
|
+
| JetBrains Junie | `.junie/skills/` | `~/.junie/skills/` |
|
|
54
|
+
| Amp | `.agents/skills/` | `~/.config/agents/skills/` |
|
|
55
|
+
| Kiro | `.kiro/skills/` | `~/.kiro/skills/` |
|
|
56
|
+
| TRAE | `.trae/skills/` | — (project only) |
|
|
57
|
+
| Tabnine | `.tabnine/agent/skills/` | `~/.tabnine/agent/skills/` |
|
|
58
|
+
| Factory (Droid) | `.factory/skills/` | `~/.factory/skills/` |
|
|
59
|
+
|
|
60
|
+
## Manual install (no npx)
|
|
61
|
+
|
|
62
|
+
Clone or download this repo and copy the skill folder into your agent's skills directory from the table above:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cp -r src/skill/webplus ~/.claude/skills/ # or any path above
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
That's the whole skill: `src/skill/webplus/SKILL.md`. Edit it freely.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
package/bin/webplus.js
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// webplus (web+) — install one Agent Skill that makes any agent fetch the latest, official,
|
|
5
|
+
// verified info from the web (its own web tools) instead of answering from memory.
|
|
6
|
+
// Writes a SKILL.md folder into each tool's native skills directory. Vendor-neutral,
|
|
7
|
+
// npx-only, zero dependencies. SKILL.md is an open standard read by many tools.
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const readline = require('readline');
|
|
13
|
+
|
|
14
|
+
const ROOT = path.join(__dirname, '..');
|
|
15
|
+
const VERSION = require(path.join(ROOT, 'package.json')).version;
|
|
16
|
+
const SKILL_SRC = path.join(ROOT, 'src', 'skill', 'webplus', 'SKILL.md');
|
|
17
|
+
const SKILL_NAME = 'webplus'; // the installed skill folder name
|
|
18
|
+
const SKILL_ID = 'webplus'; // frontmatter name — used to verify a folder is ours
|
|
19
|
+
|
|
20
|
+
const home = (...p) => path.join(os.homedir(), ...p);
|
|
21
|
+
|
|
22
|
+
// Catalog of skills-compatible tools (verified discovery paths, Jun 2026). Each `dir` is the
|
|
23
|
+
// PARENT skills directory; we write `<dir>/webplus/SKILL.md`. `pre` = pre-checked in the
|
|
24
|
+
// wizard. `detect` flips it on when the tool's footprint is present. `.agents/skills` is the
|
|
25
|
+
// cross-tool open standard read by Codex, Goose, OpenHands and others, so the `agents` entry
|
|
26
|
+
// covers all three; Amp also reads it at project scope but has its own global path.
|
|
27
|
+
const TARGETS = [
|
|
28
|
+
{ id: 'agents', label: '.agents/skills — universal standard', project: '.agents/skills', global: home('.agents', 'skills'),
|
|
29
|
+
pre: true, keywords: 'codex openai goose block openhands universal standard agents', detect: () => exists('.agents') },
|
|
30
|
+
{ id: 'claude', label: 'Claude Code', project: '.claude/skills', global: home('.claude', 'skills'),
|
|
31
|
+
pre: true, keywords: 'anthropic claude', detect: () => exists('.claude') },
|
|
32
|
+
{ id: 'gemini', label: 'Gemini CLI', project: '.gemini/skills', global: home('.gemini', 'skills'),
|
|
33
|
+
keywords: 'google gemini', detect: () => exists('.gemini') },
|
|
34
|
+
{ id: 'copilot', label: 'GitHub Copilot / VS Code', project: '.github/skills', global: home('.copilot', 'skills'),
|
|
35
|
+
keywords: 'github copilot vscode visual studio microsoft', detect: () => exists('.github') },
|
|
36
|
+
{ id: 'cursor', label: 'Cursor', project: '.cursor/skills', global: home('.cursor', 'skills'),
|
|
37
|
+
keywords: 'cursor', detect: () => exists('.cursor') },
|
|
38
|
+
{ id: 'windsurf', label: 'Windsurf', project: '.windsurf/skills', global: home('.codeium', 'windsurf', 'skills'),
|
|
39
|
+
keywords: 'windsurf codeium cascade', detect: () => exists('.windsurf') },
|
|
40
|
+
{ id: 'opencode', label: 'OpenCode', project: '.opencode/skills', global: home('.config', 'opencode', 'skills'),
|
|
41
|
+
keywords: 'opencode sst', detect: () => exists('.opencode') },
|
|
42
|
+
{ id: 'cline', label: 'Cline', project: '.cline/skills', global: home('.cline', 'skills'),
|
|
43
|
+
keywords: 'cline', detect: () => exists('.cline') || exists('.clinerules') },
|
|
44
|
+
{ id: 'roo', label: 'Roo Code', project: '.roo/skills', global: home('.roo', 'skills'),
|
|
45
|
+
keywords: 'roo roocode', detect: () => exists('.roo') },
|
|
46
|
+
{ id: 'junie', label: 'JetBrains Junie', project: '.junie/skills', global: home('.junie', 'skills'),
|
|
47
|
+
keywords: 'junie jetbrains intellij', detect: () => exists('.junie') },
|
|
48
|
+
{ id: 'amp', label: 'Amp (Sourcegraph)', project: '.agents/skills', global: home('.config', 'agents', 'skills'),
|
|
49
|
+
keywords: 'amp sourcegraph', detect: () => exists('.agents') },
|
|
50
|
+
{ id: 'kiro', label: 'Kiro (AWS)', project: '.kiro/skills', global: home('.kiro', 'skills'),
|
|
51
|
+
keywords: 'kiro aws amazon', detect: () => exists('.kiro') },
|
|
52
|
+
{ id: 'trae', label: 'TRAE (ByteDance)', project: '.trae/skills', global: null,
|
|
53
|
+
keywords: 'trae bytedance', tag: 'project only', detect: () => exists('.trae') },
|
|
54
|
+
{ id: 'tabnine', label: 'Tabnine', project: '.tabnine/agent/skills', global: home('.tabnine', 'agent', 'skills'),
|
|
55
|
+
keywords: 'tabnine', detect: () => exists('.tabnine') },
|
|
56
|
+
{ id: 'factory', label: 'Factory (Droid)', project: '.factory/skills', global: home('.factory', 'skills'),
|
|
57
|
+
keywords: 'factory droid', detect: () => exists('.factory') },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
class CancelError extends Error {}
|
|
61
|
+
|
|
62
|
+
// ---------- core: write / remove the skill folder ----------
|
|
63
|
+
|
|
64
|
+
function exists(rel) {
|
|
65
|
+
try { fs.accessSync(path.resolve(process.cwd(), rel)); return true; } catch { return false; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function homeExists(file) {
|
|
69
|
+
try { fs.accessSync(file); return true; } catch { return false; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function loadSkill() {
|
|
73
|
+
return fs.readFileSync(SKILL_SRC, 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The skills PARENT dir for a target at a scope, or null if none documented.
|
|
77
|
+
function dirOf(t, scope) {
|
|
78
|
+
const d = scope === 'global' ? t.global : t.project;
|
|
79
|
+
if (!d) return null;
|
|
80
|
+
return path.isAbsolute(d) ? d : path.resolve(process.cwd(), d);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The folder we actually write/remove: <parent>/webplus.
|
|
84
|
+
function skillDirOf(t, scope) {
|
|
85
|
+
const parent = dirOf(t, scope);
|
|
86
|
+
return parent ? path.join(parent, SKILL_NAME) : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// What writing would do, without writing. 'create' | 'update'.
|
|
90
|
+
function planAction(skillDir) {
|
|
91
|
+
return fs.existsSync(path.join(skillDir, 'SKILL.md')) ? 'update' : 'create';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A folder is ours iff it holds a SKILL.md declaring our frontmatter name.
|
|
95
|
+
function isOurs(skillDir) {
|
|
96
|
+
try { return new RegExp(`name:\\s*${SKILL_ID}\\b`).test(fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf8')); }
|
|
97
|
+
catch { return false; }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Write SKILL.md into <skillDir>. Returns 'created' | 'updated'.
|
|
101
|
+
function writeSkill(skillDir, body) {
|
|
102
|
+
const existed = fs.existsSync(path.join(skillDir, 'SKILL.md'));
|
|
103
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
104
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), body);
|
|
105
|
+
return existed ? 'updated' : 'created';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Delete ONLY our skill folder, and only if it is ours. Never touch sibling skills.
|
|
109
|
+
function removeSkill(skillDir) {
|
|
110
|
+
if (!fs.existsSync(skillDir) || !isOurs(skillDir)) return null;
|
|
111
|
+
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
112
|
+
return 'removed';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------- manifest (records exactly what we installed, for clean removal) ----------
|
|
116
|
+
|
|
117
|
+
function manifestPath(scope) {
|
|
118
|
+
return scope === 'global'
|
|
119
|
+
? path.join(os.homedir(), '.config', 'webplus', 'manifest.json')
|
|
120
|
+
: path.join(process.cwd(), '.webplus.json');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readManifest(scope) {
|
|
124
|
+
try { return JSON.parse(fs.readFileSync(manifestPath(scope), 'utf8')); } catch { return null; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function writeManifest(scope, entries) {
|
|
128
|
+
const mp = manifestPath(scope);
|
|
129
|
+
const data = { version: VERSION, scope, skills: entries };
|
|
130
|
+
fs.mkdirSync(path.dirname(mp), { recursive: true });
|
|
131
|
+
fs.writeFileSync(mp, JSON.stringify(data, null, 2) + '\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function clearManifest(scope) {
|
|
135
|
+
try { fs.unlinkSync(manifestPath(scope)); } catch {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------- target resolution ----------
|
|
139
|
+
|
|
140
|
+
function dedupeByDir(arr) {
|
|
141
|
+
const seen = new Map();
|
|
142
|
+
for (const x of arr) seen.set(x.dir, x); // last wins; dir is absolute
|
|
143
|
+
return [...seen.values()];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function displayPath(p) {
|
|
147
|
+
return p.startsWith(os.homedir()) ? p.replace(os.homedir(), '~') : path.relative(process.cwd(), p) || p;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Resolve selected catalog entries into concrete { id, label, dir } write targets for a scope,
|
|
151
|
+
// dropping any that have no documented path at that scope (e.g. TRAE global), and deduping by dir.
|
|
152
|
+
function resolveTargets(selected, scope) {
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const t of selected) {
|
|
155
|
+
const dir = skillDirOf(t, scope);
|
|
156
|
+
if (dir) out.push({ id: t.id, label: t.label, dir });
|
|
157
|
+
}
|
|
158
|
+
return dedupeByDir(out);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------- apply / undo ----------
|
|
162
|
+
|
|
163
|
+
function applyInstall(scope, targets, styled = false) {
|
|
164
|
+
const body = loadSkill();
|
|
165
|
+
const results = [];
|
|
166
|
+
for (const t of targets) results.push({ action: writeSkill(t.dir, body), dir: t.dir });
|
|
167
|
+
|
|
168
|
+
const prev = readManifest(scope);
|
|
169
|
+
const merged = dedupeByDir([...(prev ? prev.skills : []), ...targets.map(t => ({ id: t.id, dir: t.dir }))]);
|
|
170
|
+
writeManifest(scope, merged.map(m => ({ id: m.id, dir: m.dir })));
|
|
171
|
+
|
|
172
|
+
if (styled) {
|
|
173
|
+
for (const r of results) gline(`${c.green(S.tick)} ${c.gray(r.action.padEnd(8))} ${displayPath(r.dir)}/SKILL.md`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
console.log(`webplus v${VERSION} — installed the skill into:`);
|
|
177
|
+
for (const r of results) console.log(` ${r.action.padEnd(8)} ${displayPath(r.dir)}/SKILL.md`);
|
|
178
|
+
console.log('\nDone. Open a new agent session to pick up the skill.');
|
|
179
|
+
console.log('Undo anytime with: npx webplus remove');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Every place a webplus skill lives for a scope: catalog + manifest, that is ours right now.
|
|
183
|
+
function findInstalled(scope) {
|
|
184
|
+
const fromCatalog = TARGETS.map(t => ({ id: t.id, dir: skillDirOf(t, scope) })).filter(x => x.dir);
|
|
185
|
+
const man = readManifest(scope);
|
|
186
|
+
const candidates = dedupeByDir([...fromCatalog, ...(man ? man.skills : [])]);
|
|
187
|
+
return candidates.filter(c2 => isOurs(c2.dir));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function applyRemove(scope, targets, styled = false) {
|
|
191
|
+
const removed = [];
|
|
192
|
+
for (const t of targets) if (removeSkill(t.dir)) removed.push(displayPath(t.dir));
|
|
193
|
+
|
|
194
|
+
if (findInstalled(scope).length === 0) clearManifest(scope);
|
|
195
|
+
else {
|
|
196
|
+
const man = readManifest(scope);
|
|
197
|
+
if (man) {
|
|
198
|
+
const gone = new Set(targets.map(t => t.dir));
|
|
199
|
+
writeManifest(scope, man.skills.filter(s => !gone.has(s.dir)));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (styled) {
|
|
203
|
+
for (const d of removed) gline(`${c.green(S.tick)} ${c.gray('removed')} ${d}`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (removed.length) {
|
|
207
|
+
console.log('webplus — removed the skill from:');
|
|
208
|
+
for (const d of removed) console.log(' ' + d);
|
|
209
|
+
} else {
|
|
210
|
+
console.log('webplus — nothing to remove (no managed skill found).');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------- zero-dep clack-style UI (TTY arrow-keys, numbered fallback) ----------
|
|
215
|
+
|
|
216
|
+
const useColor = out => (out || process.stdout).isTTY && !process.env.NO_COLOR;
|
|
217
|
+
const paint = (s, code, out) => useColor(out) ? `\x1b[${code}m${s}\x1b[0m` : s;
|
|
218
|
+
const c = {
|
|
219
|
+
gray: (s, o) => paint(s, 90, o),
|
|
220
|
+
cyan: (s, o) => paint(s, 36, o),
|
|
221
|
+
green: (s, o) => paint(s, 32, o),
|
|
222
|
+
red: (s, o) => paint(s, 31, o),
|
|
223
|
+
yellow:(s, o) => paint(s, 33, o),
|
|
224
|
+
bold: (s, o) => paint(s, 1, o),
|
|
225
|
+
dim: (s, o) => paint(s, 2, o),
|
|
226
|
+
};
|
|
227
|
+
const S = { top: '┌', bar: '│', end: '└', step: '◇', on: '◉', off: '◯', radioOn: '●', radioOff: '○', ptr: '❯', tick: '✓' };
|
|
228
|
+
|
|
229
|
+
function intro(title, output = process.stdout) { output.write(`${c.gray(S.top, output)} ${c.bold(title, output)}\n${c.gray(S.bar, output)}\n`); }
|
|
230
|
+
function outro(text, output = process.stdout) { output.write(`${c.gray(S.bar, output)}\n${c.gray(S.end, output)} ${text}\n`); }
|
|
231
|
+
function gline(text = '', output = process.stdout) { output.write(c.gray(S.bar, output) + (text ? ' ' + text : '') + '\n'); }
|
|
232
|
+
|
|
233
|
+
function rawOn(input) { readline.emitKeypressEvents(input); if (input.isTTY && input.setRawMode) input.setRawMode(true); }
|
|
234
|
+
function rawOff(input, onKey) { input.removeListener('keypress', onKey); if (input.isTTY && input.setRawMode) input.setRawMode(false); }
|
|
235
|
+
|
|
236
|
+
function checklist({ message, items, input = process.stdin, output = process.stdout, interactive = input.isTTY, pageSize = 12 }) {
|
|
237
|
+
if (!interactive) return numberedChecklist({ message, items, input, output });
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const selected = new Set(items.filter(it => it.checked));
|
|
240
|
+
let query = '';
|
|
241
|
+
let idx = 0;
|
|
242
|
+
let lines = 0;
|
|
243
|
+
rawOn(input);
|
|
244
|
+
|
|
245
|
+
const filtered = () => {
|
|
246
|
+
if (!query) return items;
|
|
247
|
+
const q = query.toLowerCase();
|
|
248
|
+
return items.filter(it => it.label.toLowerCase().includes(q) || (it.keywords || '').toLowerCase().includes(q));
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const render = () => {
|
|
252
|
+
if (lines && output.isTTY) { readline.moveCursor(output, 0, -lines); readline.clearScreenDown(output); }
|
|
253
|
+
const list = filtered();
|
|
254
|
+
if (idx >= list.length) idx = Math.max(0, list.length - 1);
|
|
255
|
+
let start = 0;
|
|
256
|
+
if (list.length > pageSize) start = Math.min(Math.max(0, idx - Math.floor(pageSize / 2)), list.length - pageSize);
|
|
257
|
+
const end = Math.min(list.length, start + pageSize);
|
|
258
|
+
|
|
259
|
+
const search = query ? c.cyan(query, output) + c.cyan('▏', output) : c.gray('type to filter…', output);
|
|
260
|
+
let out = `${c.cyan(S.step, output)} ${c.bold(message, output)} ${c.gray('(' + selected.size + ' selected)', output)}\n`;
|
|
261
|
+
out += `${c.gray(S.bar, output)} ${c.gray('search', output)} ${search}\n`;
|
|
262
|
+
if (!list.length) {
|
|
263
|
+
out += `${c.gray(S.bar, output)} ${c.yellow('no matches — keep typing or ⌫ to clear', output)}\n`;
|
|
264
|
+
} else {
|
|
265
|
+
if (start > 0) out += `${c.gray(S.bar, output)} ${c.gray('↑ ' + start + ' more', output)}\n`;
|
|
266
|
+
for (let i = start; i < end; i++) {
|
|
267
|
+
const it = list[i];
|
|
268
|
+
const active = i === idx;
|
|
269
|
+
const ptr = active ? c.cyan(S.ptr, output) : ' ';
|
|
270
|
+
const box = selected.has(it) ? c.green(S.on, output) : c.gray(S.off, output);
|
|
271
|
+
const hint = it.hint ? ' ' + c.gray('·' + it.hint + '·', output) : '';
|
|
272
|
+
const label = active ? it.label : (selected.has(it) ? it.label : c.dim(it.label, output));
|
|
273
|
+
out += `${c.gray(S.bar, output)} ${ptr} ${box} ${label}${hint}\n`;
|
|
274
|
+
}
|
|
275
|
+
if (end < list.length) out += `${c.gray(S.bar, output)} ${c.gray('↓ ' + (list.length - end) + ' more', output)}\n`;
|
|
276
|
+
}
|
|
277
|
+
out += `${c.gray(S.bar, output)} ${c.gray('↑↓ move · space pick · type to search · enter ok · esc cancel', output)}\n`;
|
|
278
|
+
output.write(out);
|
|
279
|
+
lines = out.split('\n').length - 1;
|
|
280
|
+
};
|
|
281
|
+
const done = () => {
|
|
282
|
+
rawOff(input, onKey);
|
|
283
|
+
if (lines && output.isTTY) { readline.moveCursor(output, 0, -lines); readline.clearScreenDown(output); }
|
|
284
|
+
const chosen = items.filter(it => selected.has(it));
|
|
285
|
+
const names = chosen.length === 0 ? 'none' : chosen.length <= 4 ? chosen.map(x => x.shortLabel || x.label).join(', ') : chosen.length + ' selected';
|
|
286
|
+
output.write(`${c.green(S.step, output)} ${c.bold(message, output)} ${c.gray('· ' + names, output)}\n`);
|
|
287
|
+
resolve(chosen);
|
|
288
|
+
};
|
|
289
|
+
const onKey = (str, key) => {
|
|
290
|
+
key = key || {};
|
|
291
|
+
const list = filtered();
|
|
292
|
+
if (key.name === 'return' || key.name === 'enter') return done();
|
|
293
|
+
else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { rawOff(input, onKey); return reject(new CancelError()); }
|
|
294
|
+
else if (key.name === 'up') idx = list.length ? (idx - 1 + list.length) % list.length : 0;
|
|
295
|
+
else if (key.name === 'down') idx = list.length ? (idx + 1) % list.length : 0;
|
|
296
|
+
else if (key.name === 'space') { const it = list[idx]; if (it) selected.has(it) ? selected.delete(it) : selected.add(it); }
|
|
297
|
+
else if (key.name === 'backspace') { query = query.slice(0, -1); idx = 0; }
|
|
298
|
+
else if (str && str.length === 1 && str >= ' ' && !key.ctrl && !key.meta) { query += str; idx = 0; }
|
|
299
|
+
else return;
|
|
300
|
+
render();
|
|
301
|
+
};
|
|
302
|
+
render();
|
|
303
|
+
input.on('keypress', onKey);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function numberedChecklist({ message, items, input, output }) {
|
|
308
|
+
const pre = items.map((it, i) => it.checked ? i + 1 : null).filter(Boolean).join(',');
|
|
309
|
+
gline(c.bold(message, output), output);
|
|
310
|
+
items.forEach((it, i) => gline(`${i + 1}) ${it.label}${it.checked ? ' ' + c.gray('·detected·', output) : ''}`, output));
|
|
311
|
+
return question(`${c.gray(S.bar, output)} numbers (comma-separated, enter = ${pre || 'none'}): `, input, output)
|
|
312
|
+
.then(ans => {
|
|
313
|
+
const picked = ans.trim() ? ans.split(/[\s,]+/).map(n => parseInt(n, 10) - 1) : items.map((it, i) => it.checked ? i : -1);
|
|
314
|
+
return items.filter((_, i) => picked.includes(i));
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function select({ message, items, input = process.stdin, output = process.stdout }) {
|
|
319
|
+
if (!input.isTTY) {
|
|
320
|
+
gline(c.bold(message, output), output);
|
|
321
|
+
items.forEach((it, i) => gline(`${i + 1}) ${it.label}`, output));
|
|
322
|
+
return question(`${c.gray(S.bar, output)} choose (enter = 1): `, input, output).then(a => items[(parseInt(a, 10) || 1) - 1]);
|
|
323
|
+
}
|
|
324
|
+
return new Promise((resolve, reject) => {
|
|
325
|
+
let idx = 0, lines = 0;
|
|
326
|
+
rawOn(input);
|
|
327
|
+
const render = () => {
|
|
328
|
+
if (lines && output.isTTY) { readline.moveCursor(output, 0, -lines); readline.clearScreenDown(output); }
|
|
329
|
+
let out = `${c.cyan(S.step, output)} ${c.bold(message, output)} ${c.gray('↑↓ move · enter select', output)}\n`;
|
|
330
|
+
items.forEach((it, i) => {
|
|
331
|
+
const active = i === idx;
|
|
332
|
+
const dot = active ? c.green(S.radioOn, output) : c.gray(S.radioOff, output);
|
|
333
|
+
const label = active ? it.label : c.dim(it.label, output);
|
|
334
|
+
out += `${c.gray(S.bar, output)} ${active ? c.cyan(S.ptr, output) : ' '} ${dot} ${label}\n`;
|
|
335
|
+
});
|
|
336
|
+
output.write(out); lines = out.split('\n').length - 1;
|
|
337
|
+
};
|
|
338
|
+
const onKey = (s, key) => {
|
|
339
|
+
key = key || {};
|
|
340
|
+
if (key.name === 'up' || key.name === 'k') idx = (idx - 1 + items.length) % items.length;
|
|
341
|
+
else if (key.name === 'down' || key.name === 'j') idx = (idx + 1) % items.length;
|
|
342
|
+
else if (key.name === 'return' || key.name === 'enter') {
|
|
343
|
+
rawOff(input, onKey);
|
|
344
|
+
if (lines && output.isTTY) { readline.moveCursor(output, 0, -lines); readline.clearScreenDown(output); }
|
|
345
|
+
output.write(`${c.green(S.step, output)} ${c.bold(message, output)} ${c.gray('· ' + items[idx].label, output)}\n`);
|
|
346
|
+
return resolve(items[idx]);
|
|
347
|
+
}
|
|
348
|
+
else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { rawOff(input, onKey); return reject(new CancelError()); }
|
|
349
|
+
else return;
|
|
350
|
+
render();
|
|
351
|
+
};
|
|
352
|
+
render(); input.on('keypress', onKey);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function confirm({ message, def = true, input = process.stdin, output = process.stdout }) {
|
|
357
|
+
if (!input.isTTY) return Promise.resolve(def);
|
|
358
|
+
return new Promise(resolve => {
|
|
359
|
+
rawOn(input);
|
|
360
|
+
output.write(`${c.cyan(S.step, output)} ${c.bold(message, output)} ${c.gray(def ? '(Y/n)' : '(y/N)', output)} `);
|
|
361
|
+
const onKey = (s, key) => {
|
|
362
|
+
key = key || {};
|
|
363
|
+
let v;
|
|
364
|
+
if (key.name === 'y') v = true;
|
|
365
|
+
else if (key.name === 'n') v = false;
|
|
366
|
+
else if (key.name === 'return' || key.name === 'enter') v = def;
|
|
367
|
+
else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { rawOff(input, onKey); output.write('\n'); return resolve(def); }
|
|
368
|
+
else return;
|
|
369
|
+
rawOff(input, onKey);
|
|
370
|
+
output.write(c.gray(v ? 'yes' : 'no', output) + '\n');
|
|
371
|
+
resolve(v);
|
|
372
|
+
};
|
|
373
|
+
input.on('keypress', onKey);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function question(q, input, output) {
|
|
378
|
+
const rl = readline.createInterface({ input, output });
|
|
379
|
+
return new Promise(res => rl.question(q, ans => { rl.close(); res(ans); }));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---------- wizards ----------
|
|
383
|
+
|
|
384
|
+
async function runInstallWizard(opts) {
|
|
385
|
+
intro(`web+ ${c.gray('v' + VERSION)}`);
|
|
386
|
+
let scope = opts.global ? 'global' : opts.local ? 'project' : null;
|
|
387
|
+
if (!scope) {
|
|
388
|
+
const pick = await select({ message: 'Install webplus for', items: [
|
|
389
|
+
{ id: 'project', label: 'This project (current folder)' },
|
|
390
|
+
{ id: 'global', label: 'Globally (your whole machine)' },
|
|
391
|
+
] });
|
|
392
|
+
scope = pick.id;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const items = TARGETS.map(t => {
|
|
396
|
+
const detected = scope === 'global' ? homeExists(dirOf(t, 'global') || '\0') : t.detect();
|
|
397
|
+
const noPath = !dirOf(t, scope);
|
|
398
|
+
return {
|
|
399
|
+
...t,
|
|
400
|
+
checked: !noPath && (t.pre || detected),
|
|
401
|
+
hint: noPath ? 'no path at this scope' : ((detected && !t.pre) ? 'detected' : (t.tag || null)),
|
|
402
|
+
};
|
|
403
|
+
});
|
|
404
|
+
const selected = await checklist({ message: `Select agents (${scope}) — type to search`, items });
|
|
405
|
+
const targets = resolveTargets(selected, scope);
|
|
406
|
+
if (!targets.length) { outro(c.yellow('Nothing to install (none selected, or no path at this scope).')); return; }
|
|
407
|
+
|
|
408
|
+
gline(c.bold('Planned changes'));
|
|
409
|
+
for (const t of targets) {
|
|
410
|
+
const a = planAction(t.dir);
|
|
411
|
+
const tag = a === 'create' ? c.green('create ') : c.cyan('update ');
|
|
412
|
+
gline(`${tag} ${displayPath(t.dir)}/SKILL.md`);
|
|
413
|
+
}
|
|
414
|
+
gline();
|
|
415
|
+
if (!(await confirm({ message: 'Proceed?', def: true }))) { outro(c.yellow('Cancelled. Nothing changed.')); return; }
|
|
416
|
+
|
|
417
|
+
applyInstall(scope, targets, true);
|
|
418
|
+
outro(`${c.green('Done.')} Open a new agent session to pick up the skill. Undo: ${c.cyan('npx webplus remove')}`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function runRemoveWizard(opts) {
|
|
422
|
+
intro(`web+ remove ${c.gray('v' + VERSION)}`);
|
|
423
|
+
const scopes = opts.global ? ['global'] : opts.local ? ['project'] : ['project', 'global'];
|
|
424
|
+
let found = [];
|
|
425
|
+
for (const s of scopes) found.push(...findInstalled(s).map(t => ({ ...t, scope: s })));
|
|
426
|
+
if (!found.length) { outro('Nothing installed to remove.'); return; }
|
|
427
|
+
|
|
428
|
+
const items = found.map(t => ({ ...t, checked: true, label: `${displayPath(t.dir)} ${c.gray('(' + t.scope + ')')}`, shortLabel: displayPath(t.dir) }));
|
|
429
|
+
const chosen = await checklist({ message: 'Remove webplus from', items });
|
|
430
|
+
if (!chosen.length) { outro(c.yellow('Nothing selected. Nothing changed.')); return; }
|
|
431
|
+
if (!(await confirm({ message: `Remove from ${chosen.length} location(s)? (only our skill folder)`, def: true }))) { outro(c.yellow('Cancelled. Nothing changed.')); return; }
|
|
432
|
+
|
|
433
|
+
for (const s of scopes) {
|
|
434
|
+
const forScope = chosen.filter(c2 => c2.scope === s);
|
|
435
|
+
if (forScope.length) applyRemove(s, forScope, true);
|
|
436
|
+
}
|
|
437
|
+
outro(`${c.green('Removed.')} Only webplus's skill folder was deleted; other skills are intact.`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---------- commands ----------
|
|
441
|
+
|
|
442
|
+
function nonInteractiveTargets(opts) {
|
|
443
|
+
const scope = opts.global ? 'global' : 'project';
|
|
444
|
+
let selected;
|
|
445
|
+
if (opts.only) selected = TARGETS.filter(t => opts.only.includes(t.id));
|
|
446
|
+
else if (opts.all) selected = TARGETS.slice();
|
|
447
|
+
else selected = TARGETS.filter(t => (scope === 'global' ? (t.pre || homeExists(dirOf(t, 'global') || '\0')) : (t.pre || t.detect())));
|
|
448
|
+
return { scope, targets: resolveTargets(selected, scope) };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function cmdInit(opts) {
|
|
452
|
+
const wizard = process.stdin.isTTY && !opts.all && !opts.only && !opts.yes;
|
|
453
|
+
if (wizard) {
|
|
454
|
+
try { await runInstallWizard(opts); }
|
|
455
|
+
catch (e) { if (e instanceof CancelError) console.log('\nCancelled. Nothing changed.'); else throw e; }
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const { scope, targets } = nonInteractiveTargets(opts);
|
|
459
|
+
if (!process.stdin.isTTY && !opts.all && !opts.only)
|
|
460
|
+
console.log('(no interactive terminal — installing detected defaults; use --all/--only to choose)');
|
|
461
|
+
if (!targets.length) { console.log('webplus — nothing to install (no detected agents; try --all or --only).'); return; }
|
|
462
|
+
applyInstall(scope, targets);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function cmdRemove(opts) {
|
|
466
|
+
const wizard = process.stdin.isTTY && !opts.all && !opts.only && !opts.yes;
|
|
467
|
+
if (wizard) {
|
|
468
|
+
try { await runRemoveWizard(opts); }
|
|
469
|
+
catch (e) { if (e instanceof CancelError) console.log('\nCancelled. Nothing changed.'); else throw e; }
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const scope = opts.global ? 'global' : 'project';
|
|
473
|
+
let targets = findInstalled(scope);
|
|
474
|
+
if (opts.only) targets = targets.filter(t => opts.only.includes(t.id));
|
|
475
|
+
applyRemove(scope, targets);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function cmdList() {
|
|
479
|
+
console.log(`webplus v${VERSION} — skills-compatible agent catalog:`);
|
|
480
|
+
for (const t of TARGETS) {
|
|
481
|
+
const on = t.pre || t.detect();
|
|
482
|
+
const g = t.global ? displayPath(skillDirOf(t, 'global')) : '—';
|
|
483
|
+
console.log(` [${on ? 'x' : ' '}] ${t.id.padEnd(9)} project: ${(t.project + '/webplus').padEnd(30)} global: ${g}${t.detect() && !t.pre ? ' (detected)' : ''}`);
|
|
484
|
+
}
|
|
485
|
+
console.log('\n[x] = pre-selected/detected on a bare `init`. `npx webplus` for the wizard,');
|
|
486
|
+
console.log('`init --all` for every tool, `init --global` for machine-wide. The `agents` entry');
|
|
487
|
+
console.log('(.agents/skills) is the cross-tool standard read by Codex, Goose, OpenHands and more.');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function parse(argv) {
|
|
491
|
+
const opts = { _: [] };
|
|
492
|
+
for (let i = 0; i < argv.length; i++) {
|
|
493
|
+
const a = argv[i];
|
|
494
|
+
if (a === '--all') opts.all = true;
|
|
495
|
+
else if (a === '--global' || a === '-g') opts.global = true;
|
|
496
|
+
else if (a === '--local' || a === '-l') opts.local = true;
|
|
497
|
+
else if (a === '--yes' || a === '-y') opts.yes = true;
|
|
498
|
+
else if (a === '--only') opts.only = (argv[++i] || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
499
|
+
else if (a === '--help' || a === '-h') opts.help = true;
|
|
500
|
+
else if (a === '--version' || a === '-v') opts.version = true;
|
|
501
|
+
else opts._.push(a);
|
|
502
|
+
}
|
|
503
|
+
return opts;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const HELP = `webplus v${VERSION} — make any agent fetch the latest, official, verified info.
|
|
507
|
+
|
|
508
|
+
npx webplus Interactive wizard: pick scope + agents, then install
|
|
509
|
+
npx webplus init Same wizard (auto-detects your agents)
|
|
510
|
+
npx webplus init --all Non-interactive: install into every catalog tool
|
|
511
|
+
npx webplus init --only ids Install into specific ones (e.g. --only claude,agents,cursor)
|
|
512
|
+
npx webplus init --global Machine-wide skills dirs (~/.claude/skills, ~/.agents/skills, ...)
|
|
513
|
+
npx webplus init --local Force project scope (current folder)
|
|
514
|
+
npx webplus remove Interactive: pick which installs to remove
|
|
515
|
+
npx webplus remove --all Remove every webplus skill it can find
|
|
516
|
+
npx webplus list Show the agent catalog and what's detected
|
|
517
|
+
|
|
518
|
+
Installs a SKILL.md folder into each tool's native skills directory. SKILL.md is an open
|
|
519
|
+
standard; the .agents/skills entry covers Codex, Goose, OpenHands and other cross-tool readers.
|
|
520
|
+
Remove deletes only webplus's own skill folder, never your other skills.`;
|
|
521
|
+
|
|
522
|
+
async function main() {
|
|
523
|
+
const opts = parse(process.argv.slice(2));
|
|
524
|
+
if (opts.version) return console.log(VERSION);
|
|
525
|
+
if (opts.help) return console.log(HELP);
|
|
526
|
+
const cmd = opts._[0];
|
|
527
|
+
if (!cmd) return cmdInit(opts);
|
|
528
|
+
switch (cmd) {
|
|
529
|
+
case 'init': return cmdInit(opts);
|
|
530
|
+
case 'remove': case 'uninstall': return cmdRemove(opts);
|
|
531
|
+
case 'list': return cmdList(opts);
|
|
532
|
+
case 'help': return console.log(HELP);
|
|
533
|
+
default:
|
|
534
|
+
console.error(`Unknown command: ${cmd}\n`);
|
|
535
|
+
console.log(HELP);
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
module.exports = { writeSkill, removeSkill, planAction, isOurs, findInstalled, nonInteractiveTargets, resolveTargets, checklist, skillDirOf, manifestPath, TARGETS };
|
|
541
|
+
|
|
542
|
+
if (require.main === module) {
|
|
543
|
+
main().catch(e => { console.error(e.message); process.exit(1); });
|
|
544
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@katipally/webplus",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "web+ — make any AI agent fetch the latest, official, verified info from the web instead of answering from memory. One Agent Skill (SKILL.md), installed into every tool's skills directory via npx. Spends searches smartly, cites sources with dates.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"webplus": "bin/webplus.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"src",
|
|
11
|
+
"LICENSE",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node test/smoke.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ai",
|
|
19
|
+
"agents",
|
|
20
|
+
"agent-skills",
|
|
21
|
+
"skill.md",
|
|
22
|
+
"web-search",
|
|
23
|
+
"research",
|
|
24
|
+
"verification",
|
|
25
|
+
"fact-check",
|
|
26
|
+
"claude",
|
|
27
|
+
"codex",
|
|
28
|
+
"cursor",
|
|
29
|
+
"copilot"
|
|
30
|
+
],
|
|
31
|
+
"author": "katipally",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"homepage": "https://github.com/katipally/webplus",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/katipally/webplus.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/katipally/webplus/issues"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=16"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webplus
|
|
3
|
+
description: >
|
|
4
|
+
Fetches the latest, official, verified information from the web instead of answering from
|
|
5
|
+
memory. Use whenever a task depends on current or external facts: software versions, releases,
|
|
6
|
+
API or library docs, pricing, news, events, people, organizations, standards, or anything about
|
|
7
|
+
the current state of the world. Establishes the real current date, then cross-checks primary
|
|
8
|
+
sources and cites them with dates.
|
|
9
|
+
license: MIT
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# web+ — get the latest, official, verified facts
|
|
13
|
+
|
|
14
|
+
Use the web tools this agent already has: web search, fetch, or native browse.
|
|
15
|
+
|
|
16
|
+
## First, get the real date
|
|
17
|
+
Before judging what is "current" or "latest," establish today's actual date. Run the system clock
|
|
18
|
+
if you can (e.g. a `date` command), otherwise read it off a timestamp on an authoritative page.
|
|
19
|
+
Don't assume today's date from training, your internal sense of "now" can be wrong. Anchor every
|
|
20
|
+
recency judgment to the real date you obtained.
|
|
21
|
+
|
|
22
|
+
## When this applies
|
|
23
|
+
Any claim about the current state of the world: software versions and releases, API or library
|
|
24
|
+
docs, prices, news, events, people, organizations, standards, "what is newest." Don't answer such
|
|
25
|
+
things from training memory, verify on the web first. If a fact is stable and timeless (basic math,
|
|
26
|
+
settled history, language syntax that hasn't changed), answer directly, no search needed.
|
|
27
|
+
|
|
28
|
+
## How to source
|
|
29
|
+
- **Go to the primary source.** Rank: official docs / vendor site / GitHub releases / standards
|
|
30
|
+
body / regulator or government / the org's own announcement > reputable secondary reporting >
|
|
31
|
+
forums and blogs. Never treat content farms, SEO spam, or AI-generated filler as authority.
|
|
32
|
+
- **Cross-verify.** Confirm a fact in at least two independent reputable sources before stating it.
|
|
33
|
+
A single source means you label it single-source / unconfirmed.
|
|
34
|
+
- **Date discipline.** Check each page's publish or update date against the real current date.
|
|
35
|
+
Prefer the most recent authoritative version. Prefer canonical URLs (the official `/latest/`
|
|
36
|
+
docs, the real changelog) over old mirrors. State the as-of date in your answer. Watch for stale
|
|
37
|
+
caches and outdated tutorials.
|
|
38
|
+
- **Be exact.** Quote the precise version string, number, or date from the source rather than
|
|
39
|
+
paraphrasing. Distinguish stable from beta / pre-release and say which one you mean.
|
|
40
|
+
- **Resolve conflicts by authority and recency, not popularity.** When credible sources disagree,
|
|
41
|
+
prefer the more authoritative and more recent, and report the disagreement.
|
|
42
|
+
|
|
43
|
+
## Honesty
|
|
44
|
+
- **Never assume.** If web access is unavailable, or sources conflict and can't be reconciled, say
|
|
45
|
+
so plainly. Give what's verified and mark the rest unknown. Don't fill gaps with confident guesses.
|
|
46
|
+
- **Stay unbiased.** Gather before concluding. Represent each source faithfully, don't cherry-pick
|
|
47
|
+
to fit a prior, and surface credible disagreement instead of hiding it.
|
|
48
|
+
- **Cite.** Every fresh claim gets the exact source URL and its date.
|
|
49
|
+
|
|
50
|
+
## Spend searches wisely
|
|
51
|
+
Every search and fetch costs tokens, so spend them on the question, not around it.
|
|
52
|
+
- Search only what the task actually needs. Skip tangents, background trivia, and off-topic detours.
|
|
53
|
+
- Write one precise query (exact product, version, and the real date) instead of many vague ones.
|
|
54
|
+
Refine a weak query rather than firing the same idea repeatedly.
|
|
55
|
+
- Fetch only the one or two pages that answer the question, then extract the answer. Don't pull
|
|
56
|
+
whole pages or chase every link.
|
|
57
|
+
- Stop once the claim is verified. More searching after a confident, cross-checked answer is waste.
|