@turmind/halo 1.5.5 → 1.5.7
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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/989-e0faeff1c4a0b0b9.js +1 -0
- package/admin-out/_next/static/chunks/app/page-44f707ea8dad57da.js +1 -0
- package/admin-out/_next/static/css/b8c87b919758c52b.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +14 -12
- package/bundled-docs/design/architecture.md +12 -11
- package/bundled-docs/design/canvas-extensions.md +19 -4
- package/bundled-docs/design/session.md +2 -2
- package/bundled-docs/design/storage.md +15 -12
- package/bundled-docs/dev/add-model-provider.md +33 -24
- package/bundled-docs/dev/api.md +4 -1
- package/bundled-docs/dev/deploy.md +1 -1
- package/bundled-docs/dev/desktop-packaging.md +4 -3
- package/bundled-docs/dev/env.md +14 -11
- package/bundled-docs/dev/tools.md +2 -2
- package/bundled-docs/guide/agents.md +2 -2
- package/bundled-docs/guide/cli.md +5 -4
- package/bundled-docs/guide/delegation-and-access.md +1 -1
- package/bundled-docs/guide/getting-started.md +1 -1
- package/bundled-docs/guide/skills.md +3 -3
- package/bundled-docs/guide/workspace.md +2 -2
- package/bundled-docs/requirements/editor.md +3 -2
- package/bundled-docs/requirements/settings.md +17 -8
- package/dist/index.js +9671 -9631
- package/package.json +1 -1
- package/templates/agents/default/agent.yaml +1 -0
- package/templates/models/anthropic.yaml +1 -0
- package/templates/models/aws-bedrock-claude-invoke.yaml +1 -0
- package/templates/models/aws-bedrock-mantle.yaml +1 -0
- package/templates/models/aws-bedrock-openai.yaml +1 -0
- package/templates/models/deepseek.yaml +1 -0
- package/templates/models/doubao.yaml +1 -0
- package/templates/models/hunyuan.yaml +1 -0
- package/templates/models/kimi.yaml +1 -0
- package/templates/models/mimo-token-plan-china.yaml +1 -0
- package/templates/models/minimax.yaml +1 -0
- package/templates/models/openai.yaml +1 -0
- package/templates/models/qwen.yaml +1 -0
- package/templates/models/zhipu.yaml +1 -0
- package/templates/skills/extension/SKILL.md +25 -14
- package/templates/skills/extension/config.yaml +9 -0
- package/templates/skills/extension/templates/ext.sh +166 -34
- package/templates/skills/web-search/SKILL.md +20 -6
- package/templates/skills/web-search/config.yaml +13 -0
- package/templates/skills/web-search/search.py +27 -6
- package/admin-out/_next/static/chunks/989-bc743317498d3fbc.js +0 -1
- package/admin-out/_next/static/chunks/app/page-0a390059f1914664.js +0 -1
- package/admin-out/_next/static/css/b53542687ebd4c65.css +0 -3
- /package/admin-out/_next/static/{cuVcldfdg1PXevaQMQSzb → rTD3zkMvzwtqUwzhz6QJH}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{cuVcldfdg1PXevaQMQSzb → rTD3zkMvzwtqUwzhz6QJH}/_ssgManifest.js +0 -0
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
id: aws-bedrock-mantle
|
|
2
|
+
runtime: bedrock-mantle
|
|
2
3
|
displayName: AWS Bedrock Mantle (OpenAI GPT-6.1 / 6 / 5.6)
|
|
3
4
|
displayName_zh: AWS Bedrock Mantle(OpenAI GPT-6.1 / 6 / 5.6)
|
|
4
5
|
description: OpenAI GPT-6.1 Sol, GPT-6 (Astra / Sol / Luna) and GPT-5.6 (Sol / Terra / Luna) on Amazon Bedrock via the bedrock-mantle endpoint (OpenAI Responses API only — not Converse/InvokeModel). Region-split — GPT-6.1 Sol and GPT-6 Sol / Luna are on us-east-1 only, Astra on us-west-2 only.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
id: aws-bedrock-openai
|
|
2
|
+
runtime: bedrock-mantle
|
|
2
3
|
displayName: AWS Bedrock OpenAI-compatible (GPT-6.1 / 6 / 5.6 · Grok 4.6 · Kimi K3)
|
|
3
4
|
displayName_zh: AWS Bedrock OpenAI 兼容接口(GPT-6.1 / 6 / 5.6 · Grok 4.6 · Kimi K3)
|
|
4
5
|
description: Frontier models on Amazon Bedrock via the bedrock-runtime endpoint's OpenAI Responses API (/openai/v1) — OpenAI GPT-6.1 Sol, GPT-6 (Astra / Sol / Luna) and GPT-5.6 (Sol / Terra / Luna), xAI Grok 4.6, Moonshot Kimi K3. Cross-Region inference profiles (global.*) only — every region serves every model, no region split. No server-side web_search (that is Mantle-only).
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
id: openai
|
|
2
|
+
runtime: openai-chat
|
|
2
3
|
displayName: OpenAI (Generic)
|
|
3
4
|
displayName_zh: OpenAI(通用)
|
|
4
5
|
description: Generic OpenAI-compatible chat completions. Use for native OpenAI or any third-party OpenAI-compatible gateway. Capabilities are exposed openly — the user is expected to know what their endpoint supports.
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: extension
|
|
3
|
-
description: Install / list / remove admin preview extensions (`~/.halo/global/extensions/<id>/`) — static viewers the admin editor loads for file types it has no built-in preview for (e.g. `.glb`). `/extension install <id|zip|url>` pulls the latest `<id>-v*` release from turmind/halo-hub
|
|
3
|
+
description: Install / list / remove admin preview extensions (`~/.halo/global/extensions/<id>/`) — static viewers the admin editor loads for file types it has no built-in preview for (e.g. `.glb`). `/extension install <id|zip|url>` pulls the latest `<id>-v*` release from the configured hub (default turmind/halo-hub). Activate when the user wants to preview a file type the editor can't open, or asks to install / update / remove an extension.
|
|
4
4
|
command: /extension
|
|
5
5
|
requiresAccess: full
|
|
6
6
|
verbs:
|
|
7
|
-
- { name: install, desc: "Install or upgrade an extension from
|
|
7
|
+
- { name: install, desc: "Install or upgrade an extension from the hub (by id), a local zip path, or a zip URL" }
|
|
8
8
|
- { name: list, desc: List installed extensions with version and status }
|
|
9
9
|
- { name: remove, desc: Remove an installed extension by id }
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# extension
|
|
13
13
|
|
|
14
|
-
The requested action is **`$1`**;
|
|
15
|
-
natural language ("install the glb
|
|
14
|
+
The requested action is **`$1`**; everything after it is the payload
|
|
15
|
+
(`$ARGUMENTS` minus the first token). With natural language ("install the glb
|
|
16
|
+
viewer"), infer both.
|
|
16
17
|
|
|
17
18
|
Preview extensions are static bundles the admin editor loads in a sandboxed
|
|
18
19
|
iframe to preview file types it has no built-in viewer for (e.g. `.glb`).
|
|
@@ -30,23 +31,33 @@ Pick EXT = the first that exists, then:
|
|
|
30
31
|
|
|
31
32
|
## install
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
When the payload names a source, run the install right away — no confirmation,
|
|
35
|
+
no explanation first. Ask only when no source was given.
|
|
36
|
+
|
|
37
|
+
`HALO_HUB_REPO='{{params.hub_repo}}' bash EXT install <source>` — keep
|
|
38
|
+
`{{params.hub_repo}}` exactly as-is (the system substitutes it at runtime), where
|
|
39
|
+
`<source>` is one of:
|
|
40
|
+
- an extension id (e.g. `glb`) → the newest `glb-v*` release zip from the hub
|
|
41
|
+
(a GitHub / Gitea / Forgejo / GitLab repo), or the newest `glb-v<x.y.z>` tag
|
|
42
|
+
when the hub is any other git URL / local repo;
|
|
36
43
|
- a local `.zip` path;
|
|
37
44
|
- an `https://…zip` URL.
|
|
38
45
|
|
|
46
|
+
The hub is set in Settings → Skills → extension → `hub_repo`; empty means
|
|
47
|
+
https://github.com/turmind/halo-hub.
|
|
48
|
+
|
|
39
49
|
The script unpacks to a temp dir, pre-checks `halo-extension.json` (id matches,
|
|
40
50
|
entry exists), then atomically replaces `~/.halo/global/extensions/<id>/`.
|
|
41
51
|
Reinstalling the same id upgrades (or downgrades) it. Relay the script's last
|
|
42
52
|
line to the user (`installed glb 1.0.0` / the error). Never edit files inside an
|
|
43
53
|
installed extension — reinstall instead.
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
On failure relay the script's hint. "release API failed" → rate-limited
|
|
56
|
+
(GitHub: 60 anonymous requests/hour), private, or a wrong `hub_repo`; setting
|
|
57
|
+
`GITHUB_TOKEN` / `GITLAB_TOKEN` / `GITEA_TOKEN` in the server environment helps.
|
|
58
|
+
"no release for <id>" → the id isn't on that hub. "needs a build step" → that
|
|
59
|
+
extension can't be installed from git tags; get its release zip. Either way the
|
|
60
|
+
user can download the zip by hand and run `install <path>`.
|
|
50
61
|
|
|
51
62
|
## list
|
|
52
63
|
|
|
@@ -63,8 +74,8 @@ the user first if they didn't name the id explicitly.
|
|
|
63
74
|
|
|
64
75
|
- Extensions are global (all workspaces on this server). Say so if the user
|
|
65
76
|
asks "install it for this project".
|
|
66
|
-
-
|
|
67
|
-
|
|
77
|
+
- Mention the configured hub (its `extensions/` directory) only when the user
|
|
78
|
+
asks what extensions exist or an install fails; do not scrape it.
|
|
68
79
|
- The script only pre-checks the required fields; the server's scanner is the
|
|
69
80
|
authority. A package it rejects shows up in `list` (and the admin) as
|
|
70
81
|
`ERROR` with the reason.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
displayName_zh: 预览扩展
|
|
2
|
+
description: Install / list / remove admin preview extensions.
|
|
3
|
+
description_zh: 安装 / 列出 / 删除管理后台的预览扩展。
|
|
4
|
+
|
|
5
|
+
params:
|
|
6
|
+
- key: hub_repo
|
|
7
|
+
description: "Hub that `install <id>` pulls from: a GitHub / Gitea / Forgejo / GitLab repo URL (newest `<id>-v*` release zip), `owner/repo` for GitHub, or any git URL / local repo path (newest `<id>-v<x.y.z>` tag). Empty = https://github.com/turmind/halo-hub."
|
|
8
|
+
description_zh: "`install <id>` 的来源仓库:GitHub / Gitea / Forgejo / GitLab 仓库地址(取最新的 `<id>-v*` release zip),GitHub 可简写为 `owner/repo`;也可以是任意 git 地址或本地仓库路径(取最新的 `<id>-v<x.y.z>` 标签)。留空 = https://github.com/turmind/halo-hub。"
|
|
9
|
+
default: https://github.com/turmind/halo-hub
|
|
@@ -9,17 +9,18 @@
|
|
|
9
9
|
# ext.sh list
|
|
10
10
|
# ext.sh remove <id>
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
12
|
+
# `install <id>` reads the hub in $HALO_HUB_REPO (the skill's hub_repo param;
|
|
13
|
+
# empty → https://github.com/turmind/halo-hub). GitHub / Gitea-Forgejo / GitLab
|
|
14
|
+
# hubs go through their public release API (GITHUB_TOKEN / GITEA_TOKEN /
|
|
15
|
+
# GITLAB_TOKEN, if set, are sent — lifts GitHub's 60 req/h anonymous limit);
|
|
16
|
+
# any other git URL or local repo installs from `<id>-v<semver>` tags.
|
|
17
|
+
# Needs curl + unzip + node (+ git for tag-based hubs). No `gh`.
|
|
14
18
|
set -euo pipefail
|
|
15
19
|
|
|
16
20
|
ROOT="$HOME/.halo/global/extensions"
|
|
17
|
-
|
|
21
|
+
DEFAULT_HUB="https://github.com/turmind/halo-hub"
|
|
18
22
|
ID_RE='^[a-z0-9][a-z0-9_-]{0,63}$' # keep in sync with server extensions/registry.ts ID_RE
|
|
19
23
|
|
|
20
|
-
cmd=${1:?usage: ext.sh install <id|zip|url> | list | remove <id>}
|
|
21
|
-
shift || true
|
|
22
|
-
|
|
23
24
|
# Print `<id> <version> <name>` for the extension dir $1, or fail with the
|
|
24
25
|
# reason. $2 (optional) = the id the directory / request claims.
|
|
25
26
|
precheck() {
|
|
@@ -38,44 +39,175 @@ precheck() {
|
|
|
38
39
|
' "$1" "${2:-}"
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
#
|
|
42
|
+
# ── Hub source ──────────────────────────────────────────────────────────────
|
|
43
|
+
# Self-contained hub_* functions (no globals besides DEFAULT_HUB) so a later
|
|
44
|
+
# `/skill install` / `/workspace import` can lift them out unchanged.
|
|
45
|
+
|
|
46
|
+
# Normalize a hub setting: empty / unsubstituted `{{…}}` → default,
|
|
47
|
+
# `owner/repo` → GitHub, an existing dir → absolute path, drop trailing `/`.
|
|
48
|
+
# A `.git` suffix stays (git mode clones the URL as given); the API helpers
|
|
49
|
+
# below drop it.
|
|
50
|
+
hub_url() {
|
|
51
|
+
local h=${1:-}
|
|
52
|
+
if [[ -z $h || $h == '{{'* ]]; then h=$DEFAULT_HUB; fi
|
|
53
|
+
if [[ $h =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9_.-]+$ ]]; then h="https://github.com/$h"
|
|
54
|
+
elif [[ -d $h ]]; then h=$(cd "$h" && pwd)
|
|
55
|
+
fi
|
|
56
|
+
echo "${h%/}"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# curl with the platform's optional token header.
|
|
60
|
+
hub_curl() {
|
|
61
|
+
local p=$1; shift
|
|
62
|
+
case $p in
|
|
63
|
+
github) curl -fsSL ${GITHUB_TOKEN:+-H "Authorization: Bearer $GITHUB_TOKEN"} "$@" ;;
|
|
64
|
+
gitlab) curl -fsSL ${GITLAB_TOKEN:+-H "PRIVATE-TOKEN: $GITLAB_TOKEN"} "$@" ;;
|
|
65
|
+
gitea) curl -fsSL ${GITEA_TOKEN:+-H "Authorization: token $GITEA_TOKEN"} "$@" ;;
|
|
66
|
+
esac
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Repo endpoint of <platform>'s API for hub URL $2.
|
|
70
|
+
hub_repo_api() {
|
|
71
|
+
[[ $2 =~ ^(https?://[^/]+)/(.+)$ ]] || return 1
|
|
72
|
+
local base=${BASH_REMATCH[1]} path=${BASH_REMATCH[2]%.git}
|
|
73
|
+
case $1 in
|
|
74
|
+
github) echo "https://api.github.com/repos/$path" ;;
|
|
75
|
+
gitea) echo "$base/api/v1/repos/$path" ;;
|
|
76
|
+
gitlab) echo "$base/api/v4/projects/${path//\//%2F}" ;;
|
|
77
|
+
esac
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
hub_releases_api() {
|
|
81
|
+
local q='per_page=100'
|
|
82
|
+
if [[ $1 == gitea ]]; then q='limit=50'; fi
|
|
83
|
+
echo "$(hub_repo_api "$1" "$2")/releases?$q"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# 0 when the host answers <platform>'s repo endpoint for $2 with JSON.
|
|
87
|
+
hub_probe() {
|
|
88
|
+
local api ct
|
|
89
|
+
api=$(hub_repo_api "$1" "$2") || return 1
|
|
90
|
+
ct=$(hub_curl "$1" --max-time 10 -o /dev/null -w '%{content_type}' "$api" 2>/dev/null) || return 1
|
|
91
|
+
[[ $ct == application/json* ]]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# github | gitlab | gitea | git (= tag-based fallback) for a normalized hub URL.
|
|
95
|
+
hub_platform() {
|
|
96
|
+
if [[ ! $1 =~ ^https?://([^/]+)/[^/]+/.+$ ]]; then echo git; return; fi
|
|
97
|
+
case ${BASH_REMATCH[1]} in
|
|
98
|
+
github.com) echo github ;;
|
|
99
|
+
*gitlab*) echo gitlab ;;
|
|
100
|
+
codeberg.org|*gitea*|*forgejo*) echo gitea ;;
|
|
101
|
+
*) if hub_probe gitea "$1"; then echo gitea
|
|
102
|
+
elif hub_probe gitlab "$1"; then echo gitlab
|
|
103
|
+
else echo git; fi ;;
|
|
104
|
+
esac
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
hub_releases_page() {
|
|
108
|
+
if [[ $1 == gitlab ]]; then echo "${2%.git}/-/releases"; else echo "${2%.git}/releases"; fi
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# stdin = <platform>'s release list (newest-first); print the first `.zip`
|
|
112
|
+
# asset URL of the newest non-draft / non-prerelease / non-upcoming release
|
|
113
|
+
# tagged `<id>-v*`, exit 2 if there is none.
|
|
114
|
+
hub_pick_release() {
|
|
115
|
+
node -e '
|
|
116
|
+
let s = ""; process.stdin.on("data", (d) => s += d).on("end", () => {
|
|
117
|
+
const [platform, id] = process.argv.slice(1)
|
|
118
|
+
// "<id>-v<digit>", so id glb never matches a glb-viewer-v… tag.
|
|
119
|
+
const r = JSON.parse(s).find((r) => new RegExp("^" + id + "-v\\d").test(r.tag_name) && !r.draft && !r.prerelease && !r.upcoming_release)
|
|
120
|
+
const assets = !r ? [] : platform === "gitlab"
|
|
121
|
+
? ((r.assets && r.assets.links) || []).map((l) => ({ name: l.name, url: l.direct_asset_url || l.url }))
|
|
122
|
+
: (r.assets || []).map((a) => ({ name: a.name, url: a.browser_download_url }))
|
|
123
|
+
const a = assets.find((a) => String(a.name).endsWith(".zip"))
|
|
124
|
+
if (!a) process.exit(2)
|
|
125
|
+
console.log(a.url)
|
|
126
|
+
})' "$1" "$2"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# stdin = `git ls-remote --tags --refs`; print the highest `<id>-vX.Y.Z` tag
|
|
130
|
+
# (prerelease `-…` suffixes skipped), nothing if there is none.
|
|
131
|
+
hub_pick_tag() {
|
|
132
|
+
local v
|
|
133
|
+
v=$(sed -n "s|.*refs/tags/$1-v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)$|\1|p" | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)
|
|
134
|
+
if [[ -n $v ]]; then echo "$1-v$v"; fi
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Tag-based hub: shallow-clone the newest `<id>-v<semver>` tag and echo
|
|
138
|
+
# `extensions/<id>/` minus the build inputs scripts/pack.mjs leaves out of a
|
|
139
|
+
# release zip. A package with a build step is refused — its output isn't in git.
|
|
140
|
+
hub_git_package() {
|
|
141
|
+
local hub=$1 id=$2 tmp=$3 url=$1 refs tag dir
|
|
142
|
+
if [[ $url == /* ]]; then url="file://$url"; fi # a plain path makes git ignore --depth
|
|
143
|
+
refs=$(git ls-remote --tags --refs "$url") || { echo "cannot list tags of $hub (git ls-remote failed)" >&2; exit 1; }
|
|
144
|
+
tag=$(printf '%s\n' "$refs" | hub_pick_tag "$id")
|
|
145
|
+
[[ -n $tag ]] || { echo "no release for $id in $hub (no $id-v<x.y.z> tag)" >&2; exit 1; }
|
|
146
|
+
git -c advice.detachedHead=false clone -q --depth 1 --branch "$tag" "$url" "$tmp/repo" || { echo "git clone of $tag from $hub failed" >&2; exit 1; }
|
|
147
|
+
dir="$tmp/repo/extensions/$id"
|
|
148
|
+
[[ -d $dir ]] || { echo "$hub tag $tag has no extensions/$id/" >&2; exit 1; }
|
|
149
|
+
if [[ -e $dir/fetch-deps.sh || -e $dir/build.sh ]]; then
|
|
150
|
+
echo "$id needs a build step — install it from a release zip (install <path/to.zip> or <https://…zip>)" >&2; exit 1
|
|
151
|
+
fi
|
|
152
|
+
rm -rf "$dir/node_modules" "$dir/src" "$dir/package.json" "$dir/package-lock.json"
|
|
153
|
+
echo "$dir"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# Fetch hub package <id> from hub URL $1 into $3: echo a zip path (release
|
|
157
|
+
# API) or the package dir (git tags). API errors never fall back to git — a
|
|
158
|
+
# git checkout of a build-step package would install broken.
|
|
159
|
+
hub_fetch() {
|
|
160
|
+
local hub=$1 id=$2 tmp=$3 platform api url token page
|
|
161
|
+
platform=$(hub_platform "$hub")
|
|
162
|
+
if [[ $platform == git ]]; then hub_git_package "$hub" "$id" "$tmp"; return; fi
|
|
163
|
+
api=$(hub_releases_api "$platform" "$hub")
|
|
164
|
+
page=$(hub_releases_page "$platform" "$hub")
|
|
165
|
+
case $platform in github) token=GITHUB_TOKEN ;; gitlab) token=GITLAB_TOKEN ;; *) token=GITEA_TOKEN ;; esac
|
|
166
|
+
hub_curl "$platform" "$api" -o "$tmp/releases.json" \
|
|
167
|
+
|| { echo "$platform release API failed for $hub — rate-limited, private or wrong repo? set $token, or download the zip from $page and run: install <path>" >&2; exit 1; }
|
|
168
|
+
url=$(hub_pick_release "$platform" "$id" <"$tmp/releases.json") \
|
|
169
|
+
|| { echo "no release for $id in $hub — check the id, or download the zip from $page and run: install <path>" >&2; exit 1; }
|
|
170
|
+
# No token here: asset links redirect to other hosts and curl -L would
|
|
171
|
+
# forward a custom header like PRIVATE-TOKEN there.
|
|
172
|
+
curl -fsSL "$url" -o "$tmp/pkg.zip" || { echo "download failed: $url" >&2; exit 1; }
|
|
173
|
+
echo "$tmp/pkg.zip"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Echo a local zip or an unpacked package dir for source $1 (path, https URL, or hub id).
|
|
42
177
|
resolve_source() {
|
|
43
|
-
local src=$1
|
|
178
|
+
local src=$1
|
|
44
179
|
if [[ -f $src ]]; then echo "$src"; return; fi
|
|
45
180
|
if [[ $src =~ ^https?:// ]]; then curl -fsSL "$src" -o "$TMP/pkg.zip"; echo "$TMP/pkg.zip"; return; fi
|
|
46
181
|
[[ $src =~ $ID_RE ]] || { echo "invalid id or missing file: $src" >&2; exit 1; }
|
|
47
|
-
|
|
48
|
-
url=$(curl -fsSL ${GITHUB_TOKEN:+-H "Authorization: Bearer $GITHUB_TOKEN"} \
|
|
49
|
-
"https://api.github.com/repos/$REPO/releases?per_page=100" \
|
|
50
|
-
| node -e '
|
|
51
|
-
let s = ""; process.stdin.on("data", (d) => s += d).on("end", () => {
|
|
52
|
-
const id = process.argv[1]
|
|
53
|
-
const r = JSON.parse(s).find((r) => r.tag_name.startsWith(id + "-v") && !r.draft && !r.prerelease)
|
|
54
|
-
const a = r && r.assets.find((a) => a.name.endsWith(".zip"))
|
|
55
|
-
if (!a) process.exit(2)
|
|
56
|
-
console.log(a.browser_download_url)
|
|
57
|
-
})' "$src") \
|
|
58
|
-
|| { echo "no release for $src in $REPO — download the zip from https://github.com/$REPO/releases and run: install <path>" >&2; exit 1; }
|
|
59
|
-
curl -fsSL "$url" -o "$TMP/pkg.zip"
|
|
60
|
-
echo "$TMP/pkg.zip"
|
|
182
|
+
hub_fetch "$(hub_url "${HALO_HUB_REPO:-}")" "$src" "$TMP"
|
|
61
183
|
}
|
|
62
184
|
|
|
185
|
+
# Sourced (tests, later reuse of the hub_* functions): definitions only.
|
|
186
|
+
if [[ ${BASH_SOURCE[0]} != "$0" ]]; then return 0; fi
|
|
187
|
+
|
|
188
|
+
cmd=${1:?usage: ext.sh install <id|zip|url> | list | remove <id>}
|
|
189
|
+
shift || true
|
|
190
|
+
|
|
63
191
|
case $cmd in
|
|
64
192
|
install)
|
|
65
193
|
src=${1:?usage: ext.sh install <id|zip|url>}
|
|
66
194
|
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
195
|
+
pkg=$(resolve_source "$src")
|
|
196
|
+
if [[ -d $pkg ]]; then
|
|
197
|
+
dir=$pkg
|
|
198
|
+
else
|
|
199
|
+
# Same refusals as the server's zip installer: no `..`, no absolute paths,
|
|
200
|
+
# no symlinks (zipinfo mode column starts with `l`).
|
|
201
|
+
if unzip -Z1 "$pkg" | grep -Eq '(^|/)\.\.(/|$)|^/'; then echo "zip contains ../ or absolute entries" >&2; exit 1; fi
|
|
202
|
+
if unzip -Z "$pkg" | grep -q '^l'; then echo "zip contains symlinks" >&2; exit 1; fi
|
|
203
|
+
mkdir -p "$TMP/x"
|
|
204
|
+
unzip -q "$pkg" -d "$TMP/x"
|
|
205
|
+
# `zip -r glb.zip glb/` wraps everything in one directory — descend into it.
|
|
206
|
+
dir="$TMP/x"
|
|
207
|
+
if [[ ! -f $dir/halo-extension.json ]]; then
|
|
208
|
+
only=$(find "$dir" -mindepth 1 -maxdepth 1 | head -2)
|
|
209
|
+
if [[ $(printf '%s\n' "$only" | wc -l) -eq 1 && -f $only/halo-extension.json ]]; then dir=$only; fi
|
|
210
|
+
fi
|
|
79
211
|
fi
|
|
80
212
|
expect=""
|
|
81
213
|
if [[ $src =~ $ID_RE && ! -f $src ]]; then expect=$src; fi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: web-search
|
|
3
|
-
description: Real-time web search with two gears. Fast (default, Amazon Nova grounding, ~3s, cheap) — current events, news, prices, any routine lookup. Deep (--deep, OpenAI GPT-5.6 web_search via Bedrock Mantle, ~20-40s, token-expensive) — multi-round retrieval with first-hand sources and per-claim citations, for verifying facts behind important decisions or double-checking weak/contradictory fast results. Default to fast; escalate to deep only when the question deserves depth. For AWS documentation, service announcements or regional availability, use the aws-knowledge skill instead.
|
|
3
|
+
description: Real-time web search on AWS Bedrock (needs AWS credentials with Bedrock access) with two gears. Fast (default, Amazon Nova grounding, ~3s, cheap) — current events, news, prices, any routine lookup. Deep (--deep, OpenAI GPT-5.6 web_search via Bedrock Mantle, ~20-40s, token-expensive) — multi-round retrieval with first-hand sources and per-claim citations, for verifying facts behind important decisions or double-checking weak/contradictory fast results. Default to fast; escalate to deep only when the question deserves depth. For AWS documentation, service announcements or regional availability, use the aws-knowledge skill instead.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
# Web Search
|
|
@@ -22,12 +22,18 @@ searches against official sites), leans toward authoritative first-hand sources
|
|
|
22
22
|
Use `<workspace>/.halo/skills/web-search/search.py` if that exists, else
|
|
23
23
|
`~/.halo/global/skills/web-search/search.py`.
|
|
24
24
|
|
|
25
|
+
Always prefix the call with the two region variables, keeping
|
|
26
|
+
`{{params.fast_region}}` / `{{params.deep_region}}` exactly as written (the
|
|
27
|
+
system substitutes them at runtime; unset = built-in defaults):
|
|
28
|
+
|
|
25
29
|
```bash
|
|
26
30
|
# fast (default) — everyday lookups
|
|
27
|
-
|
|
31
|
+
WEB_SEARCH_FAST_REGION='{{params.fast_region}}' WEB_SEARCH_DEEP_REGION='{{params.deep_region}}' \
|
|
32
|
+
python3 ~/.halo/global/skills/web-search/search.py "What are the latest news about the China A-share market today?"
|
|
28
33
|
|
|
29
34
|
# deep — verify facts that must be right
|
|
30
|
-
|
|
35
|
+
WEB_SEARCH_FAST_REGION='{{params.fast_region}}' WEB_SEARCH_DEEP_REGION='{{params.deep_region}}' \
|
|
36
|
+
python3 ~/.halo/global/skills/web-search/search.py --deep "What did the US Federal Reserve announce most recently about interest rates?"
|
|
31
37
|
```
|
|
32
38
|
|
|
33
39
|
- Output: answer text + a `Sources:` URL list (gear/model/token usage on stderr).
|
|
@@ -40,8 +46,16 @@ python3 ~/.halo/global/skills/web-search/search.py --deep "What did the US Feder
|
|
|
40
46
|
English works best; Chinese works too — deep gear rewrites queries bilingually.
|
|
41
47
|
- Search output is still a lead, not gospel: for critical figures, `web_fetch`
|
|
42
48
|
the cited source URL and confirm before acting on it.
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
- Both gears run on AWS Bedrock with the machine's AWS credentials (no API key):
|
|
50
|
+
fast = Amazon Nova 2 Lite + web grounding (`bedrock:InvokeModel` +
|
|
51
|
+
`bedrock:InvokeTool` on `amazon.nova_grounding`, US regions only); deep =
|
|
52
|
+
OpenAI GPT-5.6 on Bedrock Mantle (`bedrock-mantle:CreateInference` +
|
|
53
|
+
`bedrock-mantle:CallWithBearerToken`). Deep gear needs the
|
|
54
|
+
`aws-bedrock-token-generator` pip package (one-time:
|
|
55
|
+
`pip3 install --user aws-bedrock-token-generator`). An AccessDenied /
|
|
56
|
+
credentials error means this machine lacks that access — say so, don't retry.
|
|
57
|
+
- Regions are set in Settings → Skills → web-search (`fast_region` default
|
|
58
|
+
us-east-1; `deep_region` default us-west-2 for luna, us-east-1 for sol — sol
|
|
59
|
+
isn't offered in us-west-2).
|
|
46
60
|
- A deep call can take 40 s — be patient in shell_exec. One clear question per
|
|
47
61
|
call; don't fan out parallel calls.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
displayName_zh: 联网搜索
|
|
2
|
+
description: Real-time web search on AWS Bedrock — needs AWS credentials with Bedrock access.
|
|
3
|
+
description_zh: 基于 AWS Bedrock 的实时联网搜索,需要有 Bedrock 权限的 AWS 凭证。
|
|
4
|
+
|
|
5
|
+
params:
|
|
6
|
+
- key: fast_region
|
|
7
|
+
description: "AWS region for the fast gear (Amazon Nova 2 Lite + web grounding, US regions only: us-east-1 / us-east-2 / us-west-2). Empty = us-east-1."
|
|
8
|
+
description_zh: "快速档(Amazon Nova 2 Lite + Web Grounding)使用的 AWS 区域,仅限美国区域:us-east-1 / us-east-2 / us-west-2。留空 = us-east-1。"
|
|
9
|
+
default: us-east-1
|
|
10
|
+
- key: deep_region
|
|
11
|
+
description: "AWS region for the deep gear (OpenAI GPT-5.6 on Bedrock Mantle). Luna: us-east-1 / us-east-2 / us-west-2; Sol: us-east-1 / us-east-2. Empty = us-west-2 for Luna, us-east-1 for Sol."
|
|
12
|
+
description_zh: "深度档(Bedrock Mantle 上的 OpenAI GPT-5.6)使用的 AWS 区域。Luna:us-east-1 / us-east-2 / us-west-2;Sol:us-east-1 / us-east-2。留空 = Luna 用 us-west-2,Sol 用 us-east-1。"
|
|
13
|
+
default: us-west-2
|
|
@@ -12,27 +12,44 @@ Usage:
|
|
|
12
12
|
Auth is automatic in both gears (machine AWS credentials): fast uses boto3's
|
|
13
13
|
default credential chain; deep mints a 12-hour bearer token on every call via
|
|
14
14
|
aws-bedrock-token-generator — no API key to configure or rotate.
|
|
15
|
+
|
|
16
|
+
Regions: WEB_SEARCH_FAST_REGION / WEB_SEARCH_DEEP_REGION override the defaults
|
|
17
|
+
below (the skill passes its fast_region / deep_region params through them).
|
|
15
18
|
"""
|
|
16
19
|
import json
|
|
20
|
+
import os
|
|
17
21
|
import sys
|
|
18
22
|
import urllib.error
|
|
19
23
|
import urllib.request
|
|
20
24
|
|
|
21
|
-
MANTLE_URL = "https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses"
|
|
22
25
|
MODELS = {"luna": "openai.gpt-5.6-luna", "sol": "openai.gpt-5.6-sol"}
|
|
26
|
+
# Nova web grounding is US-only; us-east-1 / us-east-2 / us-west-2 all verified.
|
|
27
|
+
FAST_REGION = "us-east-1"
|
|
28
|
+
# bedrock-mantle: Luna is in us-east-1 / us-east-2 / us-west-2, Sol only in
|
|
29
|
+
# us-east-1 / us-east-2. us-west-2 answered 3-8x faster than us-east-1 in
|
|
30
|
+
# 2026-10-04 tests, so Luna defaults there; Sol takes us-east-1.
|
|
31
|
+
DEEP_REGION = {"openai.gpt-5.6-luna": "us-west-2", "openai.gpt-5.6-sol": "us-east-1"}
|
|
23
32
|
# luna (default) runs effort=max to squeeze out its retrieval planning; sol's
|
|
24
33
|
# retrieval is already exhaustive at medium (measured 48 search queries on one
|
|
25
34
|
# question), so it gets xhigh to avoid pointless token doubling — max over
|
|
26
35
|
# xhigh measured only ~6% more reasoning tokens.
|
|
27
36
|
EFFORTS = {"openai.gpt-5.6-luna": "max", "openai.gpt-5.6-sol": "xhigh"}
|
|
37
|
+
SOL_REGIONS = ("us-east-1", "us-east-2")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def env_region(name: str) -> str:
|
|
41
|
+
"""Region from env; empty or an unsubstituted `{{…}}` skill-param placeholder = unset."""
|
|
42
|
+
v = os.environ.get(name, "").strip()
|
|
43
|
+
return "" if v.startswith("{{") else v
|
|
28
44
|
|
|
29
45
|
|
|
30
46
|
def fast_search(question: str) -> None:
|
|
31
47
|
import boto3
|
|
32
48
|
from botocore.config import Config
|
|
33
49
|
|
|
50
|
+
region = env_region("WEB_SEARCH_FAST_REGION") or FAST_REGION
|
|
34
51
|
bedrock = boto3.client(
|
|
35
|
-
"bedrock-runtime", region_name=
|
|
52
|
+
"bedrock-runtime", region_name=region, config=Config(read_timeout=60)
|
|
36
53
|
)
|
|
37
54
|
response = bedrock.converse(
|
|
38
55
|
modelId="us.amazon.nova-2-lite-v1:0",
|
|
@@ -55,13 +72,17 @@ def fast_search(question: str) -> None:
|
|
|
55
72
|
for u in sources:
|
|
56
73
|
print(" -", u)
|
|
57
74
|
usage = response.get("usage", {})
|
|
58
|
-
print(f"\n[gear=fast model=nova-2-lite total_tokens={usage.get('totalTokens')}]", file=sys.stderr)
|
|
75
|
+
print(f"\n[gear=fast model=nova-2-lite region={region} total_tokens={usage.get('totalTokens')}]", file=sys.stderr)
|
|
59
76
|
|
|
60
77
|
|
|
61
78
|
def deep_search(question: str, model: str) -> None:
|
|
62
79
|
from aws_bedrock_token_generator import provide_token
|
|
63
80
|
|
|
64
|
-
|
|
81
|
+
region = env_region("WEB_SEARCH_DEEP_REGION") or DEEP_REGION[model]
|
|
82
|
+
if model == MODELS["sol"] and region not in SOL_REGIONS:
|
|
83
|
+
print(f"[sol is not offered in {region}; using {DEEP_REGION[model]}]", file=sys.stderr)
|
|
84
|
+
region = DEEP_REGION[model]
|
|
85
|
+
token = provide_token(region=region)
|
|
65
86
|
body = json.dumps(
|
|
66
87
|
{
|
|
67
88
|
"model": model,
|
|
@@ -71,7 +92,7 @@ def deep_search(question: str, model: str) -> None:
|
|
|
71
92
|
}
|
|
72
93
|
).encode()
|
|
73
94
|
req = urllib.request.Request(
|
|
74
|
-
|
|
95
|
+
f"https://bedrock-mantle.{region}.api.aws/openai/v1/responses",
|
|
75
96
|
data=body,
|
|
76
97
|
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
|
77
98
|
method="POST",
|
|
@@ -101,7 +122,7 @@ def deep_search(question: str, model: str) -> None:
|
|
|
101
122
|
for u in sources:
|
|
102
123
|
print(" -", u)
|
|
103
124
|
usage = data.get("usage", {})
|
|
104
|
-
print(f"\n[gear=deep model={model} total_tokens={usage.get('total_tokens')}]", file=sys.stderr)
|
|
125
|
+
print(f"\n[gear=deep model={model} region={region} total_tokens={usage.get('total_tokens')}]", file=sys.stderr)
|
|
105
126
|
|
|
106
127
|
|
|
107
128
|
def main() -> None:
|