@passioncode-ai/passioncode 0.2.45 → 0.2.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/family.json +1 -1
  3. package/package.json +1 -1
  4. package/payload/.claude-plugin/marketplace.json +2 -2
  5. package/payload/manifest.json +8 -8
  6. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/payload/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
  8. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
  9. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +5 -1
  10. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +73 -18
  11. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +6 -5
  12. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +70 -20
  13. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +7 -1
  14. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +9 -0
  15. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -5
  16. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -0
  17. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +1 -1
  18. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -1
  19. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +16 -9
  20. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +87 -156
  21. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +180 -0
  22. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
  23. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +4 -2
  24. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +2 -0
  25. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +15 -3
  26. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -0
  27. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +12 -1
  28. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +12 -1
  29. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
  30. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +32 -13
  31. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +115 -147
  32. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +173 -0
  33. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +9 -42
  34. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +53 -4
  35. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +7 -3
  36. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -2
  37. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +87 -11
  38. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +19 -8
  39. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +30 -6
  40. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +9 -1
  41. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +15 -1
  42. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +73 -18
  43. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -1
  44. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +134 -24
  45. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +155 -21
  46. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
  47. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +10 -10
  48. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -1
  49. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +39 -13
  50. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -2
  51. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -3
  52. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -6
  53. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +10 -39
  54. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +190 -0
  55. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -1
  56. package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
@@ -58,7 +58,7 @@ SESSION_ID = re.compile(r"[A-Za-z0-9._:-]{1,128}")
58
58
  LOOPBACK_ORIGIN = re.compile(r"http://127\.0\.0\.1:(\d{1,5})")
59
59
 
60
60
 
61
- VERSION = "0.55.0"
61
+ VERSION = "0.56.0"
62
62
 
63
63
 
64
64
  HOSTS = {"claude": "Claude Code", "codex": "Codex", "kimi": "Kimi Code",
@@ -100,21 +100,26 @@ def nudge_shown(line: str) -> str:
100
100
  WATCH_NUDGES_FILE = "nudges.watch.json"
101
101
  MAX_NUDGES = 5
102
102
  MAX_NUDGE_CHARS = 400
103
+ #: At most this many items of an answer's nudges are read (FAA-143): a broken Fabric's long list costs linear time.
104
+ MAX_NUDGE_ITEMS = 1000
103
105
  _CONTROL = re.compile(r"[\x00-\x1f\x7f-\x9f\u2028\u2029]+")
104
106
 
105
107
 
106
108
  def nudge_lines(value: Any, limit: Optional[int] = MAX_NUDGES) -> List[str]:
107
109
  """The `says` of a nudges list as clean lines: one line each (controls and line breaks become a space), cut to
108
- MAX_NUDGE_CHARS, blanks and repeats dropped, at most `limit` (None: all). Anything that is not such a list gives []."""
110
+ MAX_NUDGE_CHARS, blanks and repeats dropped, at most `limit` (None: all) from the first MAX_NUDGE_ITEMS items.
111
+ Anything that is not such a list gives []."""
109
112
  lines: List[str] = []
113
+ seen = set()
110
114
  if not isinstance(value, list):
111
115
  return lines
112
- for item in value:
116
+ for item in value[:MAX_NUDGE_ITEMS]:
113
117
  says = item.get("says") if isinstance(item, dict) else None
114
118
  if not isinstance(says, str):
115
119
  continue
116
120
  line = " ".join(_CONTROL.sub(" ", says).split())[:MAX_NUDGE_CHARS].rstrip()
117
- if line and line not in lines:
121
+ if line and line not in seen:
122
+ seen.add(line)
118
123
  lines.append(line)
119
124
  if limit is not None and len(lines) == limit:
120
125
  break
@@ -480,6 +485,10 @@ NO_SESSION_MARK = "no-session"
480
485
  #: `repositories` as {host, owner, name}: a worker writes it from the tool's schema and removes it when the schema stops
481
486
  #: saying so. Until it exists the Stop hook reads no transcript (FAA-140, seam and product readings 1).
482
487
  REPOSITORIES_MARK = "observe-takes-repositories"
488
+ #: The file in the hooks' own folder that turns the transcript read off (FAA-160): the turn's git facts are still
489
+ #: measured. The environment variable FABRIC_SESSION_NO_REPOSITORIES=1 does the same for one environment.
490
+ NO_REPOSITORIES_MARK = "no-repositories"
491
+ NO_REPOSITORIES_ENV = "FABRIC_SESSION_NO_REPOSITORIES"
483
492
  NO_SESSION_HOLD_S = 15 * 60.0
484
493
 
485
494
 
@@ -531,10 +540,18 @@ def _write_private(path: Path, text: str) -> None:
531
540
  os.replace(str(tmp), str(path))
532
541
 
533
542
 
543
+ def repositories_off(env: Optional[Dict[str, str]] = None) -> bool:
544
+ """Whether the person turned the transcript read off (FAA-160): FABRIC_SESSION_NO_REPOSITORIES set to anything but
545
+ "" or "0", or the file NO_REPOSITORIES_MARK in the hooks' own folder."""
546
+ value = (os.environ if env is None else env).get(NO_REPOSITORIES_ENV, "")
547
+ return value not in ("", "0") or (state_root(env=env) / NO_REPOSITORIES_MARK).exists()
548
+
549
+
534
550
  def repositories_wanted(env: Optional[Dict[str, str]] = None) -> bool:
535
551
  """Whether the Stop hook reads the transcript for the turn's other repositories (FAA-140): only once a worker
536
- left the mark, and never on Windows, where a Bash command's paths are not this host's (FAA-175)."""
537
- return sys.platform != "win32" and (state_root(env=env) / REPOSITORIES_MARK).exists()
552
+ left the mark, and not when the person turned it off (FAA-160). On Windows too (FAA-175): bash_paths reads a Git
553
+ Bash command's /c/... and C:/... paths as this host's."""
554
+ return not repositories_off(env) and (state_root(env=env) / REPOSITORIES_MARK).exists()
538
555
 
539
556
 
540
557
  def session_dir(session_id: str, env: Optional[Dict[str, str]] = None) -> Path:
@@ -16,6 +16,7 @@ from typing import List
16
16
  from typing import Optional
17
17
  from typing import Tuple
18
18
 
19
+ from fabric_session_paths import _windows_path, bash_paths # noqa: E402,F401 - read by turn_paths and the tests
19
20
  from fabric_session_env import ( # noqa: E402 - the kit (FAA-65)
20
21
  BOARD_STATUS_TOOL,
21
22
  GIT_TIMEOUT_S,
@@ -122,7 +123,7 @@ TRANSCRIPT_TAIL_BYTES = 512 * 1024
122
123
 
123
124
  #: FAA-140 (seam reading 1): the Bash command text one turn may split, in characters; a long unterminated quote costs
124
125
  #: shlex its whole length, so this caps the Stop hook's time on it.
125
- BASH_BYTES = 65536
126
+ BASH_BYTES = 65536 # characters of Bash command text a turn, despite the name (kept: the facade exports it)
126
127
 
127
128
 
128
129
  #: FAA-140 (REQ-1): at most this many other repositories are named for one turn.
@@ -177,41 +178,6 @@ def parse_origin(url: str) -> Optional[Dict[str, str]]:
177
178
  return None
178
179
 
179
180
 
180
- def bash_paths(command: str, cwd: Path) -> List[str]:
181
- """The folders a Bash command names as `cd <absolute path>` or `git -C <path>` (absolute, or relative to `cwd`),
182
- in order. Nothing else of the command is kept; a command that does not split is [] (FAA-140)."""
183
- import shlex # only a Stop hook with a transcript needs it
184
- try:
185
- lexer = shlex.shlex(command, posix=True, punctuation_chars=True)
186
- lexer.whitespace_split = True
187
- tokens = list(lexer)
188
- except ValueError:
189
- return []
190
- found: List[str] = []
191
- separators = set("();&|<>")
192
- start = True # a command word may stand here
193
- index = 0
194
- while index < len(tokens):
195
- token = tokens[index]
196
- if token and set(token) <= separators:
197
- start, index = True, index + 1
198
- continue
199
- if start and token == "cd" and index + 1 < len(tokens) and os.path.isabs(tokens[index + 1]):
200
- found.append(os.path.normpath(tokens[index + 1]))
201
- elif start and os.path.basename(token) == "git":
202
- look = index + 1
203
- while look < len(tokens) and tokens[look].startswith("-") and not set(tokens[look]) <= separators:
204
- if tokens[look] == "-C" and look + 1 < len(tokens) and not set(tokens[look + 1]) <= separators:
205
- found.append(os.path.normpath(os.path.join(str(cwd), tokens[look + 1])))
206
- look += 1
207
- elif tokens[look] == "-c":
208
- look += 1
209
- look += 1
210
- start = False
211
- index += 1
212
- return found
213
-
214
-
215
181
  def _prompt(entry: Dict[str, Any]) -> bool:
216
182
  """Whether a transcript entry is a prompt the person typed: a user entry that is no tool result, no meta entry
217
183
  and no subagent's."""
@@ -291,7 +257,7 @@ def turn_paths(transcript: Any, cwd: Path, until: Optional[float] = None) -> Lis
291
257
  value = arguments.get(CHANGING_TOOLS[name])
292
258
  named = [os.path.normpath(os.path.join(str(cwd), value))] if isinstance(value, str) and value else []
293
259
  elif name == "Bash" and isinstance(arguments.get("command"), str) and left > 0 \
294
- and ("cd" in arguments["command"] or "git" in arguments["command"]):
260
+ and any(word in arguments["command"].lower() for word in ("cd", "git", "pushd")): # GIT.EXE on Windows
295
261
  command = arguments["command"][:left]
296
262
  left -= len(command)
297
263
  named = bash_paths(command, cwd)
@@ -845,13 +811,18 @@ def _keep_nudges(directory: Path, value: Any, note: Callable[[str], None], sessi
845
811
  _write_private(directory / name, json.dumps(merged))
846
812
  note("session=%s event=turn nudges=%d dropped=%d" % (session_id[:8], len(offered), dropped))
847
813
  except OSError as exc:
814
+ saved: List[str] = []
848
815
  try:
849
816
  if kept:
850
817
  _write_private(directory / name, json.dumps(kept))
818
+ saved = kept
851
819
  except OSError:
852
- kept = []
820
+ pass
821
+ # Lost: every distinct line, pending or this turn's (capped ones included, FAA-143), that is not back in the file.
822
+ # Both lists are clean lines already: counted as a set, never through nudge_lines' item cap (0.56.0 unit r1).
823
+ lost = len(set(kept).union(offered) - set(saved))
853
824
  note("session=%s event=turn nudges=%d outcome=lost lost=%d reason=%r"
854
- % (session_id[:8], len(offered), len(merged) - len(kept), type(exc).__name__))
825
+ % (session_id[:8], len(offered), lost, type(exc).__name__))
855
826
 
856
827
 
857
828
  def _drop_stale_ends(entries: List[Path], note: Callable[[str], None], session_id: str) -> List[Path]:
@@ -0,0 +1,190 @@
1
+ """working-with-fabric, part of fabric_session.py (FAA-65): reading the folders a Bash command names, for the Stop
2
+ hook's repositories (FAA-140, FAA-166, FAA-175). Standard library only."""
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import sys
7
+ from pathlib import Path, PurePath
8
+ from typing import Any
9
+ from typing import List
10
+ from typing import Optional
11
+
12
+
13
+ _DRIVE_MOUNT = re.compile(r"/(?:cygdrive/)?([A-Za-z])(?=/|$)")
14
+
15
+
16
+ def _windows_path(token: str) -> Optional[str]:
17
+ """A Git Bash path as this Windows host's (FAA-175): `/c/...` and `/cygdrive/c/...` are drive C, `C:/...` and
18
+ `C:\\...` stay a drive path, `\\\\host\\share` a share. None for another `/...` (Git's own root, which only
19
+ `cygpath` knows) and for a relative path."""
20
+ import ntpath
21
+ mount = _DRIVE_MOUNT.match(token)
22
+ if mount:
23
+ token = mount.group(1).upper() + ":\\" + token[mount.end():].lstrip("/")
24
+ elif token.startswith("/"):
25
+ return None
26
+ if not ntpath.isabs(token) or not (ntpath.splitdrive(token)[0]):
27
+ return None
28
+ return ntpath.normpath(token)
29
+
30
+
31
+ #: Words after which a command word may still stand (reserved words and the prefixes that run their command here).
32
+ KEYWORDS = frozenset(("{", "!", "then", "do", "else", "elif", "if", "while", "until", "time", "command", "builtin"))
33
+ ASSIGNMENT = re.compile(r"[A-Za-z_][A-Za-z0-9_]*=")
34
+
35
+
36
+ def bash_paths(command: str, cwd: Path, windows: Optional[bool] = None) -> List[str]:
37
+ """The folders a Bash command names as `cd <absolute path>` or `git -C <path>`, in order. A relative `-C` is read
38
+ against the folder the command last changed into with an absolute `cd`, else `cwd`; after a `cd` to a relative or
39
+ unknown folder it is dropped (FAA-166). A `cd` inside `( )`, a pipeline's stage or a list sent to the background
40
+ with `&` moves only a subshell, so the folder after it is the one before; a newline ends a command, a here-document's
41
+ body is text, `{`, `then`, `x=1` and the like still leave a command word to come, and `pushd` moves like `cd`. Nothing
42
+ else of the command is kept; a command that does not split is [] (FAA-140). On Windows (`windows`, default
43
+ `sys.platform == "win32"`) paths are read as Git Bash writes them, with its quoting: an unquoted `C:\\work` is
44
+ `C:work` there too, so a native path counts when it is quoted (FAA-175); Cygwin and MSYS2 Pythons read their own
45
+ POSIX paths (FAA-177)."""
46
+ import shlex # only a Stop hook with a transcript needs it
47
+ windows = sys.platform == "win32" if windows is None else windows
48
+ if windows:
49
+ import ntpath as paths
50
+ else:
51
+ import posixpath as paths
52
+ try:
53
+ # A `\` before a newline joins the lines, as in bash; a `;` and a second newline after each newline keep a `#`
54
+ # comment, which the lexer reads to the end of its line, newline included, from swallowing the next command or
55
+ # the line end a here-document counts.
56
+ command = command.replace("\\\n", "").replace("\n", "\n;\n")
57
+ lexer = shlex.shlex(command, posix=True, punctuation_chars="();<>|&\n")
58
+ lexer.whitespace = " \t\r" # a newline ends a command, as `;` does
59
+ lexer.whitespace_split = True
60
+ tokens = list(lexer)
61
+ except ValueError:
62
+ return []
63
+
64
+ def absolute(token: str) -> Optional[str]:
65
+ if windows:
66
+ return _windows_path(token)
67
+ return paths.normpath(token) if paths.isabs(token) else None
68
+
69
+ found: List[str] = []
70
+ separators = set("();&|<>\n")
71
+ # Where a relative -C points: None once a cd went somewhere unknown. POSIX rules read the folder with `/`, whatever
72
+ # the host writes (a test on Windows reads POSIX commands too).
73
+ base: Optional[str] = str(cwd) if windows else PurePath(cwd).as_posix()
74
+ # A cd in a pipeline's stage, in `( )` or in a backgrounded list moves only a subshell: the folder goes back to
75
+ # where that pipeline, subshell or list began.
76
+ pipe_start = list_start = base
77
+ in_pipeline = False
78
+ stack: List[Any] = []
79
+
80
+ def end_pipeline() -> None:
81
+ nonlocal base, pipe_start, in_pipeline
82
+ if in_pipeline:
83
+ base = pipe_start
84
+ pipe_start, in_pipeline = base, False
85
+
86
+ heredocs: List[str] = [] # the delimiters of here-documents whose bodies start at the next newline
87
+
88
+ def past_heredocs(at: int) -> int:
89
+ """The index of the newline ending the last pending here-document's delimiter line (or the end): the body is
90
+ text for a command, never commands of its own."""
91
+ while heredocs and at < len(tokens):
92
+ line_end = at
93
+ while line_end < len(tokens) and "\n" not in tokens[line_end]:
94
+ line_end += 1
95
+ if tokens[at:line_end] == [heredocs[0]]:
96
+ heredocs.pop(0)
97
+ if not heredocs:
98
+ return line_end
99
+ at = line_end + 1
100
+ heredocs.clear()
101
+ return len(tokens)
102
+
103
+ start = True # a command word may stand here
104
+ index = 0
105
+ while index < len(tokens):
106
+ token = tokens[index]
107
+ if token and set(token) <= separators:
108
+ at, ends_command, newline = 0, False, False
109
+ while at < len(token):
110
+ pair, char = token[at:at + 2], token[at]
111
+ if pair in ("&&", "||"):
112
+ end_pipeline()
113
+ at, ends_command = at + 2, True
114
+ continue
115
+ if token.startswith("<<<", at): # a here-string: its word is text, no body follows
116
+ at += 3
117
+ continue
118
+ if pair == "<<" and index + 1 < len(tokens):
119
+ delimiter = tokens[index + 1] # <<EOF, <<'EOF', <<-EOF: shlex has taken the quotes off
120
+ if delimiter == "-" and index + 2 < len(tokens):
121
+ index, delimiter = index + 1, tokens[index + 2]
122
+ heredocs.append(delimiter[1:] if delimiter.startswith("-") and len(delimiter) > 1 else delimiter)
123
+ index += 1 # the delimiter is no command word
124
+ at += 2
125
+ continue
126
+ if pair == "&>": # `&>` and `&>>` redirect, they background nothing
127
+ at += 3 if token[at + 2:at + 3] == ">" else 2
128
+ continue
129
+ if char in "<>": # a redirection: `2>&1` backgrounds nothing, `>|` pipes nothing
130
+ at += 2 if token[at + 1:at + 2] in ("&", "|") else 1
131
+ continue
132
+ ends_command = True
133
+ if pair == "|&" or char == "|":
134
+ base, in_pipeline = pipe_start, True
135
+ at += 2 if pair == "|&" else 1
136
+ continue
137
+ if char in ";\n":
138
+ end_pipeline()
139
+ list_start = base
140
+ newline = newline or char == "\n"
141
+ elif char == "&":
142
+ end_pipeline()
143
+ base = pipe_start = list_start
144
+ elif char == "(":
145
+ stack.append((base, pipe_start, list_start, in_pipeline))
146
+ pipe_start = list_start = base
147
+ in_pipeline = False
148
+ elif char == ")":
149
+ end_pipeline()
150
+ if stack:
151
+ base, pipe_start, list_start, in_pipeline = stack.pop()
152
+ at += 1
153
+ start = start or ends_command # a redirection alone starts no command
154
+ index = past_heredocs(index + 1) if newline and heredocs else index + 1
155
+ continue
156
+ if start and (token in KEYWORDS or ASSIGNMENT.match(token)):
157
+ index += 1 # `{ cd /a; }`, `then cd`, `x=1 cd`, `command cd`: a command word may still follow
158
+ continue
159
+ if start and token in ("pushd", "popd"):
160
+ target = tokens[index + 1] if token == "pushd" and index + 1 < len(tokens) else None
161
+ folder = absolute(target) if target and not set(target) <= separators else None
162
+ if folder:
163
+ found.append(folder)
164
+ base = folder # popd, or pushd somewhere not known here: the folder is not known
165
+ elif start and token == "cd":
166
+ target = tokens[index + 1] if index + 1 < len(tokens) and not set(tokens[index + 1]) <= separators else None
167
+ folder = absolute(target) if target else None
168
+ if folder:
169
+ found.append(folder)
170
+ base = folder
171
+ elif start and (paths.basename(token).lower() if windows else paths.basename(token)) in ("git", "git.exe"):
172
+ look, at, moved = index + 1, base, False
173
+ while look < len(tokens) and tokens[look].startswith("-") and not set(tokens[look]) <= separators:
174
+ if tokens[look] == "-C" and look + 1 < len(tokens) and not set(tokens[look + 1]) <= separators:
175
+ # git reads each relative -C against the one before it and runs in the last
176
+ folder = absolute(tokens[look + 1])
177
+ if folder is None and at is not None and not tokens[look + 1].startswith("/"):
178
+ folder = paths.normpath(paths.join(at, tokens[look + 1]))
179
+ if windows and not (paths.isabs(folder) and paths.splitdrive(folder)[0]):
180
+ folder = None # `C:work`, a drive-relative path: not known here
181
+ at, moved = folder, True
182
+ look += 1
183
+ elif tokens[look] == "-c":
184
+ look += 1
185
+ look += 1
186
+ if moved and at:
187
+ found.append(at)
188
+ start = False
189
+ index += 1
190
+ return found
@@ -154,7 +154,7 @@ def main(argv=None, env=None, out=print, err=print):
154
154
  'opencode':['hosts/context.mjs','hosts/opencode/fabric.mjs'],
155
155
  'pi':['hosts/context.mjs','hosts/pi/fabric.mjs'],
156
156
  'openclaw':['hosts/context.mjs','hosts/openclaw/index.mjs','hosts/openclaw/package.json','hosts/openclaw/openclaw.plugin.json']}[args.host]
157
- required += ['fabric_session_env.py','fabric_session_mcp.py','fabric_session_mail.py','fabric_session_hosts.py','fabric_session_cli.py'] # the helper's own modules (FAA-65)
157
+ required += ['fabric_session_env.py','fabric_session_mcp.py','fabric_session_paths.py','fabric_session_mail.py','fabric_session_hosts.py','fabric_session_cli.py'] # the helper's own modules (FAA-65)
158
158
  if any(not (script.parent/p).is_file() for p in required):
159
159
  raise fs.Refused('native adapter source is missing; update the complete skill before installing hooks')
160
160
  path=Path(args.config).expanduser().absolute() if args.config else destination(args.host,env)
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "passioncode",
4
4
  "displayName": "PassionCode.ai",
5
- "version": "0.2.45",
5
+ "version": "0.2.46",
6
6
  "description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check at session start that keeps the PassionCode.ai skill set current.",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",