@turmind/halo 1.6.0 → 1.6.1
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/58.d26b1c4f14404226.js +1 -0
- package/admin-out/_next/static/chunks/989-ddba0eb72566da23.js +1 -0
- package/admin-out/_next/static/chunks/app/page-be47983ba8ff20b1.js +1 -0
- package/admin-out/_next/static/chunks/webpack-74f577c1b094f942.js +1 -0
- package/admin-out/_next/static/css/3b4dcd0ac6b8eae8.css +1 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +6 -4
- package/bundled-docs/design/canvas-extensions.md +30 -4
- package/bundled-docs/design/cron.md +22 -11
- package/bundled-docs/design/prompt-system.md +10 -5
- package/bundled-docs/design/session.md +4 -1
- package/bundled-docs/design/storage.md +31 -8
- package/bundled-docs/design/ws.md +1 -1
- package/bundled-docs/dev/add-model-provider.md +15 -9
- package/bundled-docs/dev/api.md +23 -10
- package/bundled-docs/dev/deploy.md +3 -0
- package/bundled-docs/dev/desktop-packaging.md +11 -5
- package/bundled-docs/dev/env.md +1 -1
- package/bundled-docs/dev/previews.md +26 -0
- package/bundled-docs/guide/agents.md +6 -6
- package/bundled-docs/guide/cli.md +5 -4
- package/bundled-docs/guide/skills.md +12 -6
- package/bundled-docs/guide/workspace.md +1 -1
- package/bundled-docs/requirements/agent.md +5 -0
- package/bundled-docs/requirements/chat.md +7 -3
- package/bundled-docs/requirements/command.md +4 -0
- package/bundled-docs/requirements/editor.md +20 -6
- package/bundled-docs/requirements/skill.md +15 -9
- package/dist/index.js +2263 -1324
- package/package.json +1 -1
- package/templates/models/anthropic.yaml +109 -13
- package/templates/models/aws-bedrock-claude-invoke.yaml +39 -13
- package/templates/models/aws-bedrock-mantle.yaml +60 -28
- package/templates/models/aws-bedrock-openai.yaml +52 -29
- package/templates/models/deepseek.yaml +7 -3
- package/templates/models/doubao.yaml +72 -20
- package/templates/models/hunyuan.yaml +74 -14
- package/templates/models/kimi.yaml +63 -3
- package/templates/models/mimo-token-plan-china.yaml +9 -1
- package/templates/models/minimax.yaml +9 -3
- package/templates/models/openai.yaml +120 -17
- package/templates/models/qwen.yaml +90 -4
- package/templates/models/zhipu.yaml +9 -1
- package/templates/schema.sql +1 -1
- package/templates/skills/cron/SKILL.md +38 -2
- package/templates/skills/cron/manage-cron.py +115 -7
- package/templates/skills/skill/SKILL.md +22 -11
- package/templates/skills/workspace/share.md +5 -2
- package/templates/skills/workspace/stage.py +51 -14
- package/admin-out/_next/static/chunks/220.7566bc89f3d98993.js +0 -1
- package/admin-out/_next/static/chunks/989-a7f752480eaaa2dc.js +0 -1
- package/admin-out/_next/static/chunks/app/page-8173112f256bc483.js +0 -1
- package/admin-out/_next/static/chunks/webpack-dc73047e0279979b.js +0 -1
- package/admin-out/_next/static/css/981c9e5610ce535a.css +0 -1
- /package/admin-out/_next/static/{_F_Fkyh9PainUNY82EEjY → j6buZnQDPISEFgtAuTGyl}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{_F_Fkyh9PainUNY82EEjY → j6buZnQDPISEFgtAuTGyl}/_ssgManifest.js +0 -0
|
@@ -142,6 +142,70 @@ def parse_session(raw: str | None) -> str | None:
|
|
|
142
142
|
return v
|
|
143
143
|
|
|
144
144
|
|
|
145
|
+
def parse_dir(workspace: str, raw: str | None) -> str | None:
|
|
146
|
+
"""Validate --dir against the job's workspace (same rule as the admin REST
|
|
147
|
+
routes / `halo cli --dir`): workspace-relative or absolute, inside the
|
|
148
|
+
workspace, an existing directory. Returns the workspace-relative path;
|
|
149
|
+
empty / the workspace root = None (= the root)."""
|
|
150
|
+
if raw is None or not raw.strip():
|
|
151
|
+
return None
|
|
152
|
+
v = raw.strip()
|
|
153
|
+
root = os.path.abspath(workspace)
|
|
154
|
+
resolved = os.path.abspath(v if os.path.isabs(v) else os.path.join(root, v))
|
|
155
|
+
if resolved != root and not resolved.startswith(root + os.sep):
|
|
156
|
+
die(f'invalid --dir "{raw}": outside the workspace {root}')
|
|
157
|
+
if not os.path.exists(resolved):
|
|
158
|
+
die(f'invalid --dir "{raw}": does not exist')
|
|
159
|
+
if not os.path.isdir(resolved):
|
|
160
|
+
die(f'invalid --dir "{raw}": not a directory')
|
|
161
|
+
rel = os.path.relpath(resolved, root)
|
|
162
|
+
return None if rel == '.' else rel
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def effective_session_id(job_id: str, session_id: str | None) -> str:
|
|
166
|
+
"""runner.ts effectiveSessionId: the picked session, else the job's own
|
|
167
|
+
(the job id; `cron-<id>` for an id without that prefix)."""
|
|
168
|
+
return session_id or (job_id if job_id.startswith('cron-') else f'cron-{job_id}')
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def session_agent(workspace: str, session_id: str) -> str | None:
|
|
172
|
+
"""Agent of an existing session in the workspace db, else None."""
|
|
173
|
+
db = Path(workspace) / '.halo' / 'halo.db'
|
|
174
|
+
if not db.exists():
|
|
175
|
+
return None
|
|
176
|
+
try:
|
|
177
|
+
# as_uri() percent-encodes `?` / `#` / `%` in the path.
|
|
178
|
+
conn = sqlite3.connect(db.resolve().as_uri() + '?mode=ro', uri=True)
|
|
179
|
+
row = conn.execute('SELECT agent_id FROM agent_sessions WHERE id = ?', (session_id,)).fetchone()
|
|
180
|
+
conn.close()
|
|
181
|
+
except sqlite3.Error:
|
|
182
|
+
return None
|
|
183
|
+
return row[0] if row else None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def check_session_rules(job: dict):
|
|
187
|
+
"""Write-point rules on the job's final state (same as the REST routes):
|
|
188
|
+
stateless excludes a picked session, and a stateful job's agent is fixed
|
|
189
|
+
once its session exists."""
|
|
190
|
+
if job.get('stateless') and job.get('session_id'):
|
|
191
|
+
die('--stateless (new session every run) and --session are mutually exclusive')
|
|
192
|
+
if job.get('stateless'):
|
|
193
|
+
return
|
|
194
|
+
sid = effective_session_id(job['id'], job.get('session_id'))
|
|
195
|
+
agent = session_agent(job['workspace_path'], sid)
|
|
196
|
+
if agent is not None and agent != job['agent_id']:
|
|
197
|
+
die(f'session {sid} already runs agent {agent}; delete that session or turn on '
|
|
198
|
+
f'"new session every run" (--stateless) to change the agent')
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def require_v5_columns(conn: sqlite3.Connection):
|
|
202
|
+
"""cron.db gains working_dir/stateless via the server's boot migration."""
|
|
203
|
+
cols = {r[1] for r in conn.execute('PRAGMA table_info(cron_jobs)').fetchall()}
|
|
204
|
+
if 'working_dir' not in cols or 'stateless' not in cols:
|
|
205
|
+
die('cron.db has no working_dir/stateless columns yet — restart the halo server '
|
|
206
|
+
'once so it migrates cron.db, then retry')
|
|
207
|
+
|
|
208
|
+
|
|
145
209
|
def parse_run_at(raw: str | None) -> int | None:
|
|
146
210
|
"""Accepts ISO-8601 (`2026-05-24T09:00:00`) or a unix-ms integer; returns
|
|
147
211
|
epoch ms or None. Local time without offset is parsed as host TZ —
|
|
@@ -254,6 +318,11 @@ def row_to_job(row: sqlite3.Row) -> dict:
|
|
|
254
318
|
except Exception:
|
|
255
319
|
j['targets'] = []
|
|
256
320
|
j['enabled'] = bool(j['enabled'])
|
|
321
|
+
# Folder + stateless (pre-migration dbs: unset) and the session this job
|
|
322
|
+
# runs in — `null` for stateless (a new one every run).
|
|
323
|
+
j.setdefault('working_dir', None)
|
|
324
|
+
j['stateless'] = bool(j.get('stateless') or 0)
|
|
325
|
+
j['effective_session_id'] = None if j['stateless'] else effective_session_id(j['id'], j.get('session_id'))
|
|
257
326
|
# Active window: ms (as stored) + ISO in the job's timezone. Pre-migration
|
|
258
327
|
# dbs have no columns — report them as unset.
|
|
259
328
|
for k in ('active_from', 'active_until'):
|
|
@@ -293,9 +362,11 @@ def cmd_get(args):
|
|
|
293
362
|
if not row:
|
|
294
363
|
die(f'job {args.id} not found', 2)
|
|
295
364
|
job = row_to_job(row)
|
|
365
|
+
run_cols = {r[1] for r in conn.execute('PRAGMA table_info(cron_runs)').fetchall()}
|
|
296
366
|
runs = conn.execute(
|
|
297
|
-
'SELECT id, status, started_at, completed_at, exit_code, failure_reason
|
|
298
|
-
'
|
|
367
|
+
'SELECT id, status, started_at, completed_at, exit_code, failure_reason'
|
|
368
|
+
+ (', session_id' if 'session_id' in run_cols else '') +
|
|
369
|
+
' FROM cron_runs WHERE job_id = ? ORDER BY id DESC LIMIT 5',
|
|
299
370
|
(args.id,),
|
|
300
371
|
).fetchall()
|
|
301
372
|
job['recentRuns'] = [dict(r) for r in runs]
|
|
@@ -317,11 +388,16 @@ def cmd_create(args):
|
|
|
317
388
|
die('--run-at must be in the future')
|
|
318
389
|
timeout_sec = parse_timeout_sec(args.timeout_sec)
|
|
319
390
|
session_id = parse_session(args.session)
|
|
391
|
+
working_dir = parse_dir(args.workspace, args.dir)
|
|
320
392
|
active_from = parse_active_bound('--active-from', args.active_from or '', args.timezone)
|
|
321
393
|
active_until = parse_active_bound('--active-until', args.active_until or '', args.timezone)
|
|
322
394
|
validate_active_window(run_at_ms, active_from, active_until, new_until=True)
|
|
323
395
|
|
|
324
396
|
job_id = args.id or gen_job_id()
|
|
397
|
+
check_session_rules({
|
|
398
|
+
'id': job_id, 'workspace_path': args.workspace, 'agent_id': args.agent or 'default',
|
|
399
|
+
'session_id': session_id, 'stateless': args.stateless,
|
|
400
|
+
})
|
|
325
401
|
now = now_ms()
|
|
326
402
|
targets = parse_targets(args.targets) if args.targets else []
|
|
327
403
|
|
|
@@ -346,8 +422,12 @@ def cmd_create(args):
|
|
|
346
422
|
'created_at': now,
|
|
347
423
|
'updated_at': now,
|
|
348
424
|
}
|
|
349
|
-
# Only name the
|
|
350
|
-
#
|
|
425
|
+
# Only name the newer columns when used, so plain jobs can still be
|
|
426
|
+
# created on a cron.db the server hasn't migrated yet.
|
|
427
|
+
if working_dir is not None or args.stateless:
|
|
428
|
+
require_v5_columns(conn)
|
|
429
|
+
values['working_dir'] = working_dir
|
|
430
|
+
values['stateless'] = 1 if args.stateless else 0
|
|
351
431
|
if active_from is not None or active_until is not None:
|
|
352
432
|
require_active_columns(conn)
|
|
353
433
|
values['active_from'] = active_from
|
|
@@ -388,6 +468,24 @@ def cmd_update(args):
|
|
|
388
468
|
if args.targets is not None:
|
|
389
469
|
sets.append(('targets', json.dumps(parse_targets(args.targets), ensure_ascii=False)))
|
|
390
470
|
cur = dict(row)
|
|
471
|
+
if args.dir is not None or args.stateless is not None:
|
|
472
|
+
require_v5_columns(conn)
|
|
473
|
+
workspace = args.workspace if args.workspace is not None else cur['workspace_path']
|
|
474
|
+
if args.dir is not None:
|
|
475
|
+
# `--dir ""` clears back to the workspace root.
|
|
476
|
+
sets.append(('working_dir', parse_dir(workspace, args.dir)))
|
|
477
|
+
elif args.workspace is not None and cur.get('working_dir'):
|
|
478
|
+
# The stored folder must still exist under the new workspace.
|
|
479
|
+
parse_dir(workspace, cur['working_dir'])
|
|
480
|
+
if args.stateless is not None:
|
|
481
|
+
sets.append(('stateless', 1 if args.stateless == 'on' else 0))
|
|
482
|
+
merged = {**cur, **dict(sets)}
|
|
483
|
+
# The agent rule only when this update changes one of its inputs: a job
|
|
484
|
+
# saved before the rule (its session on another agent) must still take an
|
|
485
|
+
# unrelated edit (prompt, schedule, targets, …).
|
|
486
|
+
rule_keys = ('agent_id', 'session_id', 'stateless', 'workspace_path')
|
|
487
|
+
if any(merged.get(k) != cur.get(k) for k in rule_keys):
|
|
488
|
+
check_session_rules(merged)
|
|
391
489
|
if args.active_from is not None or args.active_until is not None:
|
|
392
490
|
require_active_columns(conn)
|
|
393
491
|
# Bare dates / offset-less times read in the job's timezone after
|
|
@@ -405,7 +503,7 @@ def cmd_update(args):
|
|
|
405
503
|
new_until=bool(args.active_until and args.active_until.strip()),
|
|
406
504
|
)
|
|
407
505
|
if not sets:
|
|
408
|
-
die('nothing to update — pass at least one --label/--workspace/--agent/--prompt/--schedule/--run-at/--timezone/--timeout-sec/--session/--targets/--active-from/--active-until')
|
|
506
|
+
die('nothing to update — pass at least one --label/--workspace/--agent/--prompt/--schedule/--run-at/--timezone/--timeout-sec/--session/--dir/--stateless/--targets/--active-from/--active-until')
|
|
409
507
|
sets.append(('updated_at', now_ms()))
|
|
410
508
|
|
|
411
509
|
placeholders = ', '.join(f'{k} = ?' for k, _ in sets)
|
|
@@ -492,10 +590,12 @@ def cmd_channels(_):
|
|
|
492
590
|
|
|
493
591
|
def cmd_runs(args):
|
|
494
592
|
conn = open_db()
|
|
593
|
+
run_cols = {r[1] for r in conn.execute('PRAGMA table_info(cron_runs)').fetchall()}
|
|
495
594
|
sql = (
|
|
496
595
|
'SELECT id, job_id, trigger_kind, status, started_at, completed_at, '
|
|
497
|
-
'exit_code, failure_reason, output, dispatch_results
|
|
498
|
-
'
|
|
596
|
+
'exit_code, failure_reason, output, dispatch_results'
|
|
597
|
+
+ (', session_id' if 'session_id' in run_cols else '') +
|
|
598
|
+
' FROM cron_runs WHERE job_id = ?'
|
|
499
599
|
)
|
|
500
600
|
params: list[object] = [args.job_id]
|
|
501
601
|
if args.before:
|
|
@@ -554,6 +654,11 @@ def main():
|
|
|
554
654
|
help='max run time in seconds, 60–21600 (default: unset = 3600)')
|
|
555
655
|
c.add_argument('--session',
|
|
556
656
|
help="run in this existing root session id (default: the job's own session, same id as the job)")
|
|
657
|
+
c.add_argument('--dir',
|
|
658
|
+
help='workspace sub-folder the run focuses on (relative or absolute, must exist inside the '
|
|
659
|
+
'workspace; its .halo/INSTRUCTIONS.md chain joins the prompt). Default: the workspace root')
|
|
660
|
+
c.add_argument('--stateless', action='store_true',
|
|
661
|
+
help='start a new session every run (no memory between runs); excludes --session')
|
|
557
662
|
c.add_argument('--targets',
|
|
558
663
|
help='comma-separated channelType:accountId[:chatId] list, or JSON array. '
|
|
559
664
|
'chatId pins delivery to a specific chat (e.g. when scheduling from inside a chat).')
|
|
@@ -580,6 +685,9 @@ def main():
|
|
|
580
685
|
u.add_argument('--session',
|
|
581
686
|
help='root session id to run in; pass "" to clear back to the job\'s own session '
|
|
582
687
|
'(same id as the job — an older job pinned to cron-cron-… then starts a fresh one)')
|
|
688
|
+
u.add_argument('--dir', help='workspace sub-folder the run focuses on; pass "" to clear back to the workspace root')
|
|
689
|
+
u.add_argument('--stateless', choices=['on', 'off'],
|
|
690
|
+
help='on = a new session every run (clear --session first); off = one stable session again')
|
|
583
691
|
u.add_argument('--targets')
|
|
584
692
|
u.add_argument('--active-from', dest='active_from',
|
|
585
693
|
help='recurring only: YYYY-MM-DD (= that day 00:00), ISO-8601 or unix ms, in the job timezone; '
|
|
@@ -31,20 +31,30 @@ natural language, infer it from the request.
|
|
|
31
31
|
|
|
32
32
|
- `<workspace>/.halo/skills/<skill-id>/` — workspace-scoped
|
|
33
33
|
- `~/.halo/global/skills/<skill-id>/` — shared across all workspaces
|
|
34
|
+
- `~/.halo/global/extensions/<ext-id>/skills/<skill-id>/` — shipped by an
|
|
35
|
+
installed canvas extension. **Read-only**: replaced when the extension is
|
|
36
|
+
reinstalled, gone when it is uninstalled.
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
Precedence is workspace > global > extension: a higher folder **wholly
|
|
39
|
+
overrides** a lower one with the same id — the whole folder, SKILL.md plus
|
|
40
|
+
every resource file. There is NO per-file fallback: if the overriding copy
|
|
41
|
+
lacks a resource file the lower one has, that file is simply absent.
|
|
42
|
+
Consequences you must apply:
|
|
39
43
|
|
|
40
44
|
- **Create**: ask (or infer) which scope. Default to **workspace** unless the
|
|
41
|
-
user says "global" / "all workspaces". If the id already exists globally
|
|
42
|
-
say so — the new
|
|
45
|
+
user says "global" / "all workspaces". If the id already exists globally or
|
|
46
|
+
in an extension, say so — the new folder will shadow that one entirely.
|
|
43
47
|
- **Update**: resolve which folder is actually in effect FIRST — workspace
|
|
44
|
-
path if it exists, else global. Edit the live one;
|
|
45
|
-
behavior in a workspace where a workspace copy
|
|
46
|
-
global skill for one workspace, copy the WHOLE
|
|
47
|
-
(including resource files — remember: no per-file
|
|
48
|
+
path if it exists, else global, else the extension's. Edit the live one;
|
|
49
|
+
never edit global to fix behavior in a workspace where a workspace copy
|
|
50
|
+
shadows it. To customize a global skill for one workspace, copy the WHOLE
|
|
51
|
+
folder to workspace scope (including resource files — remember: no per-file
|
|
52
|
+
fallback), then edit.
|
|
53
|
+
- **Extension skills**: never edit anything under `~/.halo/global/extensions/`
|
|
54
|
+
(lost on the next reinstall). If the live folder is an extension's, copy the
|
|
55
|
+
WHOLE folder to global or workspace scope first (resource files included —
|
|
56
|
+
a copy holding only SKILL.md shadows the extension's folder and drops its
|
|
57
|
+
resources), then edit the copy.
|
|
48
58
|
- **Built-in skills** (`agent`, `skill`, `workspace`, `cron`, `send-file`,
|
|
49
59
|
`self`, `aws-knowledge`, `web-search`): their global copies are force re-seeded on every server
|
|
50
60
|
start — direct edits to those global files are lost. Customize at workspace
|
|
@@ -99,7 +109,8 @@ If the user doesn't say which agent, add it to `default`.
|
|
|
99
109
|
|
|
100
110
|
## Update
|
|
101
111
|
|
|
102
|
-
`file_read` the live SKILL.md (workspace
|
|
112
|
+
`file_read` the live SKILL.md (workspace, then global, then extension — see
|
|
113
|
+
override rules; an extension's is copied out before any edit), change
|
|
103
114
|
only what's requested, write back. Don't rewrite untouched sections. If the
|
|
104
115
|
skill declares `verbs`, keep the frontmatter list and any builtin handlers in
|
|
105
116
|
sync (builtin verbs are platform code; only their desc text lives here).
|
|
@@ -75,7 +75,9 @@ Excluded:
|
|
|
75
75
|
|
|
76
76
|
Lead with the most important callouts: anything in the **redactions** section, anything in
|
|
77
77
|
**missing_skills**, and anything that looks wrong (unexpected agent included, expected one
|
|
78
|
-
missing).
|
|
78
|
+
missing). **extension_skills** lists referenced skills that come from an installed canvas
|
|
79
|
+
extension: they are not bundled (read-only, owned by the extension) — tell the user the
|
|
80
|
+
receiver needs to install that extension.
|
|
79
81
|
|
|
80
82
|
### 4. Walk through suspicious markdown findings
|
|
81
83
|
|
|
@@ -147,7 +149,8 @@ To share: send the zip. Receiver unzips into their project root, then fills in a
|
|
|
147
149
|
workspace agent of the same id**. Those get copied into the bundle's `agents/` so the receiver
|
|
148
150
|
doesn't depend on having identical globals.
|
|
149
151
|
- Skills that are **referenced by an included agent's `skills:` list**. Workspace skill wins;
|
|
150
|
-
otherwise global skill (if not disabled).
|
|
152
|
+
otherwise global skill (if not disabled). A skill only an installed extension ships is not
|
|
153
|
+
bundled; it is listed under `extension_skills` instead.
|
|
151
154
|
- `prompts/{bootstrap,all,root}/` — workspace dir if it exists, else global fallback.
|
|
152
155
|
- `<ws>/.halo/settings.yaml` (if present) — values under any `secrets` map or a secret-named
|
|
153
156
|
key (`api_key`, `token`, …) are cleared to `""`; `<<ENV>>` placeholders stay. The receiver
|
|
@@ -81,27 +81,46 @@ BUILTIN_SKILL_IDS = {
|
|
|
81
81
|
|
|
82
82
|
def load_disabled(db_path: Path):
|
|
83
83
|
"""Return disabled items from the workspace DB, split by scope:
|
|
84
|
-
(global_agents, global_skills, ws_agents, ws_skills).
|
|
84
|
+
(global_agents, global_skills, ws_agents, ws_skills, ext_skills).
|
|
85
85
|
|
|
86
86
|
A disabled item never ships in the bundle (share == "don't take it
|
|
87
|
-
along"). We track
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
along"). We track the scopes separately because they take different code
|
|
88
|
+
paths below — workspace agents are staged from one loop, un-overridden
|
|
89
|
+
globals from another, extension skills are only reported. Only a
|
|
90
|
+
`workspace` row affects a workspace item."""
|
|
90
91
|
if not db_path.exists():
|
|
91
|
-
return set(), set(), set(), set()
|
|
92
|
+
return set(), set(), set(), set(), set()
|
|
92
93
|
try:
|
|
93
94
|
conn = sqlite3.connect(db_path)
|
|
94
95
|
cur = conn.execute("SELECT item_type, item_id, scope FROM disabled_items")
|
|
95
|
-
g_agents, g_skills, w_agents, w_skills = set(), set(), set(), set()
|
|
96
|
+
g_agents, g_skills, w_agents, w_skills, x_skills = set(), set(), set(), set(), set()
|
|
96
97
|
for item_type, item_id, scope in cur:
|
|
97
98
|
if scope == "global":
|
|
98
99
|
(g_agents if item_type == "agent" else g_skills).add(item_id)
|
|
99
|
-
|
|
100
|
+
elif scope == "workspace":
|
|
100
101
|
(w_agents if item_type == "agent" else w_skills).add(item_id)
|
|
102
|
+
elif scope == "extension" and item_type == "skill":
|
|
103
|
+
x_skills.add(item_id)
|
|
101
104
|
conn.close()
|
|
102
|
-
return g_agents, g_skills, w_agents, w_skills
|
|
105
|
+
return g_agents, g_skills, w_agents, w_skills, x_skills
|
|
103
106
|
except sqlite3.DatabaseError:
|
|
104
|
-
return set(), set(), set(), set()
|
|
107
|
+
return set(), set(), set(), set(), set()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def extension_skill_owner(extensions_dir: Path, sid: str):
|
|
111
|
+
"""Id of the installed extension shipping skill `sid`
|
|
112
|
+
(`<ext>/skills/<sid>/SKILL.md`, sid == ext or starts with `ext-`), else
|
|
113
|
+
None. Lower extension id wins, like the server (skill-sources.ts); the
|
|
114
|
+
manifest is only checked for presence, not fully validated."""
|
|
115
|
+
if not extensions_dir.is_dir():
|
|
116
|
+
return None
|
|
117
|
+
for ext in sorted(d.name for d in extensions_dir.iterdir() if d.is_dir() and not d.name.startswith(".")):
|
|
118
|
+
if sid != ext and not sid.startswith(f"{ext}-"):
|
|
119
|
+
continue
|
|
120
|
+
ext_dir = extensions_dir / ext
|
|
121
|
+
if (ext_dir / "halo-extension.json").is_file() and (ext_dir / "skills" / sid / "SKILL.md").is_file():
|
|
122
|
+
return ext
|
|
123
|
+
return None
|
|
105
124
|
|
|
106
125
|
|
|
107
126
|
def load_yaml(path: Path):
|
|
@@ -278,13 +297,14 @@ def main():
|
|
|
278
297
|
"memory_count": 0,
|
|
279
298
|
"assets_count": 0,
|
|
280
299
|
"missing_skills": [],
|
|
300
|
+
"extension_skills": [],
|
|
281
301
|
"settings_skipped": None,
|
|
282
302
|
"always_excluded": ALWAYS_EXCLUDED,
|
|
283
303
|
},
|
|
284
304
|
"redactions": {"yaml_params": [], "settings": [], "markdown_auto": [], "markdown_suspicious": []},
|
|
285
305
|
}
|
|
286
306
|
|
|
287
|
-
disabled_agents, disabled_skills, ws_disabled_agents, ws_disabled_skills = load_disabled(ws_halo / "halo.db")
|
|
307
|
+
disabled_agents, disabled_skills, ws_disabled_agents, ws_disabled_skills, ext_disabled_skills = load_disabled(ws_halo / "halo.db")
|
|
288
308
|
|
|
289
309
|
# --- INSTRUCTIONS.md (root + sub-dir chain), with global fallback if no root ws version
|
|
290
310
|
ws_root_instr = ws_halo / "INSTRUCTIONS.md"
|
|
@@ -453,12 +473,21 @@ def main():
|
|
|
453
473
|
for sid in sorted(ws_skill_ids):
|
|
454
474
|
stage_skill(ws_skills_dir / sid, "workspace")
|
|
455
475
|
|
|
456
|
-
# 2. agent-referenced skills missing from the workspace → global fallback
|
|
476
|
+
# 2. agent-referenced skills missing from the workspace → global fallback,
|
|
477
|
+
# else an installed extension's copy: read-only and owned by the
|
|
478
|
+
# extension, so it isn't bundled — the receiver installs the extension.
|
|
457
479
|
for sid in sorted(referenced_skills - ws_skill_ids):
|
|
458
480
|
if sid in BUILTIN_SKILL_IDS:
|
|
459
481
|
continue # receiver's server seeds these itself
|
|
460
|
-
if (global_skills_dir / sid / "SKILL.md").exists()
|
|
461
|
-
|
|
482
|
+
if (global_skills_dir / sid / "SKILL.md").exists():
|
|
483
|
+
if sid not in disabled_skills:
|
|
484
|
+
stage_skill(global_skills_dir / sid, "global")
|
|
485
|
+
else:
|
|
486
|
+
manifest["excluded"]["missing_skills"].append(sid)
|
|
487
|
+
continue
|
|
488
|
+
ext = extension_skill_owner(home_global / "extensions", sid)
|
|
489
|
+
if ext and sid not in ext_disabled_skills:
|
|
490
|
+
manifest["excluded"]["extension_skills"].append({"id": sid, "extension": ext})
|
|
462
491
|
else:
|
|
463
492
|
manifest["excluded"]["missing_skills"].append(sid)
|
|
464
493
|
|
|
@@ -506,6 +535,12 @@ def main():
|
|
|
506
535
|
f"- `.halo/settings.yaml` ({len(manifest['redactions']['settings'])} secret value(s) cleared)\n"
|
|
507
536
|
if manifest["included"]["settings"] else ""
|
|
508
537
|
)
|
|
538
|
+
ext_skills = manifest["excluded"]["extension_skills"]
|
|
539
|
+
ext_line = (
|
|
540
|
+
"- Skills shipped by canvas extensions (not bundled — install the extension to get them): "
|
|
541
|
+
+ ", ".join(f"`{s['id']}` (extension `{s['extension']}`)" for s in ext_skills) + "\n"
|
|
542
|
+
if ext_skills else ""
|
|
543
|
+
)
|
|
509
544
|
readme = (
|
|
510
545
|
"# Shared Halo Workspace\n\n"
|
|
511
546
|
"This bundle contains a Halo workspace's `.halo/` configuration: agents, skills,\n"
|
|
@@ -528,7 +563,8 @@ def main():
|
|
|
528
563
|
"- `USER.md` (personal profile, all scopes)\n"
|
|
529
564
|
"- `memory/`, `assets/` (workspace-specific)\n"
|
|
530
565
|
"- `sessions/`, `logs/`, `tmp/`, `*.db*`\n"
|
|
531
|
-
"- `~/.halo/secrets/settings.yaml` (global settings, never included)\n
|
|
566
|
+
"- `~/.halo/secrets/settings.yaml` (global settings, never included)\n"
|
|
567
|
+
f"{ext_line}\n"
|
|
532
568
|
"## Auto-redactions\n\n"
|
|
533
569
|
f"Markdown files were scanned. {len(manifest['redactions']['markdown_auto'])} unambiguous "
|
|
534
570
|
f"leak(s) were auto-redacted to `[REDACTED:<type>]`. "
|
|
@@ -559,6 +595,7 @@ def main():
|
|
|
559
595
|
"excluded_memory_files": manifest["excluded"]["memory_count"],
|
|
560
596
|
"excluded_asset_files": manifest["excluded"]["assets_count"],
|
|
561
597
|
"missing_skills": manifest["excluded"]["missing_skills"],
|
|
598
|
+
"extension_skills": [f"{s['id']} (extension {s['extension']})" for s in manifest["excluded"]["extension_skills"]],
|
|
562
599
|
},
|
|
563
600
|
}, indent=2, ensure_ascii=False))
|
|
564
601
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[220],{10912:(e,t,n)=>{function l(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"font-family:sans-serif;padding:40px;max-width:800px;margin:0 auto",l=window.open("","_blank");l&&(l.document.write("<!DOCTYPE html><html><head><title>".concat(e.replace(/[&<>"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e]),"</title><style>body{").concat(n,"}img{max-width:100%}</style></head><body>").concat(t,"</body></html>")),l.document.close(),l.onload=()=>{l.print()})}n.d(t,{H:()=>l})},39250:(e,t,n)=>{n.d(t,{A:()=>l});let l=(0,n(79020).A)("Printer",[["path",{d:"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2",key:"143wyd"}],["path",{d:"M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6",key:"1itne7"}],["rect",{x:"6",y:"14",width:"12",height:"8",rx:"1",key:"1ue0tg"}]])},62839:(e,t,n)=>{n.r(t),n.d(t,{MediaPreview:()=>y});var l=n(41440),r=n(54604),a=n(79020);let i=(0,a.A)("ZoomOut",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]]),c=(0,a.A)("ZoomIn",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"11",x2:"11",y1:"8",y2:"14",key:"1vmskp"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]]);var o=n(23715),s=n(39250),u=n(37050),d=n(10912),x=n(27173),h=n(17119),m=n(79906);function y(e){let{name:t,viewUrl:n,downloadUrl:a,onOpenAsText:y}=e,f=(0,h.kj)(),g=function(e){var t,n;let l=null!=(n=null==(t=e.split(".").pop())?void 0:t.toLowerCase())?n:"";return x.dj.includes(l)?"image":x.Tv.includes(l)?"video":x._H.includes(l)?"audio":null}(t),p=function(){let e=(0,r.useRef)(null),[t,n]=(0,r.useState)(1),[l,a]=(0,r.useState)({x:0,y:0}),i=(0,r.useRef)(null),[c,o]=(0,r.useState)(!1),s=(0,r.useCallback)(()=>{n(1),a({x:0,y:0})},[]),u=(0,r.useCallback)((e,t,l)=>{n(n=>{let r=Math.min(20,Math.max(.1,n*l)),i=r/n;return a(n=>({x:e-i*(e-n.x),y:t-i*(t-n.y)})),r})},[]),d=(0,r.useCallback)(t=>{let n=e.current;n&&u(n.clientWidth/2,n.clientHeight/2,t)},[u]),x=(0,r.useCallback)(t=>{var n;t.preventDefault();let l=null==(n=e.current)?void 0:n.getBoundingClientRect();l&&u(t.clientX-l.left,t.clientY-l.top,t.deltaY<0?1.15:.8695652173913044)},[u]),h=(0,r.useCallback)(e=>{0===e.button&&(e.preventDefault(),e.target.setPointerCapture(e.pointerId),i.current={startX:e.clientX,startY:e.clientY,origX:l.x,origY:l.y},o(!0))},[l]),m=(0,r.useCallback)(e=>{i.current&&a({x:i.current.origX+(e.clientX-i.current.startX),y:i.current.origY+(e.clientY-i.current.startY)})},[]),y=(0,r.useCallback)(()=>{i.current=null,o(!1)},[]);return(0,r.useEffect)(()=>{let t=e.current;if(!t)return;let n=e=>e.preventDefault();return t.addEventListener("wheel",n,{passive:!1}),()=>t.removeEventListener("wheel",n)},[]),{containerRef:e,scale:t,translate:l,dragging:c,reset:s,zoomCenter:d,onWheel:x,onPointerDown:h,onPointerMove:m,onPointerUp:y}}(),k="image"===g?(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(u.IB,{onClick:()=>p.zoomCenter(.8695652173913044),title:f("editor.zoomOut"),children:(0,l.jsx)(i,{className:"h-3 w-3"})}),(0,l.jsxs)("span",{className:"min-w-[3ch] text-center text-[10px] text-[var(--muted-foreground)]",children:[Math.round(100*p.scale),"%"]}),(0,l.jsx)(u.IB,{onClick:()=>p.zoomCenter(1.15),title:f("editor.zoomIn"),children:(0,l.jsx)(c,{className:"h-3 w-3"})}),(0,l.jsx)(u.IB,{onClick:p.reset,title:f("editor.resetZoom"),children:(0,l.jsx)(o.A,{className:"h-3 w-3"})}),(0,l.jsxs)(u.IB,{onClick:()=>(0,d.H)(t,'<img src="'.concat(n,'" />'),"margin:0;display:flex;justify-content:center;align-items:center;min-height:100vh;background:#fff"),title:f("editor.print"),children:[(0,l.jsx)(s.A,{className:"h-3 w-3"}),(0,l.jsx)("span",{children:f("editor.print")})]})]}):null;return(0,l.jsxs)(u.no,{name:t,downloadUrl:a,onOpenAsText:y,extraToolbar:k,children:["image"===g&&(0,l.jsx)("div",{ref:p.containerRef,className:"h-full w-full overflow-hidden bg-[#1a1a1a]",style:{cursor:p.dragging?"grabbing":"grab"},onWheel:p.onWheel,onPointerDown:p.onPointerDown,onPointerMove:p.onPointerMove,onPointerUp:p.onPointerUp,onPointerCancel:p.onPointerUp,children:(0,l.jsx)(m.p,{src:n,alt:t,draggable:!1,placeholderClassName:"h-full w-full rounded-none bg-white/5 text-white/60",style:{transformOrigin:"0 0",transform:"translate(".concat(p.translate.x,"px, ").concat(p.translate.y,"px) scale(").concat(p.scale,")")}})}),"video"===g&&(0,l.jsx)("div",{className:"flex h-full items-center justify-center bg-black p-4",children:(0,l.jsx)("video",{src:n,controls:!0,className:"max-h-full max-w-full",children:(0,l.jsx)("track",{kind:"captions"})})}),"audio"===g&&(0,l.jsx)("div",{className:"flex h-full flex-col items-center justify-center gap-4 bg-[var(--background)]",children:(0,l.jsx)("audio",{src:n,controls:!0,className:"w-full max-w-md"})})]})}}}]);
|